ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

ArcMap数据驱动页面+arcpy实现卫片图斑批量出图

ArcMap数据驱动页面+arcpy实现卫片图斑批量出图

简介:面向GIS数据处理与制图人员的ArcGIS按图斑批量出图操作文档,聚焦卫片图斑的自动化出图场景,帮助需要把大量图斑逐一导出为JPG图片的读者摆脱手工排版与重复导出。文档围绕数据驱动页面(Data Driven Pages)展开,说明图层、名称字段、排序字段、显示范围与比例尺的配置方法,并讲解在布局视图中添加图斑编号、所在区域、面积、类型等动态文本,使每张成图都带有对应图斑属性。压缩包内为1个doc文档,压缩后约29KB,体量轻便,适合随查随用。结合Python的arcpy脚本,读者可了解遍历页面、切换currentPageID、读取页名及调用ExportToPNG导出的完整思路,也能据此改写为JPG输出、调整图像尺寸并补充错误处理。目前已有8322人学习,适合具备ArcGIS 10.2及以上环境、希望掌握批量制图自动化的中初级GIS从业者与测绘、自然资源技术人员参考。

1. 卫片图斑出图,真正累人的是重复劳动

卫片图斑核查的活一到手,最磨人的往往不是判读,而是出图。一个乡镇两百多个图斑,每个都要一张带村名、面积、图斑类型的 JPG,手工在 ArcMap 里挪数据框、改文本、点导出,一天下来手都僵了。Arcgis 批量出图这件事其实不用插件、不用二次开发平台,ArcMap 自带的数据驱动页面配一段二十行的 arcpy 脚本就够——把索引图层设成图斑层,一页对一个图斑,循环切页、导出,两百张图压到几分钟。适合手上有一层图斑矢量、需要按要素批量出图的 GIS 作业人员,也适合想把这段流程接进自己工具链的开发者。硬门槛只有一个:ArcGIS 必须 10 及以上,9.x 没有这个功能。

2. 环境与选型:ArcMap 10.x 加数据驱动页面的最小依赖

动手前先把环境确认清楚,不然脚本写完跑起来一堆报错,排查的方向都会跑偏。这一章把版本、解释器、路线选择和目录组织讲明白,后面配置图层和写脚本才有基础。

2.1 版本红线与解释器选择

数据驱动页面(Data Driven Pages)是 ArcGIS 10.0 才引入的功能,脚本里调用的arcpy.mapping模块同样是 10.0 之后才有的东西。9.x 时代用的是arcgisscripting,接口完全不一样,网上能搜到的 9.x 出图脚本放到 10 上跑不通,反过来也一样。项目正文里强调「Arcgis10 以上,9 不行」,说的就是这条线。

版本之外最容易出错的是解释器。ArcGIS Desktop 10.x 自带的是 Python 2.7,装在类似C:\Python27\ArcGIS10.2\这样的路径下,arcpy只认这一个。你要是用系统里另外装的 Anaconda 或者 Python 3 去执行,第一行import arcpy就会抛ImportError: No module named arcpy。Notepad++ 的「运行」按钮本质上也是去调用系统关联的 python.exe,关联错了,坑就在这里。

我自己的习惯是两条路二选一:要么直接在 ArcMap 的 Python 窗口里粘贴执行,解释器天然正确;要么开命令行,显式写全路径:

"C:\Python27\ArcGIS10.2\python.exe" "F:\GeoData\script\batch_export.py"

提示:写脚本时别用 Pro 的思路。ArcGIS Pro 里arcpy.mapping已经换成arcpy.mp,数据驱动页面换成 Map Series,currentPageID那套 API 全变了。这篇里的代码只在 Desktop 10.x 有效。

2.2 两条批量出图路线的对比

按图斑批量出图,业内其实有两套常见做法,选错了后面越写越别扭。

第一套是手写循环加定义查询:遍历图斑属性表的每一行,arcpy.mapping.Layer拿到图层,改definitionQuery,改数据框范围dataFrame.extent,再导出。这套的好处是全靠代码控制,不需要在界面上做任何配置,适合嵌进无人值守的服务里。坏处是数据框范围得自己算,比例尺、要素居中、留白全要手写几何运算,遇上跨带、跨坐标系的图斑很容易出错。

