ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

ArcGIS Python脚本自动化:批处理、游标与工具封装避坑

ArcGIS Python脚本自动化:批处理、游标与工具封装避坑

简介:面向ArcGIS初学者与地理信息从业者的入门文档,围绕如何在ArcGIS环境中用Python脚本替代重复手工操作,降低地理处理自动化的上手门槛。资源包仅含1个docx文件,约490KB,体量轻便,适合通勤或课间快速通读。内容按两条主线展开:前半部分梳理Python数学运算符、字符串操作、模块导入、def函数、if/while/for流程控制及简单输入输出;后半部分进入ArcGIS集成,讲解Geoprocessor对象的创建与帮助查询、Lists参数组织、Toolbox与别名调用、ModelBuilder中嵌入脚本、PythonWin断点调试,以及Describe、Exists和地图代数在脚本中的用法。目录由浅入深,示例与工具说明穿插,便于对照练习并逐步搭建自己的处理脚本。目前已有1492人学习,适合零基础GIS用户和需要补强脚本能力的学生作为入门参考。

1. 从点到脚本:ArcGIS 里的 Python 脚本到底解决什么问题

手上有一百个 shp 要重投影、要裁剪到同一个研究区、要按同一套规则算面积,鼠标点一遍两小时,第二天换个目录还得再点一遍——这是绝大多数人开始学 ArcGIS 中 Python 脚本的真实起点,不是想学编程,是不想再点鼠标。这份资料的价值在于它没有一上来就丢公式,而是先把 Python 的运算符、字符串、模块、函数、流程控制过一遍,再落到地理处理对象、工具别名、Lists/Describe、游标和地图代数,是一条从语法到真干活的完整链路。适合刚摸 ArcGIS 脚本、手头压着一堆重复处理任务的人;也适合写过几段脚本、但总在环境、别名、参数转换上翻车的人,前六节可以快速跳读,后四节值得逐字看。

2. 先把 Python 地基打对:ArcGIS 脚本里最容易翻车的六个语法点

这一章看着像语法复习,其实是后面所有报错的源头。我见过太多脚本,逻辑全对,最后死在5/2等于 2、死在路径里的反斜杠、死在sys.argv进来的字符串没转浮点。下面六个点按原料的顺序走,每个点我都补了在 GIS 场景里真正会咬人的那一面。代码示例沿用老版本的 print 写法(不带括号),挪到新解释器里要补括号,这一点先记住。

2.1 数学运算符:整型和浮点型的隐性差异

Python 的常用运算符就那几个:+ - * / ** %。规则只有一条,但极其要命——参与运算的只要有一个浮点型,结果就是浮点型;全是整型,结果才是整型。这条规则放到 GIS 里就是灾难:算坡度时用垂直距离除以水平距离,两边都是整数米,结果直接归零,你还以为是数据坏了。

输入表达式结果说明
2+35整型结果
2.+35.0有一个是浮点型,结果就是浮点型
5/22两边都是整型,小数被直接截掉
5./22.5改成浮点型才保留小数
5%21取模,除完的余数
270+180再%36090方位角加 180 逆转方向后做角度归一
5**225幂运算

取模那个例子值得单独说。方位角做 180 度翻转,加完可能超过 360,用%360一步就归一到 0 到 359 之间,不用写 if 判断。这是脚本里处理角度、经度环绕最省事的写法。

math模块补的是运算符给不了的东西:log10、log、pi、sin/cos/tan、hypot、sqrt。注意三角函数吃的是弧度,不是角度,这是新手第一个必踩的坑。

import math # 三角函数要弧度制,先把角度换算过去 degrad = math.pi / 180 print math.sin(45 * degrad) # 0.707106781187 print math.cos(90 * degrad) # 1.0 附近,浮点误差忽略 # 两点间距离,三种写法结果一致 x1, y1 = 520382, 4152373 x2, y2 = 520475, 4152963 xr, yr = x2 - x1, y2 - y1 print math.hypot(xr, yr) # 597.28... print math.sqrt(xr**2 + yr**2) # 同上 print (xr*xr + yr*yr) ** 0.5 # 同上,不用 import 也能算

逻辑说明:math.hypot内部做了溢出保护,坐标差值很大时比手写平方和更稳;**0.5开方虽然不用导入模块,但可读性差,工程代码里我一般还是用sqrt。参数上degrad就是pi/180这个常量,sin只接受弧度值,传角度进去结果一定错,且错得很安静——不会报错,只是结果不对,这种最难查。

