Python调用高德API批量采集POI数据:从入门到实践
做数据分析、市场调研或者城市规划的时候十有八九会碰上“把某个城市所有药店、加油站、充电桩、小区名字全量捞出来”这种需求。以往的办法是爬网页、人工整理效率低还容易封IP。后来我发现用Python高德API能稳定地批量获取POI数据也就是地图上的兴趣点比如餐厅、停车场、公司、学校这些。这篇文章我就把整套思路和能用得上的代码完整拆出来从申请Key到接口调用、翻页、存CSV一条龙讲清楚适合刚接触爬虫和数据采集的Python学习者直接抄作业没问题。1. 这个需求是怎么来的以及为什么非高德不可1.1 POI数据到底能干什么POI就是Point of Interest的缩写翻译过来叫“兴趣点”。地图上每一个小图标不管是餐饮、酒店、公交站还是超市背后都是一条POI记录通常包含名称、地址、经纬度、所属城市等字段。这些数据看起来不起眼用起来价值很大。商圈分析把某商圈周围500米内所有奶茶店捞出来看密度和品牌分布能辅助门店选址。充电桩布局统计各个区新能源充电桩数量结合小区分布做供需分析。城市配套评估看看片区有多少学校、医院、菜市场判断生活便利度。论文和行业研究做城市地理、交通、零售方向的研究POI数据是常见的基础数据源。没有POI数据之前这些信息要人工一个个查过去几百个点就得搞一天。用API写个脚本几分钟搞定一个中大型城市效率和人工完全不在一个量级。1.2 为什么选择高德API而不是爬虫或其它地图商很多人第一反应是直接爬百度地图或高德的网页端接口但网页端的接口逻辑经常变而且返回的数据结构不稳定今天能跑明天就报错还容易触发反爬机制。高德开放平台提供的Web服务API是官方接口有文档、有配额、有稳定的返回结构长期维护成本低。虽然个人开发者配额有限但对于中小规模的数据需求完全够用。和其它地图厂商的同类接口对比高德的优势也比较明显文档清晰示例多遇到的坑基本都能在社区找到答案。数据更新频率高特别是在餐饮、零售这类动销快的业态上POI新鲜度有保障。类型分类体系完善POI有大类、中类、小类三层分类标签比如“餐饮服务”下面还有中餐厅、快餐店、蛋糕甜品店方便按行业筛选。citylimit参数可以限制在当前城市避免同名城市干扰。用爬虫去抓网页端属于“绕路”用官方API才是正路。而且高德API的文本搜索接口免费额度对于个人开发者来说只要策略合理能撑起不小的采集任务。2. 搭好环境申请Key把接口规则吃透2.1 Python环境与三方库准备开始写代码前先确认本机Python环境已经装好版本最好在3.7及以上太老的版本有些语法和库支持不太好。本次项目用到的三方库其实只要两个requests负责发HTTP请求获取接口返回的JSON数据。pandas负责把数据整理成表格并导出CSV如果不想用pandas直接用Python内置的csv库也可以。安装命令pip install requests pandas如果你用的是Anaconda那pandas是自带安装的只需要装requestspip install requests装完可以在Python交互环境里验证一下import requests import pandas as pd print(依赖库导入成功)没有报错就说明环境已经就绪。2.2 注册高德开放平台并申请Key使用高德API第一步是注册开发者账号。打开高德开放平台用手机号注册一个账号然后进入控制台。点击“应用管理” - “我的应用” - “创建新应用”。创建完成后在应用下点击“添加Key”。服务平台选择“Web服务”不要选“Web端(JS API)”那是给网页JavaScript用的。Key生成后复制保存这一段字符串后续所有接口请求都要带上它。个人认证是免费的提交一些基础信息后审核也算快。认证之后能拿到比未认证更高的配额这点建议尽早做不花时间也不花钱。拿到Key之后先做一次最简单的请求验证import requests key 你的Key url https://restapi.amap.com/v3/place/text params { key: key, keywords: 全家便利店, city: 上海, citylimit: true, offset: 25, page: 1, extensions: base } resp requests.get(url, paramsparams) data resp.json() print(data[status], data[count]) print(data[pois][0][name])如果返回status为1且有正常pois列表说明你的Key和网络链路都是通的可以进入下一步了。2.3 v3搜索POI接口的核心参数高德Web服务里的“搜索POI”接口官方叫“关键字搜索”请求地址是https://restapi.amap.com/v3/place/text这个接口虽然叫“关键字搜索”但它同时支持按类型搜索用参数types传官方类型编码即可。很多人踩坑就在这里keywords和types同时传或者传了不存在的类型编码导致返回0条数据。几个关键参数解释一下key你的Web服务Key必填。keywords查询关键字比如“银行”“超市”。如果需要按类型全量采集这个参数可以留空改用types。typesPOI类型编码用大类两位编码即可比如060000代表餐饮服务080000代表购物服务。city城市名称或adcode城市编码推荐直接传adcode避免同名城市问题。citylimit布尔值传true时只返回当前城市的结果不涉及周边城市。offset每页记录数取值范围1到25一次最多25条。page当前页码从1开始。extensions默认base返回基础字段如果传all可以拿到照片、营业时间等更多扩展信息不过响应的数据量也会变大。返回的字段里最常用的是name名称location经纬度格式是“经度,纬度”address地址pname省份cityname城市adname区县type人类可读的类型描述typecode类型编码把这几个字段落库后续做可视化或者计算距离都够用了。3. 完整代码按城市类型批量拉取POI这一节是核心实操部分。我直接给一套能跑的批量脚本然后逐段讲清楚每条逻辑的作用方便你不能只“复制粘贴”还能自己改着用。3.1 先获取行政区划列表如果只是采集单个城市不清楚城市名称对应的adcode可以先用高德的行政区划查询接口拿到城市列表顺便把下属区县的adcode也拿到因为后面做大范围采集时按区县切割数据是突破返回上限的关键手段。请求地址是https://restapi.amap.com/v3/config/district关键参数是keywords和subdistrictsubdistrict代表下级行政区层级传2就能返回省-市-区三层结构。import requests key 你的Key def get_districts(parent_id100000): url https://restapi.amap.com/v3/config/district params { key: key, keywords: parent_id, subdistrict: 2, extensions: base } resp requests.get(url, paramsparams) data resp.json() if data[status] ! 1: print(行政区划请求失败, data) return [] provinces data[districts] result [] for province in provinces: for city in province.get(districts, []): city_code city.get(adcode) city_name city.get(name) districts city.get(districts, []) result.append({ province: province.get(name), city: city_name, city_adcode: city_code }) for district in districts: result.append({ province: province.get(name), city: city_name, district: district.get(name), adcode: district.get(adcode) }) return result district_list get_districts() print(district_list[:10])拿到区县adcode之后再把采集范围精确到区县配合下一页要说的1000条上限问题就能把数据“啃”得很完整。3.2 单页请求函数与翻页逻辑POI搜索接口每次请求最多返回25条所以批量采集必须有翻页逻辑。我把单页请求封装成一个函数方便反复调用import requests import time def fetch_poi_page(key, city_adcode, poi_type, page1, offset25): url https://restapi.amap.com/v3/place/text params { key: key, types: poi_type, city: city_adcode, citylimit: true, offset: str(offset), page: str(page), extensions: all } resp requests.get(url, paramsparams, timeout10) data resp.json() if data[status] ! 1: print(f请求失败{data}) return None, 0 pois data.get(pois, []) total int(data.get(count, 0)) return pois, total这里有几个细节值得留意timeout设置为10秒防止接口长时间不返回导致程序卡死。返回的total_count是当前条件下的总条数用来判断是否继续翻页。extensions传all之后数据更全但响应体更大如果只要坐标和名称改用base更快。翻页循环的逻辑就是只要当前页还有数据就继续请求下一页直到没有数据为止。def fetch_all_pois(key, city_adcode, poi_type, max_pages100): all_data [] page 1 while page max_pages: pois, total fetch_poi_page(key, city_adcode, poi_type, page) if not pois: print(f第{page}页无数据停止翻页。) break all_data.extend(pois) page 1 # 避免请求频率过高 time.sleep(0.8) return all_data设置sleep让请求间隔在0.8秒左右既是给服务器留缓冲也避免触发QPS限制。3.3 批量循环与CSV存储采集的逻辑往往是“多个城市”ד多个类型”我用两层循环来遍历最后统一存入CSV。import pandas as pd key 你的Key city_adcodes [110000, 310000] # 示例北京、上海 poi_types [ (060000, 餐饮服务), (080000, 购物服务), (170000, 汽车服务) ] all_rows [] for city in city_adcodes: for poi_type, type_name in poi_types: print(f开始采集城市编码 {city}类型 {type_name}) pois fetch_all_pois(key, city, poi_type) for poi in pois: location poi.get(location, ) lon, lat location.split(,) if location else (, ) all_rows.append({ poi_id: poi.get(id), name: poi.get(name), type: poi.get(type), typecode: poi.get(typecode), lng: lon, lat: lat, address: poi.get(address), city: poi.get(cityname), adname: poi.get(adname), pname: poi.get(pname) }) df pd.DataFrame(all_rows) df.to_csv(poi_data.csv, indexFalse, encodingutf-8-sig) print(f采集完成共 {len(df)} 条记录)保存CSV时用utf-8-sig编码是很多新手容易忽略的细节。直接用utf-8保存的中文CSV用Excel打开时会乱码改成utf-8-sig带上BOM就正常了。经纬度单独拆成了lng和lat两列方便后续做地图可视化。location字段本身是“经度,纬度”格式直接存一个字段也行但分析时需要再拆一次没必要。3.4 完整代码整合版把上面几个部分整合成一个独立的脚本文件比如amap_poi_crawler.py可以直接跑import requests import time import pandas as pd KEY 你的高德Web服务Key def get_districts(): url https://restapi.amap.com/v3/config/district params {key: KEY, keywords: 100000, subdistrict: 2, extensions: base} data requests.get(url, paramsparams, timeout10).json() result [] for province in data.get(districts, []): for city in province.get(districts, []): for district in city.get(districts, []): result.append({ province: province.get(name), city: city.get(name), district: district.get(name), adcode: district.get(adcode) }) return result def fetch_poi_page(adcode, poi_type, page1, offset25): url https://restapi.amap.com/v3/place/text params { key: KEY, types: poi_type, city: adcode, citylimit: true, offset: str(offset), page: str(page), extensions: base } resp requests.get(url, paramsparams, timeout10) data resp.json() if data.get(status) ! 1: return [], 0 return data.get(pois, []), int(data.get(count, 0)) def fetch_all_pois(adcode, poi_type, max_pages100): all_pois [] for page in range(1, max_pages 1): pois, _ fetch_poi_page(adcode, poi_type, page) if not pois: break all_pois.extend(pois) time.sleep(0.6) return all_pois def main(): # 这里替换成你要采集的区县编码和POI类型 target_adcodes [110101, 110102] # 示例东城区、西城区 target_types [(060000, 餐饮服务), (080000, 购物服务)] rows [] for adcode in target_adcodes: for poi_type, type_name in target_types: print(f采集adcode{adcode}, type{type_name}) pois fetch_all_pois(adcode, poi_type) for poi in pois: location poi.get(location, ) lon, lat location.split(,) if location else (, ) rows.append({ poi_id: poi.get(id), name: poi.get(name), type: poi.get(type), typecode: poi.get(typecode), lng: lon, lat: lat, address: poi.get(address), city: poi.get(cityname), adname: poi.get(adname), pname: poi.get(pname) }) df pd.DataFrame(rows) df.to_csv(poi_data.csv, indexFalse, encodingutf-8-sig) print(f完成共{len(df)}条) if __name__ __main__: main()这段代码是完整可运行的直接替换KEY和target_adcodes就能采集到一份标准化的POI数据集。4. 优化、加量、避坑配额不够怎么办4.1 个人开发者配额到底有多少很多人看到网上吐槽“高德API收费坑人”其实大部分情况是没有搞清个人开发者和企业开发者配额的区别。试用阶段或个人开发者认证后高德平台会分配一定量的每日调用额度具体数值以控制台“配额管理”显示为准。对于个人项目来说单看总额度可能觉得不够但换一种思路就够用了优先按区县层面切割任务每次请求只取一个区县的一种类型。设置错误重试和日志记录避免某次请求失败后白费配额。不用的时间段错峰跑例如午休或凌晨时段执行大批量脚本。不要重复采集相同数据落地数据前做一个状态标记增量更新只跑新增部分。合理规划后个人免费配额撑起一次“单个城市核心POI采集”是可行的。如果项目确实需要千万级POI数据那就要考虑申请企业认证或者购买官方资源包这是合规路径。4.2 突破单次1000条限制的正确做法高德搜索POI接口对单次请求组合有结果数量上限实际经验中大约在1000条左右。也就是说你传了一个大类型的编码比如“餐饮服务”在某个核心城市里可能对应几万条POI但接口不会真的全部返回翻页到一定次数后就会变成空数据。解决办法就是拆粒度。按区县拆比如“北京市朝阳区餐饮服务”和“北京市海淀区餐饮服务”结果集合小了翻页能取完整。按中类拆餐饮服务060000下面有060100中餐厅、060200外国餐厅、060300快餐厅等多个中类拆到中类或小类去采集数据更精准。按关键词拆如果类型编码不够细再加keywords去缩小范围比如“奶茶”“咖啡”等关键词组合。按POI编码的周边网格拆极端情况下用经纬度网格分区中心点半径方式搜索配合后去重。用这些维度组合可以最大程度绕开1000条限制同时保证数据不丢。一个重要的经验是不同类型和区域的采集难度不一样。一线城市的市中心餐厅密度极高区县级粒度都可能超过1000条而偏远区县可能整个县才两三百条一次就能拉完。所以脚本里一定要保留count字段做日志哪个区县异常偏大单独拆开补采。4.3 数据清洗与去重API拿到的原始数据并不一定是干净的常见的坑有重复采集手动跑了两次脚本同一条POI会被存两遍。坐标漂移个别POI的坐标和实际位置有偏差尤其是新增或搬迁的门店。类型缺失部分POI的type字段为空需要根据名称或其它字段做兜底归类。名称不规范比如“麦当劳(XX路店)”和“麦当劳XX餐厅”其实是同一个实体但文本合并非常困难。在落库前至少要做一轮基础清洗。最核心的是按poi_id去重这是高德POI的唯一标识df pd.read_csv(poi_data.csv) df df.drop_duplicates(subsetpoi_id, keepfirst) df df.dropna(subset[name, lng, lat]) df.to_csv(poi_data_clean.csv, indexFalse, encodingutf-8-sig)对于名称合并问题可以加一个模糊匹配规则比如去掉括号和空格后再对比。我在实际项目中用simhash或编辑距离做过一轮尝试效果还行但不要在第一次采集时就过度清洗先把原始数据留一份再做清洗版本出了偏差还能回退。5. 高频踩坑实录如果你也碰见了这些报错5.1 常见报错对照表搜集了一些高频的问题很多是从社区和评论里看到的也有我自己踩过的。报错或现象可能原因解决方案status为0info显示USER_DAILY_QUERY_OVER_LIMIT当日配额耗尽等待次日重置或申请更高配额status为0info显示INVALID_USER_KEYKey无效或未开Web服务去控制台检查Key类型确认选了Web服务返回0条数据types和keywords使用不当或该区县确实无对应POI单独用城市名关键词测试再逐步缩小条件翻页翻到100还有数据类型粒度太粗结果超上限改成按区县、按中类拆分count很大但pois数组为空数据量超过单次查询上限接口返回了空结果拆粒度缩小范围后再采集请求超时网络不稳定或请求太频繁增加睡眠时间使用重试机制提高超时阈值获取到的cityname为空部分POI关联的城市信息缺失不做依赖直接用你传入的adcode做城市字段兜底Excel打开CSV乱码编码用了utf-8而不是utf-8-sig改用encodingutf-8-sig保存看到这里你可能会问为什么网络那么多人说高德API“坑”实际多数情况是参数没有理解透或者是用企业级需求套个人免费额度当然会觉得不够用。按照上面表格里的思路排查大部分问题都能定位。5.2 我的几个实操建议最后分享几条我自己的习惯不一定写在官方文档里但在实际项目中非常管用。第一请求重试机制一定要加。网络请求偶尔失败是正常的推荐用Retry库或者自己写个简单的重试装饰器遇到超时或者网络异常时最多重试3次间隔1秒。这比一次失败就中止整个脚本要稳得多。第二日志一定要记录。每次请求城市、类型、页码、返回条数都写到日志里跑完反而不会看出了故障就特别有用。尤其是大批量任务没有日志的话跑到一半挂了根本不知道断在哪个区县、哪个类型。我的做法是直接在脚本里加一行print同时用logging模块输出到文件双保险。第三多加一个“去重上报”机制。在采集完成后用poi_id对比已有数据只保留新增ID这样增量更新时既节省配额又避免数据重复。思路很简单existing_ids set(df_old[poi_id]) df_new df_new[~df_new[poi_id].isin(existing_ids)]第四休息时间要够。不要在短时间内把一个账号的额度打光因为配额是按日统计的用完之后当天就不能再请求而很多采集任务其实可以拆分成几天分步执行。在同一天内过度集中请求还容易触发平台的QPS限制。第五别忽略高德的type体系。POI类型编码不是随便填的一定要先看官方《POI分类编码表》。有些大类下面嵌套着更细的中类和小类比如060000餐饮服务下面还有060100中餐厅、060200外国餐厅、060300快餐厅等把类型编码理解到位采集出来的数据才符合预期。我在实际项目中做得最多的一件事就是用一个“类型区县”的组合清单每天自动跑一遍把新增POI追加到数据库里这个流程已经稳定跑了大半年除了偶尔配额被耗尽导致某一天空跑之外整体效果很不错。你把这个脚本跑通之后也可以按自己的业务需求把范围扩到全省、全国甚至做定期增量更新只要注意拆分维度和配额控制整套方案是撑得住长期使用的。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →