ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ArcGIS要素稳定编号:顺序、分组与沿线编号实践

ArcGIS要素稳定编号:顺序、分组与沿线编号实践

简介:面向GIS从业者与ArcGIS初学者的编号辅助工具,针对mdb、gdb、shp三类常见地理数据格式,解决批量要素缺少顺序标识、手工编号低效易错的问题。工具调用Esri ArcGIS API组件,可读取并更新数据源属性字段:选定数据源、编号字段与起始值,即可自动追加序列号,也支持按图层或指定要素定向处理,适用于数据整理、制图与空间规划等场景。

压缩包共74个文件、约7.1MB,以32个dll动态链接库和32个配套xml文档为主体,另有4个config配置、4个exe程序与2个manifest清单。dll覆盖Carto、Geodatabase、Geometry、Controls、Display等模块,xml为各组件接口说明,便于对照理解调用关系。

已有3992人学习下载。对需要反复处理要素编号的用户,工具可直接落地使用,省去自行编写脚本的环节;组件与文档的对应关系,亦可作为了解ArcGIS二次开发接口的参考。

1. 一条燃气管网要编八千个号,我为什么不再手动拉序号

要素一多,编号这件事就从体力活变成玄学。某公司做管线普查,八千多条管段要在属性表里按巡检路线从头编一遍,手动拉序号拉了三天,改一个图斑就得全表重来。ARCGIS 自带的字段计算器能填 OBJECTID,可那个号跟着要素创建顺序走,删几个补几个就断了,交出去被打回来两次。这套 ARCGIS 编号小工具干的就是把这件事固定下来:给定排序键和分组键,稳定地吐出一串连续、可复现的编号,顺序编号、分组编号、沿线编号三种口径都能覆盖。适合手里有几千到几十万条要素、又要反复重编的测绘、规划、管网、林业调查类工作——尤其是那种「下周还要再交一版」的项目。

2. 编号工具的三根支柱:排序键、分组键、输出字段类型

写编号脚本之前,先把三件事定死:按什么排、按什么分组、号写成什么类型。这三件事定错了,代码写得再漂亮也是白跑。我见过太多人一上来就CalculateField填!OBJECTID!,跑完看着挺顺,一删要素全乱,回头重来。

2.1 OBJECTID 为什么不能直接当编号用

OBJECTID(shapefile 里叫 FID)是要素类内部维护的行标识,不是业务字段。它的行为有几个坑:地理数据库里从 1 开始,shapefile 里从 0 开始;删掉要素后不重排,新增要素可能复用被删掉的号;导出、投影、合并、裁剪,每一步都可能重新分配一遍。也就是说,它在整个数据生命周期里是不稳定的。

更麻烦的是它跟你想要的顺序没关系。要素创建顺序取决于当初是「先画哪条」,跟你现在要的「从北到南」「沿主管线走向」完全是两码事。所以正确做法永远是:新建一个业务字段,把编号值算出来写进去,OBJECTID 只在排序时当一个兜底键用。

2.2 排序键怎么定:属性、几何、还是混着来

排序键决定编号的「阅读顺序」,常见三类:

排序键类型典型字段/几何适用场景注意点
属性键图幅号、片区代码、日期、名称已有业务分类,顺序由业务定文本排序会按字典序,"10" < "2"
几何键质心 X/Y、四角坐标、包围盒出图好看,从上到下从左到右必须在投影坐标系下算
混合键先片区、再质心 Y、最后质心 X分幅分片又要求片内有序键越多,重复概率越低

几何键那条要重点说。地理坐标系(WGS84、CGCS2000 经纬度)下 X/Y 是度,同一纬度带里经度差 0.001 度和纬度差 0.001 度对应的实地距离差好几十个百分点,排出来的「从上到下」是歪的。用几何排序前先投影到当地 3 度带或高斯投影,坐标单位换成米,顺序才对得上眼睛看到的。

还有一个隐性要求:排序键必须能唯一定序。如果两个要素的排序键完全一样,Python 的sort虽然稳定,但数据读取顺序本身可能因为压缩、重建索引而变化,两次跑出来的结果就不一致。我一般在排序键末尾强制追加OID@,保证每次结果可复现。