2.2 字符串:切片、转义和路径拼接

字符串下标从 0 开始,切片是含头不含尾,这两条记住,后面拼路径、截文件名全靠它。GIS 脚本里字符串用得最多的地方不是文本处理,是路径和 SQL 表达式。

s = 'zhulj' print s.capitalize() # Zhulj print s[0] # z,第一个字符 print s[1] # h print s[-1] # j,倒数第一个,等价于 s[len(s)-1] print s[2:4] # ul,从下标 2 取到下标 4 之前 print s[2:] # ulj print s[:5] # zhulj print s * 3 # zhuljzhuljzhulj,重复 print s.upper() # ZHULJ print s.isupper() # False # 路径解析:find 返回下标,split 拆成列表 p = 'd:/work/lu.shp' print p.find('.') # 10 plist = p.split('/') print plist # ['d:', 'work', 'lu.shp'] # 反斜杠是转义符,路径要么加 r,要么写双斜杠 print r'd:\work\soil.shp' # d:\work\soil.shp

逻辑说明:split('/')得到一个列表,plist[-1]就是文件名,把.shp换成.tif一行就够,这是批处理改扩展名最常用的手法。find找不到字符时返回 -1,所以判断路径里有没有某个片段别用if p.find('x'),要为真的话 0 也会被判成假,老老实实写if p.find('x') >= 0。

转义这块是血泪经验。\n是换行、\'是单引号、\\才是反斜杠,Windows 路径不加r前缀,脚本怎么挂的都不知道。我的习惯是路径统一用正斜杠,ArcGIS 认;实在要贴 Windows 原始路径,前面加r。另外写 SQL 表达式时引号会嵌套,比如'"elev" > 1000',外层单引号内层双引号,反了工具就解析不了。

2.3 模块、def 和流程控制:把「选择」写进自动化

模块就是别人的代码库,用之前import,一行 import 可以带好几个,逗号分隔,放在程序顶部。地理处理的核心模块是那一个,加上sys、os、math、random基本够用。

自定义函数用def,冒号结尾、缩进是函数体、return给返回值。下面这组角度弧度互转是 ArcGIS 脚本里复用率最高的两行代码,建议直接抄进自己的工具库。

import math def radians(angdeg): return angdeg * math.pi / 180 def degrees(angrad): return angrad * 180 / math.pi print math.sin(radians(45)) # 0.707106781187 print degrees(math.acos(0.5)) # 60.0

流程控制三件套if / while / for的写法都一样:条件行冒号结尾,下一行缩进。区别在语义——if只走一遍,while条件为真就一直转,for遍历一个序列。GIS 里最典型的if场景是太阳高度角:正午太阳角超过 90 度,说明太阳跑到另一侧去了,高度角和方位角都得翻。

lat = 30 # 研究区纬度,南半球为负 decl = 20 # 太阳倾角,夏至 +23.44,春秋分 0,冬至 -23.44 sunangle = 90 - lat + decl azimuth = 180 if sunangle > 90: sunangle = 180 - sunangle azimuth = 0 print sunangle, azimuth

逻辑说明:sunangle由纬度、太阳倾角推出来,超过 90 就做镜像,方位角从 180 翻到 0。参数lat和decl是这套算法的全部输入,做四季山影批处理时,就是把decl循环成 23.44、0、-23.44 三组值。

while和for更多用在批量遍历上。下面这段扫目录拿 shp 列表,是每个批处理脚本的开头几行。

import os ws = "c:/prog/hmbarea" ilist = os.listdir(ws) # 目录下所有条目 fcs = [] # 空列表,装 shp for i in ilist: if i.endswith(".shp"): fcs.append(i) for fc in fcs: print fc

逻辑说明:os.listdir拿到的是文件名列表,不带路径;endswith做后缀过滤,注意大小写敏感,.SHP不会被.shp匹配到。append是往列表尾部追加,空列表先建好再往里塞,别边遍历边改原列表。

还有一个常被拿来练手的例子,验证正态分布 95% 置信区间:

import random mu, s = 50, 10 z5 = mu - s * 1.96 z95 = mu + s * 1.96 count = 0 n = 100000 for i in range(n): x = random.gauss(mu, s) if x < z5 or x > z95: count = count + 1 print float(count) / n # 每次不同,稳定在 0.05 附近

