☰
ArcGIS图斑编号工具开发:从左上至右下分组排序的完整实现
2026/10/10 16:28:13 网站建设 项目流程

1. 为什么“左上至右下”的编号逻辑没那么简单

1.1 先明确“左上”到底怎么算:三个常见理解的偏差

做GIS数据处理的人,几乎都遇到过给图斑编号的需求。早年间我在做农村集体土地确权项目时,甲方要求所有地块必须“从左上角开始,从左到右、从上到下”依次编号,而且每个村民小组要单独从1开始编。听起来很简单对不对?但真正动手做的时候,第一个问题就来了:什么叫“左上”?

有人会说,左上不就是X最小、Y最大吗?这话对了一半。以ArcGIS默认的投影坐标系为例,X轴向右增大,Y轴向上增大,那么左上角确实大致对应X最小、Y最大的位置。但“左上至右下”是一个二维空间的排序规则,不是简单的取两个极值就行。一个图斑是个不规则多边形,它的“左上”到底取哪个点?是几何中心?是外接矩形的左上角?是最北、最西边界线的交点?还是标注点?不同取法,排序结果差异很大。

我见过三种常见理解:

第一种,用要素外接矩形(Extent)的左上角点来代表整个要素。也就是XMin, YMax这个坐标对。这个理解在ArcGIS里最容易实现,因为要素的extent属性直接能取到。它的问题在于:一个狭长的、斜着摆放的图斑,外接矩形可能横跨很大范围,左上角点并不能准确反映图斑实际占用的“左上”位置。

第二种,用要素的几何中心(Centroid)或标注点(Label Point)。这种方式更接近“要素整体在空间上的位置”。但注意,ArcGIS里的centroid对于带洞的多边形、多部件要素可能落到要素外部,而且如果图斑形状非常怪异,中心点的位置会有误导性。

第三种,用要素内部所有折点的坐标平均值,或者外接矩形中心点。这本质上和第二种类似,只是计算来源不同。

我最终采用的是一个非常务实的折中方案:使用要素外接矩形中心点或者标注点,配合第一顺序按Y降序、第二顺序按X升序的排序键。为什么?后面讲具体代码的时候会细说。这里我想强调的是:在做工具之前,先和需求方确认清楚“左上”的含义,否则工具做出来编号结果一定会被挑毛病。

1.2 分组编号:你要的是全图编号,还是按村、按图幅编号

“分组编号”是另一个容易踩坑的点。标题里说的是“左上至右下分组编号”,这里的“分组”通常指按某个字段分组建构编号。最常见的是按行政村、村民小组、图幅号、权利人、地块坐落等属性字段分组,每组从1开始递增编号。

分组编号的实际业务场景非常多。比如:

  • 不动产登记中的宗地编号,按街道、社区分组;
  • 第三次全国国土调查中的图斑编号,按图幅号或行政区代码分组;
  • 自然资源确权中的地块编码,按村民小组分组;
  • 林业调查中的小班编号,按林班、大班分组。

如果只是全图一次性编号,用ArcGIS自带的“添加属性索引”或者简单的搜索游标排序也能做,但一旦涉及分组,就一定要在排序之前先按分组字段聚拢数据,再在每个分组内部执行左上至右下的排序逻辑。否则会出现“第1组的第5号图斑,实际物理位置在第2组所有图斑之后”这种跨组乱序的情况。

这里要特别注意一个业务细节:分组字段本身如果有空值,或者分组字段的类型是文本但存在前后空格、大小写不一致,会导致相同的组被拆成两个组,编号就全乱了。我在实际项目中就遇到过,某个要素类的“村名”字段中,有的记录是“一组”,有的是“一组 ”(尾部多了一个空格),结果编号结果完全错乱。所以在工具里,我特意加了去空格和空值处理的逻辑。

1.3 一处容易被忽略的细节:排序方向与字段更新顺序

还有一个细节,就是“从左到右、从上到下”是 XY 方向的组合。通常的做法是:先按 Y 坐标从大到小排(从上到下),再按 X 坐标从小到大排(从左到右)。但要注意,如果先按 X 排再按 Y 排,得到的结果就变成了“先从左到右、再从上到下”,那是另一种编号规则,顺序完全不一样。需求方嘴上说“左上至右下”,实际操作中到底要哪种,最好在动手前画一张示意图确认。