2.3 输出字段的类型与格式:整型、文本、补零前缀

字段类型选错,后面全是补丁。对照表如下:

字段类型取值范围什么时候用备注
SHORT(短整型)-32768 ~ 32767要素数明确不超过 3 万超了直接报溢出
LONG(长整型)约 ±21 亿绝大多数场景的默认选择GDB 推荐
TEXT取决于长度要前缀、要补零,如A0001shapefile 上限 254
DOUBLE浮点基本不用来存编号会出现1.0这种显示

补零这件事单独说。业务上经常要PS-0001这种格式,很多人先算整数再手动改,其实一步到位:字段建成 TEXT,长度按「前缀 + 补零位数」算,写入时"{}{}".format(prefix, str(val).zfill(pad))。pad=4就是四位补零,超过四位不截断,zfill只补不裁。

建字段这一步建议封成函数,重复跑不会报「字段已存在」:

# -*- coding: utf-8 -*- import arcpy def ensure_field(fc, name, ftype="LONG", length=None, precision=None): """字段不存在则创建,存在则校验类型,返回 True 表示新建""" exist = {f.name.lower(): f for f in arcpy.ListFields(fc)} key = name.lower() if key in exist: f = exist[key] if f.type.upper() != ftype.upper(): raise ValueError( "字段 {} 已存在,类型为 {},与期望的 {} 不符".format(name, f.type, ftype) ) return False kwargs = {"in_table": fc, "field_name": name, "field_type": ftype} if length is not None: kwargs["field_length"] = length # 仅 TEXT 生效 if precision is not None: kwargs["field_precision"] = precision # 仅数值型生效 arcpy.management.AddField(**kwargs) return True

逻辑说明:先ListFields拿现成字段做小写比对,避免大小写导致的重复建字段;类型不一致直接抛异常而不是硬改,因为改字段类型本身要重建字段、丢数据,属于该由人来决策的事。length和precision只对特定类型生效,按需传,传错类型 ArcGIS 会自己忽略。

参数说明:ftype建议直接用"LONG"、"TEXT"、"SHORT"这种大写常量;length对 TEXT 必填,比如前缀 3 位加补零 4 位就设 7;数值型不要传length,传了也没用。

注意:shapefile 不支持长整型以外的很多类型,字段名还有 10 字符上限。要在 shapefile 上跑,字段名一律压到 10 字符以内,别用中文名。

3. 在 ArcGIS Pro 里跑通第一条编号:Python 窗口、字段计算器与参数

原理清楚之后,落到 ArcGIS Pro 里其实就三条路:Python 窗口跑脚本、字段计算器填表达式、做成工具箱。日常最顺手的是第一条,改一行就能重跑,调参不用来回点对话框。

3.1 Python 窗口里的最小可运行版本

打开 Pro 的「分析 → Python 窗口」,先把当前工程的图层路径拿到手。直接写死路径也行,但从当前地图取图层更省事,换工程不用改代码:

import arcpy # 从当前工程的活动地图里按名字找图层 aprx = arcpy.mp.ArcGISProject("CURRENT") m = aprx.activeMap lyr = m.listLayers("管线")[0] # 图层名按实际情况改 fc = lyr.dataSource # 拿到真实数据源路径 print(fc)

拿到fc之后,核心编号函数长这样:

def number_by_order(fc, out_field, order_fields=None, start=1, step=1, prefix="", pad=0, reverse=False, where=None): """ fc : 要素类路径 out_field : 写入编号的字段名(已存在) order_fields: 排序字段列表,可为 None start/step : 起始值与步长 prefix/pad : 文本前缀与补零位数,pad=0 表示不补零 reverse : True 时倒序编号 where : 只对满足条件的要素编号 """ oid = arcpy.Describe(fc).OIDFieldName order = list(order_fields or []) # 第一步:只读,把 OID 和排序键一次性取出来 rows = [] with arcpy.da.SearchCursor(fc, ["OID@"] + order, where_clause=where) as cur: for r in cur: # None 单独处理,避免排序时 TypeError keys = tuple((1, "") if v is None else (0, v) for v in r[1:]) rows.append((r[0], keys)) del cur # 末尾追加 OID 兜底,保证排序结果唯一且可复现 rows.sort(key=lambda x: x[1] + ((0, x[0]),), reverse=reverse) # 第二步:算值,编号只跟排序后的位置有关 result = {} for i, (o, _) in enumerate(rows): val = start + i * step if pad > 0: result[o] = "{}{}".format(prefix, str(val).zfill(pad)) else: result[o] = val # 第三步:写回,单独开一个游标,避免读写混在一个游标里 n = 0 with arcpy.da.UpdateCursor(fc, ["OID@", out_field], where_clause=where) as cur: for o, _ in cur: cur.updateRow([o, result[o]]) n += 1 del cur return n

