包包baobaolin.com
date
entry
028
topic
integration
rev
—

查公司登記的開放資料 API:$filter 會被 shell 吃掉,$format 錯了還回 200

要用程式查公司登記,該走的是官方的開放資料 API,不是有阻擋與驗證碼的查詢頁。但這支 API 把 $format 錯誤也放在 HTTP 200 裡,而它的參數名放進雙引號就會被 shell 展開。

要用程式查台灣公司的登記資料,我的第一個念頭是去抓經濟部商工登記公示資料查詢服務(findbiz)的頁面。那條路這次走不通,我也不打算硬走。同樣的基本登記資料,商工行政資料開放平台有一支請求裡不必帶金鑰的 API,回 JSON。

但這支 API 有三個地方會讓腳本在失敗時看起來像成功:查詢參數的 $ 會被 shell 吃掉,$format 錯了回 HTTP 200,查無資料也回 200 而且 body 是空的。這篇依序回答四件事:為什麼不抓查詢頁、正確的請求長什麼樣、三個陷阱各自的樣子、我現在怎麼驗收回應。

為什麼不抓查詢頁

2026-10-12 用 curl 直接打查詢頁:

findbiz.nat.gov.tw/fts/company/88187108    403  Attention Required! | Cloudflare
fbfh.trade.gov.tw/fb/web/queryBasicf.do    200  頁面含「驗證碼」欄位(verifyCode)

這次的 curl 請求,findbiz 回的是 Cloudflare 的阻擋頁。國際貿易署的出進口廠商查詢頁可以打開,但表單要填驗證碼。我把這兩者讀成「這些頁面是給人在瀏覽器裡用的」,所以不繞過它們。需要批次查詢的時候,該找的是官方給程式用的入口。

正確的請求

商工行政資料開放平台的「公司登記基本資料-應用一」API,用 OData(一種以網址參數描述查詢的 REST 慣例)風格的查詢參數:$format、$filter、$skip、$top。以上市公司春雨工廠(統一編號 88187108)為例,加上 -w 把狀態碼、Content-Type 與長度一起印出來:

API=https://data.gcis.nat.gov.tw/od/data/api/5F64D864-61CB-4D0D-8AD9-492047CC1EA6
curl -sS -o ok.json -w 'http=%{http_code} type=%{content_type} bytes=%{size_download}\n' \
  "$API"'?$format=json&$filter=Business_Accounting_NO%20eq%2088187108&$skip=0&$top=50'
http=200 type=application/json;charset=UTF-8 bytes=546

ok.json 的內容(節錄,省略部分欄位):

[{"Business_Accounting_NO":"88187108","Company_Status_Desc":"核准設立",
  "Company_Name":"春雨工廠股份有限公司","Capital_Stock_Amount":3920696000,
  "Paid_In_Capital_Amount":3021627000,"Responsible_Name":"翁重鈞",
  "Company_Setup_Date":"0540327", ...}]

日期是民國年三碼加月日各兩碼,0540327 是民國 54 年 3 月 27 日,加 1911 得 1965 年。注意查詢字串用的是單引號,下一節就是原因。

「不必帶金鑰」不等於「不必打招呼」。平台的介接說明要求使用系統介接 API 的單位或個人填寫使用告知書,並寫明「本平臺系統介接API係透過來源IP進行白名單管控」,另有同時連線數與每日介接次數的上限。我這幾次零星的測試沒有填告知書也拿到了回應,至於未登記的 IP 能用到什麼程度,我不知道。要正式批次跑,先填告知書。

陷阱一:雙引號裡的 $format 是 shell 變數

把整個網址放進雙引號是很自然的寫法:

$ echo "$API?$format=json&$filter=Business_Accounting_NO%20eq%2088187108&$skip=0&$top=50"
https://data.gcis.nat.gov.tw/od/data/api/5F64D864-...?=json&=Business_Accounting_NO%20eq%2088187108&=0&=50

$format、$filter、$skip、$top 都被當成 shell 變數展開。這幾個變數沒有設定、shell 也沒開 set -u 時,它們就是空字串(開了 set -u 會直接報 unbound variable,反而比較好)。用同一個雙引號網址實際送出,伺服器的回應:

http=200 type=text/html;charset=UTF-8 bytes=43
$format參數有誤,請查明後繼續。

這句錯誤訊息和平台介接說明列出的錯誤訊息一字不差。

陷阱二:$format 錯了,狀態碼還是 200