逻辑说明:random.gauss(mu, s)按均值和标准差出随机数,循环十万次统计落在区间外的比例。注意最后那句float(count) / n,不转浮点的话整型除整型直接得 0,前面 2.1 讲的规则在这儿现世报。这个例子本身不干 GIS 的活,但它把for、if、random、类型转换串在了一起,拿来验环境很合适。

另外os.path.exists配if/elif/else是脚本开头做环境自检的标准动作:

import os.path if os.path.exists("d:/testfolder/test.txt"): print "文件存在" elif os.path.exists("d:/testfolder"): print "文件夹存在,但文件不在了" else: print "两者都不存在"

有个例子很能说明脚本的价值,跟地理处理一点关系没有,但 GIS 人必须知道。USGS 的 7.5 分幅 DEM 是文本格式,水平坐标单位是米,垂直高程单位可能是英尺也可能是米,这个标志位藏在文件头第 539 个字符里,1是英尺、2是米。不知道这件事,拿英尺高程去算坡度,结果差 3.28 倍,看着还挺像那么回事。

import fileinput infile = r"c:\prog\pendata\woodside.dem" firstline = fileinput.input(infile)[0] # 读第一行 unitchar = firstline[539] # 取第 539 个字符 unit = "(unknown: not a 7.5' DEM?)" if unitchar == "1": unit = "feet" if unitchar == "2": unit = "meters" print "\nElevation in " + unit fileinput.close()

逻辑说明:fileinput.input打开文件后按行索引取,[0]就是第一行。参数539是写死的偏移,换数据源一定要先用文本编辑器数一遍再改,别直接套。输出那句拼了换行符和字符串,纯粹为了读起来清楚,实际脚本里用消息接口更好。

2.4 从 sys.argv 到脚本参数:外部输入怎么进来

前面所有例子都是把参数写死在代码里,改一次跑一次。真实自动化必须从外面传参数。最原始的方式是sys.argv,列表里第一个是脚本名,从第二个开始才是参数,且全都是字符串。

import sys lat = float(sys.argv[1]) decl = float(sys.argv[2]) sunangle = 90 - lat + decl azimuth = 180 if sunangle > 90: sunangle = 180 - sunangle azimuth = 0 print "正午太阳角=" + str(sunangle) print "方位角=" + str(azimuth)

逻辑说明:运行脚本时在参数栏里填40 23.44,argv[1]拿到 "40"、argv[2]拿到 "23.44",不float()转换就全是字符串,后面一做减法立刻报类型错误。输出时反过来要str(),数字和字符串不能直接加。

位置含义进来时的类型用法
argv[0]脚本自身路径str一般不用
argv[1]纬度strfloat()后参与运算
argv[2]太阳倾角strfloat()后参与运算

sys.argv在命令行好使,但脚本一旦挂进工具箱就不该用它了,工具参数的取法换成另一套接口,第六章讲。现在只要记住:进来的永远是字符串,用之前先转类型,这一步偷懒,后面全是隐式报错。

3. 地理处理对象与工具别名:脚本调工具的第一道关

语法过关之后,真正开始调工具。所有地理处理脚本开头都干同一件事——拿到一个地理处理对象,后面所有工具、所有环境设置都挂在它身上。这一步没搞对,后面写什么都没意义。

3.1 创建地理处理对象:两种写法的取舍

原料给了两种拿对象的方式,长得像但适用场景完全不同。

# 写法一:随桌面端一起装的模块 import arcgisscripting gp = arcgisscripting.create(9.3) gp.workspace = "c:/Tongass" gp.clip_analysis("standb4", "clipcov", "standb4_clip", "POLY", "1.25")
# 写法二:走 COM 派发 import win32com.client gp = win32com.client.Dispatch("esriGeoprocessing.GpDispatch.1") gp.workspace = "c:/Tongass" gp.clip_analysis("standb4", "clipcov", "standb4_clip", "POLY", "1.25")

逻辑说明:两种写法拿到的是同一个对象,后面所有调用一模一样,差别全在环境依赖上。模块版绑死随桌面端安装的那套解释器,解释器版本不能自己升;COM 版靠系统注册表找组件,可以在别的解释器里跑,但位数必须匹配,32 位和 64 位对不上就直接报组件找不到。

