如何采集 Google 地图数据,用于研究与分析
难的不是拿到商户条目,而是把它们全部拿到、每条只出现一次,并且类别在任何地方都指同一件事。
- 作者
- HuiTu Technology
- 发布时间
- 更新时间
地图平台掌握着关于商户位置最完整的公开记录。从里面取几条商户信息很容易。但要拿到覆盖整座城市、已去重、类别口径一致的完整数据集,是另一回事——大多数项目正是栽在这个差距上。
第一步:选对采集路径
获取这类数据有三条路径,适用的场景各不相同。一开始选错,后面纠正的代价很高。
| 路径 | 适用场景 | 主要约束 |
|---|---|---|
| 官方 Places API | 线上产品功能、长期持续调用、小体量数据 | 规模上去后成本高,且授权条款限制数据的存储与再分发 |
| 商业数据供应商 | 需要多国覆盖并有覆盖率承诺 | 价格高,且不同市场的覆盖质量参差不齐 |
| 采集公开的商户页面 | 城市级或全国级的一次性研究 | 需要谨慎对待访问频率限制与使用条款,并非所有用途都适用 |
把搜索区域切成网格,不要按名称搜
数据集不完整,最常见的原因就是直接按城市名搜索。任何搜索接口的单次查询都有返回条数上限,所以在一座大城市里搜「餐厅」,几千家里只会返回几十条,而且明显偏向市中心、偏向知名度高的店。
解决办法是切网格。用互相重叠的小格子铺满研究区域,格子要小到单格内的结果数不超过一次查询的返回上限,然后逐格查询、再合并。格子大小必须随密度调整:足够覆盖市中心的精细网格,铺到农田上会白白浪费几千次查询。
# Adaptive tiling: split any cell that returns a full page of results
def collect(cell, depth=0, max_depth=5):
results = query(cell)
if len(results) >= PAGE_LIMIT and depth < max_depth:
# The cell is saturated, so we are almost certainly missing records
return [r for quad in cell.split_into_quads()
for r in collect(quad, depth + 1, max_depth)]
return results规则很简单:只要某个格子返回了满页结果,就认定它被截断了,继续往下细分。直到格子返回的结果少于单页上限,或者触及递归深度上限为止。仅这一个技巧,就决定了覆盖率是 40% 还是 95%。
开跑之前先把字段定下来
- 名称,以及一份用于后续匹配的归一化名称。
- 类别,同时单独保留来源的原始标签。
- 完整地址,并解析为街道、城市、省/州与邮编。
- 经纬度,保留六位小数已经足够。
- 电话与网址,以商户自行公开的为准。
- 评分与评价数量,把它当作热度的代理指标,而不是质量指标。
- 营业时间,归一化为结构化的每周时间表。
- 经营状态,避免已永久关闭的门店把计数抬高。
- 每一条记录都要带采集时间戳。
按一个写明的主键去重
网格互相重叠,意味着同一家店会在多次查询里出现。平台自带的标识符能解决大部分情况,但一旦要合并第二个来源,或者几个月后重新跑一遍,就需要一个真正的业务主键:归一化名称加地址,并对相近的候选记录设一个距离阈值。
把规则写下来,随数据一起交付。在任何跨市场对比中,去重率都是最先被追问的指标之一;一个说不清来历的去重率,会让整个对比失去意义。
先校验,再分析
- 把所有点画到地图上。落在海里或落到邻国的点一眼就能看出来,而且必然是错的。
- 统计完全相同的坐标。成堆重合说明地理编码回退到了某个中心点。
- 把类别分布和预期对一遍。如果某一个类别装下了数据集的大半,说明映射规则有问题。
- 手工抽查五十条记录,逐条回到来源页面核对。这只要二十分钟,却能抓出其他手段发现不了的解析错误。
- 重点检查研究区域边缘的覆盖情况,网格切分最容易在这里出问题。
- 餐厅 · 34
- 咖啡馆 · 22
- 零售 · 26
如果这份数据不止用一次
一开始就按可重复运行来设计。为每家门店保留一个稳定的标识符,每次运行的结果单独存放而不是覆盖上一次,并输出一份新增、消失与变更记录的变更日志。开店与关店往往才是最有价值的产出,而只有留着上一次的结果,你才看得见它们。