☰
Python GDAL实战:GeoJSON转Shapefile的完整指南与避坑技巧
2026/10/12 17:06:54 网站建设 项目流程

1. 从一个真实的格式转换需求说起

做GIS数据处理的人,迟早会撞上格式转换这件事。我最早接触这个需求,是帮一个做城市规划的朋友处理一批开源地图数据——他从各种渠道拿到的矢量数据几乎全是GeoJSON格式,但下游的分析工具和出图流程只认Shapefile。当时我第一反应是找个在线转换工具,结果文件一大就卡死,字段一多就乱码,坐标系还经常莫名其妙跑偏。后来老老实实用Python的GDAL/OGR库写脚本,才发现这件事远比想象中可控,也比想象中有坑。

GeoJSON和Shapefile这两种格式,表面上看都是"存矢量数据"的容器,但底层的设计哲学完全不同。GeoJSON是基于JSON的文本格式,一个文件里可以同时装点、线、面、多面,属性字段用键值对表示,坐标系默认是WGS84经纬度,结构灵活、可读性强,适合网络传输和快速查看。Shapefile则是ESRI时代的老古董,但它至今仍是行业交换格式的事实标准——它其实不是一个文件,而是一组文件的集合(.shp存几何、.shx存索引、.dbf存属性、.prj存投影),每个文件只能存一种几何类型,字段名限制在10个字符以内,属性表用的是dBASE格式。正是这些差异,导致转换过程绝不是"读进来再写出去"这么简单。

这篇文章面向的是需要批量处理矢量数据的朋友——不管你是做遥感、测绘、城市规划、物流路径分析,还是单纯想把网上下载的GeoJSON丢进QGIS做可视化,只要你有"把GeoJSON转成Shapefile"这个具体需求,下面的内容都能直接拿去用。我会从环境准备讲到完整代码,再重点拆解那些真正会让人卡住的细节:中文乱码、字段名截断、坐标系丢失、几何类型混杂、大文件内存溢出。这些坑我都踩过,有些还踩了不止一次。

提示:本文所有代码基于Python 3.8+和GDAL 3.x版本验证,GDAL 2.x与3.x在OGR的API上有一些差异,涉及的地方我会单独标注。

2. 环境准备:GDAL的安装为什么总是出问题

2.1 为什么pip install gdal经常失败

如果你之前尝试过pip install gdal,大概率见过那个经典的报错:Microsoft Visual C++ 14.0 or greater is required,或者干脆在编译阶段就卡死。这不是你的问题,是GDAL本身的特性决定的——它是一个用C/C++写的大型地理空间库,Python的gdal包只是它的绑定层(binding),安装时需要先有编译好的GDAL二进制库,再编译Python绑定。在Windows上,如果没有预编译的wheel,pip就会尝试从源码编译,而源码编译需要完整的C++工具链和一堆依赖,普通人根本配不起来。

正确的做法是绕开源码编译,直接用预编译好的包。目前最省心的方案有三个,我按推荐程度排序:

方案适用场景优点缺点
conda install -c conda-forge gdal用Anaconda/Miniconda的用户自动解决所有依赖,版本匹配需要装conda,环境体积大
pip install GDAL==对应版本(配合预编译wheel)纯pip环境轻量需要手动匹配版本,容易出错
OSGeo4W安装包Windows重度用户自带完整工具链配置PATH麻烦

我个人最推荐conda方案,一条命令搞定:

conda install -c conda-forge gdal

conda-forge频道维护的GDAL包质量很高,而且会自动把proj、geos、sqlite这些依赖一起装好。装完之后验证一下:

from osgeo import gdal, ogr, osr print(gdal.__version__)

能打印出版本号(比如3.6.2)就说明成功了。如果报ModuleNotFoundError: No module named 'osgeo',说明装是装了但Python找不到,通常是环境没激活或者装到了别的Python里。

2.2 纯pip环境的版本匹配技巧

如果你坚持用pip,那必须让pip安装的GDAL版本和系统里已有的GDAL库版本严格一致。查看系统GDAL版本的方法:

gdalinfo --version

