
“把全城所有小区的边界导成一份 GeoJSON能不能干”这是半个月前一位做城市更新调研的朋友发给我的需求。后来又有几个做房产数据的人问了类似的问题我干脆把这套流程完整记录了一遍。整个过程下来最深的体会是小区边界爬取的难点从来不是“爬”而是“边界”这两个字。百度地图开放平台能直接拿到的是 POI 点、地址、行政区边界但小区围栏这种多边形轮廓官方接口基本不会直接给。所以你只能分两步走先把小区的位置点弄全再去想办法把轮廓多边形抠出来。这篇就围绕这个主线展开内容包括开放 API 的用法、网页版抓包思路、坐标转换、清洗入库以及我把一个城市跑下来之后踩过的坑。1. 先搞清楚你要的“小区边界”是哪种边界很多新手会把话里的“边界”理解成百度地图页面上那条圈出来的蓝色线条但真实工作流里至少有三种不同级别的数据千万别混为一谈。1.1 点、面、行政区划是三种东西第一种是 POI 点也就是小区门口或中心点的一个坐标通常包含名称、地址、经纬度、唯一标识 uid。百度地图 open API 里的 place 检索返回的就是这类只能知道“这里有个小区”不知道小区占多大地方。第二种是行政区划边界比如某个区、某个街道的边界多边形这是官方对外开放的有明确接口结果是一串经纬度点围成的面。第三种才是真正让人头疼的“小区围合轮廓”也就是南门到北门、围墙到围墙在百度地图渲染层里那个不规则的多边形。这类数据多数要从百度地图网页版的渲染接口里“抠”接口不公开而且参数经常带签名需要靠抓包动态分析。清楚你要哪一种后面的技术选型完全不同。官方 API 只能覆盖前两种情况第三种必须有爬虫思路介入。1.2 百度地图各层接口到底开放了什么我把这次实践中用到的数据来源整理成了一张表方便你判断边界数据从哪里下手。数据类型来源返回内容公开程度小区 POI 点place/v2/search名称、地址、中心点坐标、uid公开有配额POI 详情place/v2/detail电话、标签、更多属性公开有配额行政区边界place/v2/area/search区县或街道的多边形点串公开小区渲染轮廓百度地图网页版内部接口高亮区域的多边形点串不公开需抓包看清楚这张表之后你大概能理解我的策略用公开接口解决“有哪些小区”用网页版接口解决“边界长什么样”最后把两部分按 uid 或名称拼接。2. 开工前的地基AK、坐标系和依赖动手前有几个基础问题必须处理否则后面会反复返工。2.1 申请服务端 AK 并配置 IP 白名单去百度地图开放平台创建应用类型选择“服务端”这样会拿到一组 AK 和 SK。SK 的作用是给请求做 sn 签名校验如果只做低频率的本地小规模抓取可以先不管签名直接拿 AK 访问所有公开接口。但我强烈建议你创建完应用之后马上把出口 IP 加到 IP 白名单里。原因很简单百度地图开放 API 按访问来源做校验如果你在服务器上跑就把服务器出口 IP 加进去如果在自己电脑上跑就填宽带出口 IP。填错或漏填会导致请求返回 403 或认证失败特别容易在深夜调试的时候把人整崩溃。还要记住服务端 AK 不要塞进浏览器端代码里也不要提交到公开仓库。这种 AK 一旦泄露对方可以拿你的配额随便刷轻则接口被限流重则整个账号被封。2.2 三个坐标系的事必须一次说清国内地图数据的坐标是“带偏移”的。GPS 拿到的原始坐标是 WGS84经过国测局加密后变成 GCJ02也就是我们常说的火星坐标百度在这基础上又二次加密得到 BD09。百度地图所有公开接口返回的坐标默认是 BD09你从网页版抓包拿到的点串大概率也是 BD09。如果你要把数据导入 ArcGIS、QGIS或者和其他 WGS84 数据源做叠加分析就必须转换。转换代码我直接贴在最下面这段里逻辑不算复杂。BD09 转 GCJ02 是固定公式GCJ02 转 WGS84 用迭代逼近精度在绝大多数应用场景够用。import math def bd09_to_gcj02(lng, lat): x lng - 0.0065 y lat - 0.006 z math.sqrt(x * x y * y) - 0.00002 * math.sin(y * 3000 * math.pi / 180) theta math.atan2(y, x) - 0.000003 * math.cos(x * 3000 * math.pi / 180) return z * math.cos(theta), z * math.sin(theta) def wgs84_to_gcj02(lng, lat): a 6378245.0 ee 0.006693421622965943 dlat _transform_lat(lng - 105.0, lat - 35.0) dlng _transform_lng(lng - 105.0, lat - 35.0) radlat lat / 180.0 * math.pi magic math.sin(radlat) magic 1 - ee * magic * magic sqrtmagic math.sqrt(magic) dlat (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi) dlng (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi) return lng dlng, lat dlat def _transform_lat(x, y): ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(y * math.pi) 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0 ret (160.0 * math.sin(y / 12.0 * math.pi) 320 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0 return ret def _transform_lng(x, y): ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(x * math.pi) 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0 ret (150.0 * math.sin(x / 12.0 * math.pi) 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0 return ret def gcj02_to_wgs84(lng, lat): lng0, lat0 lng, lat for _ in range(10): glng, glat wgs84_to_gcj02(lng0, lat0) lng0 - glng - lng lat0 - glat - lat return lng0, lat02.3 依赖安装和目录规划这次实战用 Python核心依赖是 requests、pandas、shapely、pyproj如果想把 GeoJSON 直接画出来可以再装 geopandas。Windows 上 geopandas 安装容易出问题最简单的方式是直接用 conda 安装一条命令搞定。conda install -c conda-forge geopandas shapely pyproj同时建议把工程目录建清楚因为爬取过程很可能中断要续跑。我会建 data/、cache/、output/ 三个目录data 存中间抓取结果cache 存网页版接口返回的原始 JSONoutput 存最终清洗后的 GeoJSON 和 SQLite。3. 第一步先把全市小区的“点”抓全在搞边界以前要先把目标区域的小区点位抓下来。点位列表越全后面做边界关联时命中率越高。3.1 关键词和行政区拆解百度地图 place/v2/search 接口支持 query 和 region 参数region 可以填城市名也可以填区县名。如果你直接填城市名搜“小区”大概率能搜到一批但这些结果会被一个“每页 20 条、最多翻页到某个上限”的逻辑卡住。正确的做法是一级一级拆。先按区县拆再按关键词拆。因为城市范围太大时模糊匹配的结果会被其他类型 POI 稀释比如“某某小区”里混进“工业园区”和“菜市场”的概率会增大。我在杭州跑的时候把上城区、拱墅区、西湖区、滨江区、萧山区、余杭区、临平区、钱塘区这些行政区单独循环每个区下面再搜“小区”“花园”“家园”“公寓”“雅苑”“名苑”“新村”等关键词。关键词怎么加取决于你想抓的是商品房小区还是老旧小区否则很容易漏掉。3.2 分页、限速和断点续跑下面的脚本是这次抓 POI 的核心骨架你根据自己的 AK 和行政区列表改一下就能跑。import requests import time import json AK 你的AK REGIONS [滨江区, 西湖区, 拱墅区] # 按实际需要填 QUERIES [小区, 花园, 家园, 公寓, 雅苑, 名苑, 新村] rows [] for region in REGIONS: for q in QUERIES: for page in range(0, 20): params { query: q, region: region, output: json, ak: AK, page_size: 20, page_num: page, scope: 2, } try: resp requests.get(https://api.map.baidu.com/place/v2/search, paramsparams, timeout10) data resp.json() except Exception as e: print(请求异常, region, q, page, e) break if data.get(status) ! 0: # 状态码非0常见是配额超限、参数错误或权限失败 print(返回错误, data.get(status), data.get(message)) break results data.get(results, []) if not results: break for item in results: rows.append({ name: item.get(name), uid: item.get(uid), address: item.get(address), lng: item[location][lng], lat: item[location][lat], region: region, query: q, }) # 个人认证的QPS通常不高1.2秒是保守间隔 time.sleep(1.2) # 用uid去重因为不同关键词很容易匹配到同一个小区 unique {} for row in rows: unique[row[uid]] row with open(data/communities.json, w, encodingutf-8) as f: json.dump(list(unique.values()), f, ensure_asciiFalse, indent2) print(总数:, len(unique))这里要注意几个典型情况。如果你请求频率太高接口会返回配额相关的状态码这时候要停止并发加长 sleep甚至可以完整退避一分钟再继续。还有网络抓取最怕跑到一半挂了。我把原始结果存到 JSON而不是追着更新 SQLite这样重跑时可以用 uid 做增量合并避免重复请求。3.3 用 UID 补一些公共属性place/v2/search 返回的字段已经够用但如果你想看更多比如电话、标签、营业时间可以在拿到 uid 后调用 place/v2/detail。我这里用它主要是为了交叉验证因为同一个 uid 在 detail 接口里返回的地址和标签更准。def fetch_detail(uid): params { uid: uid, output: json, scope: 2, ak: AK, } resp requests.get(https://api.map.baidu.com/place/v2/detail, paramsparams, timeout10) return resp.json()有个容易误踩的点网上很多资料说scope2会返回更丰富的小区坐标范围实际上 place/detail 也不会给你边界多边形。它最多给你一个中心点和一些属性字段。所以别在这里耗时间重点直接进入下一步。4. 难点攻坚小区轮廓多边形从哪拿到这一步你已经有一个个小区的中心点位了可以做热力图、可以做点图但拿不到“边界”。我在这个环节验证了三套方案每套的复杂度和效果都不一样。4.1 方案A从百度地图网页版内部接口拿轮廓效率最高但依赖抓包这是我实际最推荐的方式尤其是你只想要几个区县小区边界时。思路不是靠读文档而是用浏览器开发者工具观察百度地图网页版在“画出小区边界”的那一瞬间请求了哪个接口、返回了什么字段。具体抓包步骤打开 Chrome 无痕窗口访问 map.baidu.com按 F12 打开开发者工具切到 Network 面板并清空请求记录。在搜索框输入一个明确的小区名比如“融创滨江壹号院”等搜索结果出来。点击搜索结果里的目标小区。地图上通常会高亮显示一个多边形轮廓。回到 Network 面板筛选 XHR 和 Fetch 请求。在搜索框里敲boundary、polygon、geo、points这些关键词逐个观察响应内容。找到返回多边形点串的请求后右键复制响应内容先存成 JSON再写程序解析。这里要提前给你打预防针百度地图网页版改版很频繁我上周抓到的接口参数下周可能就变了。所以不要在文章里等我贴一个固定 URL 出来那不是可持续的姿势。正确姿势是抓包实时观察把请求参数尽量泛化成配置文件。下面这代代码是一个通用解析示意核心是“从响应 JSON 里找到 boundary 或 polygon 这类字段”参数结构以你抓包得到的实际请求为准。import requests # 这里以实际抓包看到的URL和参数为准下面只是示意 url https://map.baidu.com/ params { qt: s, # 代表search类请求随版本变化 wd: 融创滨江壹号院, c: 179, # 城市代码随版本变化 rn: 10, } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://map.baidu.com/, } resp requests.get(url, paramsparams, headersheaders, timeout10) data resp.json() # 根据实际抓包结果调整字段路径 content data.get(content, []) for item in content: if boundary in item: print(item[name], item[boundary][:200])如果响应里的字段名做了混淆这时候就去 Sources 面板搜“boundary”字符串找到赋值代码的位置打上断点再回到页面点击小区等断点触发后看局部变量。这个方法比盲猜字段名要可靠得多。这种方式的风险在于网页版接口可能是加密的带动态 token 或签名。遇到这种情况最简单的是换用浏览器自动化方案用 Playwright 打开地图搜索小区等待高亮出现然后通过执行 JS 从页面地图对象里把覆盖物或图层里的坐标读出来。虽然慢但省去逆向签名的功夫。4.2 方案B行政边界降级适合快速出图如果你只是做展示原型并不一定要精确到小区围墙那完全可以用“区县边界 内部点位”的方案替代。这个方法能救急而且不会被网页版接口牵着走。先用 place/v2/area/search 把某个区的行政边界多边形拉出来脚本如下params { keyword: 滨江区, sub_admin: 0, output: json, ak: AK, } resp requests.get(https://api.map.baidu.com/place/v2/area/search, paramsparams, timeout10) data resp.json() polygon data.get(result, {}).get(polygon, )这里返回的 polygon 是一长串lng,lat;lng,lat;...字符串存下来后转成 Shapely Polygon 就行。因为它像行政边界而不是小区边界只用来当地图底图或范围约束。降级方案的思路是小区 POI 点落进哪个区县的多边形就给这个小区挂上哪个区县的边界标识。你至少能做到“知道小区在哪一片”而不是精确知道“小区自己的围墙”。4.3 方案C开源地图数据做交叉验证我在完成边界字段后经常用 OSM 数据交叉验证。从 Geofabrik 下载对应区域的数据用 osmium 或 osmfilter 过滤出居民地相关面要素再把过滤结果跟百度抓回来的边界做空间对比。这样做有两个好处第一如果某个小区百度返回的轮廓明显异常比如面积几百倍偏差用 OSM 能快速发现问题第二对少量缺失轮廓的小区可以直接把 OSM 里的地块面拿过来兜底至少保证数据完整性。适合自己跑的小块数据可以直接在 Overpass API 里写查询全量数据下载推荐 Geofabrik 的 pbf 文件再用工具转换。过程不复杂但不展开写了因为和“百度地图”这个主题有关的重点还在上面。5. 拿到原始数据之后坐标转换、清洗和入库不管是网页版接口直接返回的多边形还是开源数据兜底的多边形拿到的原始数据都还不能直接交付。你一定逃不过下面这三个环节。5.1 把多边形点串解析成能用的对象网页版接口里的多边形最常见的格式是lng,lat;lng,lat;...也可能存在坐标对之间用|隔开的情况所以要统一处理。def parse_boundary(raw): raw raw.replace(|, ;) points [] parts raw.split(;) for part in parts: if , in part: lng, lat part.split(,) points.append((float(lng), float(lat))) return points解析出 points 之后交给 Shapely 构造 Polygon。如果接口返回的是多个环比如外面一个大圈、里面一个小圈那还要手动拆环再判断内外关系。大多数小区边界是单环不会遇到这种复杂情形。5.2 坐标统一避免“图层错位”很多人在最后会忽略坐标系统一。结果就是从百度网页版拿到的 BD09 坐标和从公开 API 拿到的 WGS84 边界在 GIS 里一叠全都错开几十米到几百米。我在第二节给了转换函数你可以直接在拿到每条边界后调用。转换成 WGS84 再入库这样以后跟 GPS 采集数据、天地图、其他平台数据做叠加都能对齐。from shapely.geometry import Polygon from shapely.ops import transform import pyproj wgs84 pyproj.CRS(EPSG:4326) bd09 pyproj.CRS(EPSG:???) # 实际没有标准EPSG转为自定义再转注意百度 BD09 没有公开 EPSG 编号所以最简单的方式还是用纯 Python 公式转而不是依赖 pyproj。我在实战里就是直接用 2.2 节那套函数对每个点循环转换速度完全可以接受。5.3 用 Shapely 修复自相交和简化即使拿到了点串也不能直接构建 GeoJSON因为原始坐标点可能非常密而且可能出现自相交直接存入数据库会导致各种莫名其妙的报错。用 Shapely 做两件事from shapely.geometry import Polygon from shapely.wkt import dumps poly Polygon(points) if not poly.is_valid: poly poly.buffer(0) # 修复自相交问题 poly_simplified poly.simplify(0.00003, preserve_topologyTrue)buffer(0)是 GIS 数据处理里的常见技巧它能强制修正拓扑错误simplify则压缩点数量。0.00003这个经纬度容忍值大概相当于几米在小区尺度下已经够平滑又能让 GeoJSON 文件体积大幅下降。5.4 导出 GeoJSON 和 SQLite最终我一般会把数据导成两份。一份是 GeoJSON方便丢到 QGIS、Leaflet、Mapbox 里直接画一份是 SQLite方便后面做关联和空间查询。import json from shapely.geometry import mapping features [] for item in merged_data: features.append({ type: Feature, properties: { name: item[name], uid: item[uid], region: item[region], source: item[source], }, geometry: mapping(item[polygon]), }) geojson { type: FeatureCollection, features: features, } with open(output/community_boundaries.geojson, w, encodingutf-8) as f: json.dump(geojson, f, ensure_asciiFalse, indent1)SQLite 就更简单了直接建一张表存 WKT 字符串就行普通场景不需要上 PostGIS。等以后数据量真的到几十万条再考虑空间数据库也不迟。6. 实际跑完一个城市后的经验总结最后这部分不写教科书全是我在这轮实战中真实碰到的问题。6.1 配额和速度比预期更难受个人认证的百度地图开放平台QPS 和日配额都不高。我一开始用 1 秒间隔跑还能跑动但一旦把关键词和区县组合全展开请求量立刻涨上去很容易在半夜触发配额限制。所以我现在的策略是先跑一个小区的数据确认整个流程通了再扩大规模跑的过程中做好日志和断点续跑每次请求结果原样落盘方便出错后增量重试。6.2 网页版接口改版是常态我的经验是每周固定时间重新抓包检查一遍。接口参数很可能换字段名很可能换但“点击小区后高亮轮廓”这个交互不会消失所以抓包入口一直还在。如果某天响应里突然多了一段看不懂的编码字段不要急着硬解先从浏览器页面里观察渲染效果对照页面高亮是否和抓包数据一致再决定是否继续。6.3 数据质量要靠人工抽检我最终交付的数据里只有一部分小区能从百度网页版直接拿到理想轮廓剩下的要么找不到匹配项要么边界明显是区域级数据和真实小区围墙差得很远。这种时候我会优先用 OSM 补再补不上就干脆标记为“无边界”不让错误数据混进去。宁可少一个不能错一个。6.4 合规底线和可持续性最后必须提醒一句。抓取网页版接口获取不公开的轮廓数据是存在合规风险的做法。它只适合你用来做个人学习、内部研究或者是已经确认有授权的项目不要拿爬到的数据做成商业接口对外卖不要高频请求冲击百度地图服务更不要碰用户隐私相关字段。公开接口的配额能走官方就尽量走官方网页版抓包这种手段尽量轻量使用。如果你想正式做一款产品建议直接走百度地图开放平台的商业授权或企业配额咨询官方是否提供地理围栏、边界数据服务。就算最后决定自己爬也要控制频率、尊重平台规则。这个尺度心里要有数。我自己现在的流程已经固定下来先用官方接口抓 POI 点再用网页版抓包补轮廓没补上的用 OSM 兜底最后统一转 WGS84入库后让人工在 QGIS 里随机抽验一批。整个流程虽然听起来步骤多但每一步都是前面踩过坑之后沉淀下来的。你要是也想跑强烈建议先拿一个小县城或者一个区练手把整个链路打通之后再往上放大范围。