第二套就是数据驱动页面:地图里配置一次索引图层,之后的居中、比例尺、动态文本全部由 ArcMap 渲染引擎负责,脚本只做「切页 + 导出」这一件事。代价是你得有一份保存好的 mxd,前期配置在界面上完成。

对比项定义查询循环数据驱动页面
要素居中与范围自己算 extent引擎自动处理
比例尺控制手写 scale 赋值配置界面里选固定/自动
动态文本需要逐页替换元素文本配置一次,全页生效
前期配置无需要在 ArcMap 里配索引图层
适合场景无人值守服务、批量脚本人工出图、报告式版面

卫片核查这种「版面要求统一、带村名面积等属性文字」的场景,第二套明显省事,所以项目里走的是数据驱动页面这条线。

2.3 工程目录骨架:一个能反复用的出图工作区

批量出图最容易乱的阶段是文件散落各处:mxd 放在桌面,输出图片丢到下载目录,脚本扔在临时文件夹里,下次换个乡镇又要重新找。我一般一开始就把目录固定下来,后面换数据只换路径。

路径作用注意点
GeoData\mxd\存放工程文件mxd 里勾选「存储相对路径」,整包拷贝不丢源
GeoData\vector\图斑索引图层(shapefile 或 gdb 要素类)索引图层单独放,别和底图混在一个 gdb 里
GeoData\basemap\影像、行政区、路网等底图数据量大时建议建金字塔,不然每页渲染都慢
GeoData\out\输出图片目录按批次建子目录,比如out\2024_03\
GeoData\script\py 脚本和日志日志和图片分开放,出问题先看日志

相对路径这一步别省。File > Map Document Properties里把「Store relative pathnames」勾上,之后整个GeoData文件夹打包发给同事,他那边路径不一样也能直接打开。不勾的话,脚本里换了盘符,图层全是红叹号,导出来一堆空白图,排查起来非常费劲。

3. 地图装配:索引图层、动态文本和比例尺怎么配到位

脚本之前的功夫全在 mxd 里。配置得好,脚本就是十几行;配置得糙,脚本里就得写一堆补救逻辑。三个方面要配置到位:索引图层、动态文本、比例尺与范围。

3.1 打开数据驱动页面并配好索引图层

入口在Customize > Toolbars > Data Driven Pages,勾上之后工具栏会浮出来,点第一个按钮打开Data Driven Pages Setup对话框。这里四个参数决定一切:

  • 图层(Layer):选图斑矢量层,也就是那份卫片图斑数据。它必须已经加载进当前数据框,且没有被定义查询过滤掉(过滤掉的要素不会生成页面)。
  • 名称字段(Name Field):后续脚本拿它当输出文件名。建议用图斑编号,形如WP2024_001,不要用带空格、斜杠、冒号的中文名,文件名合法性会出问题。
  • 排序字段(Sort Field):决定页码顺序。按编号排最稳,便于和台账对账;按面积从大到小排也常见,方便先看重点图斑。
  • 显示范围与比例尺:见 3.3 节。

配完之后工具栏上会有页码输入框和前后翻页箭头,随手翻几页看效果,图斑是不是居中、有没有被裁掉,这里是最后一道人工检查关口。

索引图层的字段建议在配置前就准备好。面积这种字段 if 是图形算出来的,用「计算几何」先落到属性表里,别指望动态文本自己算。所在村、图斑类型这类挂接字段也提前 join 好并导出为实体字段——动态文本读的是索引图层的真实字段,关联表的虚拟字段有时读不出来。

3.2 动态文本:把村名、面积、图斑类型印到图上

版面里那些「这一页是哪个图斑」的文字,靠的是动态文本。在布局视图里Insert > Dynamic Text,常用的几项:

  • Page Name:等于名称字段的值,标题最常用。
  • Page Attribute Field:选索引图层的某个字段,村名、面积、类型都走这个。插入时它会让你选图层和字段,选错图层就是空白。
  • Page Number:当前页码 / 总页数,做「第 3 页 共 215 页」很方便。

