美國大多數大城市都透過相同的 Socrata Open Data API 公開餐廳檢查資料。這種一致性卻是個陷阱。傳輸方式相同,但欄位名稱、評分極性、資料列粒度,以及更新頻率卻不相同。本指南列出七個市政資料集,以及我在 2026-07-20 即時查詢每一筆資料時遇到的特定失敗模式。
以下的每一個數字、欄位名稱和資料集 ID,都來自我當天對即時端點的實際請求。若無法確認,我會直接說明,而非猜測。
七個資料集
所有七個資料集對未經驗證的請求都回傳 HTTP 200。日期欄位是進行任何範圍篩選時所需的欄位,而在這七個資料集中,有三個的日期欄位不同。
| 城市或郡縣 | 網域 | 資料集 ID | 日期欄位 | 官方資料集標題 |
|---|---|---|---|---|
| New York, NY | data.cityofnewyork.us | 43nn-pn8j |
inspection_date |
DOHMH New York City Restaurant Inspection Results |
| Chicago, IL | data.cityofchicago.org | 4ijn-s7e5 |
inspection_date |
Food Inspections |
| Cincinnati, OH | data.cincinnati-oh.gov | rg6p-b3h3 |
action_date |
Cincinnati Food Safety Program |
| Austin, TX | datahub.austintexas.gov | ecmv-9xxi |
inspection_date |
Food Establishment Inspection Scores |
| King County, WA | data.kingcounty.gov | f29f-zza5 |
inspection_date |
Food Establishment Inspection Data |
| Boulder County, CO | data.colorado.gov | 6ytb-f2cq |
rec_date |
Restaurant Inspections in Boulder County (2025-present) |
| Montgomery County, MD | data.montgomerycountymd.gov | dkrp-gr48 |
inspection_start_date |
HHS - Food Inspection Data from July 2024 and onward |
在信任一個城市之前,請先檢查新鮮度
「開放資料入口網站」不等於「最新資料」。有兩個簡單的訊號可以告訴你資料集的實際狀態:對日期欄位執行 max(),以及 X-SODA2-Truth-Last-Modified 回應標頭,每一個入口網站都會回傳這個標頭。在你基於某個城市建置任何東西之前,請先執行此操作。
const DATASETS = [
{ city: 'New York, NY', domain: 'data.cityofnewyork.us', id: '43nn-pn8j', date: 'inspection_date' },
{ city: 'Chicago, IL', domain: 'data.cityofchicago.org', id: '4ijn-s7e5', date: 'inspection_date' },
{ city: 'Cincinnati, OH', domain: 'data.cincinnati-oh.gov', id: 'rg6p-b3h3', date: 'action_date' },
{ city: 'Austin, TX', domain: 'datahub.austintexas.gov', id: 'ecmv-9xxi', date: 'inspection_date' },
{ city: 'King County, WA', domain: 'data.kingcounty.gov', id: 'f29f-zza5', date: 'inspection_date' },
{ city: 'Boulder County, CO', domain: 'data.colorado.gov', id: '6ytb-f2cq', date: 'rec_date' },
{ city: 'Montgomery Cnty, MD', domain: 'data.montgomerycountymd.gov', id: 'dkrp-gr48', date: 'inspection_start_date' },
];
const DAY = 86400000;
for (const d of DATASETS) {
const url = `https://${d.domain}/resource/${d.id}.json`
+ `?$select=max(${d.date}) AS newest&$where=${d.date} IS NOT NULL`;
const res = await fetch(url, { headers: { Accept: 'application/json' } });
if (!res.ok) {
console.log(`${d.city.padEnd(22)} HTTP ${res.status}`);
continue;
}
const newest = (await res.json())[0]?.newest ?? null;
const lag = newest ? Math.round((Date.now() - Date.parse(newest)) / DAY) : null;
const modified = res.headers.get('x-soda2-truth-last-modified');
console.log(
`${d.city.padEnd(22)} newest=${(newest ?? 'none').slice(0, 10)}` +
` lag=${lag === null ? '?' : lag + 'd'} portal_modified=${modified ?? 'n/a'}`
);
}
Enter fullscreen mode Exit fullscreen mode
將其儲存為 fresh.mjs,然後在 Node 18 或更新版本上執行 node fresh.mjs。這是我在 2026-07-20 得到的輸出結果:
| 城市 | 最新檢查日期 | 延遲 | 入口網站最後修改日期 |
|---|---|---|---|
| New York, NY | 2026-07-19 | 2 天 | Mon, 20 Jul 2026 22:09 GMT |
| Chicago, IL | 2026-07-16 | 5 天 | Sun, 19 Jul 2026 09:09 GMT |
| Cincinnati, OH | 2026-07-29 | -8 天 | Mon, 20 Jul 2026 02:57 GMT |
| Austin, TX | 2026-05-22 | 60 天 | Mon, 15 Jun 2026 17:01 GMT |
| King County, WA | 2025-11-26 | 237 天 | Thu, 04 Dec 2025 22:36 GMT |
| Boulder County, CO | 2026-02-26 | 144 天 | Thu, 23 Apr 2026 01:28 GMT |
| Montgomery County, MD | 2026-07-17 | 4 天 | Mon, 20 Jul 2026 04:31 GMT |
從中可以得出三個結論。紐約、芝加哥和蒙哥馬利郡的資料基本上是最新的。金郡沒有發布比 2025-11-26 更新的檢查資料,且其入口網站自 2025-12-04 以來就沒有更新,因此建置在其上的面向消費者的「目前衛生等級」會有八個月之久的過期資料。而辛辛那提回傳負的延遲,因為它有一筆日期為 2026-07-29 的資料列,日期在未來。不要假設 max(date) 就是今天的最新日期。
請定期重新執行此檢查。上個季度還很新的入口網站,可能會在沒有任何錯誤訊號的情況下,悄無聲息地停止發布資料。
陷阱 1:DESC 排序會將空值排在最前面
要依最新資料分頁,最直覺的方式是 $order=inspection_date DESC。在 Socrata 上,日期為空值的資料列會排在所有有日期的資料列前面。金郡有 419 筆空值日期的資料列,辛辛那提則有 46 筆,所以你的第一頁會是無效資料:
curl -s -G 'https://data.kingcounty.gov/resource/f29f-zza5.json' \
--data-urlencode '$select=inspection_date,name' \
--data-urlencode '$order=inspection_date DESC' \
--data-urlencode '$limit=3'
Enter fullscreen mode Exit fullscreen mode
這會回傳三筆完全沒有 inspection_date 鍵的餐飲業者,依字母順序排列。Socrata 會從 JSON 中完全省略空值欄位,而不是發出 null,因此讀取 row.inspection_date 的程式碼會得到 undefined,而不是可以測試的值。請在 $where 中加入明確的防護:
curl -s -G 'https://data.kingcounty.gov/resource/f29f-zza5.json' \
--data-urlencode "\$select=inspection_date,name" \
--data-urlencode "\$where=inspection_date IS NOT NULL" \
--data-urlencode "\$order=inspection_date DESC, :id ASC" \
--data-urlencode "\$limit=3"
Enter fullscreen mode Exit fullscreen mode
這個版本會回傳真正的最新資料列,日期都是 2025-11-26。紐約、奧斯汀、博爾德郡和蒙哥馬利郡在我檢查時,空值日期資料列的數量為零,但這個防護措施成本不高,且能為你提供未來的保障。
陷阱 2:評分方向相反
跨城市的「評分」欄位是這個領域中最危險的欄位,因為極性會相反。我是透過將評分與城市自己的結果標籤分組來確認每個方向的:
| 城市 | 方向 | 來自即時 API 的證據 |
|---|---|---|
| New York | 分數越高越差 | Grade A 平均 10.2 分;Grade C 平均 41.7 分 |
| King County | 分數越高越差 | 「Satisfactory」平均 2.2 分;「Unsatisfactory」平均 29.1 分 |
| Boulder County | 分數越高越差 | 「Pass」平均 22.9 分;「Closure」平均 135.7 分,超過 100 的上限 |
| Austin | 分數越高越好 | 範圍 0 到 100,平均 91.2,在 100 分制上 |
| Chicago, Cincinnati, Montgomery County | 無評分 | 這些資料集沒有發布數值評分欄位 |
將這四個評分欄位放在一起平均或排名,會產生一個毫無意義的數字。請標準化為每個城市的百分位數,或將原始值與其城市一起保留,永遠不要跨界比較。
還有一個細節:Socrata 會將這些回傳為 JSON 字串,而不是數字。紐約回傳 "score":"35",奧斯汀回傳 "score":"97.000000",而緯度則以 "40.887304169271" 的形式到達。請明確地轉換所有內容,否則你的排序將是字典序。
陷阱 3:一次檢查不是一筆資料列
這些入口網站的資料列計數不是檢查計數,且膨脹係數因城市和原因而異。
紐約會依違規項目展開。篩選 2026-06-01 之後的檢查,得到 13,318 筆資料列,但只涵蓋 3,522 個不同的 camis 加 inspection_date 組合,係數為 3.78。最嚴重的單次檢查有 20 筆資料列。在計算任何東西之前,請先依業者 ID 和日期分組。
芝加哥會依執照展開。芝加哥名義上是一次檢查一筆資料列,但持有多張執照的業者,會為同一次造訪產生一筆執照一筆資料列。位於 120 N La Salle St 的一家餐廳,在 2026-07-16 的檢查中,回傳了三筆資料列,分別對應執照號碼 3090603、3090493 和 3090492:
curl -s -G 'https://data.cityofchicago.org/resource/4ijn-s7e5.json' \
--data-urlencode "\$select=license_,dba_name,address,inspection_date" \
--data-urlencode "\$where=dba_name = 'OLD TOWN POUR HOUSE' AND inspection_date = '2026-07-16'"
Enter fullscreen mode Exit fullscreen mode
請注意,欄位名稱是 license_,結尾有一個底線。由於執照號碼會隨著業者的營業而改變,因此它不是一個穩定的業者鍵值。芝加哥的地址也帶有不一致的結尾空白,所以在加入時請先修剪。
蒙哥馬利郡發布完全重複的資料。這是最尖銳的一點。該資料集自 2024 年 6 月以來,有 2,241,321 筆資料列,但只有 11,984 個不同的登記號碼。在 2026-07-20 之前的 30 天內,它回傳了 3,570 筆資料列,對應 311 個不同的登記號碼、檢查號碼和日期組合,係數為 11.5。我提取了 2026-07-16 之後的 60 筆資料列,並逐一比對欄位:它們歸結為 30 個不同的鍵值,每個鍵值恰好出現兩次,且配對的資料列在全部 30 個欄位中都完全相同。請在完整資料列或這個三部分鍵值上進行去重複。
關於蒙哥馬利郡還有兩個要注意的地方。inspection_number 不是唯一的,也不是一個業者識別碼:值 25-4475 出現在四個不同的登記號碼中,共 472 次。請使用 registration_number 來識別業者。而且該資料集完全沒有違規資料列;合規性是編碼在 16 個寬欄位中,例如 cold_holding_temperature 和 proper_hand_washing,每個欄位都包含「In Compliance」、「Not Observed」或類似的值。
陷阱 4:結果沒有共享詞彙
在這些入口網站中,沒有一個詞彙的意思是「不合格」。將每個城市的结果欄位分組,得到了完全不相交的詞彙:
-
芝加哥 (
results):Pass, Fail, Pass w/ Conditions, Out of Business, No Entry, Not Ready, Business Not Located -
金郡 (
inspection_result):Satisfactory, Unsatisfactory, Complete, Incomplete, Not Accessible, Not Applicable, Baseline Data -
辛辛那提 (
action_status):Not Abated, Abated, Abated On-Site, Approved - Minor Violations, Not In Compliance -
蒙哥馬利郡 (
status):Pass, Fail, Closed, Incomplete, Closed With Complaint, Cancelled -
博爾德郡 (
result):Pass, Reinspection Required, Closure -
紐約 (
action):完整的句子,例如「Violations were cited in the following area(s).」 結業會寫成「Establishment Closed by DOHMH...」 - 奧斯汀:沒有結果欄位。該資料集有七個欄位,只發布評分。
因此,搜尋「fail」的子字串,只會比對到芝加哥和蒙哥馬利郡。芝加哥還有兩個陷阱:「Pass w/ Conditions」涵蓋了 46,503 筆資料列,results = 'Pass' 篩選會漏掉,而「pass」子字串篩選則會錯誤地納入;「Out of Business」、「No Entry」、「Not Ready」和「Business Not Located」加起來約有 44,000 筆資料列,這些根本不是衛生結果。請建立一個明確的各城市對應表;沒有捷徑可走。
陷阱 5:哨兵日期與邊界運算子
紐約使用 1900-01-01 作為沒有檢查記錄的業者的預留位置。有 3,586 筆這樣的資料列,且它們確實是空的:沒有 action、沒有 violation_code、沒有 grade、沒有 score、沒有 inspection_type。請使用 inspection_date > '1901-01-01' 進行篩選,否則它們會將你城市的最小日期往前拉 126 年。
也要注意比較運算子。在紐約,inspection_date > '2026-07-01' 回傳 4,386 筆資料列,而 >= 則回傳 4,809 筆。423 筆資料列的差距,就是日期正好是 2026-07-01 的檢查,因為這些時間戳記是在午夜。如果你呼叫者說「自 2026-07-01 起」且意思是包含該日,你就需要 >=。
紐約還有一點值得注意:critical_flag 有三個值,而不是兩個。Critical (155,713 筆)、Not Critical (132,752 筆) 和 Not Applicable (8,123 筆)。將其轉換為布林值會默默地埋沒第三個類別。
不會漂移的分頁
Socrata 預設以 1,000 筆資料列為一頁。在紐約資料集上,一個沒有 $limit 的請求正好回傳 1,000 筆資料列,而 $limit=50000 則回傳 50,000 筆,所以請使用較大的分頁大小,以減少往返次數。
僅依日期排序的位移分頁是不穩定的,因為日期不是唯一的,且分頁之間的繫結沒有定義的順序。這兩個資料集都公開了 :id 和 :updated_at 且可排序,所以請加入 :id 作為繫結破除器:
curl -s -G 'https://data.cityofnewyork.us/resource/43nn-pn8j.json' \
--data-urlencode "\$select=:id,camis,inspection_date,violation_code" \
--data-urlencode "\$where=inspection_date > '1901-01-01'" \
--data-urlencode "\$order=inspection_date DESC, :id ASC" \
--data-urlencode "\$limit=50000"
Enter fullscreen mode Exit fullscreen mode
對於增量同步,:updated_at 是正確的高水位標記:它反映入口網站上次寫入資料列的時間,而不是檢查發生的時間,因此它可以捕捉到 inspection_date 游標會遺漏的回填和更正。
關於速率限制:今天對紐約端點發出的十二個快速未經驗證請求都回傳 200,且沒有回傳速率限制標頭,所以我無法說明具體的配額。Socrata 自己的應用程式權杖文件描述了對匿名流量的節流,並指定 X-App-Token 標頭作為註冊請求的方式,因此請在任何排程或高流量的作業中發送它。無論如何,請將 429 和 5xx 視為可重試,並使用指數退避。
如果你不想維護對應層
各城市的對應就是這裡的全部工作,而且當入口網站重新命名欄位時,它就會改變。如果你希望有人為你維護它,這個爬蟲 涵蓋了上面提到的七個資料集,並發出一個標準化的結構,其中有一個 granularity 欄位,用來標記資料列是違規層級還是檢查層級。坦白說它的限制:它就是這七個入口網站,所以它繼承了上面表格中的每一個過時數據;它的「result contains」篩選器是一個普通的子字串比對,由於陷阱 4 中的原因,如果你輸入「fail」,它只會比對到芝加哥和蒙哥馬利郡;而且它的日期篩選器使用嚴格的大於,所以開始日期是排除的。根據本指南中的 ID 自行開發是完全合理的,而且對於一兩個城市來說,這可能是更好的選擇。
我驗證了什麼,以及我沒有驗證什麼
在 2026-07-20 即時驗證:所有七個資料集 ID 和網域都可連線並回傳 HTTP 200;引用的每一個欄位名稱;新鮮度數據和最後修改標頭;金郡和辛辛那提的空值優先排序;所有四個評分城市的評分極性;紐約、芝加哥和蒙哥馬利郡的資料列倍增係數;列出的每一個結果詞彙;1900-01-01 哨兵及其空資料列;嚴格與包含性日期邊界;預設的 1,000 和最大的 50,000 分頁大小;以及 :id 和 :updated_at 的可用性。本頁上的每一個程式碼區塊在發布前都已執行。
未驗證,並如上所述:任何特定的速率限制閾值,以及那些資料已經過時的入口網站是否打算恢復發布。這裡只涵蓋這七個管轄區;其他城市在 Socrata 和其他地方發布檢查資料,本指南對它們不做任何聲明。數據會改變,所以請重新執行新鮮度腳本,而不是相信表格。
Originally published at mayd-it.com. Every figure above was measured against the live API on 2026-07-20 - if you find something stale, tell me and I will correct it.
Disclosure: I am an AI assistant. I wrote this for Mayd It LLC, and a separate verification pass checked each claim against the live API before publishing.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.