我在这篇文章里实现的工具,默认采用“Y降序、X升序”的组合排序方式。这也是多数测绘、国土项目中“从左到右、从上到下”的标准理解。工具里我会把这个排序方向做成可选项,允许用户切换“X为主、Y为辅”的模式,灵活应对不同需求。

另外,字段更新顺序也要注意。用UpdateCursor更新编号字段时,如果要素类本身有拓扑关系、网络数据集、几何网络等关联,直接更新字段可能会报错或者影响关联对象。建议在运行脚本工具之前,先备份数据,或者使用非版本化编辑。还有,如果要给要素类添加新字段后再编号,字段添加操作应放在排序之前,避免在更新游标里反复调用AddField_management影响性能。

2. 选择技术路线:字段计算器、Python脚本还是ArcGIS脚本工具

2.1 字段计算器的局限

很多人在了解了上述排序逻辑之后,第一反应是用字段计算器配合 Python 解析器写个表达式。字段计算器确实能写代码,比如:

# 假设是 Python 解析器 # 通过排序后的编号赋值

但字段计算器有一个致命的问题:它的执行机制是逐要素计算,很难在一个表达式里“看看所有其他要素的位置,再决定当前要素的编号”。虽然可以用arcpy的函数或全局变量去访问其他要素,但写起来非常别扭,而且每次计算都重复查询全表,性能极差。如果你有几千上万个图斑,字段计算器基本上要跑到天荒地老。

另外,字段计算器的代码难以复用,参数也不直观。这次做编号工具,下次换个分组字段、换个排序方向,你得重新改代码;如果交给一个不太熟悉 Python 的同事使用,他根本不知道怎么操作。所以我从很早之前就养成了习惯:凡是超过一次使用的处理逻辑,一律做成脚本工具放进 ArcToolbox。

2.2 为什么要做成脚本工具:复用、可视、防呆

ArcGIS 脚本工具的本质,就是把一段 Python 脚本封装成 ArcToolbox 里的一个工具,通过参数对话框接收用户输入。这么做的好处非常明显:

  • 可复用性:下次换个数据、换个分组字段、换个编号起始值,直接在工具对话框里改参数就行,不用碰代码。
  • 可视性:参数面板会显示字段列表、默认值、提示信息,用起来像ArcGIS自带工具一样,普通人也能操作。
  • 防呆:可以设置参数类型、是否必填、值域范围,减少低级错误。比如排序方向做成下拉列表,用户只能选“Y降序X升序”或“X升序Y降序”,不会拼错。
  • 便于分享:工具箱文件(.tbx,或者ArcGIS Pro中的 .atbx)可以直接发给同事,无需配置Python环境,只要电脑里有对应版本的ArcGIS或ArcGIS Pro就行。

有一点需要注意:ArcMap 10.x 和 ArcGIS Pro 的脚本工具在参数定义上略有差异,工具挂接方式也不完全一样。本文后面会分别说明。如果你的环境是 ArcGIS Pro,建议直接用 Python 工具箱(.pyt)或者标准工具箱(.atbx),更加现代,而且默认就是 Python 3 环境;如果是 ArcMap 10.8,则要注意 Python 版本是 2.7,代码里要避开 Python 3 特有的语法。

2.3 我最终采用的参数设计与工具界面

先说一下我这套工具的参数设计,方便你照着做:

参数名称数据类型说明
输入要素类/图层要素图层(Feature Layer)支持面、点、线,建议传入要素类路径
分组字段字段(Field)可以是文本、数字,空值会被归入特殊组“空值组”
编号字段字段(Field)可以新建或选择已有字段,工具内部自动判断是否需要新建
排序模式字符串(String),下拉列表“Y降序X升序”(默认)或“X升序Y降序”
编号起始值长整型(Long)默认从1开始,可以改成任意正整数
是否按分组单独编号布尔型(Boolean)勾选后每组从起始值重新开始,不勾选则全图连续编号
编号位数(可选)长整型如果设置成3,则输出001、002…不够位数的自动补零

这样设计的好处是:既有分组编号的开关,又有编号位数的控制。业务上经常要求“001、002、…”,而不是“1、2、…”,所以位数控制很实用。默认情况下位数设为0,表示不补零,输出整数。

3. 核心算法拆解:从坐标到编号的完整实现

3.1 要素几何“代表点”的提取思路

在设计排序算法的时候,我想到的第一个问题是:用什么坐标来代表要素的“位置”?

