简介:面向自然资源领域GIS数据处理人员的SHP矢量转TXT格式转换工具,适用于建设用地报批、设施农用地上图及卫片处理等场景,将标准Shapefile几何对象与属性信息转为通用文本,便于统计分析与跨平台数据交换。压缩包共147个文件、约17.12MB,内置主程序与配套配置、动态库,同时包含gfs、csv、wkt、xsd、xml等多类文件,覆盖坐标系统定义、参数映射及数据样例,目录结构清晰,按需取用即可。工具解压即可运行,兼容Windows 7/10系统,省去复杂安装环节,有效降低GIS数据预处理门槛。目前已有4845人浏览学习,适合自然资源业务人员、GIS数据处理初学者及需批量完成SHP转换的现场作业者,可显著提升空间数据文本化整理的效率。
1. 批量标准矢量shp互转txt:为什么我还要专门写个工具
做GIS数据交换时,你可能也遇到过这种场景:上游交付了一百多个标准矢量shp,下游系统只认txt文本;或者反过来,别人给了一份按坐标写好的txt,你要把它变成能被ArcGIS、QGIS打开的矢量图层。直接在GIS里“另存为”转出来的文本要么乱码,要么坐标精度丢得厉害,更别提批量处理几百个文件时反复手工操作。这个批量标准矢量shp互转txt工具要解决的,就是这类“标准shp与可读txt之间双向可逆、可批量跑”的数据交换问题。
它适合谁?经常处理国土、规划、测绘数据的工程师,做数据迁移或入库前预处理的人,以及需要把shp内容交给不懂GIS的同事审核、让对方打开txt就能核对属性的场景。我后面给出的方案依赖pyshp和shapely两个开源库,用Python脚本实现,不依赖特定GIS平台;转换格式可以自己定,也能和后续的shp转3dtiles、csv入库等流程衔接。
2. 先把txt格式定明白:坐标、编码、字段映射的铁规矩
很多人在shp转txt上翻车,不是脚本不会写,而是没想清楚txt里每一行该放什么、用什么分隔、字段类型怎么保留。这一节先解决格式约定,再给最小可运行的转换脚本。
2.1 shp不是“一个文件”,txt也不是“随便写写”
一个标准矢量shp在磁盘上至少包含四个文件:.shp存几何,.shx存几何索引,.dbf存属性表,.prj存坐标系描述。用pyshp读取时,前面三个是必须的,.prj经常被忽略,但转换后再也用不到坐标系时才能忽略。txt则完全不同,它只有一个文件,没有字段类型声明,没有坐标系信息,也没有编码标记。
所以shp转txt的第一步不是写代码,而是定协议:txt要能表达几何、属性、字段类型、坐标系,否则后续很难还原。我的经验是,txt文件里用三行做头:第一行字段名,第二行字段类型,第三行开始才是数据。几何统一用WKT格式写在一个叫“wkt”的字段列里,属性列用制表符分隔,不用逗号,因为属性值里出现逗号太常见了,用制表符能省掉一大半转义问题。
2.2 通用txt交换格式:WKT几何列加双行头
下面是一个具体约定,我在实际项目中一直这么用,直接照抄也能跑得通。
| 行 | 内容 | 示例 |
|---|---|---|
| 第1行 | 字段名,用制表符分隔 | wkt NAME CODE AREA |
| 第2行 | 字段类型,C为字符、N为数字、F为浮点 | C C N F |
| 第3行起 | 几何WKT加属性值,用制表符分隔 | POINT (104.06 30.67) 地块A 0101 1024.5 |
几何用WKT而不是单纯把XY拼两列,原因很直接:线和面有多个坐标点,如果只支持点要素,工具适用范围就窄了。WKT是OGC标准文本格式,线是LINESTRING (...),面是POLYGON ((...)),多部件是MULTIPOLYGON (((...))),解析和生成都有成熟库支持,没必要自创一套坐标序列格式。字段类型保留在第二行,是因为dbf的属性字段是有类型的,转成txt再转回来时,没有类型声明就只能全部按字符串处理,那样CODE这类带前导0的字段会丢0,AREA这类数值字段也无法在GIS里参与统计。
2.3 用pyshp写shp转txt的最小脚本
装好依赖后,下面的脚本可以直接跑通单个shp文件到txt的转换。需要pyshp和shapely两个库,安装命令是pip install pyshp shapely。
import csv import shapefile from shapely.geometry import shape from shapely import wkt def geom_to_wkt(geom, precision=8): # 把pyshp的几何对象转成GeoJSON字典,再交给shapely生成WKT g = shape(geom.__geo_interface__) # rounding_precision控制输出小数位数,trim去掉多余的0 return wkt.dumps(g, rounding_precision=precision, trim=True) def shp_to_txt(shp_path, txt_path, encoding='utf-8', precision=8): reader = shapefile.Reader(shp_path, encoding=encoding) # 跳过pyshp自带的DeletionFlag字段,只保留真实属性列 fields = [f[0] for f in reader.fields[1:]] types = [f[1] for f in reader.fields[1:]] with open(txt_path, 'w', encoding='utf-8', newline='') as fp: writer = csv.writer(fp, delimiter='\t', lineterminator='\n') # 写双行头 writer.writerow(['wkt'] + fields) writer.writerow(['C'] + types) # 逐要素输出 for rec in reader.shapeRecords(): row = [geom_to_wkt(rec.shape, precision)] + list(rec.record) writer.writerow(row)逻辑说明:第8行把pyshp的shape对象转成shapely几何,统一走WKT导出,点、线、面、多部件都交给库处理,自己手写坐标拼接容易漏掉多部件结构。第11行用encoding='utf-8'控制dbf属性解码;如果出现乱码,把这里换成'gbk'。第13到16行写双行头,类型列直接取自dbf的字段类型定义,这样第3章反向转换时不用猜字段类型。
参数说明:precision=8表示坐标最多保留8位小数,经纬度场景下约等于毫米级精度,完全够用;如果数据是高斯投影平面坐标,8位小数也足够。想要文件更紧凑就调到6,做长距离线要素的拓扑分析建议保持8。encoding参数在这里影响的是读dbf属性时的解码方式,和输出txt文件本身的UTF-8编码没有关系。
2.4 参数怎么调:分隔符、精度、字段类型取舍
分隔符首选制表符,它肉眼可读,也方便在Excel里直接打开分列。要对外交付给下游程序时,如果对方明确要逗号分隔,把csv.writer的delimiter改成,即可,但必须在文档里提醒对方:属性值内部的逗号没有做转义。另一种做法是属性值用双引号包起来,Python的csv模块默认就会在处理逗号时加引号,所以改为逗号分隔时不用过度担心。
字段类型这一行不是摆设。字符型字段在dbf里的最大长度是254个字节,如果你在txt里手工改了一个超长属性值,反向导入时pyshp会按字段定义截断或报错。遇到这种情况,我通常会提前检查一遍字符型字段的最大长度,超过200就考虑用C类型配合较大的size。数字型字段则要留意浮点和整数的区分:dbf的N类型不带小数位,F类型带小数位,txt第二行里保留原类型,转换回来就不会出现AREA字段变成整数的情况。
3. 反向还原不踩雷:从txt写回shp时如何重建几何和属性
只做shp转txt,工具只能说完成了一半。真正考验格式设计的是反向转换,txt里的文本要重新变成结构完整的shp,几何、属性、坐标系一个都不能少。
3.1 动手前确认三件事:几何类型、坐标系、字段定义
txt转shp前,先回答三个问题。第一,这个txt里是什么几何类型,全是点还是混合线面,pyshp写shp时一个文件只能指定一种shapeType,所以工具层面的第一件事是读取第一条数据的几何类型来创建Writer,而不是让用户手工传参。第二,坐标系怎么还原,txt本身不携带任何坐标系信息,转换时必须附带.prj文件,一般做法是保留原shp的prj文件,或者从坐标系描述文件、软件里导出对应WKT文本后写入prj文件。第三,字段类型按什么还原,这就是第2章双行头设计的价值,类型列直接用,不需要再手工建一个字段定义文件。
3.2 用shapely解析WKT,再写回shp的完整脚本
这里给出txt到shp的完整代码。它读取第2章那种双行头txt,按WKT解析几何,按类型列声明dbf字段,最后写出shp、shx、dbf三个文件。
import csv import shapefile from shapely import wkt def txt_to_shp(txt_path, shp_path, encoding='utf-8', prj_wkt=None): # 读取双行头txt with open(txt_path, encoding=encoding, newline='') as fp: rows = list(csv.reader(fp, delimiter='\t')) headers, types = rows[0], rows[1] data = [r for r in rows[2:] if len(r) > 1] geom_idx = headers.index('wkt') # 用第一条几何判断shapeType first_geom = wkt.loads(data[0][geom_idx]) stype = {'Point': shapefile.POINT, 'MultiPoint': shapefile.POINT, 'LineString': shapefile.POLYLINE, 'MultiLineString': shapefile.POLYLINE, 'Polygon': shapefile.POLYGON, 'MultiPolygon': shapefile.POLYGON}.get(first_geom.geom_type) if stype is None: raise ValueError('不支持的WKT几何类型: ' + first_geom.geom_type) writer = shapefile.Writer(shp_path, shapeType=stype) # 声明属性字段,类型和长度根据txt第二行映射 for name, typ in zip(headers, types): if name == 'wkt': continue if typ == 'C': writer.field(name, 'C', size=254) elif typ == 'N': writer.field(name, 'N', size=20, decimal=0) elif typ == 'F': writer.field(name, 'F', size=20, decimal=8) else: raise ValueError('未知字段类型: ' + typ) # 逐行写几何和属性 for row in data: geometry = wkt.loads(row[geom_idx]) if geometry.geom_type == 'Point': writer.point(geometry.x, geometry.y) elif geometry.geom_type == 'LineString': writer.line([[list(geometry.coords)]]) elif geometry.geom_type == 'MultiLineString': writer.line([list(line.coords) for line in geometry.geoms]) elif geometry.geom_type == 'Polygon': # 外环+内环都作为part写入;带洞面建议改用fiona parts = [list(geometry.exterior.coords)] for interior in geometry.interiors: parts.append(list(interior.coords)) writer.poly(parts) elif geometry.geom_type == 'MultiPolygon': parts = [] for polygon in geometry.geoms: parts.append(list(polygon.exterior.coords)) for interior in polygon.interiors: parts.append(list(interior.coords)) writer.poly(parts) attrs = [] for name, typ in zip(headers, types): if name == 'wkt': continue raw = row[headers.index(name)] if typ == 'C': attrs.append(raw) elif raw == '': attrs.append(None) else: attrs.append(float(raw)) writer.record(*attrs) writer.close() if prj_wkt: prj_path = shp_path.replace('.shp', '.prj') with open(prj_path, 'w', encoding='utf-8') as fp: fp.write(prj_wkt)逻辑说明:第6行到第8行读取双行头,数据行过滤掉空行和只有一列的无意义行。第11到19行根据第一条几何的WKT类型确定整个shp的shapeType,避免用户传错参数把点数据写成面数据。第23到32行按第二行类型声明dbf字段,字符型长度固定254,数字型按有无小数区分N和F。第35到53行处理几何写入,点、线、面分开调用pyshp的对应方法。属性值在倒数第9行到第17行统一处理,字符型保持原样,数字型遇到空值写None,否则转float。
参数说明:prj_wkt参数接收坐标系WKT字符串。不同软件生成的坐标系文本略有差异,常见做法是从原shp的.prj文件原样读取并传进来,而不是手动敲一段。encoding参数影响txt的读取编码,也作为写出dbf时的属性编码;国内数据源如果是Excel整理出来的txt,很多是ANSI编码,这时传'gbk'而不是'utf-8'。
3.3 为什么用pyshp而不是手写shp二进制
shp看起来很“简单”,但正常工程师都不应该手写这个格式。.shp文件每个要素的record有固定头,.shx里每个记录要记录offset和content length,.dbf还要自己拼文件夹头和数据字节,三个文件任何一个长度算错,GIS软件打开要么提示“损坏的shapefile”,要么要素识别不全。pyshp这类开源库帮你把shx索引、dbf编码、几何字节序都处理好了,代价只是写入复杂带洞面时限制比较多。
限制在哪里?pyshp的writer.poly(parts)方法允许传入多个环,但如何正确表达“内环是洞”在不同GIS软件里的表现不完全一致。如果你手头数据里带洞的自然面很多,建议在反向转换这一步不用纯pyshp,而是配合fiona库,直接用shapely几何写shp,fiona对Polygon的interiors处理比pyshp省心得多。上面的代码保留了一个朴素写法:外环和内环都作为part依次写入,最终识别结果在部分软件中可能把洞渲染成岛屿,这一点在第5章还会展开。
3.4 手工编辑txt后回导的容错思路
很多人在txt里直接改属性,再反向转换时报错,原因多半是WKT被改坏了。常见错误包括括号不配对、坐标对少写一个数字、行尾残留多余制表符。遇到这种情况,不要直接修改shp,而是先写几行代码按行解析,把解析失败的行号打印出来,再让数据提供方按行号核对。
我给转换工具加容错时,习惯只对属性列宽松处理,对WKT列严格处理。属性列缺失时填None,WKT列解析失败则立即报错并把文件名和行号写进日志。原因很简单:属性错了还能后面翻工,几何错了生成的shp会是坏要素。
4. 批量跑不翻车:目录扫描、编码嗅探和断点日志
单文件转换跑通后,批量才是提效的关键。但批量绝不是“for循环套一下”那么简单,实际批量跑shp转txt时,编码混乱、目录扫到自己、中断重跑这三件事最容易让整个任务作废。
4.1 批处理要面对的三个真实问题
第一个问题是编码不统一。一套数据里可能有几个shp的dbf是GBK,其余是UTF-8;用固定编码参数跑,总有几个文件的属性全变乱码。第二个问题是输入输出目录配置不好。输出目录如果建在输入目录内部,程序扫描txt或shp时会把自己刚生成的文件再扫一遍,轻则重复转换,重则无限递归。第三个问题是任务中断后没有后悔药。几十万个文件跑到一半断电,由于没有记录进度,上次转好的文件也会被覆盖重转,浪费的时间不可接受。
4.2 编码嗅探:优先按UTF-8读,失败后自动切GBK
对国内数据,与其引入复杂算法,不如用最直接的试读法。先按UTF-8解码dbf,如果抛UnicodeDecodeError,再按GBK重试。对绝大多数shp互转txt任务来说,这两种编码已经覆盖了95%以上的场景。
def shp_to_txt_with_fallback(shp_path, txt_path, precision=8): for enc in ('utf-8', 'gbk'): try: shp_to_txt(shp_path, txt_path, encoding=enc, precision=precision) return enc except UnicodeDecodeError: continue raise RuntimeError('无法识别dbf编码: ' + shp_path)逻辑说明:for enc in ('utf-8', 'gbk')表示先试最常用的UTF-8,失败后再试GBK。UnicodeDecodeError只在解码阶段抛出,如果dbf属性是纯ASCII,两种编码都能通过,这时默认用UTF-8,不影响正确性。返回的enc值可以写进日志,方便之后排查。参数说明:这个函数没有新增参数,但返回编码结果,外层批处理脚本可以据此决定是否记录编码信息。
4.3 批处理脚本:目录遍历、done清单和错误日志
批量脚本的核心不是循环,而是进度记录。我的做法是:输出目录下放一个__done__.txt文件,每完成一个文件就追加一行相对路径;下次启动时先读这个文件,已完成的直接跳过。错误信息单独写__error__.log,绝不中断整个任务。
import os import traceback def batch_convert(src_dir, out_dir, direction='shp2txt'): os.makedirs(out_dir, exist_ok=True) done_path = os.path.join(out_dir, '__done__.txt') error_path = os.path.join(out_dir, '__error__.log') done = set() if os.path.exists(done_path): with open(done_path, encoding='utf-8') as fp: done = {line.strip() for line in fp if line.strip()} # 只扫描需要处理的源文件 todo = [] suffix = '.shp' if direction == 'shp2txt' else '.txt' for root, _, files in os.walk(src_dir): for fn in files: if fn.lower().endswith(suffix): rel = os.path.relpath(os.path.join(root, fn), src_dir) todo.append(rel) for rel in sorted(todo): if rel in done: continue src_path = os.path.join(src_dir, rel) rel_out = os.path.splitext(rel)[0] + ('.txt' if direction == 'shp2txt' else '.shp') dst_path = os.path.join(out_dir, rel_out) os.makedirs(os.path.dirname(dst_path) or out_dir, exist_ok=True) try: if direction == 'shp2txt': shp_to_txt_with_fallback(src_path, dst_path) else: txt_to_shp(src_path, dst_path) except Exception: with open(error_path, 'a', encoding='utf-8') as fp: fp.write(rel + '\n' + traceback.format_exc() + '\n') else: with open(done_path, 'a', encoding='utf-8') as fp: fp.write(rel + '\n')逻辑说明:第8到12行先读取已完成清单,这是断点续跑的核心。第15到20行用os.walk扫描源目录,把相对路径保存下来,而不是边遍历边转换,这样能避免遍历过程中新增文件干扰列表。第22行到第31行逐条处理,try里的业务代码和异常处理完全分离;转换失败只是记录错误,不会让后续文件停止。第36行成功后才把相对路径追加进done文件,确保“写完输出文件”和“标记完成”是强绑定的。
参数说明:direction参数控制方向,'shp2txt'时扫描shp并输出同名txt,'txt2shp'时反过来。注意txt_to_shp没有做编码探测,因为txt文件读取时可以先按UTF-8读,失败再按GBK,建议把第3章的入口函数改成类似shp_to_txt_with_fallback的版本,逻辑完全相同,这里不再重复写法。
4.4 几十万文件场景:清单模式替代全盘扫描
当源目录文件多到几十万级别,每次启动都全盘os.walk一遍很浪费时间。另一个更稳的做法是:先一次性生成一个文件清单文本,批量脚本只读清单,不扫目录。
find /data/input -name "*.shp" -type f > /data/filelist.txt然后让批处理脚本的参数从src_dir改成filelist_path,逐行读取路径并做相对路径映射。好处有两个:一是清单文件可以手动增删,哪天发现漏了几个文件,直接编辑清单即可;二是重复运行时不用再遍历几十万个文件,启动耗时会从分钟级降到秒级。我在数据迁移项目里一直坚持这个模式,它和__done__.txt配合后,批量任务基本能做到随断随续、随改随跑。
5. shp互转txt的高频坑:五类问题的定位和修复记录
这章是我自己实际踩坑的记录。每一条都按“现象、原因、解决”三个步骤写,直接对应你跑脚本时最可能遇到的报错或结果异常。
5.1 现象:txt打开后中文属性全是乱码
有人转出来的txt是好的,有人转出来里面中文变成“锟斤拷”或“淇℃伅”,本质原因只有一个:dbf属性表实际存放的是GBK编码,而你按UTF-8解码。解决方式就两步。第一步,在第2章脚本里把encoding='utf-8'改成encoding='gbk'。第二步,如果数据来源不统一,直接用第4章2小节的shp_to_txt_with_fallback函数,自动试读。
需要留意的是,有些国产软件生成的dbf语言驱动ID标记是UTF-8,实际写入的却是GBK字符,遇到这类数据,唯一的判断依据是转换后的txt肉眼检查。建议每次批量转换后随机抽查5个文件,看中文字段是否正常,不要全量跑完再检查,那样出了问题还要重来。
5.2 现象:转出的shp在GIS里位置跑到海里
txt转shp后,要素在ArcGIS或QGIS里位置完全不对,偏移到海里或者跑到另一个半球,最常见的两个原因:一是txt里的坐标顺序是“纬度,经度”,脚本按“经度,纬度”写入;二是缺少.prj文件,GIS软件默认按WGS84经纬度解读坐标,而原始数据是CGCS2000或高斯投影平面坐标。
解决坐标顺序问题,关键是先确认txt里坐标列的含义。如果是WKT文本,POINT (x y)里的顺序和源数据一致,一般不会错;自行拼坐标串时最容易把x和y写反。解决坐标系问题,转换时务必把原shp的.prj文件或坐标系WKT文本传给txt_to_shp,输出shp旁边必须存在同名prj文件。批量场景下,如果源txt目录里附带了一个公共的coordinate.prj,直接用这个文件作为所有输出的prj即可。
5.3 现象:CODE字段“0101”变成“101”
这是最隐蔽的坑之一。txt里明明写着0101,转回shp后字段值变成了101,或者反过来shp里是0101,导出txt后变成101。原因出在字段类型上:Excel或某些文本编辑器会把0101按数字处理,去掉前导0;或者shp本身的字段类型是N(数字型),在数值型dbf字段里前导0本就不存在。
解决方式:凡是这类“看起来像数字、但没有数学含义”的编码字段,在txt第二行类型列里必须写成C而不是N。如果源shp字段已经定义成了N类型,可以先在导出脚本里做一个强制类型映射,把指定字段按字符型导出。反过来在txt转shp时,字符型字段的WORK值永远按字符串写入,不调用float(),这样前导0就能保住。
5.4 现象:带洞面、多部件面少零件
txt里是标准WKT,转回shp后,带洞的Polygon变成实心面,MultiPolygon只剩下第一块。这个问题的根子在pyshp的写入方式。第3章的脚本把外环和内环都放进writer.poly(parts),但pyshp本身不负责判断内外环朝向,部分GIS软件会按形状包含关系重新识别,另一部分软件则把所有环都当作外环处理。
解决方式是分情况选路。如果需要严格保留洞,或者数据里MultiPolygon很多,我建议不使用纯pyshp写几何,改用fiona:把shapely几何对象直接传给fiona的geometry字典,它对内环和多部件的还原更可靠。如果坚持用pyshp,那就在转换前先把带洞面拍平成不带洞的单一面,代价是面积和拓扑关系发生变化,外业调查数据这么做通常不可接受。
5.5 现象:手工改txt后回导报错,又看不出错在哪
最常见的报错是WKT解析失败,其次是属性列数量和表头不一致。原因通常是手工编辑时多删了一个坐标点、括号没配对、或者行尾多了制表符。解决方式不是靠眼睛看,而是给txt转shp的入口加一个预处理校验:每一行先按制表符拆分,列数不对直接记录行号;再尝试解析WKT,解析失败同样记录行号。把报错信息定位到具体行后,用文本编辑器打开对应行,前后对照表头看哪一列少值,一般几分钟就能修完。
我习惯在解析WKT这个环节把错误信息后面的10个字符也打印出来,因为WKT解析错误时,异常消息往往指向某个位置,但用户不知道具体是哪个字符出问题。打印附近内容后,问题通常是坐标对里多了一个空格或少了一个逗号。
6. 用校验脚本守住转换质量:坐标回读与属性抽样验证
转换工具写完,真正的工程习惯是转换后必须校验,尤其批量跑完几百个文件时,不能假设全对。我这里说一个简单但有效的回读校验法:把刚生成的shp再转回txt,和源txt对比行数、抽样点坐标差、属性值差异,全对才算完成。
# 把输出shp再转成验证用txt python convert.py --shp output/xxx.shp --txt /tmp/verify_xxx.txt # 对比数据行数 wc -l output/xxx.txt /tmp/verify_xxx.txt # 看前10行坐标是否有明显差异 diff <(cut -f1 output/xxx.txt) <(cut -f1 /tmp/verify_xxx.txt) | head -10上面用的是bash命令,适合快速验证点要素和简单的线面。对带洞面来说,shp再读回来的WKT坐标顺序可能和原txt不完全一致,直接diff会误报。更好的做法是写一个小Python脚本,分别从两个txt里解析出所有要素的坐标点集,计算对应要素坐标点的最大欧氏距离,距离超过max_shift阈值才报错。阈值设置多少看数据精度:经纬度数据一般设1e-6,约0.1米量级;平面坐标数据设1e-3,也就是毫米级。
属性校验不需要全量对比。我的习惯是按10%比例抽样,随机抽几十个要素,对比字段数量、关键编码字段、面积字段三个维度。由于shp的dbf字段类型和txt第二行类型定义存在天然约束,抽样对比能快速发现类型映射错误,比如某个浮点字段被转成整数、某个字符字段被截断。整个校验脚本跑完,确认无误后再进入后续入库或shp转3dtiles等流程,能省下大量返工时间。
这个互转方案我从单文件写起,逐步补上双行头格式、编码探测、断点日志和回读校验,现在已经成为我处理数据交换的固定套路。每次拿到一批新shp,我都会先用一个文件完整跑一遍shp转txt再转回shp,确认坐标系和字段没问题后,再放开批量开关。这几年下来,最深的教训是:格式约定永远比转换代码本身重要,txt的“可读性”如果设计得不好,后续所有校验都会变成负担。希望这套思路对你处理手头的数据能帮上忙。
本文还有配套的精品资源,点击获取