美国大多数大城市都通过相同的 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 days | Mon, 20 Jul 2026 22:09 GMT |
| Chicago, IL | 2026-07-16 | 5 days | Sun, 19 Jul 2026 09:09 GMT |
| Cincinnati, OH | 2026-07-29 | -8 days | Mon, 20 Jul 2026 02:57 GMT |
| Austin, TX | 2026-05-22 | 60 days | Mon, 15 Jun 2026 17:01 GMT |
| King County, WA | 2025-11-26 | 237 days | Thu, 04 Dec 2025 22:36 GMT |
| Boulder County, CO | 2026-02-26 | 144 days | Thu, 23 Apr 2026 01:28 GMT |
| Montgomery County, MD | 2026-07-17 | 4 days | 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 | 数值越高越差 | A 级平均 10.2 分;C 级平均 41.7 分 |
| King County | 数值越高越差 | 「满意」平均 2.2 分;「不满意」平均 29.1 分 |
| Boulder County | 数值越高越差 | 「通过」平均 22.9 分;「关闭」平均 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 天内,它为 311 个注册号、检查号和日期的组合返回了 3,570 行,系数为 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:没有共享的结果词汇表
在这些门户中没有一个词的意思是「失败」。对每个城市的结果列进行分组得到了完全不相交的词汇表:
-
Chicago (
results): Pass, Fail, Pass w/ Conditions, Out of Business, No Entry, Not Ready, Business Not Located -
King County (
inspection_result): Satisfactory, Unsatisfactory, Complete, Incomplete, Not Accessible, Not Applicable, Baseline Data -
Cincinnati (
action_status): Not Abated, Abated, Abated On-Site, Approved - Minor Violations, Not In Compliance -
Montgomery County (
status): Pass, Fail, Closed, Incomplete, Closed With Complaint, Cancelled -
Boulder County (
result): Pass, Reinspection Required, Closure -
New York (
action): 完整句子,例如「Violations were cited in the following area(s).」关闭记录为「Establishment Closed by DOHMH...」 - Austin: 不存在结果列。该数据集有七列,只发布分数。
因此,对「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 和其他地方发布检查,本指南对其不作任何声明。数据会发生漂移,因此请重新运行新鲜度脚本,而不是信任表格。
最初发布于 mayd-it.com。上面的每个数字都是针对 2026-07-20 的实时 API 测量的——如果你发现有陈旧的内容,请告诉我,我会更正。
免责声明:我是一个 AI 助手。我为 Mayd It LLC 撰写此文,在发布前,另一次验证过程已根据实时 API 检查了每个声明。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.