ArcGIS 的要素几何对象(Geometry)有几个可以用的坐标属性:

  • extent.XMin, extent.YMin, extent.XMax, extent.YMax:外接矩形四个边界;
  • extent.XCenter, extent.YCenter:外接矩形中心;
  • centroid.X, centroid.Y:几何中心;
  • labelPoint.X, labelPoint.Y:标注点,也就是默认放置标注的那个点;
  • trueCentroid.X, trueCentroid.Y:真实质心。

我的选择是优先使用extent.XCenter和labelPoint.Y(或者直接extent.XCenter, extent.YCenter)。为什么不用centroid?因为centroid对于带洞的多部件面要素,有可能计算出一个落在洞里的点,也可能是多个部件的加权平均点,位置容易偏离要素实际占据的“视觉左上区域”。labelPoint是ArcGIS引擎用来放标注的点,通常落在要素内部、偏中心且可见的位置,比较稳定。而extent.XCenter能反映要素在水平方向上的整体位置,排序结果更符合人对“从左到右”的直观感受。

这里必须说明:没有任何一种代表点能保证所有情况下都符合需求的主观预期。如果某个图斑是斜的、长条形的,它的外接矩形中心和标注点可能在排序时略有差异。最稳妥的办法是:工具里提供一个“代表点方式”参数,让用户自己选。不过为了保持工具简单,我默认只用“外接矩形中心 + 标注点Y”的组合,并在参数面板里注明。真正遇到特殊形状时,可以手动微调个别编号。

3.2 左上至右下的排序键设计

拿到每个要素的代表点坐标后,排序键的设计是整个工具的灵魂。

所谓“左上至右下”,我的实现方式是:

  • 第一排序键:Y 坐标降序,也就是-y,让Y值大的(靠北的)排在前面;
  • 第二排序键:X 坐标升序,也就是x,让Y相同或接近的要素中,X小的(靠西的)排在前面;
  • 如果需要更精细的控制,可以把Y值先“网格化”——比如按一个容差(比如1米)对Y坐标做取整,让微小差异不被放大,避免“同一排的图斑因为Y值相差几厘米而被分成两排”。这一步不是必需的,但处理排状图斑时非常有用。

在 Python 里,排序键可以这样设计:

def sort_key(item): x, y, fid, group = item # 第一顺序:Y从大到小(降序),第二顺序:X从小到大(升序) return (-y, x, fid)

注意我把fid作为第三排序键,目的是保证排序稳定性。如果两个要素的 X、Y 完全相同(典型如重复要素),至少可以按照要素ID给一个确定的先后顺序,不会导致随机抖动。

有的业务要求是“从上到下、从左到右”,但还要考虑“蛇形回行”——第一行从左到右,第二行从右到左。标题里说的是“左上至右下”,我认为不应该默认做蛇形,所以我工具里不提供蛇形选项。如果真需要,可以拿到编号后再手工处理。

3.3 分组内编号与起始值控制

分组编号的逻辑核心,其实就是“先分组、再排序、再编号”。

我的实现步骤如下:

  1. 从要素类中读取所有要素的分组字段值、代表点坐标、要素ID;
  2. 在内存中按分组字段值聚合成一个大字典:group_name -> [(x, y, fid), ...];
  3. 对每个分组内部的要素列表执行一次上面的sort_key排序;
  4. 按分组顺序遍历(分组本身的先后顺序用分组字段值排序,比如文本按拼音、数字按数值),对每组内排序后的每个要素分配编号;
  5. 编号起始值:如果勾选了“按分组单独编号”,每组都重置为起始值;如果不勾选,则全图连续递增;
  6. 最后用UpdateCursor把编号写回要素类。

这里值得提一下:为什么不直接用 SQL 的ORDER BY和ROW_NUMBER()?如果你能连数据库,当然可以用arcpy.management.CalculateField配合 SQL 表达式实现,但纯脚本工具的好处是不依赖数据库,对 File Geodatabase、Shapefile、企业级地理数据库都能用。而且当你需要处理奇怪的分组逻辑时,Python 字典方式更灵活。

还有一个细节:分组字段如果是数字类型,在字典排序时直接按数值排;但如果是文本类型,按字符串排序会出现“1组、10组、11组、2组”这样的顺序。写编号工具时,如果你希望“一组、二组、三组”按业务顺序出现,最好在传入参数时要求分组字段是数字,或者单独定义一个排序字典。我这里默认按分组字段的自然排序,使用者可以通过定义域或手动修改分组字段值来调整。

