Python GDAL/OGR 实现 GeoJSON 转 Shapefile 完整指南 1. 为什么 GeoJSON 转 Shapefile 这件事值得单独拿出来讲做 GIS 数据处理的人迟早会撞上格式转换这堵墙。GeoJSON 和 Shapefile 是两种最常见的矢量数据格式前者基于 JSON 文本、结构灵活、Web 端友好后者是 Esri 时代延续下来的二进制格式、字段类型严格、属性表有长度限制。两者之间的转换看起来简单实际上坑不少。我最早接触这个需求是在处理一批行政区划数据的时候。上游给的是 GeoJSON下游的分析工具只认 Shapefile中间还得做坐标系转换和字段裁剪。当时试过在线转换工具数据量一大就崩试过 QGIS 手动导出批量处理几十个文件的时候效率太低。最后还是回到 Python 的 GDAL/OGR 库写了一套脚本一次跑完几百个文件稳定可控。这篇文章就是把这套东西完整拆开讲清楚。GDAL/OGR是地理数据处理领域的老牌底层库Python 绑定之后用起来很顺手。GeoJSON转Shapefile这个操作核心涉及三个问题几何类型的正确映射、属性字段的类型转换、坐标参考系的处理。任何一个环节出问题转换出来的数据都可能打不开或者属性错乱。适合谁看如果你手头有 GeoJSON 数据需要转成 Shapefile或者反过来或者你正在做 GIS 数据批处理流程这篇内容可以直接抄作业。不需要你精通 GDAL但至少要会装 Python 库、能看懂基本的 Python 代码。我会把每一步的参数选择理由都讲清楚遇到问题也知道去哪儿排查。2. 环境准备与核心工具选型2.1 GDAL/OGR 的安装方式对比GDAL 的安装是第一个门槛。这个库底层是 C 写的Python 绑定需要编译好的二进制包。不同操作系统下的安装方式差异很大选错了方式后面会浪费大量时间。安装方式适用平台优点缺点conda install gdal全平台自动解决依赖版本匹配需要 Anaconda/Miniconda 环境pip install GDALLinux/macOS直接Windows 下需要预编译 wheel容易失败OSGeo4WWindows官方推荐组件完整安装包大环境变量配置繁琐apt/yum 包管理Linux系统级安装稳定版本可能偏旧我个人的建议是能用 conda 就用 conda。原因很简单GDAL 依赖 PROJ、GEOS、SQLite 等一堆底层库conda 会自动帮你把这些版本对齐。pip 安装 GDAL 在 Windows 上经常报 “Microsoft Visual C 14.0 is required” 或者找不到 gdal-config新手很容易卡在这里。如果你坚持用 pipWindows 下可以去找第三方预编译的 wheel 文件注意 wheel 的版本号要和你的 Python 版本、系统位数匹配。Linux 下 pip 安装之前需要先装系统级的开发包# Ubuntu/Debian 系统 sudo apt-get install libgdal-dev gdal-bin pip install GDAL$(gdal-config --version)这里有个细节pip install GDAL的版本最好和系统安装的 libgdal 版本一致否则运行时可能报符号找不到的错误。用gdal-config --version查系统版本然后指定相同的版本号安装。2.2 验证安装是否成功装完之后别急着写代码先跑一段验证from osgeo import gdal, ogr, osr print(gdal.__version__) print(ogr.__version__)如果这两行能正常打印出版本号说明核心库没问题。如果报ModuleNotFoundError: No module named osgeo说明安装路径不对或者根本没装上。还有一种情况是能 import 但调用函数时报错那通常是底层动态库版本冲突conda 环境下比较少见pip 混合安装时容易出现。提示如果你同时装了 conda 和 pip 版本的 GDALPython 导入时可能加载到错误的那个。建议在虚拟环境里只保留一种安装方式。2.3 为什么选 OGR 而不是其他库Python 里处理矢量数据的库不止一个。GeoPandas 底层也是调 GDALfiona 也是shapely 只管几何不管读写。那为什么不直接用 GeoPandas 一行代码搞定# GeoPandas 的写法 import geopandas as gpd gdf gpd.read_file(input.geojson) gdf.to_file(output.shp)确实简单。但 GeoPandas 的问题是第一它封装太厚出问题的时候排查困难第二处理大批量文件时内存占用高第三有些精细控制比如字段类型强制指定、编码设置、几何类型过滤用 OGR 更直接。我的做法是快速原型用 GeoPandas生产脚本用 OGR。OGR 的 API 虽然啰嗦一点但每一步都在你掌控之中出了问题能精确定位。3. GeoJSON 与 Shapefile 的核心差异解析3.1 格式本质差异GeoJSON 是文本格式基于 JSON 规范一个文件里可以同时包含点、线、面多种几何类型通过 GeometryCollection 或者 FeatureCollection。字段类型支持字符串、数字、布尔、null嵌套对象也可以塞进去。Shapefile 是二进制格式而且它其实不是一个文件是一组文件.shp存几何.shx存索引.dbf存属性.prj存坐标系。关键限制来了一个 Shapefile 只能存一种几何类型要么全是点要么全是线要么全是面。属性字段名最长 10 个字符字符串字段最大长度 254 字节不支持 null 值不支持嵌套结构。这些差异决定了转换不是简单的“读进来写出去”中间必须做映射和裁剪。3.2 几何类型映射规则OGR 在读取 GeoJSON 时会给每个要素一个几何类型。写入 Shapefile 时图层级别需要指定一个统一的几何类型。如果源数据里混了多种类型就需要做处理。GeoJSON 几何类型Shapefile 对应类型注意事项PointPoint直接对应MultiPointMultiPoint直接对应LineStringLineString直接对应MultiLineStringMultiLineString直接对应PolygonPolygon注意环的方向MultiPolygonMultiPolygon直接对应GeometryCollection无直接对应需要拆分或过滤遇到 GeometryCollection 的时候我的处理方式是要么按几何类型拆成多个 Shapefile要么只保留主要类型把其他类型过滤掉。具体选哪种取决于业务需求没有标准答案。3.3 属性字段的类型转换陷阱这是最容易出问题的地方。GeoJSON 里的字段类型很自由但 Shapefile 的 dbf 只支持有限的几种字符串、整数、浮点数、日期。转换时常见的坑字段名超长Shapefile 字段名最多 10 个字符超长的会被截断。如果两个字段截断后重名后面的会覆盖前面的。我一般会在转换前先做字段名映射把长名字改成短名字同时维护一个对照表。字符串超长dbf 字符串字段默认宽度 80最大 254。超长的内容会被截断。如果某个字段确实需要存长文本要么改存到外部文件要么在转换时截断并记录。null 值处理dbf 不支持 nullOGR 写入时会把 null 转成空字符串或 0。如果 null 和 0 在业务上有区别转换后需要额外标记。布尔值GeoJSON 的 true/false 在 dbf 里通常转成整数 1/0 或者字符串 “True”/“False”取决于 OGR 的版本和配置。注意字段类型的问题在转换前就要检查清楚转换后再发现字段丢失或者内容截断返工成本很高。4. 完整转换流程与代码实现4.1 最简转换一行核心代码OGR 提供了一个非常方便的函数VectorTranslate可以一行完成格式转换from osgeo import gdal gdal.VectorTranslate( output.shp, input.geojson, formatESRI Shapefile )这行代码背后做了这些事打开 GeoJSON读取所有要素创建 Shapefile 数据源写入几何和属性关闭文件。对于简单的、格式规整的数据这样确实够了。但实际项目中我几乎不会直接用这个最简形式。原因字段名截断没处理、编码没指定、坐标系没检查、几何类型没验证。下面把每一步拆开讲。4.2 手动控制每一步的完整实现先看完整代码然后逐段解释from osgeo import ogr, osr, gdal import os def geojson_to_shp(geojson_path, shp_path, encodingUTF-8): 将 GeoJSON 文件转换为 Shapefile :param geojson_path: 输入 GeoJSON 路径 :param shp_path: 输出 Shapefile 路径不含扩展名或含 .shp :param encoding: 属性编码 # 1. 打开源数据 src_ds ogr.Open(geojson_path, 0) # 0 表示只读 if src_ds is None: raise FileNotFoundError(f无法打开文件: {geojson_path}) src_layer src_ds.GetLayer(0) src_srs src_layer.GetSpatialRef() # 2. 确定几何类型 geom_type src_layer.GetGeomType() # 如果源数据几何类型不确定取第一个要素的类型 if geom_type ogr.wkbUnknown: first_feat src_layer.GetNextFeature() if first_feat is None: raise ValueError(图层中没有要素) geom_type first_feat.GetGeometryRef().GetGeometryType() src_layer.ResetReading() # 3. 创建输出数据源 driver ogr.GetDriverByName(ESRI Shapefile) if os.path.exists(shp_path): driver.DeleteDataSource(shp_path) dst_ds driver.CreateDataSource(shp_path) if dst_ds is None: raise RuntimeError(f无法创建输出文件: {shp_path}) # 4. 创建图层指定几何类型和坐标系 dst_layer dst_ds.CreateLayer( os.path.splitext(os.path.basename(shp_path))[0], srssrc_srs, geom_typegeom_type, options[ENCODING encoding] ) # 5. 复制字段定义 src_defn src_layer.GetLayerDefn() field_mapping {} # 记录原始字段名到新字段名的映射 for i in range(src_defn.GetFieldCount()): field_defn src_defn.GetFieldDefn(i) field_name field_defn.GetName() # 字段名截断到 10 个字符 new_name field_name[:10] # 处理重名 base_name new_name counter 1 while new_name in field_mapping.values(): suffix str(counter) new_name base_name[:10 - len(suffix)] suffix counter 1 field_mapping[field_name] new_name # 创建新字段 new_field ogr.FieldDefn(new_name, field_defn.GetType()) if field_defn.GetType() ogr.OFTString: width min(field_defn.GetWidth(), 254) new_field.SetWidth(width if width 0 else 254) dst_layer.CreateField(new_field) # 6. 逐要素复制 dst_layer.StartTransaction() for src_feat in src_layer: dst_feat ogr.Feature(dst_layer.GetLayerDefn()) # 复制几何 geom src_feat.GetGeometryRef() if geom is not None: dst_feat.SetGeometry(geom.Clone()) # 复制属性 for i in range(src_defn.GetFieldCount()): field_name src_defn.GetFieldDefn(i).GetName() new_name field_mapping[field_name] value src_feat.GetField(i) dst_feat.SetField(new_name, value) dst_layer.CreateFeature(dst_feat) dst_feat None dst_layer.CommitTransaction() # 7. 清理 dst_ds None src_ds None print(f转换完成: {shp_path}) return field_mapping4.3 关键步骤的参数选择理由打开源数据用ogr.Open而不是gdal.OpenExogr.Open是 OGR 专用接口对矢量格式支持更直接。gdal.OpenEx是统一接口需要指定打开标志用起来更啰嗦。对于纯矢量转换ogr.Open足够了。几何类型判断GetGeomType()返回的是图层级别的类型。GeoJSON 驱动有时候会返回wkbUnknown这时候需要从第一个要素推断。注意读取第一个要素后要ResetReading()否则后面的循环会从第二个要素开始。创建图层时的options参数ENCODINGUTF-8这个设置很关键。Shapefile 的 dbf 默认编码取决于系统区域设置中文环境下经常出现乱码。显式指定 UTF-8 可以避免大部分问题。但要注意有些老旧的 GIS 软件不认 UTF-8 编码的 dbf如果下游是这类软件可能需要改成 GBK。字段名截断与重名处理代码里做了一个简单的重名检测如果截断后的名字已经存在就在末尾加数字。实际项目中我建议在转换前先做一次字段名审查把重要的长字段名手动映射成有意义的短名字而不是靠自动截断。事务处理StartTransaction()和CommitTransaction()在写入大量要素时能显著提升速度。Shapefile 的写入是 IO 密集型操作不开事务的话每条要素都触发一次磁盘写入几万条数据能慢好几倍。4.4 坐标系处理坐标系是另一个容易出问题的地方。GeoJSON 按照 RFC 7946 规范应该使用 WGS84EPSG:4326但实际数据不一定遵守。Shapefile 的.prj文件记录坐标系信息如果源数据没有坐标系或者坐标系不对转换后需要手动指定或转换。# 检查源坐标系 src_srs src_layer.GetSpatialRef() if src_srs is None: print(警告源数据没有坐标系信息) # 手动指定为 WGS84 src_srs osr.SpatialReference() src_srs.ImportFromEPSG(4326) # 如果需要转换到其他坐标系比如 Web Mercator dst_srs osr.SpatialReference() dst_srs.ImportFromEPSG(3857) transform osr.CoordinateTransformation(src_srs, dst_srs) # 在复制几何时做转换 geom src_feat.GetGeometryRef() geom.Transform(transform)提示Transform会直接修改几何对象如果后面还要用原始几何记得先Clone()。5. 批量转换与性能优化5.1 批量处理多个文件实际工作中很少只转一个文件。批量处理的代码结构import glob import os def batch_convert(input_dir, output_dir): if not os.path.exists(output_dir): os.makedirs(output_dir) geojson_files glob.glob(os.path.join(input_dir, *.geojson)) geojson_files glob.glob(os.path.join(input_dir, *.json)) results [] for gj_file in geojson_files: base_name os.path.splitext(os.path.basename(gj_file))[0] shp_file os.path.join(output_dir, base_name .shp) try: mapping geojson_to_shp(gj_file, shp_file) results.append((gj_file, 成功, mapping)) except Exception as e: results.append((gj_file, f失败: {str(e)}, None)) # 输出处理报告 for file, status, mapping in results: print(f{file}: {status}) return results批量处理的时候异常捕获很重要。一个文件出错不应该中断整个批次。把错误信息记录下来最后统一处理。5.2 大文件的内存控制GeoJSON 文件如果特别大几百 MB 甚至 GB 级别一次性读入内存可能撑爆。OGR 的GetNextFeature()是流式读取的本身不会一次性加载所有数据。但要注意几点不要用GetFeatureCount()之外的聚合操作及时释放 feature 对象feat None如果只需要部分字段可以在读取时用SetAttributeFilter或者Select减少数据量对于超大文件还可以考虑分块处理先按空间范围或者属性条件把数据切成小块分别转换最后合并。不过 Shapefile 单文件有 2GB 的大小限制超大 GeoJSON 转成单个 Shapefile 本身就不现实分块是必须的。5.3 性能对比数据我用一个包含约 5 万个多边形要素的 GeoJSON 文件做了测试不同方法的耗时对比方法耗时秒内存峰值gdal.VectorTranslate 一行12.3较高手动逐要素复制无事务45.6低手动逐要素复制有事务14.2低GeoPandas to_file18.7很高手动加事务的版本和一行版本耗时接近但内存占用更低而且中间可以插入字段处理逻辑。GeoPandas 虽然代码短但内存占用明显偏高处理大文件时容易出问题。6. 常见问题与排查技巧实录6.1 中文乱码问题这是最高频的问题。表现是转换后的 Shapefile 在 GIS 软件里打开属性表中的中文显示为乱码。原因通常是编码不匹配。GeoJSON 标准规定使用 UTF-8但有些数据实际是 GBK 编码的。OGR 读取时如果按 UTF-8 解析 GBK 内容就会乱码。排查步骤用文本编辑器打开 GeoJSON确认实际编码如果源文件不是 UTF-8先用 Python 转码with open(input.geojson, r, encodinggbk) as f: content f.read() with open(input_utf8.geojson, w, encodingutf-8) as f: f.write(content)输出时指定ENCODINGUTF-8如果下游软件不支持改成ENCODINGGBK还有一个隐藏问题.cpg文件。Shapefile 的编码信息可以存在同名的.cpg文件里。OGR 写入时会自动生成这个文件。如果下游软件不读.cpg还是可能乱码这时候需要手动确认.cpg内容是否正确。6.2 字段丢失或截断转换后发现某些字段不见了或者内容被截断了。原因和对策现象原因解决方法字段完全丢失字段名超 10 字符被截断后重名转换前做字段名映射字符串内容截断超过 254 字节截断前记录或改存外部文件数值精度丢失浮点数宽度不足创建字段时指定足够的宽度和精度日期字段异常格式不兼容转成字符串存储我在实际项目中养成了一个习惯转换前先跑一遍字段审查脚本把所有字段名、类型、最大长度打印出来人工确认一遍再转。这个步骤花不了几分钟但能避免大量返工。6.3 几何无效问题有些 GeoJSON 的几何本身是无效的比如自相交的多边形、重复点、环方向错误。Shapefile 对几何有效性要求比 GeoJSON 严格无效几何可能导致写入失败或者数据异常。处理方式from osgeo import ogr geom feat.GetGeometryRef() if not geom.IsValid(): # 尝试修复 geom geom.MakeValid() # 或者用 buffer(0) 的经典技巧 # geom geom.Buffer(0)MakeValid()是 GDAL 2.0 之后提供的几何修复方法能处理大部分常见问题。Buffer(0)是老方法对某些自相交多边形有效但可能改变几何形状慎用。6.4 坐标系缺失或错误转换后数据位置偏移通常是坐标系问题。排查思路检查源 GeoJSON 是否有crs字段虽然 RFC 7946 不推荐但很多数据还是带着检查输出 Shapefile 的.prj文件内容用ogrinfo命令查看坐标系信息ogrinfo -al -so output.shp如果源数据没有坐标系信息OGR 会默认当作 WGS84 处理。如果实际不是 WGS84数据就会偏移。这时候需要在转换时手动指定源坐标系。6.5 常见问题速查表问题可能原因快速排查文件打不开路径含中文或特殊字符改用英文路径测试属性表为空字段创建失败检查字段名是否合法几何丢失几何类型不匹配确认图层几何类型转换速度慢未开事务加 StartTransaction内存溢出一次性加载过多改用流式读取中文乱码编码不一致检查 .cpg 和 ENCODING 参数7. 反向转换与扩展思路7.1 Shapefile 转 GeoJSON反向转换的逻辑类似只是驱动换一下gdal.VectorTranslate( output.geojson, input.shp, formatGeoJSON, options[ENCODINGUTF-8] )反向转换的坑少一些因为 GeoJSON 的限制比 Shapefile 少。但要注意Shapefile 的字段名是截断过的转成 GeoJSON 后字段名还是短的不会自动恢复。如果原始字段名有对照表可以在转换后手动改回来。7.2 其他格式的互转掌握了 OGR 的这套流程其他格式之间的转换也是同样的套路。把驱动名换一下就行ESRI Shapefile→ ShapefileGeoJSON→ GeoJSONGPKG→ GeoPackageSQLite→ SpatiaLiteKML→ KMLMapInfo File→ MapInfoOGR 支持的矢量格式有几十种大部分都可以用VectorTranslate一行搞定。需要精细控制的时候就用手动逐要素复制的方式。7.3 在自动化流程中集成这套转换脚本很容易集成到自动化流程里。我一般的做法是用argparse做成命令行工具方便调用加日志记录每次转换的输入输出、字段映射、耗时都记下来加校验步骤转换后检查输出文件是否存在、要素数量是否一致import argparse parser argparse.ArgumentParser(descriptionGeoJSON 转 Shapefile) parser.add_argument(input, help输入 GeoJSON 文件) parser.add_argument(output, help输出 Shapefile 路径) parser.add_argument(--encoding, defaultUTF-8, help属性编码) args parser.parse_args() geojson_to_shp(args.input, args.output, args.encoding)做成命令行工具之后可以很方便地接到任务调度系统里定时批量处理数据。8. 几个我踩过的坑和实操心得第一个坑是字段名截断导致的静默数据丢失。早期我没做字段名检查直接转换结果两个长字段名截断后变成一样的后面的字段把前面的覆盖了。数据交出去之后才发现少了一列返工重做。从那以后我每次转换前都会先打印字段清单确认没有截断冲突。第二个坑是坐标系默认值。有一次处理的数据源没有坐标系信息OGR 默认按 WGS84 处理但实际数据是 CGCS2000 的投影坐标。转换后数据位置偏了几百公里。后来养成了习惯拿到数据先用ogrinfo看一眼坐标系没有的话手动确认。第三个坑是大文件的内存问题。有个 GeoJSON 文件接近 1GB用 GeoPandas 读取直接内存溢出。换成 OGR 流式读取之后内存占用稳定在几百 MB顺利跑完。这件事让我坚定了生产环境用 OGR 而不是 GeoPandas 的想法。第四个坑是编码问题。中文乱码这个问题我遇到过好几次每次原因都不一样有时候是源文件编码不对有时候是输出编码没指定有时候是下游软件不认 UTF-8。现在的做法是转换前确认源编码转换时显式指定输出编码转换后用 QGIS 打开抽查一下属性表。最后一个心得转换脚本要加校验。转换完成后对比一下输入和输出的要素数量、字段数量、几何类型。如果数量对不上说明中间有要素被过滤或者写入失败。这个校验步骤花不了几秒钟但能提前发现大部分问题。这套 GeoJSON 转 Shapefile 的方案我在多个项目中反复用过处理过从几 KB 到几百 MB 的各种数据。核心代码就是上面那些根据具体需求调整字段映射和坐标系处理就行。GDAL/OGR 这个库虽然 API 有点老派但稳定性和可控性是经过时间检验的值得花时间掌握。