delivery-tracking

delivery-tracking

熱門

透過 CJ 大韓通運與郵局官方端點,以託運單號查詢包裹狀態,並將工作流程結構化為可擴充的貨運業者轉接器。

6445星標
736分支
更新於 2026/7/22
SKILL.md
唯讀
名稱
delivery-tracking
描述

透過 CJ 大韓通運與郵局官方端點,以託運單號查詢包裹狀態,並將工作流程結構化為可擴充的貨運業者轉接器。

配送追蹤

這個技能做什麼

使用 CJ 大韓通運與郵局官方查詢頁面,透過託運單號查詢目前配送狀態。

  • CJ 大韓通運:使用官方配送查詢頁面提供的 JSON endpoint
  • 郵局:使用官方配送查詢頁面使用的 HTML endpoint
  • 結果以通用格式(貨運業者 / 託運單號 / 目前狀態 / 最近事件)簡短整理

使用時機

  • 「幫我查 CJ 大韓通運的託運單」
  • 「郵局包裹現在到哪了」
  • 「幫我確認這個託運單號是否配送完成」
  • 「幫我把各貨運業者的查詢邏輯整理好,方便以後擴充」

不使用時機

  • 只有訂單號碼而沒有託運單號時
  • 需要直接進行包裹預約或退貨申請時
  • 想繞過官方管道使用非官方整合查詢服務時

前置需求

  • 網路連線
  • python3
  • curl
  • 選擇性:jq

輸入

  • 貨運業者識別碼:cjepost
  • 託運單號
    • CJ 大韓通運:10 位或 12 位數字
    • 郵局:13 位數字

貨運業者轉接器規則

此技能將各貨運業者的邏輯以 carrier adapter 為單位拆分。

新增貨運業者時,需先定義下列欄位:

  • carrier id:例如 cjepost
  • validator:託運單號的位數/格式
  • entrypoint:官方查詢入口 URL
  • transport:使用 JSON API / HTML form / CLI 哪一種
  • parser:從哪個欄位或表格提取狀態
  • status map:如何將各貨運業者的原始狀態碼對應到通用狀態
  • retry policy:超時/重試規則

目前有兩個轉接器:

carrier adapter official entry transport validator parser focus
cj https://www.cjlogistics.com/ko/tool/parcel/tracking page GET + tracking-detail POST JSON 10 位或 12 位數字 parcelDetailResultMap.resultList
epost https://service.epost.go.kr/trace.RetrieveRegiPrclDeliv.postal?sid1= form POST HTML 13 位數字 基本資訊 table_col + 詳細 processTable

工作流程

0. 先正規化輸入

  • 將貨運業者名稱正規化為 cjepost
  • 移除託運單號中的空格與 -
  • 若位數驗證先失敗,則不發送查詢。

1. CJ 大韓通運:官方 JSON 流程

從官方入口頁面讀取 _csrf,並將其值隨 tracking-detail POST 一起送出。

  • 入口頁面:https://www.cjlogistics.com/ko/tool/parcel/tracking
  • 詳細 endpoint:https://www.cjlogistics.com/ko/tool/parcel/tracking-detail
  • 必填欄位:_csrfparamInvcNo

基本範例使用 curl 維持 _csrf 與 cookie,Python 僅用於 JSON 整理。

tmp_body="$(mktemp)"
tmp_cookie="$(mktemp)"
tmp_json="$(mktemp)"
invoice="1234567890"  # 官方頁面佔位符性質的 smoke-test 值

curl -sS -L -c "$tmp_cookie" \
  "https://www.cjlogistics.com/ko/tool/parcel/tracking" \
  -o "$tmp_body"

csrf="$(python3 - <<'PY' "$tmp_body"
import re
import sys

text = open(sys.argv[1], encoding="utf-8", errors="ignore").read()
print(re.search(r'name="_csrf" value="([^"]+)"', text).group(1))
PY
)"

curl -sS -L -b "$tmp_cookie" \
  -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
  --data-urlencode "_csrf=$csrf" \
  --data-urlencode "paramInvcNo=$invoice" \
  "https://www.cjlogistics.com/ko/tool/parcel/tracking-detail" \
  -o "$tmp_json"

python3 - <<'PY' "$tmp_json"
import json
import sys