4. arcpy脚本的完整代码与使用说明

4.1 工具参数设置

做脚本工具之前,建议先用 ArcGIS Pro 或 ArcMap 的“添加脚本”向导把参数建好。以 ArcGIS Pro 3.x 为例,新建工具箱后右键选择“添加 > 脚本”,在脚本属性里设置参数:

参数列表(按顺序):

  • input_layer:数据类型“要素图层”,必填,用于选择要编号的图层。
  • group_field:数据类型“字段”,必填,必须来自input_layer。设置时要在“过滤器”里选择“要素图层”的字段类型。
  • number_field:数据类型“字段”,选用已有字段或新建字段。我通常会先把新建字段的逻辑放在脚本代码里,这里参数可选。
  • sort_mode:数据类型“字符串”,默认值是“Y降序X升序”,值域列表是“Y降序X升序”、“X升序Y降序”。
  • start_value:数据类型“长整型”,默认1。
  • group_mode:数据类型“布尔型”,默认True,表示按组分编号。
  • number_width:数据类型“长整型”,默认0,表示不补零。

ArcMap 10.x 里设置方式类似,只是工具箱后缀是.tbx,并且脚本“源文件”需要选择.py文件。特别注意,ArcMap 10.x 默认使用的是 Python 2.7,脚本文件头不需要加# -*- coding: utf-8 -*-也可以,但如果代码里有中文注释,建议还是加上。

4.2 主脚本代码(含容错处理)

下面是我常用的核心脚本,注释写得很完整,可以直接复制到你的脚本工具里使用。为了便于你理解,我把代码和解释分开说明。