假设输出是GDAL 3.6.2,那你就得装pip install GDAL==3.6.2。但问题是,PyPI上GDAL的wheel并不是所有版本都有,尤其是Windows平台。这时候可以去一些第三方wheel源找对应版本的预编译包,下载whl文件后本地安装:

pip install GDAL-3.6.2-cp39-cp39-win_amd64.whl

注意文件名里的cp39表示Python 3.9,win_amd64表示64位Windows,必须和你的环境完全对应,否则装了也用不了。

注意:不要同时用conda和pip装GDAL,两个包管理器装的版本会打架,出现各种诡异的导入错误。选定一个方案就坚持用下去。

2.3 验证OGR驱动是否可用

GDAL装好不代表OGR的所有驱动都能用。GeoJSON和Shapefile这两个驱动是核心驱动,默认都会编译进去,但还是验证一下比较稳妥:

from osgeo import ogr driver_geojson = ogr.GetDriverByName('GeoJSON') driver_shp = ogr.GetDriverByName('ESRI Shapefile') print(driver_geojson, driver_shp)

如果任何一个返回None,说明该驱动没被编译进你的GDAL。这种情况在精简版安装包里偶尔出现,解决办法是换一个完整的安装源重装。正常情况下两个都应该返回driver对象。

3. 核心转换逻辑:OGR的数据模型怎么理解

3.1 DataSource、Layer、Feature、Geometry四层结构

OGR的数据模型是理解整个转换过程的关键。它把矢量数据抽象成四层:最上层是DataSource(数据源),对应一个文件或一个数据库连接;往下是Layer(图层),对应Shapefile里的一个.shp文件或者GeoJSON里的一个FeatureCollection;再往下是Feature(要素),就是一条具体的记录,包含几何和属性;最底层是Geometry(几何),描述点、线、面这些空间形状。

这个层级关系决定了转换的基本流程:打开源DataSource → 获取Layer → 遍历每个Feature → 把Feature写入目标Layer。听起来很直白,但每一层都有细节。

GeoJSON的一个文件里通常只有一个Layer(一个FeatureCollection),但Shapefile的一个DataSource(一个文件夹或一组同名文件)里可以有多个Layer。反过来,GeoJSON的一个Layer里可以混合点、线、面,而Shapefile的一个Layer只能有一种几何类型。这就是转换时最核心的矛盾点。

3.2 读取GeoJSON的正确姿势

读取GeoJSON有两种方式,一种是用ogr.Open(),一种是用ogr.GetDriverByName('GeoJSON').Open()。前者让GDAL自动识别格式,后者强制指定驱动。我建议用后者,因为自动识别偶尔会在文件扩展名不规范时出错。

from osgeo import ogr # 方式一:自动识别 ds = ogr.Open('input.geojson', 0) # 0表示只读 # 方式二:指定驱动(推荐) driver = ogr.GetDriverByName('GeoJSON') ds = driver.Open('input.geojson', 0) if ds is None: raise FileNotFoundError('无法打开GeoJSON文件') layer = ds.GetLayer(0) # 获取第一个图层 print(f'要素数量: {layer.GetFeatureCount()}') print(f'几何类型: {layer.GetGeomType()}') print(f'坐标系: {layer.GetSpatialRef()}')

这里有个容易忽略的点:GetGeomType()返回的是一个整数编码,比如1是Point,2是LineString,3是Polygon,而wkbPoint、wkbLineString这些常量在ogr模块里可以直接用。如果GeoJSON里混合了多种几何类型,GetGeomType()会返回wkbUnknown(值为0),这时候就不能直接往Shapefile里写了,必须先做几何类型分离。

3.3 写入Shapefile时的字段处理

Shapefile的.dbf属性表有几个硬性限制,这是转换时最容易出问题的地方:

  • 字段名最长10个字符,超出的部分会被截断。比如population_density会变成population,如果同时有population_count,两个字段截断后可能重名,导致数据丢失。
  • 字段类型有限,只支持Integer、Real、String、Date这几种。GeoJSON里的布尔值、嵌套对象、数组都没法直接存。
  • 字符串字段有长度限制,默认254个字符,超长会被截断。
  • 不支持NULL值,空值会被写成0或空字符串。

