手里有 Shapefile?用 Python 把行政区划数据转成 GeoJSON
做空间数据,迟早会遇到同一件事:上游给的行政区数据包是 Shapefile,你的网站、地图组件、边界服务却全要 GeoJSON。Shapefile 至今还是 GIS 交换的事实标准,自然资源部、天地图下发的边界常以 .shp 提供;Web 端(Leaflet、Mapbox)和我们的边界服务只用 GeoJSON。两边不通,就得自己转。
为什么不能直接丢给前端
Shapefile 从来不是单个文件,而是 .shp(几何)、.shx(索引)、.dbf(属性表)一组,外加常见的 .prj 投影文件。它不声明编码,中文属性表经常乱码,浏览器也读不了。GeoJSON 是一段纯文本 JSON,天然适配 JavaScript 和 HTTP,把边界画到网页上省心得多。GeoJSON 的坐标约定,可以回看 GeoJSON 到底是什么。
用 Fiona 读 Shapefile,输出 GeoJSON
Fiona 封装了 GDAL/OGR,能把 .shp 直接读成 GeoJSON 兼容的记录。下面这个函数把整个图层读成一个 FeatureCollection:
import fiona
import json
def shp_to_geojson(shp_path):
features = []
with fiona.open(shp_path) as src:
for rec in src:
features.append({
"type": "Feature",
"geometry": rec["geometry"], # 已是 GeoJSON 兼容的几何
"properties": dict(rec["properties"]),
})
return {"type": "FeatureCollection", "features": features}
fc = shp_to_geojson("county.shp")
with open("county.geojson", "w", encoding="utf-8") as f:
json.dump(fc, f, ensure_ascii=False, indent=2)
rec["geometry"] 本身就是 GeoJSON 风格的字典,坐标结构不用自己搬,直接拼进 Feature 就行。
坐标系:Shapefile 多半不是 WGS84
最容易栽跟头的就是这里。Shapefile 的 .prj 可能是 CGCS2000(EPSG:4490),也可能是某个地方独立坐标系;GeoJSON 默认按 WGS84(EPSG:4326,顺序是经度、纬度)约定。不重投影,画到中国地图上会整体偏移。用 pyproj 转一次:
from pyproj import Transformer
transformer = Transformer.from_crs("EPSG:4490", "EPSG:4326", always_xy=True)
def reproj_coords(coords):
return [list(transformer.transform(x, y)) for x, y in coords]
def reproj_geom(geom):
t = geom["type"]
if t == "Point":
return {"type": t, "coordinates": reproj_coords([geom["coordinates"]])[0]}
if t in ("LineString", "MultiPoint"):
return {"type": t, "coordinates": reproj_coords(geom["coordinates"])}
if t == "Polygon":
return {"type": t, "coordinates": [reproj_coords(r) for r in geom["coordinates"]]}
if t == "MultiPolygon":
return {"type": t, "coordinates": [[reproj_coords(r) for r in p] for p in geom["coordinates"]]}
return geom
always_xy=True 不能省:它强制 pyproj 按 (经度, 纬度) 顺序走,否则 x/y 和 lat/lon 会反。坐标系的底层原理见 WGS84 与 GCJ-02 坐标系。
几何合法性:自相交与环方向
行政区边界多由多段线拼出来,转完偶尔会有自相交(self-intersection)或环方向不对,渲染时要么出空洞、要么切片失败。用 Shapely 校验再加修:
from shapely.geometry import shape
from shapely.validation import make_valid
def fix_geometry(geom_dict):
g = shape(geom_dict)
if not g.is_valid:
g = make_valid(g) # 修复自相交、环方向等拓扑错误
return g.__geo_interface__
make_valid 是 Shapely 2.x 的官方修复函数,比老办法 buffer(0) 稳,不会把尖角磨圆。修完再写回 GeoJSON。
顺手做体积控制
行政区边界动辄上万个顶点。转完 GeoJSON,把坐标量化到 6 位小数(约 0.1 米精度,行政区级别够用),再接 TopoJSON 压缩 继续瘦身。我们就是靠这套流程,把全国省、市、区县边界整理成可服务的 GeoJSON,再据此搭起 行政区划边界数据服务。
Shapefile 与 GeoJSON 怎么选
| 维度 | Shapefile | GeoJSON |
|---|---|---|
| 文件形态 | 多文件(.shp/.shx/.dbf) | 单文件 JSON |
| 编码 | 不声明,中文易乱码 | UTF-8,中文安全 |
| Web 友好 | 否 | 是 |
| 坐标系声明 | 依赖 .prj | 约定 WGS84 |
| 库支持 | GDAL/Fiona/ArcGIS | 任意 JSON 解析器 |
上游交换、桌面 GIS 里 Shapefile 还是主力;要进 Web、进服务,转成 GeoJSON 基本是必选项。三种格式怎么选、怎么转,更全的对比看 GeoJSON 转 KML/SVG/CSV/PNG。