一个实际版面通常长这样:标题写「卫片图斑 WP2024_037 核查图」,下面一行是「所在村:XX村 面积:12.4 亩 类型:建设用地」,右下角放页码和制图日期。这些文字一旦配成动态文本,翻到哪一页就自动显示哪一页的属性,不用逐页改。

注意动态文本在布局里看着是带尖括号的占位符,只有翻页或导出时才替换成真值。如果发现某页显示成<Null>,那是索引图层该字段本身为空,回属性表补齐即可,不是文本配置错了。

3.3 比例尺与显示范围的取舍

这一步直接决定成图能不能用。Data Driven Pages Setup里的 Extent 有三档常用选项:

  • 以要素范围为中心并裁剪(Center and Clip to Feature Extent):按图斑自身范围铺满数据框,大小图斑看起来一样大。适合出单图斑详图,但几张图之间没法比大小。
  • 以指定比例尺居中并裁剪(Center and Clip with Specified Scale):所有页统一比例尺,图斑大小可比。核查类出图基本用这个。
  • 最佳拟合(Best Fit):引擎自动算比例尺让图斑刚好放得下,省事但比例尺五花八门。

卫片核查我一般固定 1:2000 到 1:5000,具体看影像分辨率:0.5 米影像配 1:2000 够看清地物,2 米影像配 1:5000 更合适,再放大就是马赛克了。同时把范围边距留出来,图斑贴边观感很差,Extent 里可以设百分比边距,或者用「Map Extent」选项卡里的边距值给 10% 到 20%。

注意:固定比例尺加上大图斑,会出现图斑超出图框被裁的情况。这类图斑要么单独用「以要素范围为中心」再出一版,要么在索引图层里给它单独标注、脚本里分流处理。

3.4 保存与图层锁定

配置完记得保存 mxd。脚本读的就是磁盘上的这个文件,ArcMap 界面里改了没保存,脚本读到的还是旧配置——这个坑不显眼,但每次都有人踩。另外脚本运行期间别再去编辑同一个 mxd,读写冲突会导致导出中断。

图层层面还有两件事:一是底图数据源如果是 SDE 或者网络路径,导出时每页都要去拉数据,速度会很慢,能落到本地就落本地;二是导出前确认所有图层都是可见状态,被关掉的图层不会出现在图里,出完一看少了个行政区界,返工成本比检查高得多。

4. 脚本拆解:arcpy 遍历页面导出的每一行在干什么

前置配置做完,剩下就是脚本。这段代码不长,但每一行的含义和边界条件值得逐行过一遍,尤其是页码范围和编码这两处。

4.1 最小可用脚本逐行读

# -*- coding: utf-8 -*- import arcpy # 已经配好数据驱动页面的工程文件 mxd = arcpy.mapping.MapDocument(r"F:\GeoData\mxd\WeiPian.mxd") out_dir = r"F:\GeoData\out" # pageCount 是总页数,页码从 1 开始,所以右边界要 +1 for page_num in range(1, mxd.dataDrivenPages.pageCount + 1): # 切到当前页,渲染引擎会自动把数据框移到该图斑 mxd.dataDrivenPages.currentPageID = page_num # pageRow 是当前页对应的索引图层记录,pageNameField.name 是字段名 field_name = mxd.dataDrivenPages.pageNameField.name name = mxd.dataDrivenPages.pageRow.getValue(field_name) out_file = out_dir + "\\" + name + ".png" arcpy.mapping.ExportToPNG(mxd, out_file) print("done: " + name) del mxd

几个关键点。arcpy.mapping.MapDocument接收 mxd 的绝对路径,返回一个文档对象,后续所有操作都基于它。mxd.dataDrivenPages是数据驱动页面的入口对象,只有 mxd 里配置过索引图层它才可用,否则访问会直接报错。pageCount是总页数,currentPageID是当前页号,从 1 开始计数,不是 0。