所以在写入之前,必须对源数据的字段做一次"体检"。我的做法是先遍历一遍所有Feature,统计每个字段的实际类型和最大长度,然后据此创建目标字段:

# 先扫描一遍,确定字段类型和长度 field_info = {} layer.ResetReading() for feature in layer: for i in range(feature.GetFieldCount()): field_defn = feature.GetFieldDefnRef(i) name = field_defn.GetName() # 处理字段名截断 safe_name = name[:10] if safe_name not in field_info: field_info[safe_name] = { 'type': field_defn.GetType(), 'width': field_defn.GetWidth(), 'original': name } else: # 如果同名字段类型冲突,升级为字符串 if field_info[safe_name]['type'] != field_defn.GetType(): field_info[safe_name]['type'] = ogr.OFTString

这段代码的核心逻辑是:字段名截断后如果发生冲突,就把冲突的字段统一升级成字符串类型,牺牲一点存储效率换取数据完整性。这是我在处理一批带中文字段名的数据时总结出来的,因为中文字段名转成Shapefile后经常出现乱码或截断,统一转成拼音或英文缩写反而更稳。

4. 完整可运行的转换脚本

4.1 基础版本:单文件转换

先给一个最基础、能直接跑的版本,适合处理结构规整、几何类型单一的GeoJSON:

from osgeo import ogr, osr import os def geojson_to_shp(geojson_path, shp_path, encoding='UTF-8'): """ 将GeoJSON文件转换为Shapefile :param geojson_path: 输入GeoJSON路径 :param shp_path: 输出Shapefile路径(.shp) :param encoding: 属性编码,中文建议用GBK或UTF-8 """ # 打开源文件 src_driver = ogr.GetDriverByName('GeoJSON') src_ds = src_driver.Open(geojson_path, 0) if src_ds is None: raise FileNotFoundError(f'无法打开: {geojson_path}') src_layer = src_ds.GetLayer(0) geom_type = src_layer.GetGeomType() srs = src_layer.GetSpatialRef() # 如果目标文件已存在,先删除 if os.path.exists(shp_path): dst_driver = ogr.GetDriverByName('ESRI Shapefile') dst_driver.DeleteDataSource(shp_path) # 创建目标文件 dst_driver = ogr.GetDriverByName('ESRI Shapefile') dst_ds = dst_driver.CreateDataSource(shp_path) dst_layer = dst_ds.CreateLayer( os.path.splitext(os.path.basename(shp_path))[0], srs, geom_type ) # 复制字段定义 src_layer.ResetReading() src_defn = src_layer.GetLayerDefn() for i in range(src_defn.GetFieldCount()): field_defn = src_defn.GetFieldDefn(i) # 字段名截断到10字符 new_name = field_defn.GetName()[:10] new_field = ogr.FieldDefn(new_name, field_defn.GetType()) new_field.SetWidth(field_defn.GetWidth()) new_field.SetPrecision(field_defn.GetPrecision()) dst_layer.CreateField(new_field) # 逐要素复制 dst_defn = dst_layer.GetLayerDefn() for src_feature in src_layer: dst_feature = ogr.Feature(dst_defn) # 复制几何 geom = src_feature.GetGeometryRef() if geom is not None: dst_feature.SetGeometry(geom.Clone()) # 复制属性 for i in range(src_defn.GetFieldCount()): field_name = src_defn.GetFieldDefn(i).GetName()[:10] value = src_feature.GetField(i) dst_feature.SetField(field_name, value) dst_layer.CreateFeature(dst_feature) dst_feature = None # 释放资源 dst_ds = None src_ds = None print(f'转换完成: {shp_path}') # 使用示例 geojson_to_shp('input.geojson', 'output.shp')

这个脚本能处理大部分常规情况,但有几个地方需要特别注意。第一,CreateLayer的第三个参数是几何类型,如果源数据的几何类型是wkbUnknown,这里会报错,需要先做类型分离。第二,字段复制时我用了[:10]截断,但没有处理重名冲突,如果两个字段截断后重名,后一个会覆盖前一个。第三,编码问题没有处理,中文属性可能乱码。

