把一张 Excel 表格变成地图上能点、能高亮的区域,中间隔着的那个东西,通常就是 GeoJSON。地理信息这行做久了会发现,真正卡住大多数人的不是渲染,而是手上那份数据的坐标系、几何格式、行政边界对不上——而在线制作 GeoJSON 恰好能把这些环节压缩进浏览器里完成,不用装 ArcGIS,也不用配 Python 环境。
这篇内容写给几类人:手上有一堆门店/客户/设备的经纬度,想让它们在地图上显示出来的运营和产品同学;做数据可视化,需要省市区边界轮廓的前端和数据分析同学;刚接触地理信息,被 Shapefile 那一堆 .shp/.dbf/.prj 文件搞晕的 GIS 入门者;以及那些搜过"geojson 用什么软件打开"、结果被引到一堆看不懂的文档里的朋友。下面我会把坐标系、在线制作路线、工具选择、阿里 geojson 这类行政区划数据的获取、文件瘦身和一堆实际踩过的坑全部摊开讲,尽量做到看完能直接上手。
1. 坐标系这件事不聊透,后面的活基本白干
1.1 GeoJSON 的骨架其实只有三种东西
很多人第一次打开 GeoJSON 文件会被吓到,一屏的方括号和大括号。其实它的规范(RFC 7946)非常克制,几何体一共就七种:点、多点、线、多线、面、多面、几何集合。再加两个包装:Feature(一个几何体 + 一组属性)和 FeatureCollection(一堆 Feature 的集合)。日常业务里 90% 的场景只会用到三种——点、线、面。
结构上记住一句话就够了:外面是 FeatureCollection,里面装 Feature,Feature 的 geometry 装坐标,Feature 的 properties 装业务字段。剩下那一堆括号,全是坐标数组。地理信息数据之所以能跨工具流通,就是因为这套结构足够简单,任何语言都能几行代码解析。
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "id": 1, "name": "示例点位", "type": "store" }, "geometry": { "type": "Point", "coordinates": [116.397, 39.908] } } ] }1.2 坐标顺序反了,是新手最容易栽的跟头
上面那段代码里coordinates是[116.397, 39.908],经度在前、纬度在后。这是 GeoJSON 规范强制要求的(x 在前、y 在后),但国内大量工具和接口的习惯是lat, lng,纬度在前。你在 Excel 里把两列粘到在线工具里的时候,只要顺序反了,点会跑到南极洲附近或者干脆渲染不出来。
判断方法很简单:中国的经度范围大约是 73 到 135,纬度大约是 3 到 54。如果你看到一个点的坐标是[39.9, 116.4],那第一位数 39.9 落在纬度区间里,几乎可以肯定顺序反了。我处理过好多次别人发来的"地图打不开"的数据,三分之一都是这个问题。
1.3 WGS84、GCJ02、BD09:偏移是真实存在的
这是国内做地理信息绕不过去的一道坎。同一个地点,用不同的坐标系表达,落在地图上的位置能差几百米。
| 坐标系 | 常见来源 | 与 WGS84 的关系 |
|---|---|---|
| WGS84(EPSG:4326) | GPS 设备、卫星影像、OpenStreetMap | 基准,无偏移 |
| GCJ02 | 高德、腾讯地图及其开放接口 | 在 WGS84 基础上做了非线性偏移 |
| BD09 | 百度地图及其接口 | 在 GCJ02 基础上再偏移一次 |
偏移量在城市里通常在 300 到 600 米之间,看着不大,但在一张 15 级缩放的地图上,足够让你的门店标记跑到隔壁街区。更麻烦的是这个偏移不是简单的加减一个常数,而是跟经纬度相关的非线性函数,所以只能靠转换库或者在线转换服务来批量处理,不能手算。
1.4 三招判断手上数据是哪个坐标系
不用去查数据文档,用下面三个办法基本能定性:
- 看来源:从 GPS 设备、无人机、OSM、卫星底图上取的,基本是 WGS84;从高德、腾讯的接口或者它们底图上描的,是 GCJ02;从百度系产品里导的,是 BD09。
- 叠已知地标:挑一个你百分百确定位置的点,比如某城市火车站站前广场,把它分别按三种坐标系投到同一张底图上比对,哪个严丝合缝就是哪个。
- 看数值分布:这个方法只能辅助。有些转换工具处理后会留下极其微小的尾数差异,但大多数情况下看不出来,别指望靠这个。
转换本身不难,前端有 gcoord 这类库,Python 有 coord-convert 系列包,在线也有批量转换工具。关键是转换之后一定要把 properties 里的说明字段一起改掉,我见过太多项目里数据已经转成 GCJ02 了,字段里还写着 wgs84,半年后接手的人照着字段用,又是一轮排查。
2. 在线制作 GeoJSON 的四条路线,先选对再动手
2.1 鼠标描点:适合零星、不规则、需要人工判断的区域
这是最直观的一条路,打开一个在线编辑器,底图铺开,工具栏里选点、线、面,在图上直接画。画完在右侧的表格里填属性,导出就是 GeoJSON。适合的场景:临时圈一块配送范围、标几个候选门店位置、画一条巡检路线。
工具上,geojson.io 是这一类里认知度最高的,界面左侧地图、右侧 JSON、中间工具栏,改完立刻能看到文本变化,特别适合用来理解"我画的东西到底对应哪段结构"。它的替代品还有 geojson.net 等一批同类站点,功能大同小异。
这里有一个必须提醒的坑:你在哪张底图上描,描出来的就是那张底图的坐标系。如果你用的是高德或腾讯的瓦片底图,描出来的点实际是 GCJ02;如果你用的是 OSM 或者卫星影像,描出来的是 WGS84。为了让手绘数据跟后续要用的底图对齐,描点的时候尽量选择和最终底图同源的瓦片,能省掉一次转换和一次对不上时的排查。
2.2 表格转几何:批量点位最省事的路子
如果你的数据已经在 Excel 或 CSV 里,每行有经度和纬度两列,那就不该手绘,应该直接转。这条路的效率高出一个数量级,几百上千个点几秒钟就出来了。
在线工具里,把 CSV 拖进支持表格导入的编辑器,指定哪一列是经度、哪一列是纬度,它会自动生成点要素。需要注意几个细节:
- 列名要规范。尽量用
lng/lat或者longitude/latitude,有些工具认x/y,但经度、纬度这种中文列名识别率很低,改一下列名比事后调整划算得多。 - CSV 编码要用 UTF-8。Excel 直接"另存为 CSV"在国内环境下很容易出 GBK 编码,导入之后属性里的中文全是乱码。稳妥做法是另存为"CSV UTF-8(逗号分隔)"。
- 空值行要先清理。经纬度缺失的行会让整批转换失败,或者生成 geometry 为 null 的要素,后续渲染时又得单独处理。
如果量特别大或者要跑成固定流程,用脚本更稳。这是我最常用的写法:
import json import pandas as pd df = pd.read_csv("points.csv", encoding="utf-8") features = [] for _, row in df.iterrows(): if pd.isna(row["lng"]) or pd.isna(row["lat"]): continue features.append({ "type": "Feature", "properties": { "name": str(row["name"]), "type": str(row.get("type", "")), }, "geometry": { "type": "Point", "coordinates": [float(row["lng"]), float(row["lat"])], }, }) fc = {"type": "FeatureCollection", "features": features} with open("points.geojson", "w", encoding="utf-8") as f: json.dump(fc, f, ensure_ascii=False)ensure_ascii=False这一句不加,导出的文件里中文会变成\u5f20\u4e09这种转义形式。文件本身是合法的,但人看着难受,做代码 review 的时候也容易被当成问题。
2.3 行政区划边界:要的是现成的面,不是自己画的面
做省市区着色图、做区域筛选、做数据下钻,需要的是行政区划的边界多边形。这个东西千万别自己描,描一个区县能把人描到崩溃,而且精度和拓扑一致性都没法保证。
获取路径有几条:一是各类公开的地理数据服务平台,通常提供按行政区代码查询边界的能力;二是 OpenStreetMap,通过它的边界关系导出,数据详细但格式需要再处理;三是国家地理信息公共服务平台,数据权威,但使用前要认真读一遍它的使用条款,别踩到授权问题上。
后面第 5 节我会专门讲阿里那套被搜索最多的行政区划数据,那里面的细节坑比想象中多。
2.4 格式转换:你手里八成不是 GeoJSON
很多人的起点根本不是一张白纸,而是别人给的 Shapefile、KML、甚至 CAD 的 dxf。这时候"在线制作"的实际含义是"在线转换"。
Shapefile 要特别注意,它从来不是一个文件,而是一组:.shp存几何、.dbf存属性、.shx存索引、.prj存坐标系。四个文件缺一个就打不开。在线转换时要把这一组文件一起打包成 zip 再上传,只传 .shp 是没用的。
KML/KMZ 相对省事,是单文件,导入导出一般都支持。CAD 的 dxf 麻烦一些,线宽、图层、块参照这些东西在转换时会有信息损失,通常建议先在桌面 GIS 里导入、检查图层、再导出成 GeoJSON。坐标系同样要在这个过程中确认一遍,CAD 图纸里的坐标经常是施工坐标系或地方坐标系,不是标准的经纬度,直接转出来的 GeoJSON 坐标值可能是几十万上百万,前端根本没法用。
3. 完整走一遍:从空白画布到可交付的 GeoJSON
3.1 先把绘制顺序想清楚
假设我要给一个连锁品牌圈出三家门店的配送范围,步骤是这样的:
- 先确定输出目标。这份数据是给内部看,还是要落到前端做交互?内部看的话精度和体积都不敏感,前端交互的话后面第 6 节的瘦身必须做。
- 统一坐标系。先问清楚最终地图底图是什么。如果是高德底图,最终数据就要是 GCJ02,手绘时也应该用高德瓦片。
- 放大到 14 级以上再画。缩放级别太低的时候,屏幕上一个像素代表的实际距离可能有几百米,画出来的边界误差极大。我的习惯是至少到街道级别可见再动手。
- 画点先于画面。点位好确认,画完之后可以拿点位去反推面的边界是否合理。
- 先粗后细。先把大致形状框出来,再逐个顶点微调,别一开始就追求顶点级精确,那样改起来非常痛苦。
3.2 属性字段怎么设计才不返工
画几何不难,难的是属性字段设计得一塌糊涂,后来全部返工。我总结了几条经验:
- 字段名用英文小写加下划线。
store_name比门店名称通用得多,虽然 GeoJSON 本身支持中文键,但下游但凡经过一次 Shapefile 转换,中文键就有乱码风险。 - 一个要素一个稳定的唯一 ID。这个 ID 是后面做数据关联、做增量更新、做点击高亮的基础。千万别用数组下标当 ID,数据一排序就全乱了。
- 类型统一。数值字段就存数值,不要一会儿是
123,一会儿是"123"。前端拿到不一致的类型,排序和比较都会出问题。 - 少用 null。用空字符串或者干脆不写这个字段,都比 null 处理起来省心。
- 预留一个
category之类的分类字段。地图上做分级配色、做图例,基本都靠这个字段。
3.3 导出前的自检清单
每次导出前,我都按下面这张表过一遍。这五条检查花不了一分钟,但能挡掉绝大多数后续问题。
| 检查项 | 判断方法 | 不合格的表现 |
|---|---|---|
| 坐标顺序 | 看第一个坐标,经度是否在 73-135 之间 | 点在境外或直接不显示 |
| 坐标系匹配 | 叠加到目标底图上比对地标 | 整体或局部偏移几百米 |
| 面是否闭合 | 面的首尾坐标是否完全相同 | 渲染异常、填充失败 |
| 属性完整性 | 抽查几个要素的 properties | 字段缺失、中文乱码 |
| 文件体积 | 看导出的文件大小 | 省市级数据超过几 MB |
4. geojson 用什么软件打开:按场景分,别乱试
4.1 只想看一眼:浏览器就够
如果只是"这个文件里到底是什么",最省事的办法有几个。geojson.io 直接粘贴内容或者拖文件进去就能看,同时右侧能看到原始文本,改一个坐标地图立刻响应,非常直观。mapshaper 更适合看大文件,加载快,还能顺手做简化和格式转换。GitHub 现在也能直接渲染仓库里的 .geojson 文件,团队协作的时候把数据提交上去,点开就是一张地图,这个功能知道的人不多但非常好用。
另外像 kepler.gl 这类工具,适合把 GeoJSON 拖进去做点密度、热力、轨迹之类的可视化探索,不算"打开看看"这个层级,但如果你要做数据探索,它比在 QGIS 里点来点去快。
4.2 要编辑和分析:上桌面 GIS
QGIS 是免费里最能打的,直接把 .geojson 拖进图层面板就行,不需要任何转换步骤。它的价值在于:批量编辑属性表、按位置选择、做空间连接、坐标系动态投影。这些操作在在线工具里基本做不了。
ArcGIS 系列需要多一步,GeoJSON 不是它的原生格式,得走 JSON To Features 之类的工具转成要素类。如果你的单位只有 ArcGIS,那就按这个流程走,转完之后坐标系记得确认。
桌面 GIS 有一个容易被忽略的好处:它能明确告诉你数据现在是什么坐标系。右键图层看属性,投影信息一目了然。在线工具往往不告诉你这些,这也是很多人"在网页上看是对的,换到别的地方就偏了"的原因。
4.3 要写代码:开发者的日常姿势
做前端的话,VS Code 装一个能预览 GeoJSON 的插件就够了,边改边看。真正调试地图渲染的时候,还是得在页面里用 Leaflet、OpenLayers 或者各家地图 SDK 加载,因为你要验证的是"在实际渲染环境下对不对",而不是"在编辑器里对不对"。
做数据分析的话,Python 里 geopandas 读 GeoJSON 就是一行:gdf = gpd.read_file("data.geojson"),读进来直接当表格用,画图可以用 folium 直接出交互式 HTML。这条路适合做数据清洗和批量处理,比在图形界面里点强得多。
4.4 打开之后一片空白,按这个顺序排查
白屏是最常见的求助场景,排查顺序我一般是这样:
- 看坐标数值范围。如果坐标是
[12900000, 4800000]这种量级,那是投影坐标(米为单位),不是经纬度。GeoJSON 规范要求用经纬度,这种数据得先反投影回 WGS84。 - 看几何类型和坐标层级是否匹配。
Point的坐标是[lng, lat],Polygon的坐标是[[[lng, lat], ...]],少一层或多一层方括号都会解析失败。这是手写 JSON 时的高发错误。 - 看文件里是不是别的东西。TopoJSON、Esri JSON 的字段名和 GeoJSON 都不太一样,有时候文件后缀是 .geojson 但内容是别的格式,工具加载时会静默失败。
- 看编码。带 BOM 头的文件,某些解析库会直接报错。
- 看渲染配置。前面几步都没问题的话,问题大概率在前端:图层没加到地图上、样式里 opacity 设成了 0、或者数据范围跟当前视野完全不重叠。
5. 阿里 geojson 这类行政区划数据,拿和用都有讲究
5.1 那套工具能拿到什么
搜索量一直居高不下的"阿里 geojson",指的通常是某云平台提供的地理小工具,这个工具的核心价值是:你搜索一个行政区名字,它给你这个区域的边界数据,可以在地图上预览,也可以直接复制或下载 JSON。
能拿到的字段一般包括:行政区名称、行政区代码(六位数字的那个)、层级(省/市/区县)、所属上级、中心点坐标、以及子级数量。这几个字段组合起来,足够支撑"省市区三级联动"这类最常见的需求。
接口地址一般长这样(具体以官网页面当前提供的形式为准):
# 获取某个行政区自身的边界 https://<该服务的域名>/areas_v3/bound/{adcode}.json # 获取某个行政区及其所有下级边界(体积大很多) https://<该服务的域名>/areas_v3/bound/{adcode}_full.json{adcode}就是六位行政区划代码,比如北京市是 110000,某个区是它的下一级代码。带_full和不带_full是两个完全不同的文件,前者包含所有子孙级区域,一个省的全量文件能有几 MB 甚至更大,直接塞进前端会卡。做联动的时候,通常的节奏是:先加载省,用户选了省再请求这个省的 full 数据,用户选了市再请求市的 full 数据,逐级按需加载。
5.2 拿到的边界数据,三个问题要提前有心理准备
第一是对齐问题。这类公开边界数据跟不同底图叠加时,可能存在微小的偏移。不要默认它一定跟你的底图严丝合缝,正确做法是先选一个面积小、形状有辨识度的区域(比如某个老城区)叠加到你的底图上,放大到街道级别看一眼。如果偏了,就得整体做一次坐标系转换。这一步五分钟,能省掉后面几天的争论。
第二是精度和体积的取舍。公开数据为了控制体积,边界本身就做过简化。你在省级地图上看不出来,一旦放大到区县级,边界线和实际的地块边界、道路走向会有肉眼可见的出入。这个在"做一张示意性的着色图"场景里完全够用,但如果你要做面积计算、做地块级分析,就必须换更精细的数据源。
第三是飞地和多面问题。一个行政区在地理上不一定是一整块,可能有一个或多个不与主体相连的飞地。这种情况下导出的 geometry 类型会是 MultiPolygon 而不是 Polygon。你的前端代码必须同时处理这两种类型,不然遇到飞地区域就会渲染不出来。同样地,一个区域内部如果有被挖掉的"洞"(比如某个区内包含一个独立的开发区),坐标数组的层级会比普通面多一层。这两个问题在测试阶段很难发现,因为大部分人只拿一两个区试,一上线就出问题。
5.3 自己拼一个省市区联动数据包
如果不想每次上线都依赖外部服务,可以把数据打包到自己的静态资源里。做法是:按 adcode 逐个拉取,整理成{adcode: geojson}的结构,用一个索引文件记录父子关系。
这里有一个容易忽略的细节:索引文件里的层级关系要以行政区划代码为准,不要用名称做键。名称会变(撤县设区、更名),代码相对稳定。而且同名地区在全国范围内是存在的,用名称做键迟早出问题。
打包之后记得做一次瘦身(下一节讲),一个包含全国三四级数据的包,瘦身前后体积能差十倍以上。
6. 几 MB 的 GeoJSON 为什么能把页面卡住
6.1 小数点后十五位不是精度,是负担
打开任何一个从 GIS 软件导出的 GeoJSON,你大概率会看到这样的坐标:116.39742869572783。小数点后 14 位,对应到实际距离是纳米级。对一个用来在地图着色的区域来说,这完全是浪费。
一个粗略的换算:小数点后 6 位大约是 0.1 米,5 位大约是 1 米,4 位大约是 11 米,3 位大约是 110 米。你的区县着色图,误差容忍度是多少?如果地图最大只放大到 12 级,屏幕上一个像素代表几十米,那保留 4 位小数绰绰有余。把精度从 14 位砍到 4 位,体积能减少四成左右,肉眼几乎看不出差别。
6.2 用简化工具做拓扑简化,而不是随便删点
减体积最有效的办法是简化顶点,但简化有个大坑:独立简化每个多边形,会让相邻区域的公共边界产生缝隙或重叠,地图上一放大就是一条条白线或者色块互相盖住。
正确做法是用支持拓扑简化的工具,它会把共享边界当成同一组顶点统一处理,简化之后相邻区域依然严丝合缝。mapharper 是这类工具里最实用的,可以网页操作也可以命令行:
mapshaper input.geojson \ -simplify 5% keep-shapes \ -clean \ -o format=geojson precision=0.0001 output.geojson几个参数的意义:simplify 5%表示保留大约 5% 的顶点,具体比例要根据你的最大缩放级别调,先从小比例试,叠到底图上检查形状有没有明显变形;keep-shapes保证简化后不会出现空几何或消失的小区域;clean用来修自相交和零面积的几何;precision=0.0001就是把坐标精度压到 4 位小数。
6.3 加载策略比压缩更管用
即使压缩到极致,全国范围的精细边界还是不小。这时候要靠加载策略:
- 静态资源一定要开 gzip。GeoJSON 是纯文本,重复的字段名和数字结构,压缩比通常能到 5:1 甚至更高,这一步零成本,效果最明显。
- 按需加载,不要一次拉全量。省市区联动的正确姿势在前面说过,逐级请求。
- 考虑 TopoJSON。它是 GeoJSON 的拓扑化版本,把共享边界只存一次,体积能再小一大截,代价是前端需要额外的解码库,多一层依赖。
- 超大数据考虑后端出图。如果数据量到了几十兆的级别,就别硬塞前端了,用服务端渲染成图片瓦片或者矢量瓦片,前端只负责展示。
7. 那些绕不开的坑:几何、编码、属性丢失
7.1 环方向和自相交,是两个静默杀手
GeoJSON 规范里,面的外环要求逆时针、内环要求顺时针。现实中大量数据并不遵守,多数渲染库也能容忍,但只要你的下游有一个严格的工具,就会出现"某个区域填充变成整个地图"或者"洞没被挖掉"这类诡异现象。
自相交更常见,手绘的时候顶点连错顺序、或者从别的格式转换过来带进了重叠边,都会造成自相交。表现是有的渲染器填充异常,有的直接报错。修复办法就是用上一节提到的-clean,或者导入 QGIS 后跑一次几何有效性检查。
7.2 属性字段丢失,八成是 Shapefile 干的
Shapefile 的 .dbf 表对字段名有长度限制(经典格式是 10 个字符),中文属性还有编码问题,需要配套一个 .cpg 文件声明编码。所以你从 GeoJSON 转到 Shapefile 再转回来,字段名可能被截断、中文可能变问号。如果链路上必须经过 Shapefile,字段名提前按 10 字符以内规划,并确保 .cpg 里写的是 UTF-8。
另一个高频问题是 CSV 的编码。前面提过,Excel 存的 CSV 在国内默认是 GBK,导入工具后中文变乱码。这个问题的麻烦在于它不报错,只是静默地把内容改掉,你不逐条核对根本发现不了。
7.3 坐标偏了,用一条排查链路搞定
偏移问题最忌讳的是瞎猜。我现在的固定流程是三步:
第一步,量数值。看坐标是否在合理的经纬度区间内,这一步能排除掉投影坐标和顺序反了两种情况。
第二步,找一个锚点。挑一个你百分百确定位置的要素,把它的坐标单独拎出来,分别按三种坐标系换算成另外两种,得到三个候选点,逐个叠到底图上比对。哪个对上,数据就是哪个坐标系。
第三步,换底图验证。如果三个候选点都对不上,说明问题不在坐标系,而在底图本身——可能是底图缓存了旧版本、可能是底图服务本身的偏移、也可能是你在缩放级别很低的时候看,误差被视觉放大了。换成另一种底图再叠一次,基本就能定位。
这套流程我走了很多次,最慢的一次也就十几分钟。比起挨个试工具、试参数,效率高太多。
7.4 一个容易被忽略的细节:null 几何
FeatureCollection 里是允许存在 geometry 为 null 的 Feature 的,规范里这是合法的。但很多渲染库遇到 null 几何会直接抛异常,导致整张地图加载失败。数据清洗阶段就应该把这些空要素过滤掉,而不是等到前端报错再去查。批量处理的时候,一行过滤代码就能解决。
说个我自己的习惯:任何一份手头要用的 GeoJSON,在正式接入之前,我都会先做一件事——把它丢进地图,随机放大三个不同区域,逐个看一眼。看一眼边界跟底图对不对齐、看一眼有没有哪个区域渲染成黑块、看一眼文件大小。这三眼花不到五分钟,但它拦下的问题,比我写过的所有数据校验脚本加起来还多。数据这东西,肉眼过一遍永远比自动化检查更早发现问题。