拓冰建站拓冰建站
首页 / 资讯中心 / 正文

全国POI数据处理实战:坐标系转换、清洗去重与PostGIS入库

做实体门店选址、城市商圈分析或者交通可达性研究的朋友这几年大概率都被同一个问题卡过到底哪儿能找到一份干净、能直接用的全国POI数据尤其到了2025年城市变化快新旧POI混杂很多公开渠道抓下来的数据不是缺字段就是坐标漂移真正拿来用之前还要做一堆苦活。这篇东西就把我从数据获取、坐标系处理到入库应用的完整踩坑过程整理出来给准备自己动手拉“中国分省/分市POI点位数据”的人当一份参考手册。无论你是刚接触POI数据的新手还是已经做了几年空间数据分析的老兵里面讲的很多细节应该都能帮你少走弯路。1. 先搞懂POI数据到底是什么1.1 POI不是地图上那个小图标POI是Point of Interest的缩写翻译过来就是“兴趣点”。很多人第一反应是地图App上那些小图标餐厅、加油站、学校、医院、商场……确实这些都是POI的典型例子但把POI简单理解成“地图上的小图标”会低估它。一条合格的POI数据核心是结构化信息至少包含几样东西名称、地址、经纬度坐标、分类。有的数据源还会带上联系电话、营业状态、营业时间、评分、楼层、标签甚至人均消费。这些附加信息才是真正拉开数据价值差距的地方。比如你在高德上搜“咖啡店”返回的结果里有“评分”“人均价格”“营业时间”这些字段对品牌选址分析来说比一个孤零零的坐标有用得多。再说一个经常被忽略的点POI是有生命周期的。一家店今年还在营业明年可能就关了一个商场正在建的时候在地图上是“施工中”建成后才变成“购物中心”。所以“2025年POI数据”本质上是一个时点快照而不是永久有效的静态数据。真正做项目的时候不光要看数据是哪一年的还要看数据的更新日期、更新频率否则很容易拿旧数据做出“过时”的分析结论。1.2 2025年的POI数据规模长什么样分省/分市POI数据从规模上说已经不是一个Excel能装下的东西了。国内主流互联网地图平台覆盖的POI总量早就突破亿级细分到某个一线城市少说也有几十万个有效POI一个中等省份几百万个很正常。当你把“全国”拆成“分省/分市”来做数据结构要考虑的就不是“能不能装下”而是“能不能批量处理、能不能空间检索”。我见过很多团队一开始拿到的原始数据是几百个CSV文件每个文件按城市或区域切分字段格式还不完全一致。有的文件里“省市区”是三个字段有的合并成一个“行政区”字段有的甚至没有区县信息。这种数据要落地到数据库里第一步不是建索引而是先做字段结构统一。另外2025年的POI数据已经不只是“点数据”了。很多平台会给POI关联上道路、楼宇轮廓、楼层平面等信息形成更复杂的实体关系。但从绝大多数业务需求来看把POI作为“带属性的点”来建模仍然是最基础、最通用的做法。后面讲到的清洗、去重、入库方法也都围绕“点数据”展开。2. 数据从哪来2025年主流获取渠道2.1 免费渠道怎么选先说免费渠道。提到POI数据绕不开三个国内主流地图开放平台高德、百度、腾讯。三者都提供Web服务API注册开发者账号、申请Key之后就能按区域和分类检索POI。各有优劣我整理了一张表平台坐标系分类体系日配额普通个人认证典型问题高德GCJ-0220大类细分到几百小类个人版每天几千次到几万次单次最多返回25条需要分页百度BD-09分类体系独立和别家对不上额度相对紧张坐标偏移更大必须先转换腾讯GCJ-02分类粒度偏粗额度一般覆盖度不如高德免费渠道最大的问题不是“拿不到”而是“拿不全”。API做了访问频率限制和单次返回条数限制你要拉一个省的数据得按城市、按区县、按分类去循环请求还要处理分页。遇到晚高峰时段接口经常超时重试逻辑写得不好就断在半路。所以免费渠道适合做小范围试点一个城市的一个分类或者一个区的全量POI基本够用。想用它拉全国数据时间成本和稳定性成本都很高。另一个免费来源是OpenStreetMapOSM。OSM的国内POI数据覆盖度比商业图商差很多但胜在开放许可、字段透明、没有任何调用配额。对学术研究和开源项目来说OSM是很好的底图参考。把OSM和商业数据做交叉验证也能发现不少字段缺失和分类错误。2.2 商业数据采购和谈判要点如果你的项目要求全国分省/分市都覆盖而且后续要做持续性监测我建议直接考虑商业数据采购花钱买时间节省下来的时间用来做清洗和分析值多了。市面上的专业数据服务商很多核心交付物通常是全国POI矢量数据包格式一般是Shapefile、GeoJSON或CSV交付时会按省份或者城市拆好。买数据的时候别只问“多少钱一省”有几个关键点必须确认坐标系到底给的是什么有的是WGS84有的是GCJ-02有的是CGCS2000。如果给的是WGS84但实际上是GCJ-02后面做空间分析偏差几百米图纸全废。更新频率是多少月度更新、季度更新还是年度更新你要做的是“2025年数据”但服务商给你的可能是2024年底的快照要问清楚数据的截至时间。字段完整到什么程度有没有统一分类省市区字段齐不齐是否包含电话、营业状态授权范围是什么用在内部研究、外部咨询报告还是给客户做产品授权边界不一样价格差很多。2.3 能拿到的数据不都能随便爬关于爬虫这事必须多说一句。地图App上的POI数据从法律和服务条款上讲版权和归属权都归数据平台所有。API开放出去就是为了让你“按规则获取”这个“规则”通常包括频次限制、字段限制和使用限制。用脚本绕过限制去全量抓取既有法律风险也容易被封号。我自己只建议两种做法一种是老老实实调官方API在额度范围内按区域、分类拉数据另一种是购买有正规授权的商业数据。前一种适合小范围验证后一种适合正式项目落地。不要图省事去网上找那种“破解版全国POI全套2025”的资源数据来源不明不说坐标系和字段质量完全没法保证出了问题连个售后都找不到。3. 拿到的原始数据怎么变成能用的分省/分市数据3.1 字段设计和数据模型不管从哪个渠道拿到的原始数据最终落到自己库里的表结构都应该有一套统一标准。我建议至少包含以下字段字段名类型说明poi_idstring唯一标识用平台原始ID加来源前缀避免冲突namestring名称provincestring省直辖市/自治区citystring市地级市/自治州districtstring区县addressstring地址longitudedouble经度latitudedouble纬度lng_lat_geogeometry/point统一坐标系后的空间字段category_l1string一级分类如餐饮、购物category_l2string二级分类如中餐厅、快餐sourcestring数据来源如amap、baiduupdated_attimestamp获取/更新时间很多团队成员会问既然原始数据里已经有经度纬度两个字段为什么还要单独搞一个lng_lat_geo空间字段因为绝大多数分析场景都需要做空间计算比如距离计算、缓冲区、密度分析。如果每次都用longitude和latitude临时拼点PostGIS这种空间数据库的优势发挥不出来更关键的是长表和宽表之间做JOIN的时候有空间索引和没有空间索引的查询效率差别非常大。3.2 坐标系问题GCJ-02、BD-09和WGS84互转这是POI数据处理里踩坑最多的地方。国内互联网地图普遍采用GCJ-02坐标也就是俗称的“火星坐标”。它是在WGS84的基础上做了一次非线性偏移普通GPS设备直接采到的WGS84坐标放到高德地图上会偏几百米。百度又搞了一套BD-09在GCJ-02基础上再做一次偏移。简单说同一个POI按WGS84、GCJ-02、BD-09分别描述经纬度是三个完全不同的数。用生活化类比来解释坐标系不统一就像你用米尺量布、你的合作伙伴用英尺量布量出来的数值都对但扔到一起比较就完全错位。所以拿到数据后的第一件事确定它到底用的是哪套坐标系如果混用了必须做统一转换。下面这段Python代码是我常用的WGS84到GCJ-02转换参考实现AI共识度较高的标准算法处理几百万个点也没有问题import math def out_of_china(lng, lat): return not (73.66 lng 135.05 and 3.86 lat 53.55) def transform_wgs84_to_gcj02(lng, lat): 将WGS84坐标转换为GCJ-02坐标火星坐标 a 6378245.0 ee 0.006693421622965943 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 if out_of_china(lng, lat): return lng, lat d_lat _transform_lat(lng - 105.0, lat - 35.0) d_lng _transform_lng(lng - 105.0, lat - 35.0) rad_lat lat / 180.0 * math.pi magic math.sin(rad_lat) magic 1 - ee * magic * magic sqrt_magic math.sqrt(magic) d_lat (d_lat * 180.0) / ((a * (1 - ee)) / (magic * sqrt_magic) * math.pi) d_lng (d_lng * 180.0) / (a / sqrt_magic * math.cos(rad_lat) * math.pi) return lng d_lng, lat d_lat注意这段代码只做WGS84到GCJ-02的转换反过来做逆转换时会有一定精度损失因为GCJ-02偏移不是严格可逆的系统一般而言反向转换精度在米级做分析足够用了。百度BD-09到GCJ-02的转换算法也是公开的网上能找到标准实现。还有一点要提醒如果你和规划院、测绘院打交道他们常用的坐标系不是GCJ-02而是国家2000大地坐标系CGCS2000。GCJ-02与CGCS2000之间不是简单公式就能转换的需要带七参数或格网改正的第三方库比如pyproj配合提供的转换参数。遇到这种需求不要图省事直接套网上流传的近似公式误差很容易到几十米。3.3 清洗和去重不能只去完全相同的行POI数据清洗最容易踩的坑是“误杀”和“重复并存”。所谓重复不只是两条记录所有字段都相同更多是名称相似、坐标接近、分类一致的情况。比如同一个商场餐饮区入口一个点、购物区入口一个点、停车场入口一个点在地图平台上很可能被登记成三条POI。我自己常用的去重思路分三层第一层精确去重。把poi_id、source、name、longitude、latitude完全相同的记录去掉只保留一条。这是最基本的CSV里常见。第二层字段归一化后去重。名称里的空格、全角半角、繁体简体、品牌后面的分店名等先做清洗统一然后再按namedistrictcategory_l2去重。第三层空间去重。在PostGIS里按50米或100米缓冲区合并相邻的两个点如果分类相同且名称相似度很高就认为重复。这一步需要设置好阈值阈值太小去重不干净阈值太大会把同一个商圈里两家相邻的同类门店误删。空间去重的SQL大致长这样-- 在PostGIS中按100米距离内查找相同一级分类的潜在重复POI SELECT a.poi_id AS poi_id_a, b.poi_id AS poi_id_b, a.name AS name_a, b.name AS name_b, ST_Distance(a.geom, b.geom) AS dist FROM poi_table a JOIN poi_table b ON a.category_l1 b.category_l1 AND a.poi_id b.poi_id AND ST_DWithin(a.geom, b.geom, 100) WHERE a.city 上海 AND b.city 上海 LIMIT 100;使用空间去重之后一定要人工抽样检查结果。我见过有项目把同一连锁品牌紧挨着的两家店误合成一条结果门店数量统计少了10%以上。4. 实操自己拉一份分城市POI并落库4.1 用高德API按城市拉数据假设你想自己动手用一个普通数据工程师的配置拉某一个省份的全量POI。以高德开放平台为例流程不复杂但细节不少。第一步注册开发者账号并创建应用获取Key。第二步确认要拉取的城市列表可以从行政区划接口拉也可以直接写死一份地级市清单。第三步确定分类。高德的分类体系提供20多个一级分类每个一级分类下又细分很多二级分类。先拉一级分类避免分类太细导致请求次数爆炸。第四步循环调用搜索POI接口参数包括city、keywords或types、offset、page、key。每页返回最多25条需要循环翻页到没有数据为止。这里最关键的是控制频率。普通开发者key有每日调用量限制QPS限制通常是每秒几次。要是用多线程猛拉很快就会被限流甚至封Key。我自己习惯在请求之间加time.sleep比如每请求一次休息0.2秒虽然速度慢一些但稳定性好很多。一份参考实现如下import requests import time API_URL https://restapi.amap.com/v3/place/around def fetch_poi_by_city(city, types, key): page 1 result [] while True: params { key: key, location: , # 如果不设置按区域搜索时用city字段 city: city, types: types, offset: 25, page: page, extensions: all, } resp requests.get(API_URL, paramsparams, timeout10) data resp.json() if data.get(status) ! 1: break pois data.get(pois, []) result.extend(pois) if page int(data.get(count, 0)) // 25 1: break page 1 time.sleep(0.2) return result这段代码里有一个很重要的细节高德的“around”接口是按中心点搜索的不适合按城市遍历要按城市拉全量应该用“place/text”或者“place/around”配合city参数。具体接口名要看你申请的服务类型。拉下来的数据是JSON格式包含大量字段需要用pandas或PySpark转成规整的表。4.2 坐标纠偏和存储从高德拿回来的坐标本身就是GCJ-02不需要二次转换。但如果你把多个来源的数据合并到一起就要先做坐标统一。我建议最终的存储坐标统一为GCJ-02因为你在国内业务里最常见的叠加底图是互联网地图直接能对得上如果想做严格的测绘分析再转成CGCS2000。数据库选型上个人托管方案首选PostgreSQL加PostGIS。原因很简单空间索引性能好SQL接口灵活和Python衔接顺畅。建表语句可以这样写CREATE TABLE poi_2025 ( poi_id text PRIMARY KEY, name text NOT NULL, province text, city text, district text, address text, longitude double precision, latitude double precision, category_l1 text, category_l2 text, source text, updated_at timestamptz, geom geometry(Point, 4326) ); CREATE INDEX idx_poi_2025_geom ON poi_2025 USING GIST (geom); CREATE INDEX idx_poi_2025_city ON poi_2025 USING btree (city);注意geom字段用SRID 4326也就是WGS84的空间参考标识。如果你最终统一到GCJ-02严格意义上应该自定义一个SRID但日常分析用4326也不会出大问题只要所有数据都统一采用同一套坐标系即可。4.3 数据质量校验入库之后千万别急着分析先做四件事第一检查坐标范围。中国境内的有效经纬度大致在73.66到135.05度经度、3.86到53.55度纬度之间。超出这个范围的记录基本都是脏数据。最直接的方法是写个SQL把范围外的记录挑出来SELECT COUNT(*) AS invalid_cnt FROM poi_2025 WHERE longitude 73.66 OR longitude 135.05 OR latitude 3.86 OR latitude 53.55;第二检查分类覆盖率。按省、市分组统计一级分类的POI数量看是否有明显偏少的分类。如果一个城市连“餐饮”这个一级分类都没拉到数据很可能是分类代码传错了而不是真的没有餐饮。第三抽查坐标与地址是否匹配。随机提取100个POI在高德或百度地图里输入坐标反查位置再比对该POI的名称和地址。如果偏差超过200米说明坐标转换或坐标系识别出了问题。第四检查重复率。按“省市区namecategory_l1”分组统计重复记录。重复率超过5%就要警惕可能不是数据源重复而是同一个POI在多个平台都有登记合并策略没做好。5. 分省/分市POI能拿来干什么5.1 门店选址与商圈分析这是POI数据最经典的应用场景。品牌方要做门店选址通常需要知道每个城市的商圈分布、业态竞争度和人流潜力。拿到分省/分市POI数据后可以按500米网格统计餐饮、购物、酒店、写字楼等各类POI数量生成一张“商圈热度图”。哪个网格的餐饮密度高、便利店密度高大概率就是成熟商圈或社区中心。实际操作中我会把POI数据和夜间灯光数据或人口栅格叠加使用。POI数据反映的是“功能供给”人口数据反映的是“居住需求”。两个维度交叉分析能比单看POI密度更准确地判断某个位置适不适合开店。5.2 城市生活便利度评估15分钟生活圈是近几年城市规划里的高频词。要做这个评估需要小区、学校、医院、菜市场、公园等设施的空间分布数据。POI数据正好可以提供这些设施的位置。把每个居民小区的质心提取出来按路网或直线距离计算到最近菜市场的距离、到最近公园的距离然后聚合到街道或社区就能量化每个区域的便利度。这中间要注意一个陷阱POI点的坐标精度决定距离计算的可靠性。如果坐标误差在100米以上算出来的“到最近设施距离”就没有实际意义。所以做小尺度可达性分析之前先做坐标精度验证是必须的。5.3 历史对比与趋势预判如果你能拿到2023年、2024年和2025年三个年份的分省/分市POI数据就能做纵向对比。比如比较某个新城区餐饮POI三年间的增长率判断该区域是否进入人口导入期。更细的玩法是把新增POI按二级分类拆开观察“咖啡店”“奶茶店”“健身房”等品类在不同年份的扩张节奏这往往能反映消费升级趋势。做纵向对比的时候最怕口径不一致。2024年的数据里“餐饮”包含“小吃快餐”2025年的分类里可能把“小吃快餐”并到“美食”里去了。所以做时间序列分析前必须把两期数据的分类体系映射到同一套标准。这也是为什么我在前面强调建表时一定要保留source和category_l1/l2字段。6. 常见问题与避坑指南6.1 字段缺失和分类不一致怎么办免费接口返回的数据分类不稳定是个常见问题。同样是“银行”有的接口返回“金融保险服务”有的返回“银行”有的返回“商业设施”。我处理这类问题的方法是建一套“主分类映射表”把不同来源的分类代码统一映射到内部标准分类。比如高德的“餐饮服务”、百度的“美食”、OSM的“cuisine”都映射到内部字段“餐饮”。字段缺失没有太多捷径可走要么从其他字段反推补齐要么标记为“未知”。要注意的是不要为了追求完整性硬造数据。如果把缺失电话号码全部填“0000000”后面统计覆盖率时就被污染了。建议保留缺失值在分析时用isnull判断。6.2 数据量太大怎么处理2025年一个省会城市的POI量级可能就到几十万条全国分省/分市合并后经常上千万条。直接用pandas读取全量CSV经常内存爆掉。我推荐两种思路一是用DuckDB或SQLite做本地分析SQLLoad-on-demand避免一次性全量载入二是把CSV转成Parquet格式按省份分区存储。Parquet列式存储配合文件分区做group by和filter时速度快得多而且Python生态里用polars或duckdb读取都很方便。如果预算允许直接把PostGIS作为主存储再配合QGIS或者GeoServer做可视化。空间索引建好之后千万级点的按城市筛选和缓冲区查询秒级返回没有问题。6.3 别把两个POI搞混地理POI和Apache POI这里必须单独说一句搜“POI数据集”的时候经常会看到“Apache POI”的内容那是Apache基金会出的Java库用来读写Excel、Word等Office文件。它和地理空间里的兴趣点POI完全不是一回事。比如前几年爆出过Apache POI 4.1.0及以下版本在XSSFExportToXml功能上的XXE漏洞那是Java代码库的安全问题跟地图点位数据无关。所以你在网上看到“POI漏洞”之类的话题先确认一下讨论的是地理POI还是Java POI别被带偏了。6.4 如果你需要更专业的处理如果你只是想快速看看某个城市的POI密度直接用高德API拉一遍数据、丢进QGIS做热力图就够了。但如果你要做面向全国的业务系统或者需要持续监测POI变化我还是建议花点成本引入专业数据处理流程。坐标转换、分类映射、去重策略这三大模块每一条都有很多细节踩过一次坑之后你会深刻理解“数据工程”这四个字的重量。我在实际项目里踩过最大的坑是第一次把高德数据和GPS采集数据放到一起做叠加分析所有点都偏了几百米。排查了很久才发现一方是GCJ-02一方是WGS84没有统一坐标系就直接分析。从那之后我每接一批数据的第一件事就是确认坐标系并把统一坐标写进数据入库流程里。另外也想提醒新手一点POI数据不是越多越好。一个县的POI有20万条看起来很强但如果这20万条里有一半是重复采集、坐标漂移、分类错误那比只有5万条干净数据更难用。先把质量校验做好再谈规模。做完一套全国分省/分市POI数据你收获的不只是一堆点位更是一套能反复复用的数据处理流程。这套流程以后用来接其他城市的POI、接门店客流数据、接道路数据都是相通的。数据本身会过期但处理数据的方法论不会。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门