4.2 进阶版本:处理几何类型混杂和中文编码

实际数据往往比理想情况复杂。下面这个版本增加了几何类型分离和编码处理:

from osgeo import ogr, osr import os from collections import defaultdict def geojson_to_shp_advanced(geojson_path, out_dir, encoding='GBK'): """ 处理几何类型混杂的GeoJSON,按几何类型分离输出多个Shapefile """ src_driver = ogr.GetDriverByName('GeoJSON') src_ds = src_driver.Open(geojson_path, 0) if src_ds is None: raise FileNotFoundError(f'无法打开: {geojson_path}') src_layer = src_ds.GetLayer(0) srs = src_layer.GetSpatialRef() src_defn = src_layer.GetLayerDefn() # 按几何类型分组 geom_groups = defaultdict(list) src_layer.ResetReading() for feature in src_layer: geom = feature.GetGeometryRef() if geom is None: continue gtype = geom.GetGeometryType() geom_groups[gtype].append(feature.Clone()) # 几何类型编码到名称的映射 type_names = { ogr.wkbPoint: 'point', ogr.wkbLineString: 'line', ogr.wkbPolygon: 'polygon', ogr.wkbMultiPoint: 'multipoint', ogr.wkbMultiLineString: 'multiline', ogr.wkbMultiPolygon: 'multipolygon' } dst_driver = ogr.GetDriverByName('ESRI Shapefile') base_name = os.path.splitext(os.path.basename(geojson_path))[0] for gtype, features in geom_groups.items(): type_name = type_names.get(gtype, f'type{gtype}') shp_path = os.path.join(out_dir, f'{base_name}_{type_name}.shp') if os.path.exists(shp_path): dst_driver.DeleteDataSource(shp_path) dst_ds = dst_driver.CreateDataSource(shp_path) # 注意:Shapefile不支持Multi类型,需要降级为对应的单类型 shp_geom_type = gtype if gtype == ogr.wkbMultiPoint: shp_geom_type = ogr.wkbPoint elif gtype == ogr.wkbMultiLineString: shp_geom_type = ogr.wkbLineString elif gtype == ogr.wkbMultiPolygon: shp_geom_type = ogr.wkbPolygon dst_layer = dst_ds.CreateLayer( f'{base_name}_{type_name}', srs, shp_geom_type, options=['ENCODING=' + encoding] ) # 字段处理:截断+去重 used_names = set() field_mapping = {} for i in range(src_defn.GetFieldCount()): field_defn = src_defn.GetFieldDefn(i) orig_name = field_defn.GetName() new_name = orig_name[:10] # 处理重名 counter = 1 while new_name in used_names: suffix = str(counter) new_name = orig_name[:10-len(suffix)] + suffix counter += 1 used_names.add(new_name) field_mapping[i] = new_name new_field = ogr.FieldDefn(new_name, field_defn.GetType()) new_field.SetWidth(field_defn.GetWidth()) new_field.SetPrecision(field_defn.GetPrecision()) dst_layer.CreateField(new_field) dst_defn = dst_layer.GetLayerDefn() for src_feature in features: dst_feature = ogr.Feature(dst_defn) geom = src_feature.GetGeometryRef() if geom is not None: # Multi类型需要拆解 if geom.GetGeometryType() in (ogr.wkbMultiPoint, ogr.wkbMultiLineString, ogr.wkbMultiPolygon): for j in range(geom.GetGeometryCount()): sub_geom = geom.GetGeometryRef(j) dst_feature.SetGeometry(sub_geom.Clone()) for i in range(src_defn.GetFieldCount()): dst_feature.SetField(field_mapping[i], src_feature.GetField(i)) dst_layer.CreateFeature(dst_feature) dst_feature = ogr.Feature(dst_defn) else: dst_feature.SetGeometry(geom.Clone()) for i in range(src_defn.GetFieldCount()): dst_feature.SetField(field_mapping[i], src_feature.GetField(i)) dst_layer.CreateFeature(dst_feature) dst_feature = None dst_ds = None print(f'输出: {shp_path}, 要素数: {len(features)}') src_ds = None