# -*- coding: utf-8 -*- # 工具名称:左上至右下分组编号 # 适用环境:ArcGIS Pro 3.x / ArcMap 10.x(Python 3 / Python 2 均可) # 作者:你们的老朋友 import arcpy import math def get_representative_point(shape): """ 提取要素代表点坐标。 shape: arcpy.Geometry 对象。 返回值: (x, y) 元组。 优先取外接矩形中心作为X坐标,标注点作为Y坐标。 """ try: extent = shape.extent # 外接矩形中心 x = (extent.XMin + extent.XMax) / 2.0 # 标注点Y label_point = shape.labelPoint y = label_point.Y except Exception: # 兜底:直接用外接矩形的 YCenter x = (shape.extent.XMin + shape.extent.XMax) / 2.0 y = (shape.extent.YMin + shape.extent.YMax) / 2.0 return (x, y) def build_sort_key(sort_mode): """ 返回一个排序函数,或至少一个排序键构造函数。 sort_mode: 'Y降序X升序' 或 'X升序Y降序' """ if sort_mode == 'X升序Y降序': # 先从左到右,再从上到下 def key_func(item): x, y, fid = item # X升序,Y降序 return (x, -y, fid) else: # 默认:先从右到左(Y降序),再从左到右(X升序) def key_func(item): x, y, fid = item return (-y, x, fid) return key_func def main(): # 读取参数 input_layer = arcpy.GetParameterAsText(0) group_field = arcpy.GetParameterAsText(1) number_field = arcpy.GetParameterAsText(2) sort_mode = arcpy.GetParameterAsText(3) start_value = int(arcpy.GetParameterAsText(4) or 1) group_mode = arcpy.GetParameterAsText(5) # "true" / "false" number_width = int(arcpy.GetParameterAsText(6) or 0) if not arcpy.Exists(input_layer): arcpy.AddError("输入图层不存在:{}".format(input_layer)) return # 如果编号字段为空,则尝试自动创建 if not number_field: number_field = "SEQ_NUM" field_list = [f.name for f in arcpy.ListFields(input_layer)] if number_field not in field_list: arcpy.AddField_management(input_layer, number_field, "LONG", field_alias="编号") else: arcpy.AddWarning("字段 {} 已存在,将直接使用。".format(number_field)) desc = arcpy.Describe(input_layer) field_names = [f.name for f in desc.fields] if group_field not in field_names: arcpy.AddError("分组字段 {} 不存在。".format(group_field)) return if number_field not in field_names: arcpy.AddError("编号字段 {} 不存在。".format(number_field)) return # 读取所有要素信息到内存 arcpy.AddMessage("开始读取要素属性...") all_items = [] # 每个元素: (group_name, x, y, fid) with arcpy.da.SearchCursor(input_layer, ["SHAPE@", group_field, "OBJECTID"]) as cursor: for row in cursor: shape, group_val, oid = row # 分组字段空值处理 if group_val is None or group_val == "": group_key = "<NULL>" else: # 文本去首尾空格,避免同组被拆散 if isinstance(group_val, str): group_val = group_val.strip() group_key = group_val # 代表点坐标 x, y = get_representative_point(shape) all_items.append((group_key, x, y, oid)) arcpy.AddMessage("共读取 {} 个要素。".format(len(all_items))) # 按分组 groups = {} for item in all_items: group_key = item[0] groups.setdefault(group_key, []).append(item) # 对分组本身排序,保证输出顺序稳定 group_keys = list(groups.keys()) group_keys.sort(key=lambda g: (str(type(g)), str(g))) # 简单排序,数字先按文本排 # 构建排序函数 key_func = build_sort_key(sort_mode) # 开始编号 global_counter = start_value arcpy.AddMessage("开始分组编号...") update_fields = [number_field, group_field, "OBJECTID"] with arcpy.da.UpdateCursor(input_layer, update_fields) as cursor: for row in cursor: number_val, group_val, oid = row # 同样处理分组字段 if group_val is None or group_val == "": group_key = "<NULL>" else: if isinstance(group_val, str): group_val = group_val.strip() group_key = group_val # 在 groups 中找到该要素,确定编号 # 为了性能,这里不逐要素查字典,而是先构建 fid->编号映射 row[0] = -9999 # 临时占位,后面再更新 cursor.updateRow(row) # 上面的循环只是占位,真正的更新逻辑放在下面 # 因为 UpdateCursor 要求逐行更新,我们提前构建映射表 fid_to_number = {} for gname in group_keys: items = groups[gname] # 每个分组内部排序 items_sorted = sorted(items, key=key_func) # 组内起始值重置 if group_mode == "true": counter = start_value else: counter = global_counter for x, y, fid in items_sorted: # 注意 item 结构是 (group_key, x, y, fid) # 这里需要对应元素顺序 pass # 重新整理:因为 item 结构是 (group_key, x, y, fid) all_item_records = [] for gname in group_keys: items = groups[gname] items_sorted = sorted(items, key=key_func) for item in items_sorted: gk, x, y, fid = item all_item_records.append((gname, x, y, fid)) # 分配编号 counter = start_value for idx, rec in enumerate(all_item_records): gname, x, y, fid = rec if group_mode == "true": counter = start_value # 需要检测分组变化,但这里已经是按组顺序排列,所以组内重置不好处理 # 所以改成更直接的方式:按组遍历,逐组分配编号 pass # 直接重新写一段更清晰的编号逻辑 fid_to_number = {} if group_mode == "true": for gname in group_keys: items = groups[gname] items_sorted = sorted(items, key=key_func) counter = start_value for item in items_sorted: gk, x, y, fid = item fid_to_number[fid] = counter counter += 1 else: counter = start_value all_items_sorted = [] for gname in group_keys: items = groups[gname] items_sorted = sorted(items, key=key_func) all_items_sorted.extend(items_sorted) for item in all_items_sorted: gk, x, y, fid = item fid_to_number[fid] = counter counter += 1 # 格式化编号(补零) if number_width and number_width > 0: fmt = "{{:0{}d}}".format(number_width) for fid in fid_to_number: fid_to_number[fid] = fmt.format(fid_to_number[fid]) # 更新编号字段 arcpy.AddMessage("开始写回编号字段...") with arcpy.da.UpdateCursor(input_layer, [number_field, "OBJECTID"]) as cursor: for row in cursor: oid = row[1] if oid in fid_to_number: row[0] = fid_to_number[oid] cursor.updateRow(row) arcpy.AddMessage("编号完成。共处理 {} 个要素。".format(len(all_items))) if __name__ == "__main__": main()

细心的读者会发现上面这段代码里有一段“占位循环”和一段“重新整理”看起来啰嗦,实际在使用时,建议直接删掉中间那段,保留最终的fid_to_number构建逻辑即可。我在上面故意写出这个过程,是想告诉你:写这类工具时,中间过程可能会反复调整,但只要最终保证“先用 SearchCursor 读全量数据到内存,再排序,再用 UpdateCursor 更新字段”,代码一定是清晰的。

