第一次拿到 GeoJSON 数据的时候,我的反应是:这什么玩意儿?一个打开全是花括号和数组的文本文件,凭什么能变成地图上的点和面?后来真上手做了一阵子地理数据处理,才明白这份文件之所以能成为交换事实标准,确实有它的底气。如果你正打算做地图可视化、地理位置存储,或者被后端同事甩来一份 .geojson 文件不知道怎么办,这篇文章就是给你写的。我会尽量把格式规则、坐标系陷阱、常用工具和排错方法揉在一起讲清楚,不堆生僻术语,重点放在能落地的部分。
1. 为什么GeoJSON能成为默认的地理交换格式
1.1 从一份打不开的地图数据说起
前两年我参与一个模拟项目,需要把城市里几百个采样点的分布画到页面上。后端同事最初给的是 Excel 表格,两列经纬度数字,一列名称,看起来清清楚楚。可真到了前端地图上,点位要么飞到海里,要么挤成一团,折腾了整整一下午。最后技术负责人受不了了,让后端改用 GeoJSON 导出,前端拿到文件直接渲染,问题当场消失。
那次经历让我意识到一件事:地理数据不是简单把经纬度拿出来就能用的,它需要一套双方都能理解的结构化约定。GeoJSON 就是这套约定的标准答案。它本质上是 JSON 的一个地理扩展,所有内容都可以用文本编辑器打开查看,也是 JavaScript 原生能直接处理的数据结构。前后端传数据、存数据库、做离线包,都可以直接拿这份文件操作,不需要额外定义协议。
1.2 GeoJSON 的数据组织逻辑:要素和几何体的层次
GeoJSON 的组织方式很像我们平时填表格。一张完整的地理数据表叫做 FeatureCollection(要素集合),里面可以包含多个 Feature(要素),每一个 Feature 又有 properties(属性)和 geometry(几何体)。
这里的 properties 就是普通 JSON 对象,你想挂什么字段都行,比如采样点编号、温度值、采集时间。而 geometry 里必须有 type 和 coordinates 两个字段,type 表示几何类型,coordinates 是具体的坐标数组。这种分层结构好处非常明显:几何数据和业务属性数据完全解耦,前端画图时只关心 geometry,做筛选统计时只读 properties,谁都不干扰谁。
注意一点:FeatureCollection 是一个容器,它本身不需要坐标,真正带坐标的是被它包裹的 Feature。很多初学者会混淆这个层级,在 FeatureCollection 那一层硬找 coordinates,结果自然是 null。
1.3 一个最小可解析的 GeoJSON 长什么样
写个最朴素的点要素示例,你一看就懂:
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "采样点A", "value": 23.5 }, "geometry": { "type": "Point", "coordinates": [116.4, 39.9] } } ] }这段 JSON 表示存在一个要素集合,里面只有一个点要素,坐标是东经 116.4 度、北纬 39.9 度。随便用一个支持 GeoJSON 的地图库加载它,页面上就能出现一个标记点。规模扩大时,只需要往 features 数组里继续塞要素即可,几千个点、几百条路、几十个区域都遵循同一套规则。
2. 几何类型拆解:点线面背后的规则
2.1 六种标准几何类型与嵌套关系
GeoJSON 规范(现在通行的版本是 RFC 7946)规定了七种几何类型,常用的有六种,外加一种特殊的 GeometryCollection。它们其实可以分成三组:Point 和 MultiPoint,LineString 和 MultiLineString,Polygon 和 MultiPolygon。其中 Multi 开头的类型就是把单个几何体打包成数组,比如 MultiPoint 就是多个点坐标组成的数组,MultiLineString 就是多条折线组成的数组。
这六种类型彼此之间不能混用。比如你把一个线段的坐标数组填进 Polygon 里,解析器大概率会直接报错或者把图形画成一个诡异的三角形。原因在于每种类型对坐标数组的嵌套层数有严格要求,后面我会详细拆。
2.2 坐标数组的层级规律与常见解析错误
直接给坐标嵌套层级规律表,这是排错时的核心参考:
| 几何类型 | coordinates 数组层级 | 示例结构 |
|---|---|---|
| Point | 一层数值数组 | [lng, lat] |
| MultiPoint | 两层数组 | [[lng, lat], [lng, lat]] |
| LineString | 两层数组 | [[lng, lat], [lng, lat]] |
| MultiLineString | 三层数组 | [[[lng, lat], [lng, lat]]] |
| Polygon | 三层数组 | [[[lng, lat], [lng, lat], [lng, lat], ...]] |
| MultiPolygon | 四层数组 | [[[[lng, lat], [lng, lat]]]] |
最常见的解析错误就是层级数搞错。有一次我拿一份区域数据做渲染,前端一直画不出面,报错说“invalid geometry”。打印出坐标结构后才发现,数据源头在做格式转换时把一个 Polygon 的坐标数组外面多包了一层中括号,变成了 MultiPolygon 级别的嵌套。地图库按照 Polygon 去解析自然失败。排查这类问题最快的办法不是盯代码,而是把 JSON 打开,人工数一遍中括号层数,对照上表就能定位问题。
2.3 多边形环绕方向与自相交限制
Polygon 的坐标数组还有一个特殊规则:坐标数组的第一个元素是外环,后续的元素是内环,也就是洞。比如一个带湖泊的多边形区域,外环是陆地边界,内环是湖的边界。规范建议外环使用逆时针方向,内环使用顺时针方向,但大多数解析器在这点上其实比较宽松,正反都能画出来。
真正会让你崩溃的是自相交多边形。所谓自相交,就是多边形的边自己和自己交叉,形成一个类似蝴蝶结形状的图形。地图引擎处理自相交的逻辑五花八门,有的直接显示一个奇怪形状,有的干脆拒绝渲染。所以在生成 Polygon 数据之前,最好先用工具做一次拓扑校验。Python 里可以用 shapely 的is_valid方法检查,前端也可以用 turf.js 的booleanValid做判断。
from shapely.geometry import shape import json with open("area.geojson", encoding="utf-8") as f: data = json.load(f) geom = shape(data["features"][0]["geometry"]) print("多边形是否有效:", geom.is_valid)3. 坐标系约定:GeoJSON 默认在 WGS84 这件事
3.1 为什么必须显式注意 EPSG:4326
地理数据领域最坑人的不是数据结构,而是坐标系。同一串经纬度数字,在不同坐标系下代表的地球位置完全不一样。GeoJSON 规范明确规定,所有坐标必须使用 WGS84 坐标系,也就是通常说的 EPSG:4326,且不能附加自定义坐标系参数。
这意味着,如果你的原始数据是投影坐标系(比如常见的 Web Mercator,即 EPSG:3857),那么必须先转成 WGS84 经纬度,再写成 GeoJSON。否则前端地图加载出来以后,点位会偏移到完全不可用的位置,而且这种偏移是肉眼难以发现但量级很大的系统性错误。
我见过一个典型翻车案例:某团队把 UTM 投影坐标的数据当成经纬度直接填进了 GeoJSON,渲染出来的点位与真实位置差了上百公里,地图缩放到城市级别时点位直接落在邻近城市。事后排查花了两天,才发现是数据源没有做坐标系转换。
3.2 经纬度顺序:可怜的地理爱好者的经典翻车现场
GeoJSON 里坐标数组的第一个元素必须是经度,第二个是纬度,也就是 [lng, lat],对应数学上的 [x, y]。但很多人日常写顺手了,会习惯写成 [lat, lng],第一个写纬度,第二个写经度。
这个顺序问题在外行看来只是先后顺序的区别,实际效果却非常严重。因为纬度的数值范围是 -90 到 90,经度范围是 -180 到 180,把二者互换之后,点位可能从中国被甩到太平洋甚至南美洲。更隐蔽的是,如果经纬度数值恰好都比较小,互换之后点位偏差可能只有几十公里,比例尺大时根本看不出问题,一旦缩小地图就会发现位置明显不对。
强烈建议:凡是和 GeoJSON 打交道,一律按照“先经后纬”写坐标。前端 Leaflet 用的是 [lat, lng],正好和 GeoJSON 相反,这也是大量新手混淆的根源。
3.3 高精度坐标与浮点舍入问题
坐标数值的精度同样值得留意。WGS84 坐标系里,纬度方向每 0.000001 度大约对应 0.11 米,经度方向在赤道上类似,但高纬度地区会有偏差。理论上保留六位小数就能满足绝大多数民用场景的精度需求。
然而如果你直接从 GPS 设备或者某些 API 拿数据,原始经纬度可能带着十几位小数。这些多出来的尾数在存储和传输中没有任何实际意义,反而会带来两个问题:一是文件体积增大,二是某些解析器在浮点数精度处理上表现不佳,可能导致坐标值发生微小的异常变动。我在实际项目中会统一把经纬度保留到六位小数,用 Python 处理时写一个简单的小工具自动截断:
def trim_coords(coords, digits=6): if isinstance(coords, (int, float)): return round(coords, digits) if isinstance(coords, list): return [trim_coords(c, digits) for c in coords] return coords这个递归函数能处理任意嵌套层级的坐标数组,把浮点数统一截断到指定小数位。虽然看起来是小事,但对文件体积和后续空间计算的稳定性都有实际帮助。
4. 实际场景应用:从渲染到空间计算的完整链路
4.1 前端地图快速渲染的最小实现
GeoJSON 在前端渲染通常配合 Leaflet 或 MapLibre GL 这类开源地图库使用。以 Leaflet 为例,读取一份 GeoJSON 并渲染成点和多边形,核心代码只有几十行:
fetch("data.geojson") .then(response => response.json()) .then(data => { L.geoJSON(data, { pointToLayer: (feature, latlng) => { return L.circleMarker(latlng, { radius: 5 }); }, style: (feature) => ({ color: feature.properties.value > 30 ? "#d73027" : "#1a9850" }), onEachFeature: (feature, layer) => { layer.bindPopup(`名称: ${feature.properties.name}`); } }).addTo(map); });这里有一个关键点:Leaflet 内部虽然使用 [lat, lng] 顺序,但 L.geoJSON 方法会自动处理 GeoJSON 的 [lng, lat] 坐标顺序转换,你不需要手动倒转坐标数组。如果手动遍历 coordinates 自己创建 marker,就要小心顺序问题了。
4.2 用 Python 处理 GeoJSON 的常用套路
服务端处理 GeoJSON 更关注数据清洗、格式转换和空间计算。我用得最多的组合是 geopandas 加上 shapely。geopandas 可以直接读取 GeoJSON 文件为 DataFrame 结构,筛选、聚合、导出都非常顺手。
import geopandas as gpd gdf = gpd.read_file("data.geojson") print(gdf.crs) # 查看坐标系信息 gdf_subset = gdf[gdf["value"] > 100] gdf_subset.to_file("filtered.geojson", driver="GeoJSON")这里要特别说一下crs参数。geopandas 读取 GeoJSON 时会自动将坐标系标记为 EPSG:4326,但如果原始文件本身的坐标不是 WGS84,读取出来的 GDF 也会带着错误的坐标系标记。处理这类数据前最好先打印 crs 确认,如果源数据是其他坐标系,先用to_crs(epsg=4326)转换再落盘。
4.3 GeoJSON 在空间计算与服务端存储中的角色
除了渲染展示,GeoJSON 还常常作为空间计算的数据交换格式。比如利用 shapely 判断某个点是否落在某个区域内、计算两个多边形的交集面积、计算点与线的最短距离。这些计算不需要可视化,但底层读取的仍然是 GeoJSON 结构。
在服务端存储方面,主流关系型数据库都提供了对 GeoJSON 的良好支持。比如 PostgreSQL 的 PostGIS 扩展,可以直接把 GeoJSON 字符串转成几何对象存入数据库,空间索引、范围查询、距离排序都原生支持。转换过程大致是把 GeoJSON 的 geometry 部分提取出来,调用 ST_GeomFromGeoJSON 方法写入,查询后也可以用 ST_AsGeoJSON 把结果转回 GeoJSON 输出给前端。这套链路在现代 GIS 应用里相当成熟,岗位需求也一直很稳定。
5. 同类格式横向对比:何时不选 GeoJSON
5.1 常见地理数据格式的差异
GeoJSON 并非万能,实际工作中还有一些常见的地理数据格式,各有各的适用场景。我做过一个横向对比,整理成表格:
| 对比项 | GeoJSON | KML | Shapefile | WKT |
|---|---|---|---|---|
| 本质 | JSON 文本 | XML 文本 | 二进制多文件 | 纯文本定义 |
| 可读性 | 高 | 中等 | 低 | 高 |
| 体积 | 较大 | 较大 | 较小 | 最小 |
| 属性支持 | 完整 | 完整 | 有限制 | 无 |
| 主流支持 | 极广 | 一般 | 成熟但老旧 | 数据库内常见 |
| 典型场景 | Web 前端 | Google 地图生态 | 传统 GIS 软件 | 空间数据库交换 |
如果你面向 Web 应用,GeoJSON 几乎是首选。如果和传统 GIS 软件(如某桌面端 GIS 工具)打交道,Shapefile 仍然是绕不开的格式,但它不是单文件,而是由至少三个文件组成:存放几何的.shp、存放属性的.dbf、存放索引的.shx,缺少任何一个文件都无法正常打开,传输时很容易漏文件。
5.2 TopoJSON 与精简曲线
TopoJSON 是 GeoJSON 的一种紧凑变体,它的核心思路是复用共享的线段。举个例子,两个相邻县的边界在 GeoJSON 里会被重复存储两次,一条边界的坐标出现在两个多边形里;而 TopoJSON 把公共边界只存一次,通过引用来组装各个多边形。这种去重策略在处理高精度行政区划数据时效果极其明显,文件体积能缩小到原来的十分之一甚至更低。
TopoJSON 还支持量化简化,比如保留坐标小数点后三位,用来满足不同缩放级别下的显示需求。但代价是,TopoJSON 不能被地图库直接渲染,必须先经过 topojson-client 这类库把数据转换回 GeoJSON。因此它更适合作为传输和存储格式,渲染前再解包。
5.3 体积大户的应对策略:简化与 MBTiles 思路
GeoJSON 最大的软肋是文件体积。因为每个几何体的坐标都以明文数字形式存储,坐标点越多体积越大。一份包含全国道路网明细的 GeoJSON 可能达到几百 MB,浏览器直接加载必然卡死。
应对策略通常有三种。第一是几何简化,用算法减少坐标点数,比如 shapely 的simplify方法,通过容差参数控制简化程度。第二是空间分块,把大区域按网格切分成多个小文件,前端按需加载,类似地图瓦片的思想。第三是用 MBTiles 这种 SQLite 格式做离线数据包,它内部可以存储大量的矢量瓦片数据,地图引擎直接读取,性能和体验都比直接解析巨型 GeoJSON 好很多。
6. 踩坑总结与调试方法论
6.1 解析失败时的系统性排查顺序
GeoJSON 解析失败或者渲染异常时,我建议按照固定顺序排查,效率最高:
- 先用 JSON 语法校验工具检查文件本身是不是合法 JSON。任何多一个逗号、少一个括号的问题都会导致整体解析失败。
- 检查坐标数组的嵌套层级是否符合几何类型要求,对照第二节的表格逐层核对。
- 检查坐标值范围,经度必须在 -180 到 180 之间,纬度必须在 -90 到 90 之间。超过这个范围大概率是坐标系错误。
- 检查经纬度顺序,确认第一位是经度第二位是纬度。
- 检查要素的外环闭合性。Polygon 的外环首尾坐标必须相同,否则图形无法闭合。
这五步基本覆盖了我遇到过的九成问题。剩下的一成多是数据中混入了非法字符、文件编码不对、或者字段类型不符合预期。
6.2 验证工具与手写代码自查
排查过程中,验证工具能帮上大忙。我最常用的是 GeoJSONLint 这类在线验证服务,直接把 JSON 文本粘贴进去,它会把语法错误和结构错误一起提示出来。后端处理时,也可以用 shapely 的is_valid做几何有效性检查,用 geopandas 的read_file做读取测试。只要这几个环节都过了,数据基本就能放心交给地图渲染。
如果遇到不常见的错误,手写一段排查代码往往比工具更可靠。比如遍历所有要素,逐个解析 geometry 类型和坐标系范围:
def validate_geojson(path): import json with open(path, encoding="utf-8") as f: data = json.load(f) for idx, feat in enumerate(data["features"]): geom = feat["geometry"] coords = geom["coordinates"] if geom["type"] == "Polygon": if len(coords[0]) < 4: print(f"要素 {idx}: 外环坐标不足4个,未闭合") if coords[0][0] != coords[0][-1]: print(f"要素 {idx}: 外环未闭合") # 递归检查经纬度范围 def check(c): if isinstance(c, list): for x in c: check(x) elif isinstance(c, float): pass check(coords)实际使用时可以按需要补充更多规则,这套思路比看一眼报错盲猜要扎实得多。
6.3 我平时写 GeoJSON 的一点习惯
顺手分享几个日常养成的习惯,算不上什么高深技巧,但能避免很多不必要的返工。
第一,所有坐标统一先经后纬,写进代码注释里,防止换人维护时被改成错误顺序。第二,属性字段名全部使用英文加下划线,避免中文键名在不同编码环境下的兼容性问题。第三,经手较大的 GeoJSON 文件时,永远先写一版小体积的样例数据测试渲染效果,确认样式和交互逻辑没问题后再上全量数据。第四,保留一份数据生成脚本而不是只保留最终文件,因为上游数据更新后,重新生成总比手工改文件靠谱。
做地理数据这行,真正的门槛不在 API 调用,而在数据本身的质量。GeoJSON 规范本身不算复杂,但它作为数据交换的入口,串联起了前端展示、后端存储、空间计算整条链路。把格式规则、坐标系约定和常见坑摸透,后续遇到什么样的地理数据项目都能很快走上正轨。如果这份文件格式让你头疼过,希望这篇文章能帮你少走几个弯路。