ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

arcpy批处理:坐标系获取转换、渔网创建、位置选择与字段赋值

arcpy批处理:坐标系获取转换、渔网创建、位置选择与字段赋值

做GIS数据处理的人,应该都经历过这种场景:拿到一批矢量文件,坐标系七零八落,有的投影、有的经纬度、有的干脆连定义都没有。要统一坐标系,要在研究区打渔网,要按位置关系把要素挑出来,还得给挑出来的结果补字段。这四件事单拎出来都不难,但放在批处理里,每一步都能把人卡一下。这篇文章就把这四个需求用 arcpy 串成一个完整流程,讲清楚坐标系获取与更改、渔网创建、按位置选择导出、字段赋值的原理、参数写法和实际操作中的坑,适合做数据处理、空间分析、批量出图的同学直接参考。

1. 需求拆解与整体设计思路

1.1 一个典型的空间数据处理场景

举个最常见的场景。某区域调研项目要求把一个矢量边界切成规则的公里格网,再从格网和点位数据里挑出落在某个范围内的要素,最后给挑出来的要素标记面积等级。这个流程里,数据源往往来自不同单位和不同时期:有的用WGS84经纬度,有的用地方自定义投影,有的甚至没有写入坐标系定义。如果直接拿原始数据去建渔网、做空间选择,结果基本是错的——渔网的"2公里网格"在经纬度坐标下会变成2度网格,位置选择也会因为坐标系不一致而选不出任何要素。

这类场景最怕的就是数据源混乱。有的shp坐标系定义齐全,有的数据自己定义了投影但没有写进prj文件,有的干脆是矢量化出来的连空间参考都没有。批处理脚本的意义就在于,不管数据来源多乱,一套代码能自动把这些情况识别并处理掉,而不是每个文件都手动检查一遍属性。尤其文件数量到几十个的时候,手动操作的时间成本会直接失控。

1.2 为什么选 arcpy 而不是其他方案

有人会问,QGIS的Python控制台、GDAL的ogr,甚至PostGIS也能做这些事,为什么偏偏选arcpy?我的理由很实际:第一,arcpy直接对接ArcGIS的要素类、图层选择集、字段类型体系,和ArcMap或ArcGIS Pro里的操作完全一致,不需要额外处理数据格式转换;第二,坐标投影、渔网创建、位置选择这几个功能在ArcGIS里都是封装好的,调接口就行,稳定性有保障;第三,如果项目后续还要出图、制版、挂地理处理服务,arcpy写的东西能直接放进模型构建器或脚本工具里,衔接成本最低。

当然arcpy也有短板:它必须安装在带ArcGIS环境的本机上,许可授权不足时部分工具会受限,大数据量场景性能也不如PostGIS。但针对中小规模的矢量批处理,它仍然是最顺手的工具。还有一个隐性好处:arcpy和模型构建器可以互相转换,你可以在模型里先搭流程,导出Python脚本再定制,也可以反过来用脚本更新模型参数,调试和交付都灵活。

1.3 为什么要强调处理顺序

这里有个非常容易被忽略的原则:空间分析前必须保证坐标系一致。渔网创建的本质是在指定坐标单位下生成规则格网,如果在经纬度坐标系里让单元格宽高填"2000"(以为是2000米),实际生成的是2000度,网格会大到离谱。按位置选择也同理,INTERSECT这类空间关系虽然能自动做动态投影,但坐标系定义不一致时结果可能出现偏差甚至空选择。

所以我的处理顺序固定是:

  1. 用Describe读取源数据坐标系,判断是否为空、是否正确。
  2. 按目标坐标系做Project投影或DefineProjection修正。
  3. 基于投影后的数据范围创建渔网。
  4. 在一致坐标系下做按位置选择并导出。
  5. 对导出结果做字段赋值。

后面几个章节就会按这个顺序依次展开,每一步都会把参数和原理讲清楚。

2. 环境准备与基础配置

2.1 Python 与 ArcGIS 版本对应关系