pageNameField.name返回的是字段名而不是字段值,这一点容易看混;取值得用pageRow.getValue(字段名),pageRow是当前页对应的那一条索引图层记录,等价于属性表里的一行,理论上你想取哪个字段都行。

原始参考代码里range(1, mxd.dataDrivenPages.pageCount)少写了个+1,右开区间导致最后一页永远不出图——图斑一共 200 个,出 199 张,而且不报错,只有对着台账数才发现。这是这类脚本最常见的翻车点,别照抄。

另外原始代码文件名叫.png,正文里说的是出 JPG,两回事。要 JPG 就把导出函数换成ExportToJPEG。

4.2 导出函数的参数怎么调

ExportToPNG的完整签名是ExportToPNG(map_document, out_png, {data_frame}, {df_export_width}, {df_export_height}, {resolution}, {world_file}, {color_mode}, {transparent_color}, {position}),ExportToJPEG类似但末尾是{quality}和{progressive}。方括号包起来的都可以不填,不填就用默认值——默认宽度只有 640 像素,出来一张糊图,很多人第一次跑完觉得「怎么这么小」就是这个原因。

参数含义常用值
data_frame要导出的数据框,多个数据框时要指定不填表示主数据框
df_export_width/df_export_height输出像素尺寸2400 × 1800(4:3 版面)
resolution输出 DPI屏幕预览 96,打印 200~300
world_file是否附带坐标信息文件True 会额外生成.jgw/.pgw,方便回贴
color_mode色彩模式24-BIT_TRUE_COLOR全彩
quality仅 JPEG,压缩质量85~95

一段调过参数的导出调用长这样:

# 2400x1800 像素、300dpi,附带坐标文件,适合打印和归档 arcpy.mapping.ExportToJPEG( mxd, out_file, df_export_width=2400, df_export_height=1800, resolution=300, world_file=True, color_mode="24-BIT_TRUE_COLOR", quality=90 )

这里有个容易混淆的点:宽高是像素数,resolution 是打印分辨率,两者共同决定物理尺寸。2400 像素在 300dpi 下是 8 英寸宽,换成 96dpi 就变成 25 英寸。想要「A4 幅面、300dpi」,反推像素应该是 2480 × 1754 左右,比拍脑袋填 3000 靠谱。大批量出图时也别一味往上堆,2400 像素 300dpi 出一张大概一两秒,堆到 6000 像素就是十倍时间,两百张图等的就是十几分钟。

4.3 加固:异常捕获、跳过已出图、文件名清洗

最小脚本能跑通,但只适合一次性的活。真正投入日常使用前,我一般会补三件事:文件名清洗、断点续跑、错误不中断。

# -*- coding: utf-8 -*- import arcpy, os, re, codecs, traceback MXD_PATH = r"F:\GeoData\mxd\WeiPian.mxd" OUT_DIR = r"F:\GeoData\out" LOG_PATH = r"F:\GeoData\script\export_log.txt" # Windows 文件名里不能出现的字符,一律替换成下划线 def safe_name(raw): return re.sub(r'[\\/:*?"<>|]', "_", raw.strip()) mxd = arcpy.mapping.MapDocument(MXD_PATH) total = mxd.dataDrivenPages.pageCount field = mxd.dataDrivenPages.pageNameField.name log = codecs.open(LOG_PATH, "w", "utf-8") ok, skip, fail = 0, 0, 0 for page_num in range(1, total + 1): try: mxd.dataDrivenPages.currentPageID = page_num name = safe_name(mxd.dataDrivenPages.pageRow.getValue(field)) out_file = os.path.join(OUT_DIR, name + ".jpg") # 已存在就跳过,脚本中断后可以直接续跑 if os.path.exists(out_file): skip += 1 continue arcpy.mapping.ExportToJPEG(mxd, out_file, resolution=300, quality=90) ok += 1 log.write(u"OK\t%s\t%s\n" % (page_num, name)) except Exception: fail += 1 log.write(u"FAIL\t%s\t%s\n" % (page_num, traceback.format_exc())) log.write(u"total=%s ok=%s skip=%s fail=%s\n" % (total, ok, skip, fail)) log.close() del mxd

