做这个选题之前,我先说个真实经历。前阵子有个做教育选址分析的朋友找我,说想给公司整理一份全市学校的分布数据,第一反应就是“写个爬虫去百度地图抓数据”。结果折腾了两个晚上:验证码、IP封禁、数据字段乱码,最要命的是查了一堆资料后发现,单纯爬取并大量存储平台数据,在法律层面风险极高。最后我拉着他把方案整个换掉,用百度地图开放平台的正规API,不到一个下午就把北京市所有学校POI点数据拿到了,还顺手关联上了行政区划边界,做出了可视化的分布图。
这篇文章就以“学校”为例,带你完整走一遍合法获取百度地图POI边界数据的流程。过程中我会把POI点和边界面的关系、地点检索API和行政区划边界API的区别、坐标纠偏、数据落地与可视化验证这些坑全部拆开讲清楚。如果你正在做类似的地理数据采集、选址分析、学区研究,或者只是想把爬虫方案替换成合规方案,这篇文章可以直接照着抄。
1. 先别急着写爬虫:POI边界数据的真实面貌与三大误区
1.1 你以为的“边界”和API返回的“边界”不是一回事
很多人在需求阶段就把概念搞混了,到了技术实现阶段才发现两个词指向的东西完全不一样。
百度地图里的POI(Point of Interest,兴趣点),本质上是一个带属性的坐标点,比如“北京大学”这个POI,它的核心数据是经纬度、名称、地址、所属区域、UID等等。它不是一个多边形,不具备“边界”的几何含义。而“边界”在GIS里通常指的是一个面状要素(Polygon),比如某个学校的校区占地面积范围、某个行政区的管辖范围。
那标题里的“POI边界数据”到底指什么?在实际业务中通常有两种理解:
- 理解A:你在百度地图上搜“学校”,能拿到所有学校POI点的坐标,再把这些点落到行政区划边界里做空间分析,形成“区域内POI分布+区域边界”的组合数据。
- 理解B:你希望拿到每个学校本身的多边形轮廓,也就是校园的“地界”。
我先把结论放这儿:百度地图开放平台能稳定、合规提供的是理解A完整链路,以及理解B的间接方案。理解B的学校园区多边形,百度并没有一个公开接口直接返回,你需要通过OSM、天地图或者自动矢量化等补充手段去拼齐,这部分我在第4章会专门讲。
1.2 误区一:爬虫是唯一的免费方案
这是我在社区里看到最多人踩的坑。“免费”这个词对爬虫方案的诱惑力太大了,但把所有成本算进去之后,爬虫往往是更贵的那条路。
首先是技术成本。现代地图平台的反爬策略早就不是设置一个UA、加个延时就能绕过的了。你会遇到字体反爬(页面文字被加密映射)、Canvas指纹追踪、请求频率风控、验证码弹出、返回数据坐标偏移等一连串问题。更麻烦的是,这些策略还在不断升级,你今天写好的解析规则,下周可能就失效了。
其次是法律成本。近几年的司法案例里,因为批量爬取和存储平台数据被判刑的并不少见。百度地图这类平台的POI数据属于其核心商业资产,服务条款里明确禁止未经许可的批量抓取和存储。像热搜词里那个“因爬虫入狱”的案例,概括起来都是类似的模式:未经授权绕过技术保护措施,批量获取计算机信息系统数据。这个红线不是吓唬人,是真实存在的。
1.3 误区二:拿到了POI就等于拿到了边界
还有一部分人把“POI数据”和“边界数据”混为一谈,想着“我把POI全抓回来,不就等于拿到全部数据了吗”。真不是。
POI返回的是点,边界是面。从几何学角度,一组散点如果不做轮廓提取(Alpha Shape、凸包/凹包算法),是不构成面的。就算你用算法包络出一个外轮廓,那也不是真实的学校边界,只是点的聚合范围。真实业务比如学校选址、景区规划、学区划分,需要的是精确到地块边界的多边形,不是点云粗略包络。
所以正确做法是:把POI点数据和官方提供的行政区划边界数据分开获取,再通过空间关联把它们组织到一张表上。这样既满足业务分析需要,也完全在平台开放能力允许的范围内。
1.4 误区三:合法的API就没有任何限制
很多转用API的人以为官方接口绝对宽松,结果一调用就收到配额超限的报错。API当然有使用限制,但这些限制是透明的、可预期的,比爬虫的暗箱风控好处理一百倍。
百度地图开放平台对每个应用(AK)设有QPS(每秒请求数)和每日总配额。个人开发者刚注册时配额比较低,完成个人开发者实名认证后会有一定提升。具体数字在不同时期会有调整,以控制台当前显示为准。这些配额对大部分分析场景足够用了,真的大规模商用场景,可以向平台申请提额或购买企业服务,走的是完全合规的通道。
2. 合规路线图:百度地图开放平台的定位与能力边界
2.1 申请开放平台账号与应用AK
先到百度地图开放平台(lbsyun.baidu.com)注册并登录,进入控制台。
创建应用时需要注意,这步决定你后面能否拿到正确数据:
- 应用类型选“服务端”,因为我们要调用的是Web服务API,需要在服务端发起请求。
- 在“应用设置”里填写IP白名单。这是个高频坑点:如果留空或填错,请求会直接报“IP白名单校验失败”。本地调试时填本机出口IP,部署到服务器时填服务器公网IP。多个IP用逗号分隔,不想限制IP可以填“0.0.0.0/0”(仅限个人学习和测试使用,生产环境不要这样写)。
- 创建完成后会生成一个AK(访问密钥),后面的所有API请求都要带上它。
这里顺便提一下AK安全:AK相当于你在这个平台的“钥匙”,不要把它硬编码进前端页面或公开仓库。一旦被泄露,别人可以用你的账号额度疯狂刷接口,轻则配额被消耗完,重则账号被平台风控封禁。
2.2 地点检索API能做什么、不能做什么
地点检索API(Place API)是这次任务的主干接口,它负责“找POI点”。
调用方式是一个GET请求:
https://api.map.baidu.com/place/v2/search?query=学校®ion=北京市&output=json&ak=你的AK&page_size=20&page_num=0&scope=2核心参数如下:
| 参数 | 含义 | 说明 |
|---|---|---|
| query | 检索关键词 | 支持“学校”“大学”“小学”等,相当灵活 |
| region | 检索区域 | 可以是市级或区县名,比如“北京市海淀区” |
| page_size | 单页返回条数 | 建议设置20,拉满容易触发限制 |
| page_num | 页码 | 从0开始,配合total字段做翻页 |
| scope | 返回详略程度 | 设2时返回更丰富的POI详情 |
| output | 返回格式 | 设json,方便处理 |
这个接口能返回POI点的名称、经纬度、地址、行政区划编码(area_id)、UID等关键字段。它不能返回校园多边形,也不能返回楼栋建筑轮廓,所以在设计时就要把预期放准。
2.3 行政区划边界API:真正拿“边界”的入口
百度地图有一个专门返回行政区划边界的接口,名为行政区划区域检索,可以返回全国省市区的多边形几何。调用示例:
https://api.map.baidu.com/api_region_search/v1/?ak=你的AK&keyword=北京市&admin_type=0返回的JSON里,content.geo字段就是多边形的坐标串,是一串经纬度对组成的有序闭环。这个接口的价值在于:它是官方返回的“面”数据,合法性没有问题,精度也符合政府公开的空间数据标准。
有了“区划边界面”和“POI点”,就可以做空间关联:判断每个学校落在哪个区/县,或者反过来统计每个边界内有多少所学校。这就是咱们第一张可用的“POI边界数据表”。
2.4 配额、QPS与使用协议,正式开发前先读懂
在写代码之前,建议花10分钟读完应用控制台里展示的服务配额和开发者条款。我见过不少人辛辛苦苦写完采集脚本,结果上线第一天就遇到两类问题:
- QPS超限:一个AK设置的QPS如果是2,意味着你1秒内最多发起2次请求。脚本里不加延时,批量翻页时必然触发报错。
- 日配额不足:每次地点检索API调用会消耗一次配额,少量测试够用,全量跑完整个区县的学校数据可能消耗几千次配额,需要提前规划。
同时,开发者条款里通常会说明:通过API获取的数据可以合理使用,但未经授权的大规模转售、再分发可能构成违约。我个人的做法是:采集的数据用于内部研究分析,来源标注清楚,绝不打包出售。这条底线守住了,整个方案的安全性就稳了。
3. 手把手实操:地点检索API批量采集学校POI点
3.1 准备Python环境
整个流程用Python写比较顺,建议用一个干净的虚拟环境,避免污染系统依赖。需要安装的库不多:
pip install requests pandas如果后续要做边界可视化和空间连接,再补这两个:
pip install geopandas matplotlib foliumGeopandas在Windows上偶尔会遇到底层依赖问题,如果装不上,可以直接用Anaconda环境安装,或者退回pandas + json处理,空间分析部分用QGIS代替,不影响主流程。
3.2 第一轮请求:把“学校”POI批量拉下来
先写一个最基础的请求函数,请求“北京市”范围内的所有学校相关POI:
import requests import pandas as pd AK = "你的AK" REGION = "北京市" QUERY = "学校" def search_poi(query, region, page_num, page_size=20): url = "https://api.map.baidu.com/place/v2/search" params = { "query": query, "region": region, "output": "json", "ak": AK, "page_size": page_size, "page_num": page_num, "scope": 2 } resp = requests.get(url, params=params, timeout=10) data = resp.json() return data # 测试第一页 data = search_poi(QUERY, REGION, 0) print("返回状态码:", data.get("status")) print("总数:", data.get("total")) print("本页POI数:", len(data.get("results", []))) if data.get("results"): print("首个POI:", data["results"][0]["name"], data["results"][0]["location"])运行后观察返回。状态码为0表示成功,total字段会给出全部匹配的POI数量,比如“北京市”所有学校类POI可能是几万个。如果你看到status是非零值,优先检查AK、IP白名单和配额。
3.3 分页与全量采集:total和page_num的配合
地点检索API单页最多返回几十条,一次请求根本拉不完。这里的关键是用total字段控制循环:
def fetch_all_poi(query, region, max_pages=100): all_results = [] page_num = 0 while True: data = search_poi(query, region, page_num) if data.get("status") != 0: print("请求失败,状态码:", data.get("status"), data.get("message")) break results = data.get("results", []) all_results.extend(results) total = data.get("total", 0) print(f"第{page_num+1}页,累计{len(all_results)}条,总目标{total}条") if len(all_results) >= total or not results: break page_num += 1 if page_num >= max_pages: print("已达最大翻页限制,停止") break return all_results有几个细节值得注意:
- page_size不要拉满。我实测过,page_size设为20在稳定性上最好,拉满时接口偶尔会返回异常或超时。
- total是实时变化的。如果同时有人修改、新增POI,total可能在翻页过程中发生漂移,所以循环里用
len(all_results) >= total作为终止条件,而不是等页数耗尽。 - 请求间隔一定要加。延时0.3到0.5秒,配合低QPS配额,基本不会触发限流。如果配额优化过,可以适当缩短,但别低于0.2秒。一个简单的
time.sleep(0.5),能省掉后面大量错误码排查。
3.4 请求间隔与失败的容错重试
地图API偶尔会因为网络抖动或服务端临时压力返回非零状态码,这时候直接放弃整批数据就太亏了。更稳妥的做法是加一个带重试的包装:
import time def search_poi_with_retry(query, region, page_num, page_size=20, retries=3): for i in range(retries): try: data = search_poi(query, region, page_num, page_size) if data.get("status") == 0: return data # 状态码异常时等待更长时间 print(f"第{page_num}页状态码异常: {data.get('status')}, 重试 {i+1}/{retries}") except Exception as e: print(f"请求异常: {e}, 重试 {i+1}/{retries}") time.sleep(1 + i * 2) return None重试之后仍然失败的页面,记录下来单独补抓,不要影响整体流程。采集完成后,把结果一次性存下来:
df = pd.DataFrame(all_results) # 将经纬度展开成独立列 df["lng"] = df["location"].apply(lambda x: x["lng"]) df["lat"] = df["location"].apply(lambda x: x["lat"]) df.to_csv("beijing_school_poi.csv", index=False, encoding="utf-8-sig")utf-8-sig这个编码很关键,否则Excel直接打开CSV中文会乱码。
4. 边界数据的获取逻辑:行政区划边界API与POI的空间关联
4.1 用行政区划边界API拿区域面
拿到POI点之后,下一步就是获取“面”的数据。用行政区划边界API逐个请求目标区域:
def get_region_boundary(keyword): url = "https://api.map.baidu.com/api_region_search/v1/" params = { "ak": AK, "keyword": keyword, "admin_type": 0 } resp = requests.get(url, params=params, timeout=15) data = resp.json() if data.get("status") == 0: content = data["result"]["content"] print("区域名称:", content["name"]) print("中心点:", content["center"]) print("geo字段长度:", len(content["geo"])) return content["geo"] else: print("行政区划请求失败:", data.get("status")) return Nonecontent["geo"]是一个长字符串,里面的坐标串由分号和逗号分隔,需要解析成坐标点列表。这里如果你只是想快速看边界,可以直接存原串;如果要和POI做空间运算,就得转成GeoJSON。百度坐标系下的geo直接用于地图可视化和POI空间连接存在一个障碍:POI坐标是BD-09,行政区划geo通常也是BD-09,它们之间是兼容的,可以直接配对,不需要先转WGS-84。这点第5章会展开说。
4.2 把POI点与边界面做空间匹配
拿到所有区县的边界面之后,把之前采集的学校POI点落到面上,就能得到一张“哪个学校在哪个区”的关联表。用GeoPandas做空间连接是最干净的做法:
import geopandas as gpd from shapely.geometry import Point, Polygon import json # 1. 把每个区县的geo字符串解析为Polygon列表 def parse_baidu_geo(geo_str): # geo格式示例:lng,lat;lng,lat;... pairs = geo_str.split(";") coords = [] for pair in pairs: if "," not in pair: continue lng, lat = pair.split(",") coords.append((float(lng), float(lat))) return Polygon(coords) boundary_polygons = [] for region_name in district_names: geo = get_region_boundary(region_name) if geo: boundary_polygons.append({ "region": region_name, "geometry": parse_baidu_geo(geo) }) boundary_gdf = gpd.GeoDataFrame(boundary_polygons, crs="EPSG:4490") # 2. POI点转GeoDataFrame poi_gdf = gpd.GeoDataFrame( df, geometry=[Point(x, y) for x, y in zip(df["lng"], df["lat"])], crs="EPSG:4490" ) # 3. 空间连接 joined = gpd.sjoin(poi_gdf, boundary_gdf, how="left", op="within") print(joined[["name", "region", "address"]].head())op="within"会判断每个POI点是否落在某个区县边界内部。这里有个小前提:坐标系要一致。如果你的POI坐标和边界坐标混用了不同坐标系(比如一个用BD-09,一个用GCJ-02),空间连接结果会整体偏移几百米甚至几公里,匹配成功率惨不忍睹。
4.3 如果你需要的是学校自身多边形:三个合法补充途径
前面反复强调百度没有直接给校园多边形,那实际业务里非要用校园轮廓怎么办?我实测过,这三个途径最靠谱:
途径一:OpenStreetMap(OSM)
OSM里学校通常以amenity=school或amenity=university标记,并且大量学校已经有完整的多边形轮廓。你可以通过Overpass API按城市、按类型导出,数据许可为ODbL,允许标注来源后使用。
[out:json][timeout:60]; area["name"="北京市"]->.a; ( node["amenity"="school"](area.a); way["amenity"="school"](area.a); relation["amenity"="school"](area.a); ); out body; >; out skel qt;途径二:天地图API
天地图是国家地理信息公共服务平台,提供行政区划、道路、水系等大量基础地理数据,接口也支持服务端调用。它的数据有国测局专门的审图政策,在大规模公开、商用前需要确认审图号,个人研究和学习使用没问题。
途径三:公开规划图/卫星影像的人工矢量化
如果你只需要少量重点学校的边界,最直观的方式是拿学校官网或规划部门公开的校区平面图,在QGIS里面拉底图手动矢量化。这个方式虽然耗时,但论合法性和准确性,反而是最高的,因为它完全基于公开信息加上你自己的标注劳动。
把这三种方式拿到的校园边界和百度POI点做联动,就能形成一套完整的“POI点+行政区面+校园面”三层数据体系,绝大多数教育类场景需求都能覆盖。
5. 坐标纠偏与可视化验证:数据能用之前必须做的事
5.1 BD-09、GCJ-02、WGS-84:坐标系混乱会导致边界错位
这是地理数据项目里最容易翻车的一环。国内地图数据常用的坐标系有三套:
| 坐标系 | 全称 | 使用方 | 特点 |
|---|---|---|---|
| WGS-84 | 世界大地坐标系 | GPS、OSM、Google地球 | 国际通用原始坐标 |
| GCJ-02 | 国测局加密坐标 | 高德、腾讯等 | 在WGS-84基础上做了加偏 |
| BD-09 | 百度坐标系 | 百度地图 | 在GCJ-02基础上二次加偏 |
百度地图API返回的location坐标是BD-09,行政区划边界API返回的geo也是BD-09,所以二者直接做空间连接没问题。但如果你把这些数据导入到使用GCJ-02或WGS-84的软件(比如常见的开源可视化库、GPS轨迹)里,就会出现几百米的系统性偏移。比如把BD-09的POI直接画在OpenStreetMap底图上,学校位置会明显偏向一边。
处理办法是统一转为WGS-84再对外分发。网上有很多BD-09转GCJ-02再转WGS-84的轮子,基本原理是“逆向纠偏”,即通过已知的偏移算法做反向修正。这类转换精度在2到5米级别,对POI点分布分析够用了,但做精确到地块的地籍分析就不够看,那时候必须用官方测绘数据。
5.2 用GeoPandas转换成通用GeoJSON
统一坐标系后,我建议把数据格式直接落地成GeoJSON,它是目前生态兼容性最好的地理数据格式,浏览器、QGIS、PostGIS、各种分析框架都能吃。
# 先做BD-09 -> WGS-84的近似转换 def bd09_to_wgs84(lng, lat): import math x_pi = 3.14159265358979324 * 3000.0 / 180.0 x = lng - 0.0065 y = lat - 0.006 z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * x_pi) theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi) lng_wgs = z * math.cos(theta) lat_wgs = z * math.sin(theta) return lng_wgs, lat_wgs df["lng_wgs84"], df["lat_wgs84"] = zip(*df.apply(lambda r: bd09_to_wgs84(r["lng"], r["lat"]), axis=1))再生成完整GeoJSON:
geojson = { "type": "FeatureCollection", "features": [] } for _, row in df.iterrows(): geojson["features"].append({ "type": "Feature", "properties": { "name": row["name"], "address": row["address"], "region": row.get("region", "") }, "geometry": { "type": "Point", "coordinates": [row["lng_wgs84"], row["lat_wgs84"]] } }) with open("beijing_school_poi.geojson", "w", encoding="utf-8") as f: json.dump(geojson, f, ensure_ascii=False, indent=2)文件生成后,用QGIS或者GeoJSON在线预览工具打开,检查坐标是否落在正确位置,不要直接交付数据。
5.3 可视化快速验证思路
采集数据的最后一个验证步骤,是拿一张标准底图叠合检查。我在本地最常用Folium,因为它能在Jupyter里快速渲染交互式地图:
import folium m = folium.Map(location=[39.90, 116.40], zoom_start=10, tiles="OpenStreetMap") # 叠加区县边界 for _, row in boundary_gdf.iterrows(): folium.GeoJson(row["geometry"], style_function=lambda x: { "color": "#0078ff", "weight": 2, "fillOpacity": 0.1 }).add_to(m) # 叠加学校POI点 for _, row in joined.iterrows(): folium.CircleMarker( location=[row["lat_wgs84"], row["lng_wgs84"]], radius=3, color="#ff0000", fill=True ).add_to(m) m.save("school_map.html")验证的时候有三件事必须做:
- 看学校点是否落在对应区县面内,如果大量点落在面外,优先怀疑坐标系没有统一。
- 随机抽10个学校,把名称和实际印象比对,确认没有别的城市的同名POI混进来。
- 把边界面和底图乡镇/街道级道路比对,确认没有出现“边界面跑到隔壁省”这种明显错误。
6. 踩坑实录:限流、配额、坐标系与合规红线
6.1 QPS超限:最常见的高频错误
跑批量采集时,最常遇到的错误码是233或类似形式的QPS超限提示。它的含义很简单:你一秒内发的请求超过当前AK的阈值。
我第一版脚本就吃过亏,循环翻页时没有加任何延时,第10页之后连续报错。后来在翻页循环里加了动态延时:
# 根据配额动态计算请求间隔 qps_limit = 2 # 以你控制台实际的QPS为准 interval = 1.0 / qps_limit + 0.2 time.sleep(interval)这个加法很实用:在理论最小间隔上多塞0.2秒缓冲,不会慢多少,但能把超限错误率压到几乎为零。
6.2 AK权限与配额被降级的隐藏条件
有时候你上午跑得好好的,下午突然大量请求返回“AK不合法”或“配额不足”的报错,这不是AK坏了,而是配额周期重置或者账号状态异常。常见的隐藏条件包括:
- 未实名认证的AK配额极低,可能跑一小批数据就触顶。
- 同一账号多个应用共享日配额,不按应用单独计算。
- IP白名单写错导致请求被拒,这跟配额无关,但报错看起来很像。
我的建议是:开发调试用一个专门的应用AK,正式部署用另一个,两个AK的IP白名单和配额分开管理。这样调试流量不会污染生产配额,出问题也好定位。
6.3 数据再发布的版权边界
合法API拿到数据,不代表能随意分发。这里我建议遵守三条准则:
- 内部研究、论文、个人学习使用,可以存储转换,但不要大规模公开下载。
- 如果做商业产品或对外发布成果,注明数据来源,且不超出平台服务条款允许的范围。
- 不要把数据当成“原始数据包”二次打包出售,这是最容易触发法务风险的操作。
6.4 “因爬虫入狱”离我们并不远:法律红线复盘
最后说说热搜词里那个扎眼的话题“因爬虫入狱”。很多人觉得那是大公司才有的风险,个人抓点POI没事,但真实判例并不罕见。相关刑事罪名主要落在“非法获取计算机信息系统数据罪”和“侵犯公民个人信息罪”上,成立的关键通常有两个:一是未经授权或绕过技术保护措施,二是批量获取并使用了受法律保护的数据。
换成爬虫场景,一台机器高并发请求、伪造请求头突破频率限制、拿到数据后公开传播——每一步都在往这两个要件上靠。而按官方API流程走,所有操作都在平台授权范围内,授权链路清晰可查,数据使用边界明确,这才是真正能睡安稳觉的方案。
说到底,地图POI数据本身是空间信息,空间分析的价值恰恰在于合法、稳定、可持续地获取和维护它。爬虫方案每次反爬升级都会让数据管道一夜之间报废,而官方API的迭代有稳定的版本兼容机制,选谁长期更“划算”,算完这笔账其实不用犹豫。
最后分享一个我的小习惯:每次采集完数据,我都会在旁边生成一个meta.json,记录采集时间、API版本、AK末四位、总请求数、坐标系、转换方法。一来方便自己复现,二来将来跟别人分享数据时能说清楚数据来源和加工链路,在合规审查和项目复盘里能省掉大麻烦。你把这个习惯带上,数据处理项目的专业度就能明显上一个台阶。