选型上没什么纠结的:脚本最终要当工具挂进工具箱,用模块版,稳、快、不用管注册表;临时在外部环境里验证一段逻辑,才考虑 COM 版。后续版本把入口统一到一个arcpy模块,写法简化成import arcpy然后直接arcpy.Clip_analysis(...),对象那层隐掉了,但workspace、overwriteoutput、Describe这些概念一个没变,学会老写法再看新接口是降维。

注意:不要尝试把随桌面端安装的解释器升级到新版本,一堆模块是针对特定版本编译好的,升完基本等于把环境拆了。

3.2 工具箱和别名:同名工具怎么区分

ArcToolbox 里同一个工具名可能出现在好几个工具箱下。裁剪在 Coverage 那边有一个,在数据管理下的栅格那边也有一个,处理的数据类型不同。图形界面里你点哪个就是哪个,脚本里只有字符串,必须靠别名区分。

别名就是工具箱的短名,已经成了工具全名的一部分,写成工具名_别名。查法很土但有效:工具箱里右键工具看属性。

别名对应工具箱
analysisAnalysis Tools
managementData Management Tools
conversionConversion Tools
3d3D Analyst Tools
saSpatial Analyst Tools
cartographyCartography Tools
statsSpatial Statistics Tools
gaGeostatistical Analyst Tools
arcCoverage Tools
lrLinear Referencing Tools
import arcgisscripting gp = arcgisscripting.create(9.3) gp.workspace = "c:/prog/hmbarea" gp.overwriteoutput = 1 # 1 表示允许覆盖同名输出 # copyfeatures 属于 Data Management gp.copyfeatures_management("streams/arc", "streams.shp") # buffer 属于 Analysis,200 是缓冲距离 gp.buffer_analysis("streams.shp", "stbuff200.shp", 200) # clip 也属于 Analysis,用缓冲结果裁地质线 gp.clip_analysis("geol.shp", "stbuff200.shp", "geolstr200.shp")

逻辑说明:三条命令串成一条链——先把 coverage 转成 shp,再缓冲 200 米,最后拿缓冲面去裁。前三个参数基本都是「输入、参考、输出」的顺序,后面才是容差、几何类型之类的可选参数,顺序以工具属性里列的顺序为准,别凭记忆写。

如果一段脚本里反复调同一个工具箱的工具,可以省掉每次写别名:

gp.Toolbox = "Analysis" gp.Buffer("streams.shp", "stbuff200.shp", 200) gp.Clip("geol.shp", "stbuff200.shp", "geolstr200.shp")

逻辑说明:设了Toolbox之后工具名可以省别名,但只在这一个工具箱里有唯一同名工具时才安全,跨工具箱混用还是老老实实写全名。

3.3 Lists、Describe、Exists:先摸清数据家底再动手

批处理脚本的第一段永远不是处理,是盘点。工作空间里到底有几个要素类、字段叫什么类型、坐标系是什么,这些信息拿不到,后面就是盲写。

ListFeatureClasses按通配符和要素类型筛选,返回一个可迭代的列表:

# 拿工作空间里所有以 w 开头的面要素 fcList = gp.ListFeatureClasses("w*", "polygon")

参数说明:第一个是通配符,*代表任意,不传就是全部;第二个是要素类型,可选点、线、面、注记等,不传也是全部。这个方法的官方签名里两个参数都是可选的。

数据类型的多样性直接影响你该用哪个 List 方法。同一个「要素」,可能是单个 shp、可能是地理数据库里的要素类、也可能是 coverage 里的弧段或多边形。遍历 coverage 要用数据集列表,不能用要素类列表,这是很多人第一次跑批处理发现「一个都没列出来」的原因。

ListFields加Describe是配套动作:一个拿字段清单,一个拿数据本身的属性。

import arcgisscripting gp = arcgisscripting.create(9.3) gp.workspace = "c:/prog/hmbarea" fieldList = gp.ListFields("landuse", "*", "all") dsc = gp.describe("landuse") print "landuse " + dsc.DataType + ":" for field in fieldList: print field.Name, field.Type

逻辑说明:ListFields三个参数是数据名、通配符、字段类型(all表示全部),循环出来的是字段对象,带Name、Type、精度、小数位这些属性。describe返回的对象属性随数据类型变——要素类有ShapeType、SpatialReference、Extent,栅格有MeanCellHeight、Format,共同的是DataType。写脚本时先print dsc.DataType确认拿到的是什么东西,再取具体属性,能省掉一大半「属性不存在」的报错。