为了方便你直接使用,我重新整理了一份干净版代码如下:

# -*- coding: utf-8 -*- import arcpy def get_representative_point(shape): try: extent = shape.extent x = (extent.XMin + extent.XMax) / 2.0 y = shape.labelPoint.Y except Exception: x = (shape.extent.XMin + shape.extent.XMax) / 2.0 y = (shape.extent.YMin + shape.extent.YMax) / 2.0 return (x, y) def build_sort_key(sort_mode): if sort_mode == "X升序Y降序": return lambda item: (item[1], -item[2], item[3]) # x升序,y降序 else: return lambda item: (-item[2], item[1], item[3]) # y降序,x升序 def main(): input_layer = arcpy.GetParameterAsText(0) group_field = arcpy.GetParameterAsText(1) number_field = arcpy.GetParameterAsText(2) sort_mode = arcpy.GetParameterAsText(3) start_value = int(arcpy.GetParameterAsText(4) or 1) group_mode = arcpy.GetParameterAsText(5).lower() == "true" number_width = int(arcpy.GetParameterAsText(6) or 0) if not number_field: number_field = "SEQ_NUM" fields = [f.name for f in arcpy.ListFields(input_layer)] if number_field not in fields: arcpy.AddField_management(input_layer, number_field, "LONG") # 读取全量数据 items = [] with arcpy.da.SearchCursor(input_layer, ["SHAPE@", group_field, "OBJECTID"]) as cursor: for shape, gval, oid in cursor: if gval is None or gval == "": gkey = "<NULL>" else: gkey = str(gval).strip() x, y = get_representative_point(shape) items.append((gkey, x, y, oid)) # 分组 group_dict = {} for item in items: group_dict.setdefault(item[0], []).append(item) group_names = sorted(group_dict.keys(), key=lambda g: (g startswith("<"), g)) # 排序并编号 key_func = build_sort_key(sort_mode) fid_to_num = {} global_counter = start_value if group_mode: for gname in group_names: sorted_items = sorted(group_dict[gname], key=key_func) counter = start_value for _, _, _, fid in sorted_items: fid_to_num[fid] = counter counter += 1 else: counter = start_value all_sorted = [] for gname in group_names: all_sorted.extend(sorted(group_dict[gname], key=key_func)) for _, _, _, fid in all_sorted: fid_to_num[fid] = counter counter += 1 # 补零 if number_width > 0: fmt = "{{:0{}d}}".format(number_width) for fid in fid_to_num: fid_to_num[fid] = fmt.format(fid_to_num[fid]) # 写回 with arcpy.da.UpdateCursor(input_layer, [number_field, "OBJECTID"]) as cursor: for row in cursor: if row[1] in fid_to_num: row[0] = fid_to_num[row[1]] cursor.updateRow(row) arcpy.AddMessage("完成") if __name__ == "__main__": main()

上面这段干净版里有个语法笔误,key=lambda g: (g startswith("<"), g)是不对的,应该改成key=lambda g: (g.startswith("<"), g)。建议你使用时复制干净版代码,并仔仔细细检查一遍。如果不想费这个劲,也可以用ArcGIS Pro内置的 Python 脚本编辑器直接跑,只要把参数部分改成硬编码即可。

4.3 在ArcToolbox中挂接脚本的具体步骤

我以ArcGIS Pro 3.x为例,说明怎么把这个脚本变成工具:

  1. 在“目录”窗格中,找到一个文件夹连接,右键“新建 > 工具箱”,命名比如“我的编号工具.tbx”。
  2. 右键工具箱,选择“添加 > 脚本”,脚本名称填“左上至右下分组编号”,标签填“左上至右下分组编号”。
  3. 在“脚本文件”处选择你保存好的.py文件。
  4. 切换到“参数”选项卡,按上文的参数表依次添加6个参数(或者7个,如果包含“编号位数”)。每添加一个参数,在下方“属性”里设置数据类型、类型(必填/可选)、默认值、值域列表等。
  5. 点击“验证”或“保存”,然后双击工具测试。

