☰
GDAL创建矢量图层两种方式:标准流程与快捷创建
2026/10/13 13:07:27 网站建设 项目流程

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 两种实现的结果对比与可维护性评价

跑完上面两份代码,我习惯性地做了几个验证:

  1. 用ogrinfo打开输出的Shapefile,检查图层信息。
  2. 用QGIS打开,检查图层属性和坐标系。
  3. 用另外一份点数据做叠加,确认空间参考一致。

验证结果很直观:方式一产出的文件有完整坐标系信息,属性字段编码正常,中文不乱码,叠加分析直接能用。方式二产出的文件也能打开,但坐标系是未知,属性编码在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,属性表里一条记录都没有,或者文件大小只有几个字节。

排查顺序是:

  1. 是否在写入后显式置ds = None?如果没有,很可能是数据还在缓冲区。
  2. 是否忘记调用layer.CreateFeature(feature)?这个低级错误我见过不下三次。
  3. 字段定义是否成功?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代码时,每次遇到问题都会按下面这个顺序排查,效率极高:

  1. 驱动是否存在?(GetDriverByName返回的是不是None)
  2. 文件路径的父目录是否存在?
  3. 目标文件是否被其他程序占用?
  4. 数据源创建是否允许了覆盖?
  5. 图层创建是否传对了空间参考和几何类型?
  6. 字段定义是否成功?(检查返回值)
  7. 写入要素时字段名是否拼写正确?
  8. 是否有事务未提交?
  9. 是否有ds = None释放资源?
  10. 打开文件检查编码是否正常?

这个清单看起来长,实际上每次排查都能帮你定位到具体问题。我建议直接抄走贴在你的工位上。

8. 写在最后的一点经验分享

GDAL创建矢量图层这件事,本身并不复杂,说到底是两个对象、两个方法的事情。可它之所以能难住这么多人,根子在于GDAL的文档太“硬”了,概念又多,又缺少“为什么”的解释。

我这几年写GDAL相关代码,踩过最多的坑几乎都集中在格式适配、编码、资源释放和数据量上。格式适配要求你先想清楚输出给谁用——给ArcGIS用、给QGIS用、还是给Web系统用;编码问题要求你养成好习惯,一开始就指定UTF-8;资源释放是一道肌肉记忆题,写多了自然就不会忘;数据量则逼着你老实开事务、看内存。

至于两种创建方式,我的态度很明确:日常开发优先标准流程,快捷方式留给自己玩的时候用。两者之间并不存在绝对的谁优谁劣,关键是你能不能准确判断当前的场景需要多少控制力。控制力给多了,代码冗余;控制力给少了,后面的排查成本加倍。这个度的把握,就是经验本身。

如果你正准备用GDAL做矢量数据的创建和转换,我希望这篇能帮你少走几趟弯路。代码能跑只是一个开始,能跑得稳、跑得久、换格式不慌,才是真正把这套轮子用熟了。

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

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

立即咨询