这个版本解决了三个关键问题:几何类型分离、Multi类型拆解、字段名去重。options=['ENCODING=GBK']这一行是控制.dbf文件编码的关键,中文环境下用GBK兼容性最好,用UTF-8的话在某些老版本GIS软件里会乱码。

4.3 批量转换的目录遍历

如果你有一整个文件夹的GeoJSON要转,加一层遍历就行:

import glob def batch_convert(input_dir, output_dir): os.makedirs(output_dir, exist_ok=True) geojson_files = glob.glob(os.path.join(input_dir, '*.geojson')) geojson_files += glob.glob(os.path.join(input_dir, '*.json')) for gj in geojson_files: try: geojson_to_shp_advanced(gj, output_dir) except Exception as e: print(f'处理失败 {gj}: {e}') batch_convert('./geojson_data', './shp_output')

批量处理时建议加try-except,因为总会有几个文件因为格式问题处理失败,不能让一个坏文件中断整个批次。

5. 那些真正会让你卡住的坑

5.1 中文属性乱码:编码问题的完整排查链路

中文乱码是Shapefile转换里最经典的问题,我前后遇到过三次,每次原因都不一样。第一次是GeoJSON本身编码不是UTF-8,第二次是.dbf写入时没指定编码,第三次是读取时用了错误的编码。完整的排查链路是这样的:

第一步,确认源文件编码。GeoJSON规范要求用UTF-8,但实际数据里经常混入GBK编码的文件。用Python检查一下:

with open('input.geojson', 'rb') as f: raw = f.read(1000) try: raw.decode('utf-8') print('源文件是UTF-8') except UnicodeDecodeError: print('源文件不是UTF-8,可能是GBK')

第二步,确认OGR读取时的编码。GDAL 3.x默认按UTF-8读取GeoJSON,如果源文件是GBK,读进来就是乱码。这时候需要设置配置选项:

from osgeo import gdal gdal.SetConfigOption('SHAPE_ENCODING', 'GBK')

第三步,确认写入时的编码。就是前面代码里的options=['ENCODING=GBK']。这一步决定了.dbf文件用什么编码存储。

第四步,确认读取端的编码。用QGIS或ArcGIS打开时,如果软件默认按UTF-8解析而文件是GBK,照样乱码。这时候需要在软件里手动指定编码,或者干脆统一用UTF-8。

我的经验是:全链路统一用UTF-8最省心,但前提是所有下游软件都支持。如果下游有老版本ArcGIS,那就用GBK。最怕的是源文件GBK、中间UTF-8、输出又GBK,转来转去必然出问题。

5.2 字段名截断导致的数据覆盖

前面提过Shapefile字段名10字符限制,但实际踩坑时比想象的更隐蔽。我处理过一批数据,字段名是name_cn、name_en、name_code,截断后变成name_cn、name_en、name_code——看起来没冲突,但name_code截断后是name_code(刚好10个字符),而另一个字段name_cn_full截断后也是name_cn,和第一个字段撞了。结果就是后写入的覆盖了先写入的,数据静默丢失。

排查方法是在写入前打印所有截断后的字段名,人工检查一遍:

names = [] for i in range(src_defn.GetFieldCount()): orig = src_defn.GetFieldDefn(i).GetName() truncated = orig[:10] names.append(truncated) if names.count(truncated) > 1: print(f'警告:字段名冲突 {orig} -> {truncated}')

更稳妥的做法是维护一个字段名映射表,把原始字段名和截断后的名字对应起来,转换完成后输出一份映射日志,方便后续追溯。

5.3 坐标系丢失与.prj文件缺失

GeoJSON规范规定坐标系必须是WGS84(EPSG:4326),但实际数据里经常有例外——有些数据用Web Mercator(EPSG:3857),有些甚至没有坐标系信息。转换到Shapefile时,如果源数据没有SRS,生成的.shp就不会有.prj文件,下游软件打开时不知道坐标系,可能按默认值处理,导致位置偏移。

处理方法是显式指定坐标系:

srs = osr.SpatialReference() srs.ImportFromEPSG(4326) # WGS84 # 或者从源图层获取 src_srs = src_layer.GetSpatialRef() if src_srs is None: srs = osr.SpatialReference() srs.ImportFromEPSG(4326) else: srs = src_srs

如果源数据是Web Mercator而你需要WGS84,还得做投影转换:

from osgeo import osr src_srs = osr.SpatialReference() src_srs.ImportFromEPSG(3857) dst_srs = osr.SpatialReference() dst_srs.ImportFromEPSG(4326) transform = osr.CoordinateTransformation(src_srs, dst_srs) geom.Transform(transform)

注意:osr.SpatialReference()在GDAL 3.x里默认使用新的坐标轴顺序(经纬度),而GDAL 2.x默认是纬度经度。如果发现坐标反了,加一句srs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)就能解决。

5.4 大文件内存溢出

GeoJSON是文本格式,一个几百MB的文件读进内存后可能膨胀好几倍。如果直接for feature in layer遍历,OGR是流式读取的,内存占用还好。但如果先把所有Feature存到列表里(比如前面按几何类型分组的代码),大文件就会爆内存。

优化方案是用两遍扫描:第一遍只统计几何类型和字段信息,不存Feature;第二遍再按类型写入。或者用OGR的SetNextByIndex做随机访问,但性能不如顺序读取。对于超大文件,我建议先用命令行工具切分:

ogr2ogr -f GeoJSON -where "type='Point'" points.geojson input.geojson

按几何类型预先切分,再分别转换,内存压力会小很多。

6. 转换后的验证与常见问题排查

6.1 用ogrinfo快速验证结果

转换完成后别急着打开GIS软件,先用命令行工具验证一下:

ogrinfo -al -so output.shp

这个命令会输出图层的基本信息:几何类型、要素数量、字段列表、坐标系、范围。重点看三个地方:要素数量是否和源文件一致、字段是否完整、坐标系是否正确。如果要素数量对不上,说明转换过程中有要素被丢弃,通常是几何类型不匹配导致的。

6.2 属性数据抽查

字段数量对了不代表数据对了。我习惯随机抽查几条记录,对比源文件和目标文件的属性值:

def compare_features(geojson_path, shp_path, sample_count=5): src_ds = ogr.Open(geojson_path) dst_ds = ogr.Open(shp_path) src_layer = src_ds.GetLayer(0) dst_layer = dst_ds.GetLayer(0) src_layer.ResetReading() dst_layer.ResetReading() for i in range(min(sample_count, src_layer.GetFeatureCount())): src_f = src_layer.GetFeature(i) dst_f = dst_layer.GetFeature(i) print(f'--- 要素 {i} ---') for j in range(src_f.GetFieldCount()): name = src_f.GetFieldDefnRef(j).GetName() print(f'{name}: {src_f.GetField(j)} vs {dst_f.GetField(j)}')

抽查时特别关注中文字段、长文本字段、数值字段的精度。数值精度丢失是Shapefile的另一个坑——.dbf里的Real类型是双精度,但有些软件写入时会降级为单精度,导致小数点后几位丢失。

6.3 几何有效性检查

GeoJSON对几何有效性的要求比Shapefile宽松,有些自相交的多边形在GeoJSON里能存,转到Shapefile后可能导致渲染异常。转换后可以用IsValid()检查:

for feature in dst_layer: geom = feature.GetGeometryRef() if geom is not None and not geom.IsValid(): print(f'无效几何: {feature.GetFID()}') # 尝试修复 geom = geom.MakeValid()

MakeValid()是GDAL 3.x新增的方法,能自动修复大部分几何问题。如果数据量不大,转换时直接对每个几何调用一次MakeValid()是最省事的做法。

7. 几个提升效率的实战技巧

7.1 用ogr2ogr命令行替代Python脚本

如果你的需求是固定的、不需要复杂逻辑,其实不用写Python,GDAL自带的ogr2ogr命令一行就能搞定:

ogr2ogr -f "ESRI Shapefile" output.shp input.geojson -lco ENCODING=GBK -nln output