逐段说清楚。第一步的SearchCursor只读不写,把 OID 和排序键全部拉到内存里排序,这是关键——如果边读边写,游标遍历顺序和写入顺序会互相干扰,ArcGIS 官方文档里也明确不建议在同一个游标里做重排序写入。keys那里把None转成(1, ""),是为了让空值排在所有有效值后面,而不是让整个sort抛TypeError。

第二步的rows.sort是纯 Python 排序,键是「排序字段元组 + OID」,reverse直接控制升降序。这里没有用 SQL 的ORDER BY,原因是ORDER BY在不同数据源上对中文、空值的处理不一致,交出去的数据要跨环境复现,Python 端自己排更保险。

第三步单独开UpdateCursor。where_clause保持一致,确保只更新该更新的行,不会把未参与编号的要素刷成None。

参数上最容易搞错的是pad。如果你要A0001这种格式,out_field必须建成 TEXT,pad=4,prefix="A";如果要纯数字,pad=0,字段建 LONG。混用会直接报类型不匹配。

3.2 字段计算器快车道:什么时候能用,什么时候别碰

不是所有编号都值得写脚本。如果只是「全表从 1 到 N,顺序无所谓」,字段计算器一行就够了:

# 前提:out_field 已存在,类型为 LONG arcpy.management.CalculateField( in_table="管线", field="NH", expression="!OBJECTID!", # 直接映射内部 ID expression_type="PYTHON3" )

但这行代码有两个前提:一是能接受编号跟内部 ID 绑定,删要素后一定断号;二是要素数不超过字段类型上限。任何一个不满足,就得回到脚本。

字段计算器还能用 Code Block 做条件编号,比如「按类型分段」:

arcpy.management.CalculateField( in_table="管线", field="NH", expression="get_num(!TYPE!, !OBJECTID!)", expression_type="PYTHON3", code_block="""offset = {'给水': 100000, '排水': 200000, '燃气': 300000} def get_num(t, oid): return offset.get(t, 0) + oid """ )

逻辑说明:code_block里定义一个字典做段号偏移,同一张表里不同专业各占一个号段,避免跨专业混编。适合「一眼能从号看出专业」的场景。

代价是:OBJECTID不保证段内连续,删了要素号就断。所以这个写法我只在初版出图时用,正式交付前一定换成number_by_order,并且把排序键设成业务字段。这种「先快后稳」的切换节奏,能省下不少现场等待时间。

3.3 参数怎么调:起始值、步长、方向、范围

四个参数,逐个说清楚什么时候改:

start一般设 1,除非要预留号段。比如新老数据合并,老数据编到 3200,新数据从 3201 起,start=3201一步到位。

step平时都是 1。设成 10 或 100 是给未来插号留空间,管线、道路这类会不断新增的要素可以考虑,代价是号看上去稀疏。

reverse控制方向。按质心 Y 排序时,默认是「从下到上」,想要「从上到下」就reverse=True。这个参数建议写在调用处显式给,别藏在默认值里,否则下一次跑出来的顺序跟上次不一样,排查半天。

where是范围控制。只给某个片区的要素编号,where_clause="PIANQU = 'A'",未命中的要素字段保持原值不动。这一点很重要:UpdateCursor带where_clause时只遍历命中的行,其他行不会被刷成空。

提示:第一次跑新数据,先where限定到一个片区试跑,看一眼属性表顺序对不对,再放开全表。全量跑一次几万条要素通常十几秒,但跑错了回滚的成本远不止十几秒。

4. 分组与空间排序:按片区编、按图幅编、沿线路编怎么落地

