简介:该工具面向需要批量维护地理数据编号的GIS从业者,可在ArcGIS环境中对MDB、GDB、SHP等常见数据格式下的宗地界址线、界址点以及城市街区、建筑物、公路段等要素图层进行唯一编号,适用于土地确权、测量、规划等实际业务场景。压缩包共37个文件,包含可执行的exe主程序与配置、依赖的ESRI ArcGIS系列dll组件及对应的xml说明文档,整体体积仅2.18MB,便于拷贝部署。已有2852人学习下载。通过该工具,用户可按起始值、递增或递减规则、字段类型等参数为图层批量生成编号,减少手工操作并保障编号唯一性,适合需要快速完成大范围数据编号和属性整理的GIS人员使用。 前阵子处理一批宅基地确权数据,几个村加起来三万多个图斑,任务书上的编号规则是“县代码+乡镇代码+村代码+四位流水号”。我第一次图省事,直接拿OBJECTID当流水号填进去,结果质检环节发现中间有几十个图斑属于错误图斑要删掉,一删,后续所有编号全部断档,整张表乱成一锅粥,返工到凌晨才把编号重新理顺。也是那次之后,我把整套逻辑整理成了一个Arcgis数据编号工具——严格说是一套基于字段计算器和arcpy脚本的编号方案,支持顺序编号、分组编号、排序后编号、删除后重排。这篇就把完整思路、代码和踩过的坑一次性写清楚,适合用ArcGIS做不动产权籍、国土调查、规划数据处理的朋友参考,尤其是被“编号回补”“跳号”“重复号”折磨过的同行。
1. 动手编号前,先搞清楚OBJECTID为什么不能直接当业务编号
1.1 OBJECTID的生成机制决定了它天生不适合当业务编号
很多刚接触ArcGIS的人都会问:属性表里明明自带OBJECTID,按它排序不就是编号吗?这个问题我一开始也这么干过,但实际跑两个项目就会发现,OBJECTID是数据库内部维护的物理行标识,它只保证在单次会话中唯一,不保证连续,也不保证稳定。比如你在要素类中间删掉一条记录,数据库不会把后面的记录全部往前移动来补齐空缺;再追加新要素时,新记录的OBJECTID可能接着当前最大值,也可能复用被删除的空间,完全不受你控制。
这个特性在业务场景里是致命的。正式成果数据里的编号通常要与纸质图、调查表、管理系统中的记录一一对应,一旦中间要素作废或者追加新要素,OBJECTID一变,所有关联信息全部对不上。我见过有的同事直接在成果shp里用OBJECTID当图斑编号上报,结果平台入库的时候自动回算,导出的编号跟原始编号不一样,最后只能批量改数据,非常被动。所以第一条经验很简单:OBJECTID只能作为排序依据和临时辅助字段,永远不要直接输出为正式编号。
1.2 常见业务编号规则拆解
要做一个能长期用的编号工具,先要理清业务编号到底有几种形态,否则代码写出来也是死工具。我按实际项目接触的情况归纳了一下:
| 编号类型 | 典型规则 | 实现难度 |
|---|---|---|
| 简单流水号 | 001、002、003……全表递增 | 低 |
| 分组流水号 | 每个乡镇/村/图幅从1开始独立计数 | 中 |
| 复合编号 | 行政区代码+地类代码+顺序号拼接 | 中 |
| 空间排序编号 | 按从上到下、从左到右的顺序编号 | 较高 |
这些规则表面上看是“加个数字”,实际上背后涉及三个关键动作:确定排序依据、确定分组依据、决定是否允许回补。排序依据决定编号的先后顺序;分组依据决定计数器的重置范围;是否允许回补决定删除要素后编号要不要重排。工具能不能通用,就看这三个点有没有做成可配置的参数。明白了这一点,再看后面的代码就顺了。
2. 字段计算器里写AutoIncrement:轻量方案与全局变量陷阱
2.1 最经典的AutoIncrement写法
如果你只是临时给一张表加顺序号,不想建工具箱、不想写完整脚本,那字段计算器里的AutoIncrement函数是最快的路径。具体操作:在属性表里新建一个字段,类型选长整型或文本型,右键字段打开字段计算器,解析器选Python,然后勾选“显示代码块”,把这段代码粘进预逻辑脚本代码区域:
rec = 0 def autoIncrement(): global rec pStart = 1 pInterval = 1 if rec == 0: rec = pStart else: rec += pInterval return rec表达式框里填:
autoIncrement()点击确定,编号就出来了。这个函数在数据量几千条以内的场景下,实测是稳的。
但注意,这里有一个非常隐蔽的坑:字段计算器有时会分批处理要素,尤其当你勾选了“仅更新所选要素”或者数据量较大、计算中间报错中断时,全局变量rec的计数状态可能会重置。一旦中断再继续,会出现前面已经算好的编号不变、后面又从1开始的情况,等于白算。所以这个方案适合“一次跑完、数据量不大、不需要断点续跑”的简单场景,不适合正式成果生产。
2.2 分组编号用字段计算器也能实现
分组编号的场景是:全省图斑按照县、乡、村分组,每组从1开始重新计数。字段计算器里同样可以用字典实现:
groupDict = {} def groupAutoIncrement(groupValue): global groupDict if groupValue in groupDict: groupDict[groupValue] += 1 else: groupDict[groupValue] = 1 return groupDict[groupValue]表达式里填:
groupAutoIncrement(!XZQDM!)我拿一个镇的村庄数据试过,能跑,速度也不慢。但这个方法受全局变量状态约束,跟前面一样有中断重置的风险。另外还有一个隐藏问题:字段计算器调用自定义函数时,字典是在同一个内嵌解析器里维护的,理论上有状态,但我在ArcMap 10.8里遇到过个别机器上字典不生效、所有记录都返回1的情况,折腾了很久,最后发现是软件环境问题,重装Python组件才解决。所以,凡是需要上成果的项目,我强烈建议别在字段计算器里赌这个状态,直接用arcpy游标。
2.3 更可靠的方案:用arcpy.da.UpdateCursor跑编号
arcpy.da.UpdateCursor的可靠性在于,它是一次性的数据库游标遍历,计数逻辑完全由你自己控制,不依赖任何解析器状态,中途中断了重跑即可,不会出现部分字段没算、部分重复的问题。简单的顺序编号脚本长这样:
import arcpy fc = r"C:\data\gdb\parcels" num_field = "BH" i = 1 with arcpy.da.UpdateCursor(fc, [num_field]) as cursor: for row in cursor: row[0] = i cursor.updateRow(row) i += 1如果要在编号前补零,直接格式化字符串:
row[0] = f"{i:04d}"这段代码不管数据是file gdb还是shapefile都能跑,而且逻辑透明。我的习惯是先用这段代码把编号跑一遍,然后查一下最大和最小编号,确认符合预期再继续后续操作,能省掉很多后期返工。
3. 排序后编号、分组编号,以及避免SQL排序失效的坑
3.1 分组编号的正确姿势
真正干活的时候,很少是单纯全表顺序编号,更多是“按照某个字段分组,组内顺序编号”。比如按村代码分组,每个村内部从1开始。用UpdateCursor实现很直接:
import arcpy fc = r"C:\data\gdb\parcels" group_field = "XZQDM" num_field = "BH" group_counter = {} with arcpy.da.UpdateCursor(fc, [group_field, num_field]) as cursor: for row in cursor: g = row[0] if g in group_counter: group_counter[g] += 1 else: group_counter[g] = 1 row[1] = f"{row[0]}_{group_counter[g]:04d}" cursor.updateRow(row)这套逻辑跟字段计算器的字典写法本质一样,但状态完全掌握在脚本手里,不会因为软件解析器抽风而失效。实测百万级要素跑下来也是可控的,主要耗时在磁盘读写上。
3.2 按排序结果编号时,别让sql_clause坑了你
如果编号不是按当前数据默认顺序,而是要求“按XZQDM字段排序后再编号”,很多人会直接用arcpy.da.UpdateCursor的sql_clause参数,写一个ORDER BY。我提醒一句:arcpy.da.UpdateCursor的sql_clause排序参数,在文件地理数据库和shapefile上的支持并不可靠,很多场景下ORDER BY会被静默忽略,或者直接报错。Esri官方文档和社区里都有讨论,我自己也在shapefile上验证过,排序没有生效,编号顺序跟瞎排一样,后果很严重。
稳妥的做法分两步走:先用arcpy.Sort_management生成一个排好序的临时要素类,再对临时要素类做编号,最后把编号写回原图。或者,直接在Python里把数据读取到列表,排序后建一个映射字典,再通过UpdateCursor回写。我更喜欢第二种,因为少一次磁盘拷贝:
import arcpy fc = r"C:\data\gdb\parcels" fields = ["OBJECTID", "XZQDM", "BH"] # 1. 读取全部要素 rows = [row for row in arcpy.da.SearchCursor(fc, fields)] # 2. 按分组字段排序,同组内按OBJECTID稳定排序 rows.sort(key=lambda r: (r[1], r[0])) # 3. 生成编号映射,组内从1开始 mapping = {} group_counter = {} for oid, g, _ in rows: if g in group_counter: group_counter[g] += 1 else: group_counter[g] = 1 mapping[oid] = f"{g}_{group_counter[g]:04d}" # 4. 回写 with arcpy.da.UpdateCursor(fc, ["OBJECTID", "BH"]) as cursor: for oid, _ in cursor: cursor.updateRow([oid, mapping[oid]])这个方案不依赖数据库的ORDER BY支持,任何数据源都能跑,而且排序逻辑完全透明、可扩展。如果后面还要按“从上到下、从左到右”的空间顺序编号,只要把排序key从字段值换成要素几何的坐标值就行,比如取每个要素的质心Y坐标倒序排序,实现北到南编号。
4. 把编号逻辑封装成“Arcgis数据编号工具”
4.1 建立自定义工具箱和脚本工具
前面的代码都是在Python窗口或外部IDE里跑,虽然灵活,但每次都要打开代码改路径,效率低还容易改错。真正好用的工具,是把编号逻辑封装成ArcToolbox里的自定义脚本工具,做成一个“数据编号工具”,以后任何人打开工具箱填参数就能用。
创建步骤:在ArcMap或ArcGIS Pro的目录窗口里,找到自己的文件夹连接,右键新建工具箱,取个名字,比如“数据处理工具箱”。然后在工具箱内部右键——添加——脚本,按向导设置脚本文件路径、名称和参数。脚本文件就用前面写的代码保存成.py,参数配置是重点。
4.2 参数设计尽量覆盖实际项目
我给工具设计参数时,习惯把以下这些全部暴露出来,不要写死在代码里:
| 参数名 | 类型 | 方向 | 说明 |
|---|---|---|---|
| 输入要素类 | 要素类 | 输入 | 要编号的shp或gdb要素 |
| 编号字段 | 字段 | 输入 | 保存编号的字段,文本型或长整型 |
| 分组字段 | 字段 | 可选 | 不填则全表统一编号 |
| 排序字段 | 字段 | 可选 | 不填则按OBJECTID顺序 |
| 起始值 | 长整型 | 输入 | 默认1 |
| 前缀 | 字符串 | 可选 | 如行政区代码,拼接在最前面 |
| 补零位数 | 长整型 | 输入 | 如4表示0001 |
脚本工具调用参数的逻辑很简单,用arcpy.GetParameterAsText读取即可:
import arcpy fc = arcpy.GetParameterAsText(0) num_field = arcpy.GetParameterAsText(1) group_field = arcpy.GetParameterAsText(2) order_field = arcpy.GetParameterAsText(3) start_value = int(arcpy.GetParameterAsText(4)) prefix = arcpy.GetParameterAsText(5) zero_pad = int(arcpy.GetParameterAsText(6))这里面有几个细节值得注意。分组字段和排序字段如果留空,代码里要做成None判断,否则传空字符串进去会报错。前缀是否需要拼接分隔符,不同项目不一样,我通常直接拼成“前缀+流水号”,不自动加下划线,因为有的规则是“13开头+5位流水”,有的是“XZQDM_BH”,统一逻辑反而添乱。
4.3 参数校验一定要做
工具运行前,至少要检查三件事:编号字段是否存在、是不是文本或数值类型、当前图层有没有在编辑会话中被锁定。前两项可以通过arcpy.ListFields判断,第三项是很多人忽略的:如果数据正在ArcMap里被编辑,ArcPy拿不到写锁,更新游标会直接报错。我一般在脚本开头加一个try块,明确提示用户关闭编辑会话后再跑:
try: with arcpy.da.UpdateCursor(fc, [num_field]) as cursor: for row in cursor: pass except RuntimeError: arcpy.AddError("无法写入要素类,请确认该数据未被编辑会话锁定,或关闭ArcMap/S Pro中的编辑状态。") raise这个小检查能减少一半的报错求助。
4.4 模型构建器能替代吗
碰到不想写代码的同事,我也会给他们搭模型构建器方案:用“计算字段”工具加上一个内嵌的AutoIncrement代码块,也能实现顺序编号。但模型构建器的局限很明显:分组计数、复杂前缀拼接、跨要素类批处理这些需求,拖拽出来会非常啰嗦,尤其是全局计数状态在模型里更不可控。我的结论是:临时任务可以用模型构建器,生产环境直接上Py脚本工具,维护成本和出错率都低得多。
5. 编号工具上成果前,先过这几道检查
5.1 唯一性检查是底线
编号工具跑完,第一时间不是看效果,而是查重复。最简单粗暴的方法是用Python的set去重:
import arcpy fc = r"C:\data\gdb\parcels" num_field = "BH" all_values = [] with arcpy.da.SearchCursor(fc, [num_field]) as cursor: for row in cursor: all_values.append(row[0]) duplicates = {v for v in all_values if all_values.count(v) > 1} if duplicates: print("重复编号:", duplicates)数据量大了以后,all_values.count效率很低,可以改用set统计次数:
from collections import Counter counter = Counter(all_values) duplicates = {k: v for k, v in counter.items() if v > 1}这种检查我在每次编号后必跑,哪怕只是加个前缀这样的小修改,也不能省。批量拼接场景里,前缀不一致、补零位数不一致都容易产生隐性重复。
5.2 字段类型和补零问题
编号字段我强烈建议用文本型,不要用数值型。为什么?因为业务编号通常有前导零,比如“001”,数值字段会自动把前导零吃掉,存进去就变成“1”。就算你当时觉得无所谓,等数据导入Excel、接入管理平台时,还会遇到科学计数法、数字变小数位的花样问题,防不胜防。
如果编号是纯数字且必须保持位数,在文本字段里用格式化函数补零最保险,如:
f"{i:04d}"Python的format语法在ArcGIS Pro 3.x中完全支持,在ArcMap 10.x的Python 2.7环境中,也可以用:
str(i).zfill(4)另外,shapefile的dBase字段宽度限制很死,文本字段默认50、最大254,如果你要拼很长的复合编号,提前在字段属性里把长度改够,否则写进去的内容会被截断,编号看起来没重复,实际上后面几位已经丢了。
5.3 已使用的编号不要轻易重排
最后想特别说一个原则:如果编号已经投入使用,比如调查表已经打印、管理平台已经录入,那就算中间删了几个图斑,也不要重排。重排意味着旧的业务编号作废,关联的照片、签字、系统记录全都要跟着改,工作量远大于保留断号。正确的做法是保留现有编号,把错误图斑的编号做废标记,新增图斑接着当前最大编号继续往后编。查找当前最大编号的写法很简单:
max_value = None with arcpy.da.SearchCursor(fc, [num_field]) as cursor: for row in cursor: if max_value is None or row[0] > max_value: max_value = row[0]然后起始值设为max_value + 1,工具一样能跑,断号就让它断着,业务上完全合法。这个观念转变,是我做了几个大项目后才真正接受的。
5.4 常见的三个运行报错
第一个是“Cannot acquire a write lock”,前面提过,基本是数据正在编辑会话中被占用,关掉编辑状态再跑。第二个是“The value cannot be NULL”,一般是更新游标里给字段写了None,检查一下分组字段或者编号字段是否有个别空值。第三个是“字段计算器或脚本执行后编号没变化”,多半是更新游标里忘了调用updateRow,这是新手最容易犯的错,自己写脚本时一定要在循环里显式调用cursor.updateRow(row),而不是改完row[0]就完事。
我现在的习惯是:任何编号脚本跑正式数据前,先复制一份小范围测试数据,在副本上完整跑一遍,检查最大值、唯一性、前导零、排序顺序,确认无误后,再切到正式数据上执行。这多花五分钟,换来的是不用提心吊胆地等成果返工。做数据生产,最贵的成本从来不是跑程序的几分钟,而是返工的一整天。
本文还有配套的精品资源,点击获取