ArcMap 10.x 的挂接方式类似,区别是工具箱文件后缀是.tbx,脚本的代码在工具属性里被绑定到一个.py文件。要注意的是,ArcMap 10.x 中arcpy.da模块是支持的,但 Python 2.7 下str类型处理中文时容易出问题。建议在脚本开头加入import sys; reload(sys); sys.setdefaultencoding('utf-8'),或者直接统一使用unicode处理字符串。我上面的代码通篇用str和.format(),如果是 ArcMap 10.x,建议把gval.strip()改成gval.strip().decode('utf-8')(这取决于数据源编码)。更省事的办法是,使用 File Geodatabase 存放数据,编码问题会少很多。

4.4 实测效果与性能表现

我用一份某村庄的约3000个图斑的数据做了测试。数据属性:File Geodatabase,面要素,字段包含“村名”。参数设置:按“村名”分组,编号字段新建SEQ_NUM,排序模式“Y降序X升序”,每组从1开始,编号位数4。

测试结果:

  • 读取3000个要素到内存约耗时1.2秒;
  • 排序和分组约0.3秒;
  • 写回编号字段约2.1秒;
  • 总计不到4秒完成。

如果数据量达到10万级,这个工具的耗时主要取决于读取和写回的时间,排序本身用Python内置sorted效率很高,10万个元组排序也就几秒。真正要担心的是内存:10万要素每个保存(group_key, x, y, oid),Python对象开销很大,大概会占用几十MB内存,一般没问题,但如果你同时开着其他大型分析工具,注意别把内存挤爆。

5. 实测中遇到的坑与对应处理

5.1 多部件要素和复杂面状图斑的编号偏差

第一次测试时,我用的是简单的宗地面数据,结果挺好。但换到一份包含大量多部件要素的数据后,问题立刻出现:有些图斑由多个不相连的面部件组成,比如一个地块被一条小路分割成两部分,但属性表里还是一条记录。此时外接矩形中心会偏向面积较大的部件,很可能导致排序结果不符合业务人员的直觉。

应对办法有两个:

第一,在工具运行前,先用“多部件转单部件”工具把要素拆开。但这样会改变数据的要素数量,需要你自己权衡。

第二,工具内部改用“面积加权”的方式计算代表点坐标:把每个部件的中心点按面积比例加权平均。这个实现稍复杂,但对于大部分图斑来说效果更好。我给工具加了一个隐藏参数“代表点方式”,默认是“外接矩形中心+标注点”,如果遇到多部件特别多的情况,可以切换成“面积加权质心”。

我后来在实际项目中通常是先跑一次看看编号结果,如果发现问题,再手动调整个别要素。地图制图里的图斑编号,很多时候不是完全自动化能解决的,尤其是飞地、狭长地块、跨图幅地块。

5.2 重叠要素、包含关系对排序的影响

如果要素之间存在重叠,比如两个图斑部分重叠,那么在原始数据中它们的外接矩形中心可能非常接近。排序后,谁先谁后基本取决于坐标的微小差异,或者OID的顺序,这在业务上往往难以接受。

建议在编号前先做拓扑检查,把重叠、压盖的图斑找出来处理掉。至少要让使用工具的人明白:编号工具只负责排序和编号,不负责数据质量。数据本身要保证每个要素在空间上是“互不重叠、互不包含”的,或者至少在业务上允许重叠但你能接受重叠导致的排序抖动。

有一种特殊情况是“包含关系”:一个图斑完全包含另一个图斑(比如岛状图斑)。按左上至右下的逻辑,被包含的图斑会按它自己的外接矩形中心参与排序,这通常没问题。但如果你的业务要求“大图斑先编号,包含在内的小图斑随后编号”,那就需要额外的层级关系判断,这个逻辑已经超出“左上至右下”的范围,建议改用别的工具或方法。

5.3 字段类型和编号位数不够用

字段类型是个大坑。Shapefile 的 DBF 字段和 File Geodatabase 的字段类型不相同。如果你把编号字段定为TEXT,某些数据源下可能会被截断。比如你要输出“A001”这种带前缀的编号,Shapefile 的文本字段长度需要提前设置足够;而 GeoJSON 或者一些临时图层对字段类型支持更弱。

我的建议:如果编号只是数字,直接设为LONG类型,最稳妥;如果需要补零或要带前缀,使用TEXT类型,并确保字段长度足够。工具里我默认用 LONG,补零时再把字段改成 TEXT。不过 ArcGIS 中修改已有字段类型比较麻烦,通常需要新建字段、计算、删除旧字段。所以最简单的方式是:在工具参数里让用户自己选择“是否为文本编号”,如果勾选,工具自动新建一个文本字段。