顺序编号只是起点。真实项目里更常见的是「每个片区内部从 1 开始」「每幅图内部从 1 开始」「沿主管线走向从上游编到下游」。这三种需求分别对应分组编号、几何排序编号和沿线编号,写法差别不小。

4.1 分组编号:字典计数与 ORDER BY 的两种写法

分组编号的本质是「每个组各有一个独立计数器」。最直观的写法是一个字典,键是组值,值是当前计数:

def number_by_group(fc, group_field, out_field, order_fields=None, start=1, step=1, prefix="", pad=0): """每个 group_field 值内部独立编号,编号格式为 组值-序号""" order = list(order_fields or []) rows = [] with arcpy.da.SearchCursor(fc, ["OID@", group_field] + order) as cur: for r in cur: g = r[1] if r[1] is not None else "__NULL__" keys = tuple((1, "") if v is None else (0, v) for v in r[2:]) rows.append((r[0], g, keys)) del cur # 先按组名排,再按组内排序键排 rows.sort(key=lambda x: ((0, x[1]), x[2], (0, x[0]))) counters = {} result = {} for o, g, _ in rows: idx = counters.get(g, 0) val = start + idx * step counters[g] = idx + 1 if pad > 0: result[o] = "{}{}-{}".format(prefix, g, str(val).zfill(pad)) else: result[o] = val with arcpy.da.UpdateCursor(fc, ["OID@", out_field]) as cur: for o, _ in cur: cur.updateRow([o, result[o]]) del cur return len(result)

逻辑说明:rows.sort的键是三段式——先组名,再组内排序键,最后 OID 兜底。这样字典计数器只要顺序扫一遍就能保证「组内连续、组间独立」。counters用dict.get(g, 0)而不是defaultdict,是为了让逻辑显式一点,出问题时好打断点。

参数说明:group_field建议用短整型或文本,别用浮点,浮点做分组键会因为精度问题分出莫名其妙的组。pad和prefix同上,输出格式是前缀 + 组值 + "-" + 补零序号,比如PQ-A-0007。

这个写法的边界是数据量。十万条以内,Python 排序毫无压力;上百万条,内存和UpdateCursor的写入时间会明显上去,那时候该考虑用arcpy.management.Sort先落一个排好序的临时要素类,再顺序扫一遍写号。

4.2 按几何位置排序:质心、四角、还是包围盒

几何排序没有统一答案,取决于出图时希望人眼怎么读。三种常见口径:

按质心 Y 降序、X 升序,就是「从上到下、从左到右」的蛇形读法,最符合纸质图的阅读习惯:

def number_by_centroid(fc, out_field, y_desc=True, start=1): rows = [] with arcpy.da.SearchCursor(fc, ["OID@", "SHAPE@XY"]) as cur: for oid, (x, y) in cur: # 先按 Y 排,再按 X 排;Y 降序靠取负实现 rows.append((oid, (-y if y_desc else y, x))) del cur rows.sort(key=lambda r: (r[1][0], r[1][1], r[0])) with arcpy.da.UpdateCursor(fc, ["OID@", out_field]) as cur: for i, (oid, _) in enumerate(rows): pass del cur # 写回略,同 number_by_order 的第三步

SHAPE@XY拿的是几何的质心(对线是中点,对点是本身),比SHAPE@快一个数量级,十万条以内几乎无感。取负来排序是省掉reverse参数的老写法,可读性一般,我一般写成key=(-y, x)并在注释里标清楚。

按四角排序是另一种口径:面要素按左上角坐标排,适合地籍、地块这类要求「先上后下、先左后右」严格对齐的场景。左上角取extent.XMax, extent.YMax,但extent每次都要算包围盒,比质心慢。要素数超过二十万时,我通常先算一遍质心写进临时字段,再拿字段排序,比反复访问几何快得多。

注意:几何排序前务必确认数据框和要素类用的是投影坐标系。经纬度下SHAPE@XY返回的是度,排序结果跟实地位置对不上,这种错在出图时才被发现,返工量很大。

4.3 沿线路走向编号:measureOnLine 的用法与边界

沿管线、河道、道路走向编号是管网和水利项目的刚需。核心思路是:找一条参考线(主管线),把每个要素的质心投影到参考线上,算出沿线累计距离,按距离排序。