Exists判断数据在不在,返回值是布尔。它和os.path.exists不是一回事:前者认地理数据库里的数据集,后者只认文件系统路径。

if gp.exists("c:/prog/hmbarea/landuse"): print "数据集存在" else: print "数据集不存在,后续步骤跳过"

逻辑说明:批处理里必须先用它挡一道,否则中间某一步失败,后面会连锁报一堆看不懂的错。放在循环里就是逐条自检,放在开头就是环境预检。

3.4 工作空间和 overwriteoutput:先把规矩立好

workspace是所有相对路径的基准目录,工具里只写文件名时,就落到它下面去。这一条设错,脚本会安静地把结果写到别的地方,你在输出目录里翻半天找不到文件,还以为工具没执行。

overwriteoutput是覆盖开关,默认是关的。关着的时候第二次跑同一个脚本,输出已存在就报错退出。批处理脚本第一行就把它打开,是常规操作。

gp = arcgisscripting.create(9.3) gp.workspace = "c:/prog/hmbarea" # 相对路径的基准 gp.scratchWorkspace = "c:/prog/temp" # 中间结果落哪儿 gp.overwriteoutput = 1 # 允许覆盖同名输出

逻辑说明:scratchWorkspace管的是工具内部产生的临时数据,别让它默认落在系统临时目录里,磁盘满了排查起来很痛苦。这三个设置建议做成一个初始化函数,每个脚本开头调一次,比散落在各处的赋值好维护。

提示:overwriteoutput打开之后,同名输出会被无声覆盖。批处理里如果输入和输出同名(比如原地重算字段),先备份或者把输出名加后缀,别问我怎么知道的。

4. 数据管理、游标与地图代数:脚本真正干活的地方

前三章是装配,这一章是开火。批处理遍历、属性表逐行读写、栅格表达式计算,三件事覆盖了日常脚本八成的使用场景。

4.1 数据管理工具的批处理骨架

批处理的通用结构就四步:定目录、列清单、循环、输出。下面是骨架,换成投影、裁剪、格式转换都一样套。

import arcgisscripting, os gp = arcgisscripting.create(9.3) gp.overwriteoutput = 1 inws = "c:/prog/hmbarea" outws = "c:/prog/hmbarea_out" # 输出目录不存在就先建 if not os.path.exists(outws): os.makedirs(outws) gp.workspace = inws fcList = gp.ListFeatureClasses("", "polygon") for fc in fcList: outfc = outws + "/" + fc gp.copyfeatures_management(fc, outfc) gp.AddMessage("已处理: " + fc)

逻辑说明:ListFeatureClasses是基于workspace迭代的,循环里拿到的就是纯名字,拼输出路径时手动加上目录。AddMessage每处理一条打一行,跑长任务时这是唯一的进度反馈,不然你不知道卡在第几个。

参数说明:copyfeatures_management只有输入和输出两个必填参数,输出路径带不带扩展名都行,工具会按格式环境补;如果要做重投影,换成投影工具,多传一个坐标系参数,其余不变。

这段骨架有个容易被忽略的点:循环体里不要修改fcList本身,也不要中途改gp.workspace。工作空间是所有相对路径的基准,循环里一改,后半段的输出全落到别处,而且不报错。

4.2 Cursors:逐行读写属性表

工具解决不了的精细活,得靠游标一行一行来。三个游标分工明确:只读用搜索游标,读改都用更新游标,纯插入用插入游标。游标的标准用法是「打开、取第一行、循环处理、取下一行」,直到取到空值为止。

rows = gp.UpdateCursor("landuse.shp") row = rows.Next() while row: # 取值用字段名,赋值用 SetValue area = row.GetValue("Shape_Area") row.SetValue("面积公顷", area / 10000.0) rows.UpdateRow(row) # 改完必须写回,否则白改 row = rows.Next() del rows # 用完释放,否则文件被占住

逻辑说明:Next()返回一行,取完返回None,while row就是靠这个退出。GetValue/SetValue既能传字段名也能传字段索引,字段名可读性好,索引快,字段多的时候用索引。UpdateRow是把改完的行写回数据集,漏了这句,循环里算得再对,文件里一个值都不会变。

del rows这一行别省。游标不释放,数据集会被一直占用,之后想删除或者重命名这个文件,系统直接告诉你被别的程序打开。这是我在批处理脚本上翻过最多次的车,后来养成习惯,每个游标写完立刻释放,或者放进能自动释放的上下文里。