上面那個錯誤是 HTTP 200,curl 的結束碼是 0,Content-Type 從 application/json 變成 text/html,body 是一行中文。只檢查 curl -f 或狀態碼的腳本會把它當成成功。完全不送 $format,我得到的也是同一個 200 加同一句話。其他參數錯誤、限流或維護時回什麼,我沒有測到。

陷阱三:查無資料是 0 個位元組,不是 []

把統一編號換成 00000000 測試:

http=200 type=application/json;charset=UTF-8 bytes=0

Content-Type 說是 JSON,body 卻是空的,連 [] 都沒有。三種常見的解析方式各自的反應:

$ printf '' | jq '.[0].Company_Name'; echo "exit=$?"
exit=0                                          # 沒有輸出,也沒有錯誤
$ python3 -c 'import json; json.loads("")'
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
$ node -e 'JSON.parse("")'
SyntaxError: Unexpected end of JSON input

jq 安靜地什麼都不印、結束碼 0;Python 與 Node 丟出的是「輸入不是合法 JSON」,讀的人很容易往伺服器出錯的方向想。三者都沒有說出這次測試裡它真正的意思:這個統一編號查不到。這是我實測到的行為,平台文件裡我沒有找到「查無資料回空 body」的說明。

順帶一提,網址裡直接留空白(Business_Accounting_NO eq 88187108)的話,curl 8.7.1 會直接拒絕:curl: (3) URL rejected: Malformed input to a URL function。這個至少會報錯。

我現在的寫法

參數交給 curl 編碼,參數名保留字面的 $,回應一層一層檢查:curl 結束碼、HTTP 狀態、媒體型別、是否為空、JSON 結構與統編是否相符。

ubn=88187108
meta=$(curl -sS -G "$API" -o body.json -w '%{http_code} %{content_type}' \
  --data-urlencode '$format=json' \
  --data-urlencode "\$filter=Business_Accounting_NO eq $ubn" \
  --data-urlencode '$skip=0' --data-urlencode '$top=50') || { echo "curl 失敗" >&2; exit 1; }
code=${meta%% *}; type=${meta#* }; type=${type%%;*}
[ "$code" = 200 ] || { echo "HTTP ${code}" >&2; exit 1; }
[ "$type" = application/json ] || { echo "不是 JSON(${type}): $(head -c 200 body.json)" >&2; exit 1; }
[ -s body.json ] || { echo "查無此統編: ${ubn}" >&2; exit 2; }
jq -s -e --arg u "$ubn" 'length == 1 and (.[0] | type == "array" and length == 1 and .[0].Business_Accounting_NO == $u)' body.json >/dev/null \
  || { echo "回應結構不符預期: $(head -c 200 body.json)" >&2; exit 1; }
jq -r '.[0].Company_Name' body.json

在 bash 與 zsh 各跑三種情況:88187108 印出公司名稱、結束碼 0;00000000 印出「查無此統編」、結束碼 2;把 $format 故意改錯,印出 不是 JSON(text/html): $format參數有誤…、結束碼 1。

-G 讓 --data-urlencode 的內容接在網址後面,空白會被編成 +,實際送出的網址是 ?$format=json&$filter=Business_Accounting_NO+eq+88187108&$skip=0&$top=50,這支 API 回了同樣 546 位元組的 JSON。$filter 那行因為要帶入 $ubn 而用雙引號,所以參數名的 $ 要跳脫成 \$。

錯誤訊息裡的 ${type} 有大括號是有原因的。第一版我寫成 ($type),在這台 Mac 的 /bin/bash(3.2.57,locale 是 zh_TW.UTF-8)跑出來是 (��: ...:Content-Type 不見了,還多了亂碼。全形括號的第一個 UTF-8 位元組被吞進了變數名,bash 去找一個不存在的變數;LC_ALL=C 或交給 zsh 跑,同一行都正常。這和 023 撞到的是同一類問題,當時開著 set -u 所以直接爆掉,這次沒開,就變成安靜的亂碼。我是在故意送錯參數、測這條失敗路徑時才看到的。

這和我在部署驗收寫過的是同一件事:HTTP 200 只代表 HTTP 這一層成功,不代表查詢成功。這支 API 在我的測試裡把「$format 錯誤」「查無資料」「查到了」三種結果都放在 200 裡,要分辨它們得往下看 Content-Type、body 與 JSON 的內容。

如果只記得一件事

OData 參數名的 $ 在 shell 裡要保留字面:整段用單引號,或在雙引號裡寫成 \$。回到開頭:不抓查詢頁、改用官方 API 是對的方向,但換了入口之後,判斷成功與否的責任就落在自己的腳本上。

修訂紀錄

  1. 首次發布