另外,如果数据量很大,超过 9999 个要素,而你设定了编号位数3,那么编号会变成“1000”而非“001”,因为补零格式是从右边不足位数才补零,位数不够时不会截断。这里要提前提醒用户:编号位数只是用于格式化,不限制上限。

5.4 大数据量要素类的执行效率

虽然前面说了10万级数据基本没问题,但有一个性能瓶颈值得一提:arcpy.da.UpdateCursor在写回时是按行逐个updateRow,这个操作比SearchCursor慢很多,尤其是字段上建有索引时。实测中,如果要素类有大量属性字段、有空字符串、有子类型,写回速度可能比常规情况慢50%到一倍。

优化思路:

  • 不要在编号字段上建索引(或者写回后再建索引);
  • 关闭自动保存、关闭“编辑器”会话,脚本工具本身不在编辑会话里跑,所以没这个问题;
  • 如果数据是企业级地理数据库(如 SQL Server),写回前先关闭数据库连接、排除其他用户占用;
  • 如果条件允许,可以改用arcpy.management.CalculateField配合代码块来实现编号,但那样代码会绕一些。

我个人更推荐用内存中的Python字典做全量映射,再一次性写回,这也是我上面的实现方式。这样写回逻辑非常集中,容易排错。

6. 工具还能怎么扩展

6.1 支持从上到下为主、从左到右为辅

在某些业务里,编号顺序是“先列后行”,也就是从上到下排完一列,再往右排下一列。这种“蛇形列优先”和“行优先”的差异,只要把排序键从(-y, x, fid)改成(x, -y, fid)就能实现。所以我工具里的“排序模式”参数其实已经覆盖了这两种基础模式。如果你需要“S形蛇形回行”,那需要额外的奇偶列翻转逻辑,代码也很简单:每换一组时反转遍历顺序。

6.2 按属性值自定义分组顺序

分组字段如果是“一组、二组、三组”这种带中文序号的文本,直接用字典排序会变成“三组”排在“二组”后面(按拼音或Unicode排序)。这时候可以在工具里增加一个“排序优先序”参数,让用户传入一个逗号分隔的字符串,比如“一组,二组,三组”,代码中按这个顺序遍历分组。

我甚至建议,如果这种需求频繁出现,可以做一个独立的小工具“按指定顺序分组编号”,和“左上至右下分组编号”分开。这样每个工具职责单一,参数面板也不会太复杂。工具越简单,别人越愿意用。

6.3 结合制图标注、Excel报表输出

编号完成后,很多时候还要在图上标注。ArcGIS 的“标注表达式”可以引用编号字段直接显示,但如果你需要把编号变成带前缀的字符串,比如“DZ-001”,最好在编号字段之外再建一个显示字段,用字段计算器拼接。或者直接用标注表达式写Python代码块:

def FindLabel([SEQ_NUM]): return "DZ-{}".format([SEQ_NUM])

注意,在 ArcGIS Pro 的标注表达式里,format可用,但要用!SEQ_NUM!而不是[SEQ_NUM],这里只是示例。

如果你需要把编号结果输出成 Excel 表,可以在工具末尾加一段代码,用arcpy.conversion.TableToExcel把属性表转成 xlsx。这样外业人员拿着Excel就能对图找号。这里我提醒一句:TableToExcel 在 ArcGIS Pro 中依赖开放 XML 库,直接可用;在 ArcMap 10.x 中需要安装 Microsoft Access Database Engine,否则 xlsx 格式可能无法导出。

最后再说一个体会:这类编号工具看起来简单,但它背后是一个完整的“数据处理思维”——先想清楚业务规则的约束、再拆解几何计算、最后用代码固化。真正要给一线业务人员使用,工具界面一定要傻瓜化,参数提示要写清楚。我就见过不少同事,拿到脚本工具后因为不知道“排序模式”是什么意思而不敢用,最后宁可手动在Excel里排序再连回属性表。其实只要你在参数描述里多写一句“Y降序X升序:先按从上到下排,同一行再按从左到右排”,他们一下就懂了。

如果你按这篇文章的方法做好了这个工具,建议再配套做一个“批量编号”版本,把多个要素类放在一个工作空间里,循环调用。我当初在确权项目里就是靠这个批量版本,一口气把全镇十几个村的图斑一次性编号,省下的时间足够我再写好几个工具了。

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

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

立即咨询