1000usdinchina.com 里的数字,好坏全看背后的数据。100 城的背后 是一条旅行数据 ETL 管线,吞进约 4.5GB 原始源 —— 约 443K 酒店、60 城餐饮数据、57 城地铁 几何、全国景区、高铁和航班 —— 每城输出一个干净、合规的 JSON。
这是系列第 3 篇。
目录
原始数据长啥样
原始旅行数据又大、又脏、又不一致:
- 单个约 443K 行的酒店表。
- 60 城合计 4–5GB 的餐饮 CSV。
- 57 城的地铁站点/线路几何 CSV。
- 全国景区,加高铁、航班表。
没有一份是即插即用的。编码各异、列在漂移、价格缺失、同一座城有三种拼写。管线的活就是把它变得 枯燥而统一。
flowchart LR
subgraph Raw[原始源 ~4.5GB]
H[酒店 .xlsx ~443K]
F[餐饮 CSV 60 城]
M[地铁几何 57 城]
A[景区 / 高铁 / 航班]
end
Raw --> X[抽取]
X --> C[清洗 / 归一化]
C --> G[聚合]
G --> O[(data/cities/{slug}/*.json)]
O --> V[etl-validate]
V -->|失败| C
V -->|通过| Ship[入 git]管线分阶段
- 抽取 —— 读每种源格式(xlsx/csv),只取产品需要的字段。
- 清洗 / 归一化 —— 统一城市 slug、修编码、把价格强转数字、丢掉无可用信号的行。
- 聚合 —— 把每城几千行压成一小撮汇总值:餐费中位数、住宿档位、交通成本。
- 输出 —— 写
data/cities/{slug}/*.json(和一个交通矩阵)。这些聚合产物入 git; 原始源数据集永不入 —— 既为合规,也因为几个 GB 不该进仓库。
一个关键细节:原始数据集被显式 git-ignore,只有聚合产物 ship。仓库保持小巧,不含任何 有许可负担的内容。
合规红线
整条管线最重要的规则,是关于不输出什么。餐饮数据文件(food.json)只输出聚合 ——
餐费区间、菜系风格、招牌菜。它被严禁含任何店级字段:没有店名、shopid、电话、评分、评论数、
图片、店内 url。
| 保留(聚合) | 剔除(店级) |
|---|---|
meal_budget_cny、meal_mid_cny、meal_high_cny |
店名 |
cuisine_styles |
shopid / 电话 |
signature_dishes |
评分 / 评论数 |
| 图片 / 店内 url |
这不是锦上添花。聚合正是让数据可用、可分享、又不必转发别人店级清单的关键。它是数据产品和 爬来的副本之间的分界线。
验证闭环
因为这条红线在重构时太容易不小心越过,管线以一个每次 ETL 改动都跑的验证步骤收尾。它检查
输出 schema,并且关键地断言 food.json 里永不出现任何被禁的店级字段。验证不过,数据就不 ship。
改 ETL 和跑验证是分不开的 —— 你没法跳过自检。
诚实处理数据缺口
真实数据集有窟窿,假装没有就会算出错误预算:
- 少数城市(如香港、澳门、台湾)餐费缺失,保留
null,导出步骤直接跳过,而不是编数字。 - 个别城市餐饮覆盖薄,用对标城市的参照倍率补,明确建模,而不是行内瞎猜。
- 景点票价缺失被当成真风险:
null价会被悄悄计成 ¥0,低估预算,所以把价格核准并打补丁, 而不是任由它把总额悄悄拉低。
把缺口暴露出来 —— 而不是糊过去 —— 才是估算诚实的根本。
要点
- 旅行数据 ETL 把几个 GB 的脏源,变成小而统一的每城 JSON。
- 入 git 的是聚合产物,绝不是原始数据集 —— 为合规也为仓库整洁。
- 只输出聚合(无店级字段)是让数据成为可分享产品的规则,由自动验证步骤强制。
- 显式建模数据缺口;一个被读成 ¥0 的静默
null会悄悄污染预算。
常见问题
什么是旅行数据 ETL 管线? 一个把原始旅行源(酒店、餐饮、交通)抽取出来、通过清洗和聚合转换、再加载成干净格式的过程 —— 这里是每城一个 JSON。
为什么聚合而不存原始行? 聚合让数据可用且合规:它回答「这里一顿饭多少钱」,而不必转发单个餐厅的店名、评分或联系方式。
缺失价格怎么处理?
保留 null 并在导出时跳过,或用明确建模的参照倍率补 —— 绝不静默当成 0,那会低估预算。
下一篇 → 手画一张交互式 100 城中国 SVG 地图