arcpy的版本差异是新手掉坑的重灾区。ArcGIS Desktop 10.x系列内置的是Python 2.7,代码里表达式类型要用PYTHON_9.3,字段表达式里!字段名!的写法没有变化,但字符串编码要小心,中文路径和中文输出最好在文件头加# -*- coding: utf-8 -*-,必要时还要给print做编码处理。ArcGIS Pro用的则是Python 3.x,表达式类型改成PYTHON3,代码风格更现代。

写脚本之前先确认版本,不然写好的表达式类型在另一套环境下会直接报"ERROR 999999:调用函数失败"这种模糊错误。判断方法很简单:在Python交互环境里输入import arcpy; print(arcpy.GetInstallInfo()['Version']),就能看到当前引擎版本,再对照所用ArcGIS版本确定表达式类型。另外,ArcGIS Pro的arcpy.da模块已经很成熟,遍历游标、批量更新都推荐走这个模块,后面字段赋值部分会细讲。

2.2 工作空间与输出控制

脚本开头我习惯固定做三件事:

import arcpy arcpy.env.workspace = r"D:\work\data" arcpy.env.overwriteOutput = True arcpy.env.outputCoordinateSystem = arcpy.SpatialReference(32650)

workspace决定所有相对路径的根目录,省得每个输出都写一长串绝对路径。overwriteOutput = True必须开,否则同名输出文件第二次运行时直接报错。outputCoordinateSystem是可选设置:设了之后,大多数输出工具会带着这个坐标系写数据,相当于给输出结果上了一道保险。但要注意,CreateFishnet有自己的坐标系来源逻辑,由origin坐标字符串和模板要素类决定,这个环境变量不一定生效,后面专门讲。

还有一个中文环境下特别容易出现的问题是Python 2的print乱码。最简单的处理是把标准输出重编码:

import sys, io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

但这套代码在ArcGIS Pro(Python3)里就没有必要,注意区分。

2.3 in_memory 与临时数据管理

脚本里经常需要中间数据,我会优先写到内存工作空间:

arcpy.env.workspace = "in_memory" arcpy.MakeFeatureLayer_management(fc, "lyr")

in_memory的数据结构不落盘,读写速度比临时shp快不少,脚本退出后自动释放。但它有个坑:某些工具(包括CreateFishnet)对in_memory支持不好,而且内存数据在目录树里看不到,调试时不直观。稳妥做法是:需要人工检查的中间结果写到磁盘,纯过路的数据放in_memory。这个习惯能有效减少临时文件垃圾,也方便排查报错。

另外,批处理前一定先备份原始数据。Project是生成新文件相对安全,但DefineProjection和CalculateField是原地修改,一旦写错,源数据的坐标系定义或字段值就回不去了。我习惯把源数据复制一份到backup文件夹再开跑,这不是胆小,是吃过亏之后的教训。

3. 坐标系获取与更改:先看清它是什么,再决定改法

3.1 用 Describe 获取坐标系信息

拿到一个矢量文件,第一件事不是改坐标系,而是先搞清楚它上面记录了什么样的坐标系。arcpy里获取坐标系信息最直接的办法是arcpy.Describe:

fc = r"D:\work\data\polygon.shp" desc = arcpy.Describe(fc) sr = desc.spatialReference print("名称:", sr.name) print("类型:", sr.type) # Geographic / Projected print("工厂代码:", sr.factoryCode) # WKID,如 4326、32650 print("投影坐标系统:", sr.PCSName) print("地理坐标系统:", sr.GCSName)

这里有个关键点:sr.factoryCode为0或sr.name显示"Unknown"时,说明该数据的坐标系定义为空或无效;如果sr.name是"GCS_Unknown"这种,则说明只有一个不完整的地理坐标系外壳,没有实质定义。这时候不要急着投影,先把定义问题解决。

还有个经验:arcpy.Describe对shp、gdb要素类、图层都适用,但对没有空间参考的纯表格会返回sr = None,调用sr.name会报AttributeError。所以代码里最好加一层判断:

if sr is None: print("该数据没有空间参考信息") sys.exit(0)

3.2 Project 和 DefineProjection 的本质区别

这是最容易混淆的一对工具。它们都跟坐标系有关,但作用完全不同:

工具是否改变坐标值适用场景
DefineProjection_management否,只改写元数据中的坐标系定义数据实际坐标值正确,但定义缺失或定义错误
Project_management是,重新计算坐标值数据本身有正确定义,需要转换到另一个坐标系