-lco是图层创建选项,-nln指定输出图层名。这个命令处理常规转换足够了,而且性能比Python脚本好,因为省去了Python层的开销。只有在需要字段重命名、几何类型分离、批量处理这些复杂逻辑时,才值得写Python。

7.2 字段名映射表的维护

对于需要反复转换的数据源,建议维护一份字段名映射表(CSV或JSON),把原始字段名和Shapefile字段名的对应关系固定下来。这样每次转换结果一致,下游流程不会因为字段名变化而中断。映射表大概长这样:

{ "population_density": "pop_dens", "administrative_name": "admin_name", "create_timestamp": "create_ts" }

转换时读这个表,按映射关系创建字段,比自动截断可控得多。

7.3 并行处理多个文件

批量转换时,如果文件数量多,可以用多进程加速:

from multiprocessing import Pool import glob def convert_one(args): gj, out_dir = args try: geojson_to_shp_advanced(gj, out_dir) return (gj, 'success') except Exception as e: return (gj, str(e)) if __name__ == '__main__': files = glob.glob('./geojson_data/*.geojson') tasks = [(f, './shp_output') for f in files] with Pool(4) as p: results = p.map(convert_one, tasks) for f, status in results: print(f'{f}: {status}')

进程数建议设为CPU核心数,不要开太多,因为GDAL本身有一些全局状态,进程间隔离比线程间隔离更安全。

7.4 转换日志的记录

生产环境下,每次转换都应该记录日志:源文件、目标文件、要素数量、字段映射、耗时、异常信息。这些日志在出问题时是排查的第一手资料。我通常用一个简单的CSV记录:

import csv import time def log_conversion(src, dst, count, elapsed, status): with open('conversion_log.csv', 'a', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow([time.strftime('%Y-%m-%d %H:%M:%S'), src, dst, count, f'{elapsed:.2f}s', status])

日志积累多了还能做统计分析,比如哪些文件转换失败率高、平均耗时多少,对优化流程有帮助。

8. 关于坐标系转换的一个补充说明

前面提到坐标系时说得比较简略,这里单独展开一下,因为这是实际项目里绕不开的一环。GeoJSON标准规定用WGS84,但国内很多数据用的是GCJ-02(火星坐标系)或者BD-09(百度坐标系),这些坐标系之间的转换不是简单的投影变换,而是带偏移量的加密算法。GDAL的OGR库本身不处理这种加密坐标系的转换,需要额外的转换库或者自己实现算法。

如果你的数据涉及这类坐标系,转换流程要分成两步:先用专门的库把坐标转成WGS84,再用OGR转Shapefile。直接丢给OGR转换的话,坐标会保持原样,但.prj文件会写成WGS84,导致位置偏移几百米。这个坑我在一个物流路径项目里踩过,当时排查了一整天才发现是坐标系的问题。

判断数据是否用了加密坐标系,可以看坐标值:WGS84的经纬度范围是经度-180到180、纬度-90到90,如果坐标值明显超出这个范围(比如经度116.4、纬度39.9是正常的北京坐标,但如果出现经度11640400这种值,就是投影坐标或者加密坐标)。另一个方法是拿几个已知地点的坐标对比,偏差在几十米以内是正常的投影误差,偏差几百米就是坐标系不匹配。

9. 写在最后的一点个人体会

GDAL/OGR这套工具链的学习曲线确实陡,但一旦掌握了它的数据模型,处理各种矢量格式转换就是水到渠成的事。我最初也是被各种报错折磨得够呛,后来慢慢摸清了它的脾气——它不负责帮你做数据清洗,只负责忠实地读写,所有格式之间的差异都需要你自己在中间层处理。

几个我反复验证过的原则:字段名提前做映射不要依赖自动截断,编码全链路统一不要中途转换,几何类型提前分离不要指望自动处理,坐标系显式指定不要依赖默认值。这四条做到了,90%的转换问题都能避免。

另外,GDAL的官方文档虽然全面但比较分散,遇到具体问题时,ogrinfo和ogrinfo -al -so这两个命令是最好的调试工具,能快速看到数据的真实状态。Python层面,dir(feature)和dir(layer)能列出所有可用方法,比翻文档快得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询