参数说明:UpdateCursor(数据名, 字段列表, SQL 表达式),字段列表传了就只返回这几列,能明显提速;SQL 表达式用来过滤行,比如只处理某个字段非空的记录,语法跟查询对话框里写的一样。

4.3 地图代数:栅格计算在脚本里怎么写

栅格运算不能直接用 Python 的加减乘除——栅格不是数字,是数据集。正确做法是把整个表达式写成字符串,交给地图代数工具去解析,工具返回的是新栅格。

# 用空间分析扩展前先签出,不签出会报扩展未启用 gp.CheckOutExtension("spatial") # 表达式整体是个字符串,内部引号要转义 expr = '"elev" > 1000' gp.SingleOutputMapAlgebra_sa(expr, "c:/prog/out/high_areas")

逻辑说明:表达式里引用的图层名或字段名用双引号包起来,外层字符串用单引号,避免一起转义。输出路径不带扩展名时,格式由环境设置决定;要指定格式就在输出名里写清楚。工具名后缀_sa就是空间分析那个别名,具体名字以你当前版本里工具属性显示的别名为准。

参数说明:单输出地图代数只有一个表达式和一个输出,适合一条公式算一个结果;需要一次性产出多个栅格的分支逻辑,得用多输出那个版本,或者拆成几条单输出分别跑,可读性反而更好。签出扩展这一步必须放在所有栅格运算之前,放到后面无效。

栅格和矢量在脚本里的处理思路差别很大:矢量工具是一条命令处理一个要素类,栅格运算是一条表达式处理一张图,表达式的组合能力才是它的价值。同一段表达式里嵌套条件、重分类、邻域统计都可以,能把一串手动操作压成一行。

4.4 把脚本组织成能反复跑的结构

写到这儿,单个知识点都通了,但脚本会不会变成一坨,取决于结构。我的习惯是固定五段:参数区、环境区、检查区、处理区、消息区。参数区只放路径和常量,环境区只放工作空间、覆盖开关、扩展签出,检查区用Exists把该有的东西挡一遍,处理区纯干活,消息区统一AddMessage。

# 1) 参数 inws = "c:/prog/hmbarea" outws = "c:/prog/hmbarea_out" # 2) 环境 gp.workspace = inws gp.overwriteoutput = 1 gp.CheckOutExtension("spatial") # 3) 检查 if not gp.exists(inws): gp.AddError("输入工作空间不存在: " + inws) raise SystemExit # 4) 处理 # ...工具调用和游标... # 5) 消息 gp.AddMessage("全部完成")

逻辑说明:这个骨架的好处是排错时能一眼定位阶段,报错信息里带的是哪一段,就很清楚是环境问题还是数据问题。raise SystemExit比往下硬跑好,输入都没有还继续执行,只会产生一堆无意义的报错盖住真正的问题。

参数上inws和outws是唯二需要改的,其余都是常量。养成这个结构之后,写第二个脚本直接复制骨架,改两个路径就能跑,比每次从空白文件开始快得多。

5. 避坑与排查:脚本跑不通时先看这五处

报错信息通常只告诉你结果,不告诉你原因。下面五条是我和周围人反复踩过的,按「现象、原因、解决」写,照着对一遍能省掉大半排查时间。

5.1 现象:执行到工具那一行直接崩,提示找不到工具或命令

原因基本只有一个——工具别名没写或者写错。图形界面里点习惯了,写脚本时以为Buffer直接能调,实际要么补上_analysis,要么先设好gp.Toolbox。同名工具跨工具箱时更明显,脚本里不给别名,解释器不知道你指哪个。

解决:别凭记忆写工具名。在工具箱里右键工具看属性,别名那一栏写得清清楚楚,复制过来。不确定的话先拿最小数据集跑一条命令验证,跑通再往脚本里塞。另外工具名大小写在部分版本里不敏感,但别名是敏感的,Analysis和analysis是两回事,统一小写最保险。

5.2 现象:脚本跑完没报错,但结果数值明显不对,坡度一片 0、面积差好几个数量级

原因有两层。浅层是整型除法,两个整数一除小数被截掉,坡度 0 度、比例 0,看着像数据坏了。深层是单位不统一,垂直高程用英尺、水平坐标用米,算出来的坡度差 3.28 倍,比第一个更难发现,因为它不是 0,是一个看起来很合理的错值。

