
手里压着几十宗地块、领导明早就要看图、一个个开工程文件手动改图框、改坐标表、改地名注记……做过ArcGIS内业的人基本都经历过这种深夜批量出图的折磨。我一开始也以为批量出图只是用数据驱动页面凑合一下直到碰到需要同时输出动态文本标签和动态节点坐标表的需求才意识到这事必须做成一个独立工具。这篇就是把我自己写的按地块批量出图工具从思路、代码到踩坑记录完整捋一遍希望能给同样被出图折磨的兄弟们一个参考。这个工具解决的核心问题很明确根据地块面要素自动逐宗生成带图框、带动态文本地块编号、权利人、地类、面积等、带动态节点坐标表地块各拐点坐标列表的成果图全程不需要人工干预。适合搞国土调查、确权登记、勘测定界、村庄规划等需要大量宗地出图的朋友也适合想在ArcGIS Desktop里用ArcPy二次开发做自动化工具的人参考。1. 为什么手工出图会把人逼疯批量出图的真实痛点和常规方案对比先说痛点。很多人觉得出图嘛把数据加载进去、切到布局视图、加个图框打印就行。但一旦地块数量上来这套流程全是坑每个地块的图框范围不一样每个地块的注记位置不一样每个地块的拐点坐标表内容不一样甚至连图名、权利人、地类这些文本都要跟着地块变。手工一张一张调不光是手累关键是脑子必须时刻在线漏改一个字段就是事故。1.1 手工出图的典型流程到底有多慢我见过不少作业组是这么干的先做一个通用模板然后逐个地块另存为再手动把图框缩放到地块位置手动改图名文字手动导出JPG。单宗地顺利的话15分钟遇到形状古怪的地块光调图框比例尺就能磨半小时。算笔账按平均20分钟一宗算80宗地就是1600分钟也就是一个熟练工不吃不喝要干将近27个小时。这就是为什么批量出图不是提升效率的问题而是能不能按时交活的问题。1.2 三种自动化方案的取舍面对这个需求常见的路有三条一是ArcGIS自带的Data Driven Pages数据驱动页面二是纯ArcPy脚本三是做成插件工具。我实测下来三条路各有适用场景。方案优点痛点适用场景数据驱动页面零代码ArcMap自带按字段切分方便动态文本能力弱坐标表基本做不了样式定制受限单纯按地块出图、不做复杂坐标表纯ArcPy脚本灵活想干嘛干嘛便于批量处理参数写死换个数据要改脚本对操作者不友好自己用、能改代码的人插件工具本文方案有界面参数可配普通作业员也能用开发成本高一点需要做工具箱界面团队内部反复使用、交付给非开发人员我最终选择做插件工具是因为这个需求不是一次性任务——后面还有第二轮、第三轮数据要出图每次地块几百宗。如果是自己写死脚本每次换数据都要改代码既不安全也不方便。做成工具后作业员只需要输入地块图层和模板点一下按钮就跑完。2. 工具核心拆解动态文本和动态节点坐标表到底该怎么实现批量出图的难点不在批量两个字而在动态两个字。静态文本谁都会放真正复杂的是让图面上的内容跟着当前地块变。这个工具的两大核心功能一个是动态文本一个是动态节点坐标表很多人在ArcGIS里折腾半天搞不定的就是这两块。2.1 动态文本的两种实现方式ArcGIS布局视图里动态文本有两种常规实现。第一种是直接使用布局里的动态文本功能插入以数据驱动页面为基础的字段类动态文本比如dyn typepage propertyAttribute field权利人 /这种方式在ArcMap里能用但问题是样式控制能力有限而且高度依赖数据驱动页面的字段名。第二种方式是代码直接创建文本元素并写入值。这种方式适合批量出图循环遍历每个地块在布局视图里找到预先放好的文本元素用element.text 字段值的方式逐个替换。我在工具里用的是第二种原因很简单——它不依赖数据驱动页面的索引机制控制力更强而且可以随时调整字体、字号、位置。实际做的时候需要先在布局视图里用插入文本放一个占位符比如随便写个地块编号然后在代码里给它命名ElementName循环时按名字找到它、改掉它的text属性。这一步有个注意点ArcMap里创建文本元素时可以通过CreateTextElement来创建但更稳妥的做法是在出图模板里预先排好所有文本位置代码里只做替换否则代码动态排版会很费劲。2.2 动态节点坐标表的生成逻辑坐标表是勘测定界、确权类成果图里几乎绕不开的内容——地块每个拐点的顺序号、坐标值要列在图上。动态坐标表的难点在于每个地块的节点数量不一样表的行数也不同而布局视图里的表格框是无法自动伸缩的。我的解法是按最大节点数预留 动态填充 隐藏多余行。在出图模板里预先画出一个足够高的表格区域按整个测区里节点最多的那个地块来预估行数比如预留20行。代码里读取当前地块节点数把坐标逐行填进表格填完之后把剩余空行的文字设置为空字符串。这样既保证表格高度够用又不会出现空行占位难看的问题。坐标表里的数据来源也值得说一句地块面要素的getPart(0)返回的是环的坐标点数组但要拿到按地块边界顺序排列的节点必须考虑闭合环的问题——面要素的最后一个点通常和第一个点重合填表时要判断去掉。另外节点从哪个点开始编号也是有讲究的一般取距离图廓左下角最近的点作为起算点这样出图后视觉上更规范。我见过不少自动出图工具在这个细节上翻车——坐标表起点随意跑审图的人一眼就看出来。3. 从零搭工具数据准备、界面设计、核心代码逐步落地这个听起来很玄的插件工具说到底就是在ArcGIS的ArcToolbox里挂一个Script Tool背后跑ArcPy脚本。下面我按实际开发顺序来走一遍。3.1 数据准备字段设计和模板布局是关键脚本能力再强数据字段不规范也白搭。我建议在工具跑之前就约束好地块图层的字段——地块编号、权利人、地类、面积这些必须有而且字段类型要统一。面积字段我建议存数值型不要存带单位的文本单位放到出图模板里统一拼。出图模板.mxd的布局也很关键。我通常的做法是先打开ArcMap手工摆好一个地块的完整版式包括图框、图名、比例尺、图例、指北针、坐标表表格线、动态文本占位符然后把这个布局作为模板。脚本运行时通过arcpy.mapping.MapDocument(templatePath)读取这个模板复制一份到临时路径只替换内容不改版式。这样做的好处是所有地块图面风格完全统一比代码动态绘制版式稳妥得多。3.2 工具界面AddScriptTool的封装在ArcToolbox里右键添加脚本工具把脚本参数配好。我通常设这么几个参数地块图层Feature Layer、编号字段Field、出图保存目录Folder、模板mxd路径File、出图比例尺Double可选。参数配完ArcMap的脚本工具会自动生成一个简单的工具对话框不需要额外写界面代码。如果客户要更精致的界面就得走Add-In或独立Python工具箱.pyt。.pyt的好处是可以用纯Python写工具箱类控制参数校验和动态更新逻辑。不过我对大多数团队的建议是先用简单的Script Tool跑起来等确认逻辑没问题再考虑封装成.pyt或Add-In。3.3 核心脚本逐段讲解脚本的核心逻辑是三层循环遍历地块要素 → 更新图层定义查询 → 更新布局文本和坐标表 → 导出图片。下面给出一个简化的可直接跑的版本。# -*- coding: utf-8 -*- import arcpy import os # 参数实际做成工具后从GetParameterAsText读取 mxd_path arcpy.GetParameterAsText(0) # 模板mxd fc_path arcpy.GetParameterAsText(1) # 地块面图层 id_field arcpy.GetParameterAsText(2) # 地块编号字段 out_dir arcpy.GetParameterAsText(3) # 输出目录 mxd arcpy.mapping.MapDocument(mxd_path) df arcpy.mapping.ListDataFrames(mxd)[0] layer arcpy.mapping.ListLayers(mxd, 地块图层)[0] # 按编号字段排序保证出图顺序可控 rows arcpy.SearchCursor(fc_path, , , , id_field ASC) for row in rows: fid row.getValue(id_field) # 1. 用定义查询筛选出当前地块 layer.definitionQuery {} {}.format(id_field, fid) arcpy.RefreshActiveView() # 2. 按地块范围缩放地图 with arcpy.da.SearchCursor(fc_path, SHAPE, {} {}.format(id_field, fid)) as cur: for feat in cur: extent feat[0].extent # 按比例外扩5%避免要素贴图框边缘 x_min extent.XMin - extent.width * 0.05 y_min extent.YMin - extent.height * 0.05 x_max extent.XMax extent.width * 0.05 y_max extent.YMax extent.height * 0.05 df.extent arcpy.Extent(x_min, y_min, x_max, y_max) # 3. 替换文本元素 for elm in arcpy.mapping.ListLayoutElements(mxd, TEXT_ELEMENT): if elm.name txt_地块编号: elm.text fid elif elm.name txt_权利人: # 这里用SearchCursor取对应字段值简化代码略 elm.text 张三 elif elm.name txt_面积: elm.text {} 平方米.format(round(area, 1)) # 4. 填入坐标表 fill_coord_table(mxd, fc_path, fid) # 5. 保存mxd并导出图片 out_mxd os.path.join(out_dir, temp.mxd) mxd.saveACopy(out_mxd) arcpy.mapping.ExportToPNG(mxd, os.path.join(out_dir, fid .png), resolution300) arcpy.RefreshActiveView() # 清理 layer.definitionQuery del mxd3.4 坐标表填充函数坐标表填充是工具最核心的部分。假设布局里已经放了一个名为tbl_坐标的表格表格里的每一行有四个文本元素——序号、X、Y、备注。那么填充逻辑如下。def fill_coord_table(mxd, fc_path, fid): # 取当前地块的边界顶点 coord_list [] with arcpy.da.SearchCursor(fc_path, SHAPE, {} {}.format(地块编号, fid)) as cur: for feat in cur: geom feat[0] part geom.getPart(0) # 取第一个环 for pnt in part: if pnt: # 跳过环中的空点 coord_list.append((pnt.X, pnt.Y)) # 面要素是闭合环去掉重复的末尾点 if len(coord_list) 1 and coord_list[0] coord_list[-1]: coord_list coord_list[:-1] # 调整起算点找距离图左下角最近的点作为编号起点 min_idx 0 min_dist float(inf) for i, (x, y) in enumerate(coord_list): d (x - df.extent.XMin) ** 2 (y - df.extent.YMin) ** 2 if d min_dist: min_dist d min_idx i coord_list coord_list[min_idx:] coord_list[:min_idx] # 写入布局表格 for i in range(30): # 预设30行 row_name tbl_坐标_行{}_序号.format(i) row_x tbl_坐标_行{}_X.format(i) row_y tbl_坐标_行{}_Y.format(i) elm_no find_element(mxd, row_name) elm_x find_element(mxd, row_x) elm_y find_element(mxd, row_y) if i len(coord_list): x, y coord_list[i] elm_no.text str(i 1) # 坐标保留3位小数符合测绘成果出图常见规范 elm_x.text {:.3f}.format(x) elm_y.text {:.3f}.format(y) else: elm_no.text elm_x.text elm_y.text 这里要特别强调一个细节getPart(0)生成的坐标是有顺序的对面要素来说这个顺序是顺时针或者逆时针但不管哪种它都符合多边形边界的连续逻辑。真正影响成果表观的是起算点的选取我习惯用离图廓左下角最近的点这个和审图人员的阅读习惯一致。4. 跑起来之后的那些坑坐标系、范围偏移、文本刷新工具不是写完就能用真正让脚本稳定的关键在测试阶段。我把自己调试过程中遇到最多的三类问题列出来这些坑基本是绕不开的。4.1 坐标系不一致导致坐标表整体偏移这个坑最容易出现在数据本身是CGS2000或地方坐标系而地图文档的坐标系设置有偏差的情况下。坐标表里的X、Y看起来是地块边界的值实际输出时却整体平移了几百米或者和底图的套合对不上。排查思路先确认地块图层的坐标系和布局里地图框的坐标系是否一致。如果两个坐标系存在投影转换关系ArcGIS虽然会自动做动态投影但坐标表取的是要素的原始坐标系下的数值和显示坐标系下的值可能不同。所以我的做法是取坐标前先给图层设置一个统一的输出坐标系或者直接把地块数据用arcpy.Project_management先转成目标坐标系的一份临时副本再跑工具。临时副本出图结束后删掉不污染原始数据。4.2 出图范围异常放大、要素缩成一团还有一种情况很常见前面地块出图正常跑到某一个地块时导出的图片里要素变得特别小或者干脆只出半个地块。原因通常是这个地块的某个节点坐标有异常值——比如图形编辑时误加了一个偏离很远的点导致extent范围被撑得巨大按外扩比例缩放后地块自然就小了。解决方法是加一个范围合理性检查对每个地块计算面积如果面积和图层平均面积相差三个数量级以上就跳过或者单独标记不进入批量流程。另外对extent做上限约束也是个办法——如果地块外扩后超过预设最大幅度就用预设幅度替代保证图面比例尺稳定。4.3 动态文本刷新失败旧值残留动态文本最容易出的bug就是上一个地块的值残留到下一个地块。因为ListLayoutElements拿到的是元素对象改了text之后如果不重新赋值下一次循环里它还是上一次的值。这个问题新手很容易忽略——明明代码里写了替换逻辑结果图名下显示的还是上一个地块的编号。我的经验是每个地块处理完成后统一走一遍清空逻辑把所有动态文本元素设成空字符串再进入下一个地块的赋值。同时在代码里加一个校验替换完后把地块编号和坐标表第一个点的X值打印到日志里批量跑完随便抽查几张图就能确认无残留。5. 把脚本打磨成能交付的插件交互设计、容错和性能优化批量出图工具最后能不能在团队里推广开来看的不是脚本本身能不能跑而是别人拿到手上好不好用。5.1 参数校验和错误提示脚本做成工具后第一要务是别让人懵。比如用户选了地块图层但没选编号字段工具应该在对话框阶段就拦下来而不是跑了半天才报错。.pyt工具箱里可以用updateMessages做参数级校验Script Tool里也可以在脚本开头统一校验再进入主循环。我的校验清单是这样的地块图层存在、编号字段存在且无重复值重复值会导致定义查询筛选出多个要素、输出目录可写、模板mxd里的关键文本元素都在。任何一项不满足直接弹明确的中文提示不带含糊。5.2 性能瓶颈与断点续出循环几百个地块每个地块都要重新refresh、重写文本、导出PNG性能瓶颈主要集中在导出图片上。300DPI的PNG一张图的导出时间大约3到8秒100个地块跑下来就是5到13分钟还在可接受范围。如果地块量上千建议用ExportToPDF代替PNGPDF的导出速度和体积会更可控后续转图也更灵活。另外一个实用的小设计是断点续出出图过程中如果因为某块数据异常中断了重新跑就全部返工。我后来在工具里加了跳过已存在文件的选项——保存前先判断输出目录里有没有同名图片有就跳过。这样中断后只需要重新点一次运行就能从断点接着跑不用重新开始。5.3 模板的灵活扩展工具做出来后需求往往还会变。比如今天要出地籍图明天要出勘界图版式不一样。我的做法是把模板mxd作为外部参数交给用户配置工具本身不绑定任何版式。用户只要保证模板里的文本元素命名规范一致比如txt_地块编号、tbl_坐标_行0_X这种规则工具就能直接复用。这样以后换版式、换比例尺、换纸张大小都只需要改模板不需要改代码。按我个人的经验批量出图工具这类东西刚开始做的时候总觉得跑通就行但真正让它在项目里持续发挥作用、成为大家离不开的生产力工具靠的往往是这些边界情况的处理和使用的便利性。我最初那版脚本只用了不到一天就写完了后面的容错和交互打磨反而花了两三天但这段时间花得非常值——至少从那次之后我们组的批量出图就再没加过夜班。