举个例子:某矢量数据坐标值范围是X在500000左右、Y在4400000左右,肉眼一看就是米制投影坐标,但属性里写的是WGS84经纬度,这就是典型的"定义错误"。这时候用DefineProjection把定义修正为正确的投影坐标系即可,坐标值不用动。反之,如果数据确实定义在WGS84经纬度,但你要把它转到地方投影坐标系再建渔网,就必须用Project,因为坐标值本身要从角度单位变成距离单位。

判断用哪种的辅助手段是看extent:

ext = desc.extent print(ext.XMin, ext.YMin, ext.XMax, ext.YMax)

如果extent数值在几十到一两百之间波动,大概率是经纬度;如果在几十万到几百万之间,基本是投影坐标。把这个数值范围跟sr.name对照一下,就能判断定义是否可信。

3.3 坐标系定义异常时的处理与实操

实操中分三种情况处理。

情况一:坐标系定义为空。需要根据数据实际坐标范围推测坐标系。比如某shp的X在120到122、Y在30到32,经纬度特征明显,先给一个通用定义:

arcpy.DefineProjection_management(fc, arcpy.SpatialReference(4326))

4326是WGS84经纬度的WKID,也是目前最常见的通用坐标代码。如果后续需要转到目标投影坐标系,再执行Project。

情况二:定义与坐标值不匹配。假设数据范围显示为500000左右,但定义为WGS84,先确认正确的投影坐标系(比如项目要求的某种投影),再用DefineProjection覆盖:

sr_correct = arcpy.SpatialReference(32650) # 替换为实际正确的WKID arcpy.DefineProjection_management(fc, sr_correct)

所有空间参考对象都可以通过WKID或字符串名称构造,推荐优先用WKID,比字符串名称稳定,遇到坐标系名称拼写问题也没关系。

情况三:需要跨坐标系转换。这种我多数用Project,并且注意显式指定地理变换。如果你的源坐标是WGS84,目标是某个地方投影坐标系,跨椭球转换需要提供变换方法:

arcpy.Project_management(in_fc, out_fc, target_sr, "WGS_1984_To_...")

不同变换方法的名字在不同版本里略有差异,可以通过arcpy.ListTransformMethods查看可用列表。精度要求不高的场景可以让ArcGIS自行处理,但测绘、工程类数据建议显式指定,避免转换参数不同带来的数十米偏差。

用一句话总结:DefineProjection是改标签,Project是改数值;标签错了用Define,数值需要变用Project。

4. 渔网创建:参数逻辑与范围控制

4.1 CreateFishnet 的参数结构

CreateFishnet_management是生成规则格网的利器,参数看着多,记住核心几组就行。完整签名是:

arcpy.CreateFishnet_management( out_feature_class, # 输出渔网 origin_coord, # 左下角起点,字符串 "x y" y_axis_coord, # 上方参考点,决定旋转角度 cell_width, # 单元格宽 cell_height, # 单元格高 number_rows, # 行数 number_columns, # 列数 corner_coord, # 右上角范围,字符串 "x y" labels, # NO_LABELS / LABELS template, # 模板要素类 geometry_type # POLYGON / POLYLINE / POINT )

绝大多数情况下,我们要的是"在某个范围内生成指定大小的格子"。最实用的组合是:origin_coord取数据extent左下角,corner_coord取extent右上角,cell_width和cell_height填想要的格网尺寸,number_rows和number_columns传0。工具会自动根据范围计算行列数,把渔网铺满整个数据范围。

这里有个关键规则:当指定了corner_coord时,number_rows和number_columns会被忽略;反过来,当指定了number_rows和number_columns时,corner_coord会被忽略。两个组别别同时传有意义的数值,否则得到的网格数量可能完全超出预期。

4.2 三种创建方式的取舍

我实际用过三种方式,适用场景不一样。

方式一:按单元格宽高加范围。最常用,比如"把研究区切成2km乘2km格网"。代码里cell_width和cell_height用数字或"2000"字符串都可以,注意单位与当前数据坐标系一致。如果数据是投影坐标系,单位就是米,这也是为什么强调渔网创建前必须先投影。

方式二:按行列数加范围。适合"把数据分成10行8列"这种需求。这时cell_width和cell_height设为0,number_rows填10,number_columns填8。工具会把范围按行列平均分割,格子大小自动计算。

方式三:只给原点加单元格宽高,不给右上角。工具从原点开始按指定宽高生成网格,行数和列数由number_rows/number_columns参数决定,适合不知道数据边界、只想要一块规则延伸格网的少数场景。

从实际操作看,方式一最符合主流需求。创建时建议把template参数传成源数据要素类,这样渔网的坐标系、范围默认参考模板,减少很多设置上的麻烦。

4.3 旋转渔网的一个隐藏技巧

有时候需要生成旋转一定角度的渔网,比如沿着地块长轴方向布设采样格网。CreateFishnet表面没有旋转参数,但旋转的核心藏在y_axis_coord里。

正常情况下,如果不旋转,y_axis_coord取原点正上方的一个点即可。比如origin是"(1000, 2000)",y_axis就写"(1000, 2100)"——x坐标不变,y坐标比原点大。这个参数的本质是定义坐标系中"正北"方向的参考点。

如果你希望渔网旋转30度,就让y_axis_coord落在从原点出发、与正北方向夹角30度的一个点上。以origin为圆心,R为任意长度:

import math angle = math.radians(30) y_axis = "{} {}".format( ext.XMin + R * math.sin(angle), ext.YMin + R * math.cos(angle) )

这里的R取一个足够大的值(如1000),把方向表达出来即可。y_axis_coord离原点越远,方向越精确。改了这个点,整个渔网的长边方向就会跟着转,这是很多教程不讲的细节。

另外,如果labels参数选择LABELS,工具会额外生成一个以输出名加_labels后缀的点要素类,里面是每个格网的中心点。做随机采样布点时这个点文件非常有用,我通常把它保留下来。

5. 按位置选择导出:图层的正确用法

5.1 为什么不能直接对要素类做位置选择

很多新手直接把shp路径传给SelectLayerByLocation_management,结果报错"未找到图层"。原因很简单:这个工具操作的对象是要素图层,而不是磁盘上的要素类。要素图层是要素类在内存中的视图,可以携带选择集、定义查询、符号系统等状态,位置选择本质上是在这个视图里筛选要素。

正确流程是先用MakeFeatureLayer_management建图层,再做位置选择,最后导出:

arcpy.MakeFeatureLayer_management(in_fc, "in_layer") arcpy.SelectLayerByLocation_management( "in_layer", # 图层名 "INTERSECT", # 空间关系 select_fc, # 参考要素 "", # 搜索距离,留空表示不需要 "NEW_SELECTION" # 选择方式 ) arcpy.Select_analysis("in_layer", out_fc)

注意"in_layer"这个名字只在当前脚本会话里有效,每次运行新建图层,不用手动清理,脚本结束自动销毁。如果在ArcMap里手动操作,选择集只会高亮;用脚本导出时,必须把选择集落成实体数据,Select_analysis干的就是这个事。

5.2 空间关系与搜索距离

SelectLayerByLocation的空间关系选项不少,常用的有这几个:

  • INTERSECT:目标要素与参考要素相交,包括边界接触,最常用。
  • WITHIN_A_DISTANCE:目标要素在参考要素的指定距离范围内,适合做缓冲区分析。
  • CONTAINS:目标要素完全包含参考要素。
  • AREAL_WITHIN_A_DISTANCE:面状目标要素在参考要素指定距离内。
  • BOUNDARY_TOUCHES:两者的边界刚好接触。

search_distance参数要跟单位一起写,比如"500 Meters"。如果不写单位直接给数字,单位默认是数据坐标系单位。这里有一个易错点:如果目标图层和参考要素坐标系不一致,ArcGIS会做动态投影完成计算,但结果可能受容差影响,建议提前统一坐标系,这也是前面反复强调顺序的原因。

选择方式NEW_SELECTION每次都重新选择;如果之前已经选了一批要素,想在这个基础上继续筛,可以用SUBSET_SELECTION。比如先用属性条件选出A类要素,再用空间关系从A里筛出落在目标范围之内的部分,两步结合非常实用。

5.3 导出工具的对比

导出选中要素主要有两个工具:

arcpy.Select_analysis("in_layer", out_fc) # 推荐 arcpy.CopyFeatures_management("in_layer", out_fc) # 备选

Select_analysis语义上就是"从图层中提取选中要素",行为最明确。CopyFeatures_management也能复制图层中的选中要素,但它的本职是复制要素类,当图层没有选择集时会复制全部要素,容易误导。所以涉及选择集导出时,我统一用Select_analysis。

还有个小细节:Select_analysis输出要素类会保留源数据的全部字段。如果只想导出部分字段,需要在前面建立含目标字段的图层,或者导出后再做字段删除。这个放到"字段赋值"部分一起说更顺。

6. 字段赋值:从简单计算到条件逻辑

6.1 字段存在性检查与动态添加

给导出结果补字段是收尾阶段常见的需求,比如添加"面积(km²)""等级""所在网格编号"。直接CalculateField前,要先确保字段存在:

field_names = [f.name for f in arcpy.ListFields(out_fc)] if "area_km2" not in field_names: arcpy.AddField_management( out_fc, "area_km2", "DOUBLE", field_alias="面积(平方公里)" )

字段类型上,面积用DOUBLE,等级这类文本用TEXT,整数代码用SHORT或LONG。AddField的默认文本长度是50,如果等级值较长要显式传field_length。这里有个小坑:shp文件字段名长度限制为10个字符,字段名尽量用短英文,中文别名随便写,但字段名最好别用中文,避免编码问题。

6.2 CalculateField 表达式的正确姿势

CalculateField_management的三个核心参数是表名、字段名、表达式。表达式类型决定了语法,这里最容易出问题。

在ArcGIS Desktop的Python 2环境用PYTHON_9.3,在ArcGIS Pro的Python 3环境用PYTHON3。表达式里的字段引用是!字段名!,如果要引用几何属性,可以直接写:

arcpy.CalculateField_management( out_fc, "area_km2", "!shape.area@squarekilometers!", "PYTHON_9.3" # ArcGIS Pro 请改为 PYTHON3 )

shape.area@squarekilometers这种带面积单位的属性访问是内置的,类似的还有!shape.length!、!shape.centroid.X!、!shape.extent.XMin!等,非常方便。单位后缀支持squaremeters、squarekilometers、hectares、acres等。

稍微复杂的逻辑可以用codeblock代码块。比如按面积分级:

code = """ def level_by_area(area): if area > 100: return "L1" elif area > 10: return "L2" else: return "L3" """ arcpy.CalculateField_management( out_fc, "level", "level_by_area(!shape.area@squarekilometers!)", "PYTHON_9.3", code )

注意codeblock里定义函数,表达式只调用函数名,两者要严格对应。Python 2环境下codeblock里尽量别写中文注释,避免编码问题;非要写,确保文件头是utf-8,并且字段赋值结果本身是ASCII。

6.3 UpdateCursor 与 CalculateField 的取舍

CalculateField适合一列数据、一个规则、批量计算;但如果逻辑复杂到需要多字段联合判断、逐要素做空间计算,我会换arcpy.da.UpdateCursor:

with arcpy.da.UpdateCursor(out_fc, ["area_km2", "level"]) as cursor: for row in cursor: if row[0] > 100: row[1] = "L1" elif row[0] > 10: row[1] = "L2" else: row[1] = "L3" cursor.updateRow(row)

游标的优势在于可以写任意Python逻辑,一行一更新,不存在表达式语法限制;劣势是逐行操作性能不如数据库级的CalculateField。大数据量时优先用CalculateField;逻辑特别复杂或需要调用第三方库时,用UpdateCursor。还有个经验:with写法会自动关闭游标,避免文件锁定;用完一定记得释放,否则后续工具会报"文件被占用"。

7. 脚本串联与完整代码

7.1 完整代码(带注释)

把前面四个功能按顺序串起来,就是一个可以直接跑通的批处理脚本:

# -*- coding: utf-8 -*- import arcpy import math import sys import time # ---------- 基础环境 ---------- arcpy.env.overwriteOutput = True arcpy.env.workspace = r"D:\work\data" # 改成自己的工作目录 in_fc = "buildings.shp" # 源矢量 target_sr = arcpy.SpatialReference(32650) # 目标坐标系:WGS1984 UTM 50N fishnet_fc = "fishnet.shp" # 渔网输出 ref_fc = "sampling_points.shp" # 位置选择参考要素 out_fc = "selected_buildings.shp" # 位置选择导出结果 cell_size = 2000 # 渔网单元格大小(米) t0 = time.time() # ---------- 1. 坐标系获取与判断 ---------- desc = arcpy.Describe(in_fc) sr = desc.spatialReference if sr is None: print("源数据没有坐标系定义,退出") sys.exit(0) print("原始坐标系:", sr.name, "| WKID:", sr.factoryCode) # ---------- 2. 坐标系更改(投影) ---------- if sr.factoryCode != target_sr.factoryCode: prj_fc = "buildings_proj.shp" arcpy.Project_management(in_fc, prj_fc, target_sr) else: prj_fc = in_fc # ---------- 3. 渔网创建 ---------- ext = arcpy.Describe(prj_fc).extent origin = "{} {}".format(ext.XMin, ext.YMin) y_axis = "{} {}".format(ext.XMin, ext.YMin + 1000) # 不旋转 corner = "{} {}".format(ext.XMax, ext.YMax) arcpy.CreateFishnet_management( fishnet_fc, origin, y_axis, cell_size, cell_size, 0, 0, corner, "LABELS", prj_fc, "POLYGON" ) print("渔网已创建:", fishnet_fc) # ---------- 4. 按位置选择导出 ---------- arcpy.MakeFeatureLayer_management(prj_fc, "prj_layer") arcpy.SelectLayerByLocation_management( "prj_layer", "INTERSECT", ref_fc, "", "NEW_SELECTION" ) arcpy.Select_analysis("prj_layer", out_fc) count = int(arcpy.GetCount_management(out_fc).getOutput(0)) print("按位置选择导出完成,要素数:", count) # ---------- 5. 字段赋值 ---------- field_names = [f.name for f in arcpy.ListFields(out_fc)] if "area_km2" not in field_names: arcpy.AddField_management(out_fc, "area_km2", "DOUBLE", field_alias="面积平方公里") arcpy.CalculateField_management( out_fc, "area_km2", "!shape.area@squarekilometers!", "PYTHON_9.3" # ArcGIS Pro 环境改成 PYTHON3 ) print("字段赋值完成,总耗时:%.2f 秒" % (time.time() - t0))

这段代码基本覆盖了前面讲的所有内容。下载到自己机器上跑之前,把工作目录、文件名、坐标系WKID改成实际数据情况。

7.2 执行顺序的把控

脚本里我刻意把字段赋值放到最后,因为位置选择导出的结果才是最终要标记的数据。如果你还需要给渔网赋属性(比如给每个格网编号),可以在第3步之后紧接着对渔网执行AddField和CalculateField,再拿渔网做空间关联。这个顺序没有绝对标准,但记住一条:所有空间分析必须在坐标系统一之后进行,所有字段补充尽量在最终数据确定后一次性做。

7.3 扩展建议

这个脚本的本质是四个地理处理工具的批处理封装。可以很自然地扩展:

  • 把源数据路径、目标坐标系、单元格大小做成脚本工具参数,用arcpy.GetParameterAsText读取,这样在工具箱里就能通过界面调用。
  • 循环遍历文件夹下的所有shp,逐一执行统一坐标系、建渔网、导出的流程,实现真正的批量处理。
  • 把渔网创建部分的LABELS输出点要素用于后续随机布点,再接一个CreateRandomPoints_management之类的工具,形成采样布点流水线。
  • 把字段更新规则放到外部配置文件里管理,改规则时不用动代码。

8. 常见问题与排查技巧实录

8.1 高频问题速查表

我把实际跑脚本时遇到的高频问题整理成一张表,按现象排查会快很多:

现象可能原因解决思路
sr.name输出为Unknown或None源数据没有坐标系定义根据extent范围推断并DefineProjection
Project_management报错999999源数据没有定义坐标系,无法投影先DefineProjection再Project
渔网格网超出数据范围范围宽高不是格网大小的整数倍,工具自动向上取整创建后对渔网执行Clip_analysis裁剪到数据范围
CreateFishnet报"行列数无效"number_rows/columns传了0但没有传corner_coord补上corner_coord参数,让范围确定
位置选择结果为空两个图层坐标系不一致,或空间关系选错统一坐标系后重跑,检查INTERSECT是否符合预期
SelectLayerByLocation报"图层不存在"直接传了shp路径而不是FeatureLayer先执行MakeFeatureLayer_management
字段计算表达式无效表达式类型与Python版本不匹配Desktop用PYTHON_9.3,Pro用PYTHON3
字段值全为0或Null几何属性表达式写错,或源要素几何为空打印!shape.area!验证,检查源几何
shp字段名超10字符shp格式天然限制用gdb要素类或缩短字段名
输出结果在ArcMap里看不到没刷新目录,或overwriteOutput为False设置overwriteOutput=True,目录里刷新

8.2 排查思路

遇到报错,我的排查顺序是固定的:先看工具名和错误码,再到帮助文档查参数类型,然后用一条条print把中间结果打出来。

举例说明。有一次脚本卡在渔网创建,报错ERROR 000210。我先打印了ext.XMin、ext.YMin这些值,发现extent对象正常,但origin字符串里浮点数带了大量小数位,某些工具对极长的小数解析不稳定。后来用round把坐标值统一到3位小数,问题消失。这种问题不打印中间值根本发现不了。

同理,坐标系问题排查时,一定要同时打印sr.name和extent范围。我见过太多"坐标系明明是WGS84,但extent在几十万量级"的怪数据——十有八九是数据被人为移动过,或者定义写错。直接用Project会越转越错,必须先人工确认。

8.3 性能优化与批处理经验

小的批处理脚本性能不是最高优先级,但几条经验可以让跑批过程少受罪:

  1. 尽量避免在循环里用arcpy.Describe重复取信息。循环100个shp时,每个都Describe一次很慢;提前取好元数据存成字典,循环里直接查。
  2. 优先用in_memory缓存中间结果,尤其是Project和Select_analysis这类磁盘IO密集的工具,缓存到内存能快很多。
  3. 用arcpy.env.extent限制分析范围。当只需要处理研究区范围内的数据时,设置环境extent可以先裁剪再计算,减少不必要的计算量。
  4. 批处理脚本要有日志输出。简单就用print,复杂就写logging到文本文件。跑批时每处理完一个要素类打印一条记录,方便半夜出问题时定位到具体是哪个文件。
  5. 给大文件跑之前先测小样本。复制一小块范围的数据试跑通,再上全量,避免所有坑集中爆发。

关于arcpy.env.extent,一行就可以设置:

arcpy.env.extent = "200000 4400000 500000 4600000"

也可以直接传要素类路径取它的extent作为范围。不过渔网创建不建议只靠env.extent控制,因为CreateFishnet的范围还是以origin和corner为准,env.extent主要影响其他分析工具的默认处理范围,这一点和前面讲的参数优先规则一致。

最后再分享一个我自己的小习惯。这几个工具单独用都不难,难的是组合时数据流和选择状态的传递。我之前吃过一次亏:脚本里先做了一个位置选择,接着又做了一个属性选择,结果第二次选择把第一次的选择覆盖了,导出数据白跑一趟。后来我养成了规矩——每一步操作前打印当前选择集数量,arcpy.GetCount_management("in_layer")只比print多一行,但能避免很多"看起来没错、结果就是不对"的诡异问题。

另一个建议是:脚本里凡是修改源数据的操作,都习惯性地先打印一条备份路径。我在某次处理地形数据跑字段计算时,就因为少写了一句,把标了半天的属性值覆盖了,最后只能从备份恢复。批量处理不只是写代码,更重要的是把流程设计得像流水线一样可回溯。这套基于arcpy的坐标系处理、渔网创建、位置选择和字段赋值脚本,我一直在用,也算是最省心的组合方式了,希望你在自己的批处理项目里也能少踩几个坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进