payload = json.load(open(sys.argv[1], encoding="utf-8"))
events = payload["parcelDetailResultMap"]["resultList"]
if not events:
    raise SystemExit("查無結果。")

status_map = {
    "11": "商品收件",
    "21": "商品運送中",
    "41": "商品運送中",
    "42": "抵達配送站",
    "44": "商品運送中",
    "82": "配送出發",
    "91": "配送完成",
}

latest = events[-1]
normalized_events = [
    {
        "timestamp": event.get("dTime"),
        "location": event.get("regBranNm"),
        "status_code": event.get("crgSt"),
        "status": status_map.get(event.get("crgSt"), event.get("scanNm") or "未知"),
    }
    for event in events
]
print(json.dumps({
    "carrier": "cj",
    "invoice": payload["parcelDetailResultMap"]["paramInvcNo"],
    "status_code": latest.get("crgSt"),
    "status": status_map.get(latest.get("crgSt"), latest.get("scanNm") or "未知"),
    "timestamp": latest.get("dTime"),
    "location": latest.get("regBranNm"),
    "event_count": len(events),
    "recent_events": normalized_events[-min(3, len(normalized_events)):],
}, ensure_ascii=False, indent=2))
PY

rm -f "$tmp_body" "$tmp_cookie" "$tmp_json"
CJ 公開輸出範例

以下數值為 2026-03-27 實際 smoke test(1234567890)驗證的正規化結果。

{
  "carrier": "cj",
  "invoice": "1234567890",
  "status_code": "91",
  "status": "配送完成",
  "timestamp": "2026-03-21 12:22:13",
  "location": "京畿廣州五浦",
  "event_count": 3,
  "recent_events": [
    {
      "timestamp": "2026-03-10 03:01:45",
      "location": "清原HUB",
      "status_code": "44",
      "status": "商品運送中"
    },
    {
      "timestamp": "2026-03-21 10:53:19",
      "location": "京畿廣州五浦",
      "status_code": "82",
      "status": "配送出發"
    },
    {
      "timestamp": "2026-03-21 12:22:13",
      "location": "京畿廣州五浦",
      "status_code": "91",
      "status": "配送完成"
    }
  ]
}

額外 smoke test 也可使用 000000000000

CJ 回應中,即使 parcelResultMap.resultList 為空,parcelDetailResultMap.resultList 仍可能包含事件,因此優先查看詳細事件陣列。公開範例已根據通用結果結構(carrierinvoicestatustimestamplocationevent_countrecent_events、選擇性 status_code)保留非識別欄位,不直接顯示可能包含承辦人姓名或聯絡資訊的 crgNm 原始文字。

2. 郵局:官方 HTML 流程

郵局的官方入口頁面會再將 sid1 POST 到 trace.RetrieveDomRigiTraceList.comm

  • 入口頁面:https://service.epost.go.kr/trace.RetrieveRegiPrclDeliv.postal?sid1=
  • 實際查詢 endpoint:https://service.epost.go.kr/trace.RetrieveDomRigiTraceList.comm
  • 必填欄位:sid1

郵局使用 curl --http1.1 --tls-max 1.2 比本機 Python HTTP client 更穩定,因此以此組合為基本範例。

tmp_html="$(mktemp)"
python3 - <<'PY' "$tmp_html"
import html
import json
import re
import subprocess
import sys

invoice = "1234567890123"  # 官方頁面佔位符性質的 smoke-test 值
output_path = sys.argv[1]

cmd = [
    "curl",
    "--http1.1",
    "--tls-max",
    "1.2",
    "--silent",
    "--show-error",
    "--location",
    "--retry",
    "3",
    "--retry-all-errors",
    "--retry-delay",
    "1",
    "--max-time",
    "30",
    "-o",
    output_path,
    "-d",
    f"sid1={invoice}",
    "https://service.epost.go.kr/trace.RetrieveDomRigiTraceList.comm",
]
subprocess.run(cmd, check=True)

page = open(output_path, encoding="utf-8", errors="ignore").read()

summary = re.search(
    r"<th scope=\"row\">(?P<tracking>[^<]+)</th>.*?"
    r"<td>(?P<sender>.*?)</td>.*?"
    r"<td>(?P<receiver>.*?)</td>.*?"
    r"<td>(?P<delivered_to>.*?)</td>.*?"
    r"<td>(?P<kind>.*?)</td>.*?"
    r"<td>(?P<result>.*?)</td>",
    page,
    re.S,
)
if not summary:
    raise SystemExit("找不到基本資訊表格。")

