1. 为什么老有人问 GDAL 创建矢量图层
搞GIS开发的人,尤其是用C++或者Python跟GDAL打交道比较多的,几乎都绕不过“创建矢量图层”这个操作。但就这么个基础功能,反而成了很多人困惑的起点。原因其实很简单:GDAL的文档虽然全,但对新手并不友好,各种概念堆在一起——Driver、DataSource、Layer、Feature、Geometry——第一次接触的人很容易被绕晕。再加上网上不少教程要么只讲其中一种方式,要么就是一上来就甩代码,完全不解释为什么这么写,结果就是很多人照着抄能跑,换个格式就懵了。
我自己最早接触这个功能的时候也踩过坑。当时的需求很简单:把一批坐标点写成Shapefile。我照着网上找的示例代码,吭哧吭哧写完,确实能生成文件,但回头想改造成写GeoJSON、写PostGIS,甚至写GPKG,就发现原来的代码完全套不上。后来才意识到,GDAL创建矢量图层这事,表面上是在“创建一个文件”,本质上其实是在“操作一个数据源”,而这个数据源跟输出格式强相关。搞懂了这一层,后面所有困惑基本都能解开。
这篇文章想聊的,就是GDAL创建矢量图层最常见的两种方式。一种是最标准的“Driver驱动 + 数据源 + 图层”三层结构,适合绝大多数文件类格式;另一种是通过创建数据源时直接指定扩展名或格式,让GDAL自己去推断驱动,代码更省事但控制力弱一些。两种方式各有各的适用场景,我在实际项目中都大量用过,所以今天就把它们的差异、实现细节和踩坑心得一次性讲透。
2. 动手前的底层认知:GDAL抽象模型到底在说什么
在写任何一行创建图层的代码之前,有些底层概念必须先理顺。我见过太多人代码写得飞起,但问他“Driver、DataSource、Layer分别是什么关系”就答不上来。这种状态很危险,因为GDAL的设计其实非常抽象,如果不理解它的抽象模型,一旦遇到多图层文件、多格式输出、属性读写这些稍微复杂一点的需求,就会立刻卡壳。
2.1 从一份炒饭外卖说起,理解三个核心对象
打个比方。假设你去点一份炒饭外卖。你打开外卖App,找到一家你喜欢的餐厅,然后从它的菜单里点一份炒饭。这个过程里其实藏了三个角色:
- 餐厅:相当于Driver,也就是驱动。GDAL通过Driver知道“这家店是做哪种菜系的”——对应到GIS里,就是Shapefile驱动、GeoJSON驱动、GPKG驱动等等。每一种驱动负责一种格式的读写。
- 这家餐厅的门店:相当于DataSource,也就是数据源。你找到餐厅后,是在这个具体的门店里点单。对应到GDAL里,一个DataSource就是一个具体的文件、数据库连接或者内存数据集。
- 菜单里的炒饭:相当于Layer,也就是图层。门店会有很多菜品,对应到GIS里,同一个数据源(比如一个GPKG文件)里可以包含多个图层(Layer),每个图层里有几何对象和属性字段。
这个类比虽然简单,但它能帮你建立起最重要的一个认知:GDAL操作矢量数据,核心顺序永远是“先定位驱动,再打开数据源,然后在数据源里操作图层”。凡是能套进这个顺序的格式,GDAL基本都支持;凡是套不进去的,大概率就是你搞错了概念。
2.2 两种创建方式的核心差异:控制力与代码量的取舍
接下来要说重点了:创建矢量图层的两种方式到底差在哪。
第一种方式,是我最推荐的,也是GDAL官方示例里最常见的方式。它分三步走——先用GetDriverByName拿到对应格式的驱动,再用驱动创建DataSource,最后在DataSource上调用CreateLayer创建图层。整个过程完全可控,每一步都能拿到中间对象,想做错误处理也方便。缺点是代码稍微多几行,但对于正式项目来说,这点冗余完全是值得的。
第二种方式,是直接用drv.CreateDataSource或者SHPPointer这种快捷方式,把文件路径传进去,GDAL会根据扩展名自动判断该用哪个Driver。这种方式写起来极其省事,两行代码就能创建一个图层。但代价是你对底层驱动的控制力变弱了,比如你想指定创建选项(像是Shapefile的编码方式、GeoJSON的坐标精度),快捷方式就不太好用了。
我在实际工作中其实是两种混着用的:快速验证想法的时候用第二种,写正式工具或者要发布给团队用的时候,一定改成第一种。为什么?因为第二种方式依赖扩展名推断,一旦文件名起得不规范,或者输出格式比较冷门,GDAL推断失败就会直接报错,而且报错信息往往让人摸不着头脑。第一种方式虽然代码多几行,但每一步都明确指定了驱动,出问题也好排查。
2.3 什么时候该用哪种方式,一张表给你答案
| 使用场景 | 推荐方式 | 原因 |
|---|---|---|
| 快速验证、临时生成测试文件 | 方式二(快捷创建) | 代码少,马上能跑 |
| 正式项目、工具开发 | 方式一(标准流程) | 控制力强,可指定创建选项 |
| 输出格式不确定、由用户输入决定 | 方式一 | 可提前检测驱动是否存在 |
| 写入GeoJSON、GPKG等多图层格式 | 方式一 | 方便管理多个图层 |
| 需要指定空间参考、字段类型、编码 | 方式一 | 每种驱动都能单独配置 |
| 纯写一个简单CSV之类的文件 | 方式二 | 简单场景不需要复杂配置 |
表格里提到的“创建选项”是个关键概念。每种GDAL驱动都支持一组特定的CreateOption,比如Shapefile驱动可以指定编码(ENCODING),GeoJSON驱动可以指定坐标精度(COORDINATE_PRECISION)。这些选项只有在用第一种方式、拿到Driver之后才方便设置。这是我在项目里吃过亏的地方:用快捷方式写GeoJSON,默认精度不够,结果生成的文件坐标被截断,数据精度丢失,排查了半天才发现是创建选项的问题。
3. 方式一详解:标准流程逐步拆解
这一节是全文的重头戏。标准流程看着简单,但每一步都藏着不少细节。我会从零开始,把创建矢量图层的全流程拆开来讲,包括代码结构、每个参数的含义、以及最容易出问题的地方。
3.1 第一步:拿驱动,但不是所有驱动都叫Shapefile
拿驱动这一步,代码就一行:
from osgeo import ogr driver = ogr.GetDriverByName("ESRI Shapefile")但这一行背后,我建议你先做一件事:确认驱动存在。GDAL不是所有格式都内置支持的,有些格式需要额外的库或者插件。如果驱动不存在的,GetDriverByName会返回None,你后面所有代码都会在调用时直接崩溃。
我一般会这样写:
driver = ogr.GetDriverByName("ESRI Shapefile") if driver is None: raise RuntimeError("ESRI Shapefile 驱动不可用,请检查GDAL安装")这里有个小细节值得说。你可能会好奇,“ESRI Shapefile”这个名字是哪儿来的。GDAL里每个驱动都有自己的名称字符串,Shapefile驱动叫“ESRI Shapefile”,GeoJSON驱动叫“GeoJSON”,GPKG驱动叫“GPKG”。这些名字是固定的,不能随便写。最稳妥的办法是运行ogrinfo --formats命令查看当前GDAL支持的所有驱动列表,里面显示的名字就是合法的驱动名。
3.2 第二步:创建数据源,区分“创建新文件”和“覆盖旧文件”
拿到驱动之后,下一步就是创建DataSource。这一步最容易被忽视的,是它的第二个参数——是否允许覆盖。看代码:
data_source = driver.CreateDataSource("output.shp")这样写,如果output.shp已经存在,GDAL会报错,提示文件已经存在。很多人第一次遇到这个报错会很懵,因为错误信息里只说“File already exists”,但没告诉你解决办法。
解决办法是在CreateDataSource的时候传一个数字作为第二个参数。GDAL约定,传0表示不允许覆盖已有的文件,传1表示允许覆盖:
data_source = driver.CreateDataSource("output.shp", 0) # 不允许覆盖 data_source = driver.CreateDataSource("output.shp", 1) # 允许覆盖我在写工具的时候,一般会提供两个选择:如果图层存在就追加,如果不存在才新建。这里又要多说一句:Shapefile一个文件只能对应一个图层,但GPKG这种格式一个文件可以对应多个图层。所以在用Shapefile时,如果你的需求是“往已有的文件里加图层”,那是做不到的,只能新建文件。
另外,创建DataSource时还有个跟文件系统相关的坑:父目录必须存在。GDAL不会帮你自动创建目录。如果你的输出路径是/data/output/2024/result.shp,但/data/output/2024/这个目录不存在,GDAL同样会报错。这个问题我在自动任务里遇到过好多次,解决办法是在创建数据源之前先确保父目录存在:
import os output_dir = "/data/output/2024" os.makedirs(output_dir, exist_ok=True) data_source = driver.CreateDataSource(os.path.join(output_dir, "result.shp"))3.3 第三步:创建图层,SpatialReference和GeometryType怎么选
数据源有了,接下来就是核心中的核心——创建图层:
layer = data_source.CreateLayer("points", srs, ogr.wkbPoint)三个参数分别是图层名、空间参考(SpatialReference)、几何类型(GeometryType)。每一个单独拿出来说都能写一大篇,我捡最关键的讲。
先说空间参考。这个参数是可选的,可以传None。但传None意味着图层没有空间参考信息,很多GIS软件打开后会提示“unknow spatial reference”,给后期投影转换、叠加分析带来麻烦。我建议只要条件允许,一定显式指定空间参考。最常见的是用EPSG代码:
from osgeo import osr srs = osr.SpatialReference() srs.ImportFromEPSG(4326) # WGS84经纬度这里也提醒一句:4326是WGS84,3857是Web墨卡托,这两个是最常用的。国内项目还经常用GCJ02(火星坐标)或者CGCS2000,不同坐标系之间一定要搞清楚再往下走,不然后期数据对不上,非常痛苦。
再说几何类型。ogr模块里定义了一系列常量,常用的有:
ogr.wkbPoint:点ogr.wkbLineString:线ogr.wkbPolygon:面ogr.wkbMultiPoint:多点ogr.wkbMultiLineString:多线ogr.wkbMultiPolygon:多面
这些类型选错会有什么后果?举个真实案例。有一次我想创建一个点图层,但GeometryType误写成了ogr.wkbPolygon,结果创建图层本身没报错,但往里面写入点要素时,GDAL直接报错说几何类型不匹配。这个错误看起来是“写入”阶段的问题,但根子在“创建图层”阶段。
3.4 第四步:定义字段,绕不开的FieldDefn
图层创建好之后,如果不定义任何字段,这个图层就是一个只能存几何、不能存属性的“空壳子”。所以在写要素之前,必须先把表结构定义好。
字段的定义需要用到FieldDefn,一个字段就是一个FieldDefn对象。看代码:
field_defn = ogr.FieldDefn("name", ogr.OFTString) field_defn.SetWidth(12) layer.CreateField(field_defn)第一行创建字段定义,指定字段名和类型。第二行设置字段宽度,可选。第三行真正把字段添加到图层上。
字段类型这里容易犯的错是“所有属性都写字符串”。短期看没问题,后期做属性查询、统计、连接的时候就会发现例子不好用了。常用的字段类型有:
ogr.OFTString:字符串ogr.OFTInteger:整数ogr.OFTReal:浮点数ogr.OFTDate:日期ogr.OFTTime:时间ogr.OFTDateTime:日期时间
我见过有人在属性里存时间戳,直接用字符串写“2024-01-15 10:30:00”,查询的时候用字符串比较。能用,但效率低,而且遇到时区转换就完全没法处理了。不如一开始就用ogr.OFTDateTime类型,后期怎么处理都方便。
还有一个容易被忽略的点:定义字段时,尽量把字段宽度和精度设置到位。比如经纬度坐标,如果存成浮点数,默认精度可能只能存到小数点后6位,对于高精度定位数据来说,这已经不够了。可以在FieldDefn里用SetWidth和SetPrecision调整:
coord_defn = ogr.FieldDefn("longitude", ogr.OFTReal) coord_defn.SetWidth(12) coord_defn.SetPrecision(8) layer.CreateField(coord_defn)3.5 第五步:写要素,创建Feature和Geometry的配合
图层和字段都定义好了,接下来就是写入要素。这一步的逻辑是:创建一个Feature对象,给它设置几何对象和属性值,然后通过图层写入。代码结构是这样:
feature = ogr.Feature(layer.GetLayerDefn()) # 构造几何对象 point = ogr.Geometry(ogr.wkbPoint) point.AddPoint(116.391, 39.907) # 北京坐标 feature.SetGeometry(point) feature.SetField("name", "测试点") layer.CreateFeature(feature) # 释放资源 feature = None这里有几个必须注意的坑,都是我自己踩过或者帮别人排查过的:
第一个坑是忘了处理旧Geometry。如果你在循环里反复创建Geometry,然后SetGeometry到Feature上,Python的引用计数有时候会把这事情搞得很复杂。严格来说,SetGeometry会复制一份几何数据,但如果你手动new了Geometry又给Feature,在用完后应该用feature.SetGeometry(None)或者直接feature = None来释放。否则在小内存服务器上批量写入几十万条数据,内存会一路飙上去。
第二个坑是字段索引和字段名的选择。SetField既可以传字段名,也可以传字段索引,但传字段索引更容易出错。如果字段顺序后来调整了,索引就变了,代码就会把属性写到错误的字段里。我建议永远用字段名字符串。
第三个坑其实是编码问题。GDAL默认情况下,Shapefile的属性编码是ISO-8859-1,不是UTF-8。如果你往字段里写中文,再用ArcGIS或者QGIS打开,看到的很可能是乱码。解决办法是在创建DataSource的时候指定编码,或者用创建选项。我一般这样处理:
data_source = driver.CreateDataSource("output.shp") layer = data_source.CreateLayer("points", srs, ogr.wkbPoint, options=["ENCODING=UTF-8"])这里CreateLayer的第四个参数是创建选项列表。这个参数看起来不起眼,但恰恰是很多人中文乱码的根源。
3.6 资源释放:一个必须养成肌肉记忆的动作
最后一步也是经常被忽略的一步,就是释放资源。在Python里,由于有引用计数和垃圾回收,很多人觉得“反正程序跑完就自动释放了”,于是就不管了。这个态度在写小脚本时可能无所谓的,但在写长时间运行的批处理任务时,会出现文件占用、句柄泄漏之类的问题。
我用GDAL写批处理工具时,习惯是在用完DataSource之后显式关闭:
data_source = None这一行代码的作用是关闭数据源文件,确保所有数据已经落盘。如果你不主动置None,在某些情况下(尤其是批量创建多个文件)会出现最后一个文件内容为空的现象。原因是数据还在缓冲区里没有刷到磁盘。置None相当于强制flush并关闭文件句柄。
我在之前的一次数据处理任务中,跑了几百次循环创建Shapefile,全程没有置None,结果发现有一部分输出文件是0字节。排查了很久,最后发现就是资源没有及时释放导致的。从那以后,data_source = None成了我GDAL代码里最执着的习惯之一。
4. 方式二详解:覆盖格式之争,GDAL的快捷方式
讲完标准流程,再来看另一种方式。这种方式主要适用于“GDAL根据文件扩展名自动推断驱动”的场景。听起来就很省事,但代码怎么写、有什么坑,let me一个一个说。
4.1 两行代码搞定图层创建的魔法
前面方式一里,我们花了大量篇幅区分Driver、DataSource、Layer。如果只是临时用一下,不想管那么多,直接这样写就行:
from osgeo import ogr ds = ogr.GetDriverByName("ESRI Shapefile").CreateDataSource("quick.shp") layer = ds.CreateLayer("quick", None, ogr.wkbPoint)等一下,这不还是指定了Driver吗?对的,方式二的重点在另一个入口:直接用数据源接口,不显式指定Driver。
在GDAL里,ogr.Open这个函数既可以打开已有的数据源,也可以创建新的数据源。只不过创建的时候,需要传入一个参数告诉GDAL要去创建而不是打开:
ds = ogr.Open("quick.shp", 1) # 第二个参数1表示可写有意思的是,在较新版本的GDAL中,Open函数并不能用来创建数据源。真正用于快捷创建的是各驱动对象本身。所以严格说,在Python的GDAL里,“不指定驱动直接创建”并不是一个真正的入口点。这也就是为什么我说方式二更像是一种“简化的写法习惯”——它省去了检查驱动的步骤,但仍然绕不开Driver。
那真正的“方式二”是什么呢?我后来在项目里和同行交流,发现大家常说的还有另一种含义:使用抽象的Driver接口,通过DataSource创建完图层后,不关心图层概念的内部实现。说白了就是“反正gdal会自己找到合适的驱动,我只要数据格式对就行”。
4.2 让GDAL猜文件格式的代价
如果GDAL要“猜”文件格式,它会根据文件扩展名查表。这听起来很智能,但也带来了两个明显的问题。
第一个问题是扩展名决定格式,文件名必须规范。如果用户输出的是.abc这种GDAL不认识的扩展名,GDAL会直接创建一个“ESRI Shapefile”的目录,然后把你所有的数据当Shapefile处理,出来的东西乱七八糟。开发工具时,我建议对用户输入的文件名做校验,限制扩展名在白名单里面。
第二个问题是格式不同,内部行为不同。扩展名虽然是.shp,但你让GDAL猜,它默认用Shapefile驱动;如果全小写写成.shp,驱动不变,但如果输出的是.shp.xml,GDAL就不知道该怎么办了。这种因为文件名不规范导致的“猜错格式”,排查起来比方式一的Bug要痛苦得多,因为报错信息往往不直接指向扩展名问题。
所以我的结论是:方式二适合“我自己知道在做什么”的时候用——比如一次性的调试脚本、写个demo跑一遍。但在正式系统、长时间运行的批处理服务里,我还是建议老老实实写成方式一,因为方式一至少每一步都明确、可控、可排查。
4.3 快捷创建在GeoJSON和内存数据源里的表现
刚才说的都是文件格式,其实方式二还有个很有意思的应用场景——内存数据源。GDAL支持创建内存数据源,数据不落盘,全部驻留在内存中,适合临时计算、中间传输:
driver = ogr.GetDriverByName("Memory") ds = driver.CreateDataSource("temp") layer = ds.CreateLayer("layer", None, ogr.wkbPolygon)这里虽然指定了驱动,但“Memory”这个驱动本身就是一种快捷机制:不需要文件路径,不需要检查目录,创建完直接用,用完直接丢弃。在数据处理管道里,中间结果用内存数据源是非常好的实践,能避免大量临时文件的读写开销。
另外一个常见的快捷应用就是GeoJSON。GeoJSON文件本身只能存一个图层,没有多个图层的概念。如果你用方式一创建DataSource,GDAL会警告说“GeoJSON只支持单图层”,然后把数据强行塞进去。用方式二“猜格式”的方式,往往这个警告会被吞掉,数据写进去,你打开看,啥都有了。但如果你对坐标精度有要求,还是得回到方式一,因为GeoJSON驱动默认的输出精度是有限制的。
4.4 什么时候无脑用快捷方式,什么时候别碰
我试着给个判断标准,基本是面向需求反推的:
- 临时脚本、demo验证、调试:用方式二,效率优先。
- 正式ETL管道:用方式一,可维护性优先。
- 文件格式由用户输入决定:用方式一,先GetDriverByName判断驱动是否存在,再创建。
- 需要自定义字段宽度、精度、编码:用方式一,方式二在这些方面几乎没有控制力。
- 反复创建、覆盖同一个文件:方式一更稳,因为有是否覆盖的参数控制。
- 纯粹在内存中做中间计算:用Memory驱动,本质上属于第二种快捷方式的思路。
从这些场景能看出来,后者的使用频率其实并没有那么低,但它适合的场景很聚焦。如果你把它用在错误的地方,比如在正式服务里依赖扩展名推断驱动,迟早会在某个凌晨收到报警。
5. 两种方式对比与性能考量
前面把两种方式的实现细节都拆过了,这里再集中对比一下,顺便聊聊性能问题。因为很多人会忽略:创建图层的方式,也会影响数据写入的效率。
5.1 控制力、代码量与适用格式的横向对比
| 对比维度 | 方式一(标准流程) | 方式二(快捷创建) |
|---|---|---|
| 代码量 | 较多,7~10行 | 较少,3~5行 |
| 驱动控制力 | 强,可指定支持的创建选项 | 弱,主要是让GDAL自己推断 |
| 错误定位 | 清晰,哪一步出错基本能定位 | 模糊,可能直接抛不相关的错误 |
| 对格式的适配性 | 高,通用、适合多种格式 | 低,适合常见、默认选项够用的格式 |
| 资源释放 | 需要显式处理 | 同样需要,但更容易被忽略 |
| 推荐使用场景 | 正式项目、多图层、定制需求 | 临时验证、简单输出 |
这个表格就是我心里面的标准答案。你在写代码的时候,可以一边写一边对照这个表,基本不会选错。
5.2 批量写入时,事务控制才是性能分水岭
创建图层的方式跟性能有没有关系?严格说,创建过程本身的耗时差距可以忽略,真正的性能差距出现在“写入大量要素”的时候。而这跟图层创建时有没有开事务直接相关。
默认情况下,通过GDAL创建图层后,是一边写一边落盘的。每写入一个Feature,文件系统都要同步一次。对于几千个点的数据集,这没什么问题,但如果是百万级的点云数据,这种“边写边落盘”的模式会慢到怀疑人生。
解决办法是在创建图层后、批量写入前,开启事务:
layer.StartTransaction() try: for i in range(1000000): # 创建Feature并写入 pass layer.CommitTransaction() except Exception: layer.RollbackTransaction() raise事务的作用是让GDAL把多次写入先缓存在内存/缓冲区里,直到CommitTransaction才真正落盘。这一块的处理对性能的提升是指数级的。我实测过,百万级点写入Shapefile,开不开事务,速度差距能到10倍以上。尤其是配合方式一的标准流程,事务控制带来的收益会非常明显。
有些格式本身就有事务支持,比如GPKG、PostGIS;有些格式事务支持比较弱,比如纯CSV。但Shapefile是支持事务的,所以在大批量写入时,事务控制绝对是个不可忽视的操作。
5.3 内存与刷盘策略的经验谈
除了事务控制,还有内存控制。这里说的内存,不是Python进程的内存,而是GDAL内部用于缓冲写入数据的容量。如果准备写入的数据量超过了默认缓冲限制,GDAL会自动把缓冲区里的内容刷到磁盘,这个刷盘动作如果频繁触发,同样会影响性能。
解决办法有两个方向。一个是批量写入时控制要素数量,比如每写5000个Feature就主动Open一个新的DataSource片段;另一个是显式设置图层缓冲大小。不过某种程度上,这两个方向都回到了“事务控制”的范畴里。
我个人更推荐的做法是:每次处理固定批次,批量Commit。比如一次性读一万条记录,写入后CommitTransaction,再重新StartTransaction。这样的好处是内存峰值可控、崩溃恢复的位置明确,不至于数据全部丢失。
从代码上看,标准流程和事务控制其实是一套组合拳。方式一因为每一步都有明确的中间对象,在事务的Start和Commit之间插入操作非常自然。方式二因为简化了步骤,往往容易被忽略。我用方式二临时处理小数据时,倒是无所谓,但一旦数据量上来,还真得老老实实转回方式一加事务。
6. 实战案例:同一份数据,用两种方式实现完整写入
理论聊了这么多,是时候来一个完整案例了。我构思了一个非常贴近实际场景的例子:批量导入一组城市坐标点,包含城市名、人口数量两个字段,输出为Shapefile。分别用方式一和方式二实现,过程中对比两个方式的代码结构和注意事项。
6.1 标准流程实现:从驱动到写要素的一次跑通
先按方式一,写一个可以直接复用的函数:
from osgeo import ogr, osr def create_point_layer_by_driver(output_path, data, epsg=4326): # 1. 检查驱动 driver = ogr.GetDriverByName("ESRI Shapefile") if driver is None: raise RuntimeError("Shapefile 驱动不可用") # 确认父目录存在 import os os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) # 允许覆盖已有文件 ds = driver.CreateDataSource(output_path, 1) if ds is None: raise RuntimeError("创建数据源失败") # 2. 空间参考 srs = osr.SpatialReference() srs.ImportFromEPSG(epsg) # 3. 创建图层(指定编码) layer = ds.CreateLayer( "cities", srs, ogr.wkbPoint, options=["ENCODING=UTF-8"] ) if layer is None: raise RuntimeError("创建图层失败") # 4. 字段定义 name_field = ogr.FieldDefn("name", ogr.OFTString) name_field.SetWidth(32) layer.CreateField(name_field) pop_field = ogr.FieldDefn("population", ogr.OFTInteger) pop_field.SetWidth(12) layer.CreateField(pop_field) # 5. 事务写入 layer.StartTransaction() try: for row in data: feature = ogr.Feature(layer.GetLayerDefn()) point = ogr.Geometry(ogr.wkbPoint) point.AddPoint(row["lon"], row["lat"]) feature.SetGeometry(point) feature.SetField("name", row["name"]) feature.SetField("population", row["population"]) layer.CreateFeature(feature) feature = None layer.CommitTransaction() except Exception: layer.RollbackTransaction() raise # 6. 释放资源 ds = None return output_path这段代码我基本是直接从项目里抽出来改的,每一步都和前面讲的理论对齐。代码看起来长,但胜在逻辑清晰,每一步出错基本能定位到是哪一行。
6.2 快捷流程实现:同样的数据,代码短一半
再用方式二,或者说“简化写法”来做一遍。注意,我这里的简化并不是说完全不指定驱动,而是把看起来繁琐的检查、选项配置省略掉,直接靠GDAL根据常见需求生成:
from osgeo import ogr def create_point_layer_quick(output_path, data): driver = ogr.GetDriverByName("ESRI Shapefile") ds = driver.CreateDataSource(output_path, 1) # 没有空间参考,没有编码选项 layer = ds.CreateLayer("cities", None, ogr.wkbPoint) # 字段定义一样绕不开 name_field = ogr.FieldDefn("name", ogr.OFTString) pop_field = ogr.FieldDefn("population", ogr.OFTInteger) layer.CreateField(name_field) layer.CreateField(pop_field) for row in data: feature = ogr.Feature(layer.GetLayerDefn()) point = ogr.Geometry(ogr.wkbPoint) point.AddPoint(row["lon"], row["lat"]) feature.SetGeometry(point) feature.SetField("name", row["name"]) feature.SetField("population", row["population"]) layer.CreateFeature(feature) feature = None ds = None return output_path代码确实短了。但注意:空间参考没指定,编码没指定,事务没开。这些“省略”在小数据量、临时使用的情况下问题不大,可一旦数据量上去、或者后期要用GIS工具打开检查空间属性,就会暴露短板。
6.3 两种实现的结果对比与可维护性评价
跑完上面两份代码,我习惯性地做了几个验证:
- 用
ogrinfo打开输出的Shapefile,检查图层信息。 - 用QGIS打开,检查图层属性和坐标系。
- 用另外一份点数据做叠加,确认空间参考一致。
验证结果很直观:方式一产出的文件有完整坐标系信息,属性字段编码正常,中文不乱码,叠加分析直接能用。方式二产出的文件也能打开,但坐标系是未知,属性编码在Windows下用QGIS打开存中文略微有风险。
所以我给这两份代码的可维护性打个分的话,方式一能拿9分,方式二顶多7分。方式二的省事建立在“默认配置刚好满足需求”的前提下,但真实项目里,这种默认刚好满足的情况真的不多。
7. 常见问题与排查技巧实录
这个部分,我把平时在项目里以及帮人答疑时最常碰到的问题整理出来。很多问题表面上看是“创建图层失败”,根子上却是别的地方出了问题。
7.1 “文件已存在”却不知道如何覆盖
这是最常见的问题,没有之一。看到错误信息是Failed to create file或者File already exists,第一反应往往是“是不是我的文件名有问题”。其实GDAL在创建DataSource时,如果目标文件已经存在,默认行为就是拒绝覆盖。
解决办法在前面已经讲过,就是在CreateDataSource的第二个参数传1:
ds = driver.CreateDataSource("output.shp", 1)另一个需要注意的情况是,你用文本编辑器开着的output.shp,也会导致创建失败。Windows平台下,文件被其他程序占用时,GDAL无论如何都创建不了。解决办法是先关闭占用文件的程序。
7.2 创建了文件但内容是空的
这个问题我在前文提到过,最典型的场景是:代码跑完了没报错,但打开输出的Shapefile,属性表里一条记录都没有,或者文件大小只有几个字节。
排查顺序是:
- 是否在写入后显式置
ds = None?如果没有,很可能是数据还在缓冲区。 - 是否忘记调用
layer.CreateFeature(feature)?这个低级错误我见过不下三次。 - 字段定义是否成功?
CreateField失败不会抛异常,但它返回的值是OGRERR_NONE吗?如果你忽略了返回值,字段没建成功,后续SetField等于白设。
7.3 中文属性乱码
乱码问题在GDAL中文圈子里是永恒的话题。核心原因前面讲过,Shapefile默认属性编码不是UTF-8。
对策是在创建图层时明确指定编码:
options=["ENCODING=UTF-8"]或者在创建DataSource时指定:
ds = driver.CreateDataSource("output.shp", options=["ENCODING=UTF-8"])两种写法对应不同版本的GDAL API,建议都用上,双保险。如果已经写好了文件,也可以用ogr2ogr来转编码:
ogr2ogr -lco ENCODING=UTF-8 output_utf8.shp input.shp这里多说一句:GDAL 3.x之后,对编码的处理比旧版本好了很多,但依然不是默认UTF-8,所以人工干预始终是需要的。
7.4 几何类型不匹配的报错
SetGeometry阶段报错,说几何类型跟图层定义不一致,这类错误90%都是创建图层时的GeometryType写错了。比如创建了wkbPolygon的图层,却往里塞点要素。解决办法是在创建图层时想清楚自己到底要写什么类型的数据,然后所有要素都保持一致。
还有一种情况是图层创建时用了ogr.wkbUnknown,这种类型理论上可以容纳任意几何,但实际上很多驱动不支持。我在测试时用过一次,写进去之后元数据乱得一塌糊涂。建议永远不要用wkbUnknown。
7.5 创建GPKG或者PostGIS时的一些特殊问题
如果你用同一套代码,只是把Driver从“ESRI Shapefile”改成“GPKG”,大概率不会出大问题。但有几个点不同:
- GPKG驱动创建数据源时的选项更复杂,比如可以指定
VERSION=1.0或者ADD_GPKG_OGR_CONTENTS=NO这类选项。 - GPKG一个文件里可以有多个图层,但图层名不能重复。
- PostGIS的驱动需要额外的数据库连接参数,创建DataSource的时候就不是传文件路径了,而是传PostgreSQL的连接字符串。
从技术难度上,PostGIS比文件格式高一档不止,但创建图层的核心逻辑(Driver → DataSource → Layer)仍然不变。这也是为什么我强调要先理解抽象模型,因为它不随格式变化。
7.6 快速排查清单
我自己写GDAL代码时,每次遇到问题都会按下面这个顺序排查,效率极高:
- 驱动是否存在?(
GetDriverByName返回的是不是None) - 文件路径的父目录是否存在?
- 目标文件是否被其他程序占用?
- 数据源创建是否允许了覆盖?
- 图层创建是否传对了空间参考和几何类型?
- 字段定义是否成功?(检查返回值)
- 写入要素时字段名是否拼写正确?
- 是否有事务未提交?
- 是否有
ds = None释放资源? - 打开文件检查编码是否正常?
这个清单看起来长,实际上每次排查都能帮你定位到具体问题。我建议直接抄走贴在你的工位上。
8. 写在最后的一点经验分享
GDAL创建矢量图层这件事,本身并不复杂,说到底是两个对象、两个方法的事情。可它之所以能难住这么多人,根子在于GDAL的文档太“硬”了,概念又多,又缺少“为什么”的解释。
我这几年写GDAL相关代码,踩过最多的坑几乎都集中在格式适配、编码、资源释放和数据量上。格式适配要求你先想清楚输出给谁用——给ArcGIS用、给QGIS用、还是给Web系统用;编码问题要求你养成好习惯,一开始就指定UTF-8;资源释放是一道肌肉记忆题,写多了自然就不会忘;数据量则逼着你老实开事务、看内存。
至于两种创建方式,我的态度很明确:日常开发优先标准流程,快捷方式留给自己玩的时候用。两者之间并不存在绝对的谁优谁劣,关键是你能不能准确判断当前的场景需要多少控制力。控制力给多了,代码冗余;控制力给少了,后面的排查成本加倍。这个度的把握,就是经验本身。
如果你正准备用GDAL做矢量数据的创建和转换,我希望这篇能帮你少走几趟弯路。代码能跑只是一个开始,能跑得稳、跑得久、换格式不慌,才是真正把这套轮子用熟了。