ArcPy 的Polyline.measureOnLine(point, use_percentage)就是干这个的,返回点投影到线上的位置。use_percentage=False时返回实际长度(米),True时返回 0~1 的比例:

def number_along_route(fc, route_fc, out_field, start=1, reverse=False): """按要素质心沿参考线 route_fc 的累计距离编号""" # 参考线必须是单条线要素,多段线会自动拼成一条 with arcpy.da.SearchCursor(route_fc, ["SHAPE@"]) as cur: route = next(iter(cur))[0] del cur rows = [] with arcpy.da.SearchCursor(fc, ["OID@", "SHAPE@"]) as cur: for oid, geom in cur: pt = geom.centroid # 要素离参考线太远时 measureOnLine 会投影到最近端点 d = route.measureOnLine(pt, False) rows.append((oid, d)) del cur rows.sort(key=lambda r: (r[1], r[0]), reverse=reverse) with arcpy.da.UpdateCursor(fc, ["OID@", out_field]) as cur: for o, _ in cur: pass del cur # 写回同前,第 i 个要素写 start + i

逻辑说明:参考线读成单个Polyline对象而不是逐行处理,是因为measureOnLine是几何对象的方法,需要完整的线几何。多个要素同时投影到同一条线上,才能保证距离值的相对关系一致。

参数说明:reverse=True是「从下游往上游编」,管网项目里经常要按水流反方向出图,这个参数一定要显式给。geom.centroid对面要素是几何中心,对线要素是线的中点,如果你的要素是分段的管段,中点就是这一段的位置,合理;但如果管段很长、弯折剧烈,中点可能偏离主走向,这时候改用端点(geom.firstPoint)更准。

边界也很明确:measureOnLine是把点垂直投影到线上,如果要素离参考线很远(比如支线离主管线几百米),投影会落到最近的那个端点上,多个要素会算出同一个距离,排序退化成按 OID。所以这条路线只适合「要素大致沿参考线分布」的场景,分叉多的管网建议先按支线分组,再在组内沿线排序。

5. 排查清单:编号重复、顺序乱跳、字段被锁的五种现场

脚本跑通只是开始,真实现场的问题基本都出在数据状态和运行环境上。下面五条是我这两年改得最多的,按出现频率排。

第一条:编号重复,而且重复的位置每次还不一样。现象是跑完属性表里出现两个 17、三个 42,重跑一遍重复的位置变了。原因几乎都是排序键不唯一且没有 OID 兜底——sort本身稳定,但数据读取顺序可能因为压缩、重建索引变化,两次结果就不一致。解决办法是在排序键末尾强制追加OID@,让每个元素的排序键唯一,这样无论读多少次,顺序都锁死。这条改完之后,重复问题基本绝迹。

第二条:明明按数字排,结果 10 跑到 2 前面去了。现象是按图幅号排序,1, 10, 11, 2, 20这么出来。原因是排序字段是 TEXT 类型,Python 按字典序排,字符"1"比"2"小,所以"10"排在"2"前面。解决有两个方向:一是排序前把文本转成int,key=lambda r: int(r[1]),前提是字段里没有非数字内容;二是编号字段本身用整数,只在展示时补零。我一般选后者,字段类型在建模阶段就定死,后面不用反复转换。

第三条:字段被锁,UpdateCursor报「无法获取锁」。现象是脚本跑到写回那一步挂掉,提示锁冲突或只读。常见原因有三个:属性表窗口开着、编辑会话没保存、要素类被别的程序占用(比如同步工具、其他 Pro 工程)。逐一排查的顺序是:先关属性表,再看「编辑」选项卡里有没有未保存的编辑,最后看系统里有没有残留的ArcGISPro.exe进程。这三步走完基本都能解。血泪经验是:跑批量编号前先保存工程、关掉所有属性表,能省掉一大半莫名其妙的失败。

第四条:脚本中途崩了,字段写了一半,前功尽弃。现象是跑到第 3000 条报错退出,前 3000 条已经写进去了,后面的还是旧值,数据处于半成品状态。这个问题不是脚本 bug,是没有事务保护。解决方式有两种:简单的是先arcpy.management.CopyFeatures复制一份备份,崩了直接换回来;稳妥的是用arcpy.da.Editor开编辑会话,把整个编号过程包在startEditing / stopEditing(True)里,异常时stopEditing(False)回滚。第二种写法多一点,但真正跑大批量时值得。