def clean(raw: str) -> str:
    text = re.sub(r"<[^>]+>", " ", raw)
    return " ".join(html.unescape(text).split())

def clean_location(raw: str) -> str:
    text = clean(raw)
    return re.sub(r"\s*(TEL\s*:?\s*)?\d{2,4}[.\-]\d{3,4}[.\-]\d{4}", "", text).strip()

events = re.findall(
    r"<tr>\s*<td>(\d{4}\.\d{2}\.\d{2})</td>\s*"
    r"<td>(\d{2}:\d{2})</td>\s*"
    r"<td>(.*?)</td>\s*"
    r"<td>\s*<span class=\"evtnm\">(.*?)</span>(.*?)</td>\s*</tr>",
    page,
    re.S,
)

normalized_events = [
    {
        "timestamp": f"{day} {time_}",
        "location": clean_location(location),
        "status": clean(status),
    }
    for day, time_, location, status, _detail in events
]

latest_event = normalized_events[-1] if normalized_events else None

print(json.dumps({
    "carrier": "epost",
    "invoice": clean(summary.group("tracking")),
    "status": clean(summary.group("result")),
    "timestamp": latest_event["timestamp"] if latest_event else None,
    "location": latest_event["location"] if latest_event else None,
    "event_count": len(normalized_events),
    "recent_events": normalized_events[-min(3, len(normalized_events)):],
}, ensure_ascii=False, indent=2))
PY
rm -f "$tmp_html"
郵局公開輸出範例

以下數值為 2026-03-27 實際 smoke test(1234567890123)驗證的正規化結果。

{
  "carrier": "epost",
  "invoice": "1234567890123",
  "status": "配送完成",
  "timestamp": "2025.12.04 15:13",
  "location": "濟州郵件處理中心",
  "event_count": 2,
  "recent_events": [
    {
      "timestamp": "2025.12.04 15:13",
      "location": "濟州郵件處理中心",
      "status": "配送準備"
    },
    {
      "timestamp": "2025.12.04 15:13",
      "location": "濟州郵件處理中心",
      "status": "配送完成"
    }
  ]
}

郵局基本資訊表格依序為 등기번호보내는 분/접수일자받는 분수령인/배달일자취급구분배달결과,詳細事件則從 processTable 下的 날짜 / 시간 / 발생국 / 처리현황 行讀取。公開範例已根據與 CJ 相同的通用結果結構(carrierinvoicestatustimestamplocationevent_countrecent_events)保留配送狀態所需的值,並移除事件地點中可能夾雜的 TEL 號碼片段,不直接顯示收件人/詳細備註原始文字。

3. 為人類正規化

不要直接貼上原始回應,請依下列通用結果結構摘要。

通用結果結構
  • carrier:貨運業者識別碼(cjepost
  • invoice:正規化後的託運單號
  • status:目前配送狀態
  • timestamp:最後事件時間
  • location:最後事件地點
  • event_count:事件總數
  • recent_events:最近最多 3 個事件列表
  • status_code:僅在需要時保留的原始狀態碼(目前僅用於 CJ 範例)

4. 重試與降級策略

  • 若位數錯誤則立即停止,並要求重新輸入正確格式。
  • CJ 在重新取得 _csrf 後再嘗試一次。
  • 郵局維持 curl --retry 3 --retry-all-errors --retry-delay 1
  • 不繞道其他貨運業者。

完成條件

  • 貨運業者與託運單號已正確識別
  • 目前狀態與最近事件已整理完畢
  • 能說明使用了哪個官方查詢管道
  • 保留擴充其他貨運業者時需新增的 carrier adapter 欄位

失敗模式

  • CJ:_csrf 提取失敗或 tracking-detail 回應結構變更
  • CJ:託運單號長度非 10 位或 12 位
  • 郵局:sid1 非 13 位
  • 郵局:HTML 標記變更導致表格提取規則失效
  • 郵局:使用非 curl 客戶端時發生 timeout/reset

備註

  • 此為查詢型技能。
  • 基本查詢管道僅使用官方貨運業者 endpoint。
  • 新增其他貨運業者時,以相同格式新增一個 carrier adapter 即可擴充。