safe_name解决图斑编号里混了斜杠、冒号导致写文件失败的问题,这类脏数据在人工维护的图斑表里非常常见。os.path.exists判断加上continue,让脚本具备断点续跑能力——两百张图跑到第一百五十张崩了,重跑只补剩下的五十张。try/except包住单页循环,一页失败不影响整批,失败信息连同堆栈写进日志,事后按页码定位。

日志用codecs.open(..., "w", "utf-8")而不是普通open,因为 Python 2 的默认编码是 ASCII,写中文进去直接抛UnicodeEncodeError,而这个报错又发生在异常处理里,会把真正的错误掩盖掉。

4.4 不打开 ArcMap 也能跑

脚本不需要 ArcMap 界面开着。Desktop 授权正常的情况下,命令行直接执行就能后台出图,机器该干别的干别的。少数情况下 64 位后台处理需要额外装 Background Geoprocessing,普通的图片导出用 32 位桌面环境就够,不用折腾。

如果想脱离 Desktop 完全自动化,另一条路是脚本工具:把 py 文件加成 ArcToolbox 里的脚本工具,参数用arcpy.GetParameterAsText()接,之后可以在 ModelBuilder 里串进更大的流程。这部分放在第 6 章展开。

5. 避坑与排查:五个让脚本卡住的真实现场

这些坑的共同点是:脚本不报错或者报的错跟真实原因不沾边。把现象对上,比盲目改代码快得多。

5.1 一执行就 ImportError: No module named arcpy

现象:脚本第一行就崩,Notepad++ 里点运行、或者命令行敲python batch_export.py,报找不到 arcpy。

原因:调用的是系统里另外装的 Python,不是 ArcGIS Desktop 自带的 2.7 解释器。arcpy是随 Desktop 安装写进那个特定 Python 环境的,不在全局 site-packages 里。

解决:用全路径调用C:\Python27\ArcGIS10.2\python.exe,或者干脆在 ArcMap 的 Python 窗口里跑。检查办法是在命令行执行python -c "import sys; print(sys.executable)",看输出路径里有没有ArcGIS字样。

5.2 图斑 200 个,只出了 199 张图

现象:脚本顺利跑完,print一路 ok,去文件夹一数少一张,少的正好是最后一个图斑。

原因:range(1, pageCount)是右开区间,pageCount那一页取不到。这个错误不抛异常,纯逻辑问题。

解决:写成range(1, pageCount + 1),或者用while循环让page_num从 1 递增到pageCount。出完图之后拿文件数量和pageCount对一遍,是个十秒钟的自检动作。

5.3 中文图斑名称报编码错,或者文件名变乱码

现象:跑到某个图斑时抛UnicodeDecodeError: 'ascii' codec can't decode byte,或者文件生成出来了但名字是问号、乱码。

原因:Python 2 里字符串默认是字节串,out_dir + "\\" + name这种拼接,只要name含中文就会触发隐式解码,用 ASCII 解 UTF-8 必然失败。文件头部那行# -*- coding: utf-8 -*-只声明源文件编码,不影响运行时字符串类型。

解决:路径统一用 unicode 字面量,或者用os.path.join拼接并保证两边类型一致;写日志用codecs.open(..., "w", "utf-8");print到 GBK 控制台还可能二次报错,日志落文件最省心。

5.4 导出的图一片空白,只有底色和动态文本

现象:图片文件正常生成,尺寸也对,打开一看框里啥都没有,或者只有行政区底色找不到图斑。

原因:常见三种。索引图层数据源断了,ArcMap 里显示红叹号,渲染时该图层被跳过;图斑本身不在当前比例尺的显示范围内(固定比例尺配大图斑,或者坐标系统不一致);图层被设了最小/最大显示比例尺,出图比例尺不在区间内。

解决:先在 ArcMap 里翻几页确认显示正常再跑脚本;检查图层属性General页的可见比例尺范围,全设成「不在超出范围时显示」;数据源路径尽量用相对路径,换机器不丢。