第五条:分组字段有空值,整组编号从 0 开始或者整组消失。现象是按片区编号,PIANQU为空的那批要素,要么编号是 0 或者负数,要么根本没被编上。原因是None在字典里做键、在排序里做比较都会出问题。解决是在分组前统一把空值映射成一个显式标签,比如"__NULL__",编完再人工确认这批要素该归到哪个片区。这个坑的隐蔽之处在于:空值要素往往不多,属性表翻到后面才看得见,出图时才发现有几条没号。

提示:这五条里,第一、四条的代价最高——一个是结果不可信,一个是数据要重来。跑正式数据前,拿十条要素的样本全流程走一遍,能提前暴露 80% 的问题。

6. 把脚本收进 .pyt 工具箱:批量跑、带校验、下次直接拖进去

脚本在 Python 窗口里跑,改参数得回来改代码,交给同事还得附一段说明。真正省事的做法是包成.pyt工具箱,参数在对话框里填,双击就能跑,还能扔进模型构建器里串批处理。

最小骨架长这样,一个工具类对应一个##级功能:

# -*- coding: utf-8 -*- import arcpy class Toolbox(object): def __init__(self): self.label = "编号工具箱" self.alias = "numbering" self.tools = [SeqNumberTool] class SeqNumberTool(object): def __init__(self): self.label = "顺序编号" self.description = "按指定字段排序后连续编号,支持前缀与补零" def getParameterInfo(self): p_fc = arcpy.Parameter(name="in_fc", displayName="输入要素类", datatype="DEFeatureClass", parameterType="Required", direction="Input") p_field = arcpy.Parameter(name="out_field", displayName="编号字段", datatype="Field", parameterType="Required", direction="Input") p_field.parameterDependencies = ["in_fc"] # 依赖输入表,自动列出字段 p_order = arcpy.Parameter(name="order_field", displayName="排序字段", datatype="Field", parameterType="Optional", direction="Input", multiValue=True) p_order.parameterDependencies = ["in_fc"] p_start = arcpy.Parameter(name="start", displayName="起始值", datatype="GPLong", parameterType="Optional", direction="Input") p_start.value = 1 return [p_fc, p_field, p_order, p_start] def execute(self, parameters, messages): fc = parameters[0].valueAsText out_field = parameters[1].valueAsText order = parameters[2].values or [] start = parameters[3].value or 1 n = number_by_order(fc, out_field, [o for o in order], start=start) messages.addMessage("完成编号:{} 条".format(n)) # 跑完立刻校验,问题当场暴露 check_numbering(fc, out_field)

parameterDependencies是关键,它让「编号字段」和「排序字段」两个下拉框跟着输入要素类联动,用户不用手敲字段名。multiValue=True允许选多个排序字段,顺序就是对话框里的上下顺序。

配套的校验函数建议每个项目都跑一遍,比肉眼翻属性表靠谱得多:

def check_numbering(fc, field): """检查空值、重复、断号,结果直接打印""" vals = [r[0] for r in arcpy.da.SearchCursor(fc, [field])] nonnull = [v for v in vals if v is not None] dup = len(nonnull) - len(set(nonnull)) gaps = sum(1 for a, b in zip(sorted(nonnull), sorted(nonnull)[1:]) if b - a != 1) print("总数 {},空值 {},重复 {},断号 {}".format( len(vals), len(vals) - len(nonnull), dup, gaps)) return dup, gaps

批量处理多个要素类时,用arcpy.ListFeatureClasses()拿到工作空间下所有要素类,循环调number_by_order,把out_field统一成一个名字。这一步的前提是所有要素类的字段结构一致,不一致的先补字段再跑。

我踩过最深的一个坑,是校验函数只查了重复没查断号,交出去的数据号是连续的但中间缺了 37 号,被退回来重做了一遍。从那以后我每次交数据前,都强制把check_numbering跑一遍,三个数字都归零才敢发出去。这套习惯帮我省掉的返工时间,远比写脚本的时间多。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表