透過 CJ 大韓通運與郵局官方端點,以託運單號查詢包裹狀態,並將工作流程結構化為可擴充的貨運業者轉接器。
配送追蹤
這個技能做什麼
使用 CJ 大韓通運與郵局官方查詢頁面,透過託運單號查詢目前配送狀態。
- CJ 大韓通運:使用官方配送查詢頁面提供的 JSON endpoint
- 郵局:使用官方配送查詢頁面使用的 HTML endpoint
- 結果以通用格式(貨運業者 / 託運單號 / 目前狀態 / 最近事件)簡短整理
使用時機
- 「幫我查 CJ 大韓通運的託運單」
- 「郵局包裹現在到哪了」
- 「幫我確認這個託運單號是否配送完成」
- 「幫我把各貨運業者的查詢邏輯整理好,方便以後擴充」
不使用時機
- 只有訂單號碼而沒有託運單號時
- 需要直接進行包裹預約或退貨申請時
- 想繞過官方管道使用非官方整合查詢服務時
前置需求
- 網路連線
python3curl- 選擇性:
jq
輸入
- 貨運業者識別碼:
cj或epost - 託運單號
- CJ 大韓通運:10 位或 12 位數字
- 郵局:13 位數字
貨運業者轉接器規則
此技能將各貨運業者的邏輯以 carrier adapter 為單位拆分。
新增貨運業者時,需先定義下列欄位:
carrier id:例如cj、epostvalidator:託運單號的位數/格式entrypoint:官方查詢入口 URLtransport:使用 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. 先正規化輸入
- 將貨運業者名稱正規化為
cj或epost。 - 移除託運單號中的空格與
-。 - 若位數驗證先失敗,則不發送查詢。
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 - 必填欄位:
_csrf、paramInvcNo
基本範例使用 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 仍可能包含事件,因此優先查看詳細事件陣列。公開範例已根據通用結果結構(carrier、invoice、status、timestamp、location、event_count、recent_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 相同的通用結果結構(carrier、invoice、status、timestamp、location、event_count、recent_events)保留配送狀態所需的值,並移除事件地點中可能夾雜的 TEL 號碼片段,不直接顯示收件人/詳細備註原始文字。
3. 為人類正規化
不要直接貼上原始回應,請依下列通用結果結構摘要。
通用結果結構
carrier:貨運業者識別碼(cj或epost)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 即可擴充。