解决:所有除法先把被除数或除数写成浮点,5.比float(5)短,我一般直接加点。单位问题在脚本开头统一换算,高程是英尺就乘 0.3048,或者干脆把 Z 因子设进环境里。凡是涉及高程、坡度、体积的脚本,结果出来先手工核对一个样本,别信脚本。

5.3 现象:第一次跑成功,第二次直接报输出已存在、任务中止

原因:覆盖开关没打开。工具默认不覆盖,输出目录里已经有同名文件,工具直接退出。更麻烦的一种是部分成功——前几步覆盖了,后几步卡住,目录里一半新一半旧。

解决:脚本开头显式gp.overwriteoutput = 1。如果业务上确实不该覆盖(比如原始成果),就别开这个开关,改成输出名带日期或版本后缀,让每次结果都可追溯。两种做法都行,唯独不要「有时候开有时候不开」,那样出的问题最难复现。

注意:覆盖开关打开后,同名输出是无提示替换的,输出目录里别放你不能丢的东西。

5.4 现象:脚本里写了中文注释或者中文路径,工具直接报编码错误、文件读不到

原因:脚本文件本身的编码和解释器默认编码不一致,中文注释在读取阶段就崩了,还没轮到工具执行。路径带中文更容易出事,某些工具内部会把路径转成别的编码,转完就对不上了。

解决:脚本存成纯英文注释最稳,非要写中文就在文件头加编码声明。路径统一用正斜杠,能不用中文目录就不用,中间结果一律落在纯英文路径下。这种问题排查起来特别烦,因为报错信息指向的行号往往不是真凶,是后面好几行。

5.5 现象:属性表改完了,文件删不掉、重命名失败,或者再打开还是旧值

原因:游标没释放。游标打开会持有数据集的锁,脚本走完了但对象还在内存里,锁就一直不放。更隐蔽的是改完没调UpdateRow,循环里算得热火朝天,文件里一个值没动。

解决:每个游标用完立刻释放,别指望脚本退出自动回收。改值的地方,SetValue后面紧跟UpdateRow,两句成对出现。批量更新前先在三个要素的小数据集上跑一遍,确认值真的落盘了,再上全量。

6. 从脚本到工具:封装、调试与最后的自检

零散脚本和「工具箱里的一个工具」之间,差的是参数接口和消息输出。sys.argv那套只适合命令行,挂进工具箱后参数要从工具属性里取,输出要通过消息窗口回传,否则跑完什么都看不见。

import arcgisscripting gp = arcgisscripting.create(9.3) # 参数按工具属性里 Parameters 的顺序编号,从 0 开始 lat = float(gp.GetParameterAsText(0)) decl = float(gp.GetParameterAsText(1)) sunangle = 90 - lat + decl azimuth = 180 if sunangle > 90: sunangle = 180 - sunangle azimuth = 0 gp.AddMessage("正午太阳角=" + str(sunangle)) gp.SetParameterAsText(2, str(sunangle)) # 输出参数回填 gp.SetParameterAsText(3, str(azimuth))

逻辑说明:GetParameterAsText按索引取,索引就是你在这个脚本工具的属性里加参数的顺序,改顺序等于改接口,前面调它的模型全废。回填用SetParameterAsText,这样输出能作为下游工具的输入接下去。AddMessage打的消息会进结果窗口,这是脚本运行过程中唯一能看见的东西,我把它当黑匣子用,每一步都往里写当前状态。

调试这块,断点、单步、变量观察这些能力要有一个顺手的编辑环境,随桌面端装的那个带窗口的解释器就够用。我的做法是先在小数据上跑——三个要素、两张栅格,能在一分钟内跑完的那种,逻辑对了再换全量。全量跑挂了,把中间结果逐级打印出来,看是哪一步开始不对,比盯着最终结果猜快十倍。

最后说个习惯。从那以后我每次把脚本挂进工具箱之前,都强制走一遍三件事:拿一个空目录从零跑一遍、拿一个已有结果的目录再跑一遍、故意不给一个必填参数跑一遍。第一遍验通,第二遍验覆盖,第三遍验报错信息看不看得懂。三遍都过了才敢交给别人用,因为有问题的脚本一旦变成工具被别人引用,后面改接口的成本比写脚本本身还高。希望帮到你。

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

RELATED READING

延伸阅读

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