5.5 文字发虚、图斑边缘有锯齿

现象:图斑轮廓和标注文字都糊,打印出来更明显,比例尺没错但观感差。

原因:只改了df_export_width没改resolution,或者两个都没改,用的默认 640 像素输出。

解决:像素尺寸和 DPI 一起设。要 A4 打印就按210mm / 25.4 * 300 ≈ 2480反推宽度,高度按版面比例配。抗锯齿在导出层面控制不了,源头是底图分辨率和标注字体,标注字号在版面里别小于 8pt,小字号在任何 DPI 下都糊。

6. 二次开发:把脚本改成传参即用的出图工具

硬编码路径的脚本只能自己用,交给同事就得改代码。真正好用的是把它做成一个能传参的小工具:把 py 挂到 ArcToolbox 的脚本工具里,参数用arcpy.GetParameterAsText()接,界面上填 mxd 路径、输出目录、格式、DPI 就能跑,不用碰代码。

# -*- coding: utf-8 -*- import arcpy, os, re, codecs # 参数顺序与脚本工具里定义的参数顺序一致 mxd_path = arcpy.GetParameterAsText(0) # 工程文件 .mxd out_dir = arcpy.GetParameterAsText(1) # 输出目录 fmt = arcpy.GetParameterAsText(2) or "JPEG" dpi = int(arcpy.GetParameterAsText(3) or 300) start_no = int(arcpy.GetParameterAsText(4) or 1) mxd = arcpy.mapping.MapDocument(mxd_path) total = mxd.dataDrivenPages.pageCount field = mxd.dataDrivenPages.pageNameField.name for page_num in range(start_no, total + 1): mxd.dataDrivenPages.currentPageID = page_num name = re.sub(r'[\\/:*?"<>|]', "_", mxd.dataDrivenPages.pageRow.getValue(field)) out_file = os.path.join(out_dir, name + (".jpg" if fmt == "JPEG" else ".png")) if fmt == "JPEG": arcpy.mapping.ExportToJPEG(mxd, out_file, resolution=dpi, quality=90) else: arcpy.mapping.ExportToPNG(mxd, out_file, resolution=dpi) arcpy.AddMessage(u"%s/%s %s" % (page_num, total, name)) del mxd

start_no这个参数看着多余,实际很顶用:跑到一半中断、或者只想补出某个村的后半段图斑,改个起始页码比重新改代码快得多。arcpy.AddMessage会把进度打到工具的消息面板上,比print更适合脚本工具环境。

参数定好之后,可调项其实就那几个,我把常用的取值范围列一下,方便配默认值:

参数建议默认可调范围影响
输出格式JPEGJPEG / PNGPNG 无损但体积大,归档用 PNG,日常传阅用 JPEG
DPI30096 ~ 400打印取 300,屏幕预览 96 足够
压缩质量9070 ~ 100低于 80 时图斑边界会出块状伪影
起始页码11 ~ pageCount断点续跑用
边距百分比150 ~ 30太小图斑贴边,太大主体变小

再往前一步的改造,是把「出哪些图斑」也参数化:加一个可选参数接 CSV 或要素类,脚本里先按这个清单过滤索引图层的definitionQuery,再走页面循环。这样同一份 mxd 既能全量出图,也能按村、按批次挑着出,不必另存一个工程文件。

验证方法我固定用三步:先看pageCount是不是等于图斑总数,数量不对说明索引图层的定义查询或数据源有问题;再随机抽三张(第一张、中间一张、最后一张)打开看版面,重点看最后一张,那是页码边界最容易漏的地方;最后拿文件名列表和属性表编号做一次比对,差集不为空就回去查 5.3 的文件名清洗。三步走完,这批图基本可以放心交出去。

早些年我图省事,脚本直接硬编码路径就交货,结果换台机器第一行就崩,改完路径又发现编码报错,来回折腾一下午。从那以后我每次写这类批量脚本,都强制走一遍「解释器确认—页码边界核对—抽最后一张」的流程,再急的活也不跳。希望帮到你。

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

RELATED READING

延伸阅读

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