GeoJSONcn
GeoJSONcn / 技术专栏 / 几何校验与修复:为什么你的 Ge…

几何校验与修复:为什么你的 GeoJSON 导入 GIS 后会报 self-intersection

发布于 2026-08-10 · GeoJSONcn 技术团队 · 阅读约 10 分钟

title: 几何校验与修复:为什么你的 GeoJSON 导入 GIS 后会报 self-intersection

做数据清洗的人大多踩过这个坑:把行政区划 Shapefile 或者从 Excel 拼出来的边界转成 GeoJSON,丢进 QGIS 或 PostGIS,结果要么导入失败,要么一跑空间分析就弹 "Ring Self-intersection" 或者 "GEOS geoprocessing error"。先别急着怀疑源数据错了——十有八九是几何本身不合法。

OGC Simple Features 对"有效多边形"提了几条硬性要求,下面这几条最常被违反:

shapely 实战:先校验再修复

Python 这边用 shapely 最直接。构建几何时记得加 always_xy=True,告诉 shapely 这是经纬度(EPSG:4326)地理坐标,不是平面 XY。不声明不会报错,但部分操作的语义会悄悄错。

from shapely.geometry import shape
from shapely.ops import make_valid
from shapely.validation import explain_validity

def clean_feature(feature):
    geom = shape(feature["geometry"], validate=False)
    if not geom.is_valid:
        reason = explain_validity(geom)   # 给人话原因,比 is_valid=False 有用
        print(f"{feature['properties'].get('adcode')}: {reason}")
        geom = make_valid(geom)            # GEOS 3.8+ 覆盖模式修复
    feature["geometry"] = geom.__geo_interface__
    return feature

# 批量走一遍
import json
src = json.load(open("raw.geojson"))
src["features"] = [clean_feature(f) for f in src["features"]]
json.dump(src, open("clean.geojson", "w"), ensure_ascii=False)

make_valid 用的是 GEOS 3.8 引入的覆盖模式(overlay),能把自交面拆成合法的带洞面或 MultiPolygon。explain_validity 直接报出像 "Ring Self-intersection[116.39 39.90]" 这样的具体位置,定位比单纯看 is_valid 快得多。

三种方案怎么选

工具校验修复适用场景
shapely(Python)is_valid + explain_validitymake_valid(覆盖模式)离线 ETL、批量清洗
PostGISST_IsValid + ST_IsValidReasonST_MakeValid数据库内批量、SQL 流水线
Turf.js(JS)无 is_valid;用 @turf/kinks 找自交边,features.length===0 近似判定@turf/rewind 调环方向浏览器/Node 前端轻量校验

Turf 比较特别:它没有原生的 is_valid,只能靠 @turf/kinks 找出自交的线段,kinks 为空就当作合法。环方向则交给 @turf/rewind。

三个坑

1. make_valid 可能把面拆成 MultiPolygon,破坏"一个 adcode 对应一个面"的一对一假设。 后面按 adcode 前缀上卷成地市、省级边界时(见 区县边界上卷),会突然冒出多行。修复后务必检查 geometry 类型有没有从 Polygon 变成 MultiPolygon,必要时 explode 或保留原 adcode。

2. 顺序错了会出怪事。 应该先做"无效几何修复",再做 Douglas-Peucker 抽稀。抽稀在非法几何上行为未定义,要么直接抛异常,要么产出更离谱的几何。

3. 坐标系必须显式声明。 make_valid 是纯拓扑操作,不关心坐标单位。但你若顺手用 .area 看"面积",shapely 会把经纬度当平面度算,得到的是毫无意义的平方度。要看面积得走 turf.area(球面测地)或 PostGIS 的 geography 类型——环方向这一步和 RFC 7946 规范 的要求也直接相关。

把"校验 + 修复"作为 ETL 链路的独立步骤,放在导入 GIS 之前(见 Shapefile 转 GeoJSON 的 ETL 实战),能挡掉下游绝大多数 self-intersection 报错。更多格式与坐标系细节见 主站。