简介:《Arcgis按图斑批量出图》配套文档面向使用ArcGIS处理卫片图斑、需要把大量图斑逐一出图的GIS从业者与制图人员,重点解决手工逐幅导出效率低、命名与信息展示不统一的问题。资源包仅含1个doc文件,约29KB,以文字说明和示例脚本为主,篇幅精简,便于快速查阅与本地留存。该资料围绕数据驱动页面配置、布局动态文本设置及arcpy批量导出思路展开,涉及按图斑编号命名、JPG/PNG导出参数调整、结果目录检查与常见环境前提,可帮助读者理解自动化出图的组织方式和脚本定制方向。目前已有8302人学习,适合具备ArcGIS基础、希望提升批量制图效率的初中级用户参考。
1. 卫片图斑批量出图:一次配好,按编号一张一张吐出来
卫片图斑下来一批,少则几十、多则上百,每个图斑要单独出一张 JPG,图上还得带本图斑的编号、所在村、面积、图斑类型。手动做法是逐个缩放到图斑、调整比例尺、导出、再改文件名,出到第二十个手就开始抖,出到第五十个眼神就散了。Arcgis 批量出图这件事的本质,是把「翻页」变成可编程的动作:数据驱动页面(Data Driven Pages)负责把图斑变成一张张可以翻的布局页面,arcpy 负责替你按顺序翻页并导出成图片。
这套方案适合谁:手上有一份图斑面层、每条记录对应一个出图单元、需要固定版面出一批图的场景,比如卫片图斑核查、地块台账出图、执法图斑举证。它不适合谁:需要每个图斑自动旋转、自动避让压盖、自动配图例的复杂制图,这套流程撑不起来;也不适合还在用 ArcGIS 9 的人,数据驱动页面这个功能是 10 才有的。前置条件很朴素——ArcGIS 10.2 一类的桌面版,配一个能写 py 文件的文本编辑器,就够跑起来。
2. 数据驱动页面:把一群图斑变成可以翻页的地图
2.1 索引图层才是这套方案的「页码本」
很多人一上手就犯迷糊:数据驱动页面到底在驱动什么。它驱动的不是地图内容,而是索引图层里的行。你指定一个图层作为索引,这个图层的每一行就对应布局里的「一页」,页面总数就是行数,页名就是某个字段的值。翻页的动作,实际上是把当前页面切到第 N 行,布局里绑定了这个数据框的地图范围、动态文本、比例尺文本跟着一起重算。
所以索引图层得先过一遍筛子,四个条件缺一个后面都要还债:
- 一个出图单元一行。如果同一个图斑被拆成多行(比如按地类分类拆开),翻页就会翻出重复的图,出图数和图斑数对不上,这是后面核对清单时最常被问的问题。
- 有一个唯一且可读的名称字段。这个字段会直接进入文件名,也决定你事后能不能凭文件名找到图斑。
- 有稳定的排序字段。通常是图斑编号,按编号排序导出,中途断了也容易判断出到哪了。
- 几何完整、没有空几何。空几何的图斑会让地图框找不到中心,导出可能是一张空白图,甚至直接报错。
一个常见做法是把索引层和图面层分开:索引层只留图斑边界加几个关键字段,图面层另外叠影像、现状、行政界。这样逐页渲染时不用反复啃一个几十万要素的大图层,出图速度差得很明显。
2.2 四步配完 Data Driven Pages:字段、排序、范围、比例尺
配置本身不复杂,麻烦的是每一项背后的取舍。打开 ArcMap,在 Customize → Toolbars 里勾上 Data Driven Pages,工具栏出来后点 Data Driven Pages Setup,按下面这张表把四件事定下来:
| 配置项 | 常见取法 | 决定什么 |
|---|---|---|
| Layer(索引图层) | 图斑面层或专门的索引层 | 页面总数与页序来源 |
| Name Field(名称字段) | 图斑编号,唯一、无非法字符 | 每页的页名,脚本靠它命名文件 |
| Sort Field(排序字段) | 图斑编号或村名+编号 | 导出顺序,断了之后从哪继续 |
| Extent(范围方式) | 中心点与比例尺 / 最佳范围 | 每页地图范围怎么算 |
| Scale(比例尺) | 按图斑典型尺寸定,如 1:2000 | 只在中心点模式下生效 |
| Margins(边距) | 留 5%~10% 余量 | 图斑贴边还是留白 |
设置完点 OK,工具栏上会出现页码输入框和翻页箭头,翻两页看看版面是不是每页都跟着变。配置完一定要保存 mxd,脚本读的是磁盘上的文件,不保存等于白配。这一步我第一次做的时候翻车过一次:界面里翻页正常,跑脚本出来一百张一模一样的图,原因是没保存,脚本读的是旧配置。
排序字段还有个细节:如果名称字段是文本型、编号带前导零(比如 001、002),排序结果和你想的一致;如果编号是数值型,排序没问题但文件名里前导零会丢,最后满目录的 1.jpg、2.jpg,靠肉眼找图会很痛苦。我一般会在索引层里加一个文本型的「出图编号」字段,格式化成三位或四位补零,专门喂给数据驱动页面。
2.3 动态文本:让每张图自己报出图斑号、村名、面积
版面里那些每页都不一样的信息,靠动态文本解决,不用写代码。在布局视图下用 Insert → Dynamic Text,有两类用得最多:一类是数据驱动页面属性(Data Driven Page Attribute),可以直接选索引图层的任意字段,图斑号、所在村、面积、类型都能放;另一类是页码和页数,显示「第 N 页 / 共 M 页」。
绑定的时候注意一个容易忽略的点:动态文本引用的是索引图层的字段,不是图面层的字段。图面层里就算有同名字段,动态文本也读不到。所以索引层里该带的属性要提前挂上去,宁可冗余也别缺。
面积这类数值字段建议在索引层里先算好、单位换算好,格式化成「12.35 亩」这样的字符串字段,直接插到动态文本里。让动态文本去做单位换算,不是不行,但每页重算一次,图斑一多版面刷新就变慢,属于给自己找麻烦。
版面里放的比例尺文本、指北针、图例,只要不绑定具体字段,每页导出时都会按当前页面自动更新,不需要额外处理。图例如果要按图斑类型动态筛选,那就超出数据驱动页面的能力了,得靠图层定义查询配合脚本切换,这部分后面章节不提,量力而行。
2.4 三种出图范围的选型:统一比例尺还是统一「框住图斑」
范围方式我一般只在这两种之间选,第三种留作特殊情况:
- 中心点与比例尺:每页比例尺固定,图斑放在地图框中心。适合图斑尺寸相近的场景,出图后几张图叠在一起对比很直观。代价是大图斑会被裁掉边角,遇到长条形图斑尤其明显,得配合后面的边距参数一起调。
- 最佳范围:按每个图斑的外包自动算范围,图斑一定完整落在图内。适合图斑大小差异悬殊的批次。代价是每页比例尺都不同,图与图之间没法直接比大小。
- 仅中心点:地图框跟着中心走,比例尺完全交给你在别处控制。适合需要在同一个村内用统一比例尺、但中心点各自对齐的场景,新手用这个最容易出空白图,因为比例尺没设对就什么都看不见。
边距参数不要小看。留白太小,图斑外轮廓压在图框边上,打印出来像被切了一刀;留白太大,图斑缩成一个小块,细节全丢了。我一般先用一批里的最大图斑试导两张,看着舒服了再批量跑。
3. arcpy 批量导出:一份能在 10.2 上原样跑通的脚本
3.1 先确认你跑的是 ArcGIS 自带的 Python 2.7
ArcGIS 10.x 桌面版自带 Python 2.7,arcpy 就在里面。系统里另外装的 Python 3 跑不了这套脚本,语法层面就直接挂——脚本里的 print 是不带括号的写法,arcpy.mapping这个模块在 10.x 里才有,Pro 里换成了另一套。先确认版本再往下走:
# 用 ArcGIS 自带的解释器验证 arcpy 可用,路径按自己的安装版本调整 "C:\Python27\ArcGIS10.2\python.exe" -c "import arcpy; print arcpy.GetInstallInfo()['Version']"能打印出版本号就说明环境通了。如果提示找不到模块,多半是用成了系统里的 Python;如果提示许可相关,检查一下 ArcGIS 是不是正常授权过。编辑器用什么无所谓,Notepad++ 的优势是能直接在界面上点运行,省得开命令行切目录,但要注意保存成.py后缀,以及编码选 UTF-8 无 BOM——带 BOM 的 UTF-8 在 Python 2 里会在第一行报语法错误,这个坑很经典。
3.2 脚本骨架:读 mxd、翻页、取页名、导出
下面这份是主体,路径换成自己的就能跑。逻辑只有四步:打开地图文档、拿到数据驱动页面对象、按页码循环、每页导出。
# -*- coding: utf-8 -*- import os import arcpy # 输入输出路径,用原始字符串避免反斜杠被转义 MXD = r"D:\GISWork\SurveyBatch\survey.mxd" OUT_DIR = r"D:\GISWork\SurveyBatch\out" mxd = arcpy.mapping.MapDocument(MXD) ddp = mxd.dataDrivenPages # 数据驱动页面对象,配置存在 mxd 里 name_field = ddp.pageNameField.name # 名称字段的字段名 # 输出目录不存在就建,arcpy 不会替你建目录 if not os.path.exists(OUT_DIR): os.makedirs(OUT_DIR) total = ddp.pageCount # 页面总数,从 1 开始计数 # 关键:range 的上界要 +1,否则最后一页永远出不来 for page_num in range(1, total + 1): ddp.currentPageID = page_num # 切页,这一步会触发地图范围重算 map_name = ddp.pageRow.getValue(name_field) # 取当前页对应行的名称字段 if map_name is None: map_name = "page_%03d" % page_num # 兜底,避免空名拼出 .png out_path = os.path.join(OUT_DIR, "%03d_%s.png" % (page_num, map_name)) # resolution 给 dpi,宽高不写就按布局尺寸自动换算 arcpy.mapping.ExportToPNG(mxd, out_path, resolution=200) print("[%d/%d] %s" % (page_num, total, map_name)) print("done")几处值得展开说:
ddp.currentPageID = page_num是整套流程的心脏。赋值之后,布局里的地图框范围、动态文本、比例尺全部按这一页重算,所以取pageRow必须放在赋值之后,顺序反了拿到的是上一页的数据,这种错很难看出来,因为图还是出得来,只是属性错位。
ddp.pageCount的计数从 1 开始,range(1, pageCount)会停在倒数第二页。原稿里那句range(1, mxd.dataDrivenPages.pageCount)就是这个问题,一百个图斑出九十九张,而且缺的是最后一张,不核对数量根本发现不了。
ExportToPNG里的resolution是导出分辨率。想换成 JPG,把函数名换成ExportToJPEG,参数含义一致,只是多一个image_quality可以控制压缩质量,通常给 80~90 之间,再高文件就大得没意义了。
3.3 导出参数:分辨率、像素尺寸、格式该怎么填
导出函数的参数不少,常用的其实就四个,其他保持默认即可:
| 参数 | 含义 | 常见取值与注意 |
|---|---|---|
out_png/out_jpg | 输出文件完整路径 | 必须是绝对路径,目录要预先存在 |
data_frame | 指定导出哪个数据框 | 不填默认用布局里第一个数据框 |
df_export_width/height | 导出宽高,单位是像素 | 填了就以像素为准,会覆盖分辨率换算 |
resolution | 导出分辨率,单位 dpi | 只填它,宽高按布局实际尺寸自动算 |
world_file | 是否附带世界文件 | 需要做后续配准才开,普通出图关掉 |
image_quality | JPG 压缩质量 | 80~90 之间,掉到 60 以下文字边缘发虚 |
分辨率和像素尺寸的关系是最容易踩的地方。布局尺寸是英寸,给resolution=200,导出像素就等于布局宽高乘以 200。一张 A4 横向布局(11.69 × 8.27 英寸)在 200 dpi 下是 2338 × 1654 像素,300 dpi 就是 3507 × 2481。如果你同时把df_export_width设成 2000,那 arcpy 会按 2000 像素宽出图,resolution只影响线宽和字号的表现,最终尺寸以像素那个参数为准——这就是很多人纳闷「我设了 300 dpi 怎么图还是糊」的原因。
我的习惯是:只给resolution,宽高一律不填,让布局尺寸说话。这样版面里所有元素的相对大小、字号、线宽都跟你在 ArcMap 里看到的一致,换机器也不容易出岔子。真需要固定像素尺寸(比如某个系统要求上传 1920×1080),那就反过来,只填宽高,分辨率不写,接受比例尺在视觉上的细微变化。
3.4 输出文件名:把图斑名洗成能落盘的名字
名称字段是给人看的,不一定是给文件系统看的。带斜杠、带冒号、带星号的图斑名在 Windows 上直接写不进去,报错还很不友好。所以在拼路径之前过一道清洗:
# -*- coding: utf-8 -*- import re def safe_name(raw, fallback): """把字段值洗成能当文件名的字符串""" if raw is None: return fallback s = raw if isinstance(raw, unicode) else raw.decode("utf-8", "replace") s = s.strip() if not s: return fallback # Windows 文件名非法字符统一替换成下划线 s = re.sub(ur'[\\/:*?"<>|\r\n\t]', u"_", s) # 结尾的点和空格会被系统吞掉,导致后面判断文件存在失效 s = s.rstrip(u" .") # 截断长名,给目录前缀和扩展名留余量,避开 260 字符路径上限 if len(s) > 60: s = s[:60] return s调用的时候把fallback传成"page_%03d" % page_num,空名和图斑都有救。文件名前缀补页码有个实际好处:目录里按名字排序时,图的顺序和翻页顺序一致,翻查起来不用猜。
路径长度这条别不当回事。Windows 默认单条路径上限 260 个字符,输出目录本身如果已经嵌了三层中文文件夹,再加上清洗后的长图斑名,很容易超限,表现是「导出了但目录里找不到文件」或者直接报路径不存在。稳妥做法是输出目录尽量浅,比如固定一个批次目录,下面按乡镇或村分一层就够了。
4. 排查清单:批量出图翻车最多的五个地方
4.1 出图数量比图斑少一张
现象:索引层有 120 个图斑,输出目录里只有 119 张,程序和文件都没报错。
原因:range(1, ddp.pageCount)的上界是开区间,而pageCount本身是从 1 开始计数的总数,循环到pageCount - 1就停了。这种错最阴的地方是只差一张,不核对数量根本发现不了,交出去被打回来才知道少。
解决:上界写成ddp.pageCount + 1。跑完补一道校验:len(os.listdir(OUT_DIR))和ddp.pageCount比一下,不相等就停下来查,别直接往下一批走。
4.2 后一张把前一张覆盖了
现象:图斑数量 120,输出只有 96 张,而且某些图看起来是同一张。
原因:名称字段有重复值或空值。重复的会互相覆盖,空值会拼出同一个.png文件名。卫片图斑数据从导入到整理中间经手好几次,重复行是家常便饭。
解决:导出前先查重复和空值,简单的一条 SQL 就能看出来。更稳的做法是文件名强制带页码前缀("%03d_%s"),页码唯一,绝不会覆盖。这条我建议直接写死在脚本里,别指望数据干净。
4.3 跑到中途报错,提示文件名非法或路径不存在
现象:前几十张出得好好的,跑到某个图斑突然抛异常退出,后面全军覆没。
原因:两个可能。一是名称字段里带了/或:这类非法字符,Windows 直接拒绝写入;二是路径长度超了 260 字符,表现类似。前面没出问题只是因为恰好没碰上。
解决:用 3.4 的清洗函数统一处理,并且在循环里加 try/except 把单页失败记录下来继续跑,别让一张图卡死整批。失败列表落到日志里,改完数据单独重跑这几页。
4.4 中文图斑名变成问号或乱码
现象:文件名里的中文变成??或者一串乱码,图本身是好的。
原因:Python 2 的字符串编码在中文环境下本来就容易出玄学。脚本头没写编码声明,或者编辑器存成了 GBK,字符一路传下去就歪了。另外print输出到控制台时也可能抛UnicodeEncodeError,抛了就中断整个循环。
解决:脚本第一行固定# -*- coding: utf-8 -*-,编辑器统一存 UTF-8 无 BOM,文件名拼接前用decode/encode显式转换一次。print不确定能打出来的时候,就干脆别打中文,改成打页码和序号。
4.5 导出的图不是太大就是太糊
现象:设了 300 dpi,出来的图也就一千多像素宽,放大后文字发虚;或者反过来,图巨大到几十兆一张。
原因:同时设了df_export_width/height和resolution。像素尺寸的优先级更高,resolution只影响渲染表现,不影响最终像素数。两个参数一起给,结果就是你以为在控 dpi,其实在控像素。
解决:二选一。要按版面尺寸走,只给resolution;要按固定像素走,只给宽高。我一般选前者,初测时挑一张图看看像素数对不对,再批量跑。
5. 从「能出」到「能交付」:清单对账与断点续跑
5.1 先定目录与命名规范,再写脚本
脚本能跑通只是及格线。一百多张图交出去,对方要能一眼找到某个图斑对应的那张图,靠的就是命名规范。我现在固定用「页码_P_图斑号」的格式,比如007_图斑A015.png:页码保证排序和翻页一致,前缀切断重名,后缀是对方认识的语言。
目录结构也是同理。批次目录下面按乡镇或村分一层,目录名用编码而不用中文全称,避免嵌套过深触发路径长度限制。一个典型的结构长这样:
D:\GISWork\SurveyBatch\ ├─ survey.mxd # 配好数据驱动页面的地图文档 ├─ out\ # 输出根目录 │ ├─ 01_xz\ # 乡镇编码目录 │ └─ 02_xz\ ├─ out_list.csv # 出图清单,与输出目录一一对应 └─ log\ # 运行日志与失败清单清单 CSV 不只是给自己看的。交付时把清单一起给出去,对方按清单核数量、按图斑号找文件,比在文件夹里翻半天靠谱得多。清单里我一般留四列:页码、图斑号、输出文件名、状态(ok / skip / fail)。
5.2 出一份清单 CSV,和输出目录对账
清单的写法跟导出放同一个循环里最省事,导一张记一行,中途崩了也有记录。写完一批,再拿清单去对输出目录,缺了哪几张一目了然。
# -*- coding: utf-8 -*- import csv import os import arcpy MXD = r"D:\GISWork\SurveyBatch\survey.mxd" OUT_DIR = r"D:\GISWork\SurveyBatch\out" LIST_CSV = r"D:\GISWork\SurveyBatch\out_list.csv" mxd = arcpy.mapping.MapDocument(MXD) ddp = mxd.dataDrivenPages name_field = ddp.pageNameField.name # Python 2 下用 wb 模式写 CSV,避免多余空行 with open(LIST_CSV, "wb") as f: writer = csv.writer(f) writer.writerow(["page_id", "map_name", "out_file", "status"]) for page_num in range(1, ddp.pageCount + 1): ddp.currentPageID = page_num raw = ddp.pageRow.getValue(name_field) map_name = safe_name(raw, "page_%03d" % page_num) file_name = "%03d_%s.png" % (page_num, map_name) out_path = os.path.join(OUT_DIR, file_name) try: arcpy.mapping.ExportToPNG(mxd, out_path, resolution=200) status = "ok" except Exception as e: status = "fail: %s" % e # 单页失败不中断整批 writer.writerow([page_num, map_name, file_name, status]) f.flush() # 每行刷盘,崩溃时已完成记录不丢 print("list written")对账的时候用清单里的out_file列去os.path.exists一遍,状态和实际文件对不上的挑出来单独重跑。这一步看着多余,实际能省掉很多来回——特别是批量跑完之后发现目录里混进了上一批的旧文件,光看数量是对得上的,按清单核才看得出新旧。
另外把except写进循环里这件事,第一版脚本我从来不写,觉得出错了就该停下来看。跑到第五批的时候改主意了:一百多页里坏一页,重跑一次半小时,加个 try 三分钟的事。血泪经验。
5.3 断点续跑:中断之后不用从头再来
批量出图最怕的不是慢,是跑到 80% 崩了要从头再来。加一个存在性判断就能解决:
# 循环内,拼好 out_path 之后、导出之前 if os.path.exists(out_path): status = "skip" # 已存在就跳过,配合清单记录 else: arcpy.mapping.ExportToPNG(mxd, out_path, resolution=200) status = "ok"判断依据是文件名而不是页码,所以改过命名规则之后旧图不会被误判成已完成——这既是优点也是陷阱:改命名规则等于全部重出,心里要有数。如果只是想重出某几张,删掉对应文件再跑一遍即可,其余自动跳过。
配合清单用效果最好:先跑一轮全量,把fail的挑出来修数据,再跑一轮,skip会把已经成功的全部跳过,只补失败的那几张。一百多张图的批次,补图通常几分钟收工。
还有个小 optimization:ddp.currentPageID赋值会触发整页重绘,翻页必然慢,这一条没法绕开。能优化的是图层本身——索引层别挂不必要的标注,图面层该开定义查询就开,影像用金字塔而不是原始大图。这些不在脚本层面,但影响的是同一件事的耗时。
6. 出图前先跑一遍体检:把问题挡在导出之前
导出的报错信息大多不友好,与其出错了回头查,不如出图前先把索引层扫一遍。我现在习惯先跑一个几十行的体检脚本,检查四件事:名称为空、名称重复、名称含非法字符、几何为空或面积为零。这四类问题能覆盖上面章节里绝大多数翻车场景。
# -*- coding: utf-8 -*- import re import collections import arcpy FC = r"D:\GISWork\SurveyBatch\data.gdb\patch_index" # SHAPE@AREA 直接取面积,省得再算一遍 rows = arcpy.da.SearchCursor(FC, ["OID@", "NAME", "SHAPE@AREA"]) names, issues = [], [] for oid, name, area in rows: if name is None or not unicode(name).strip(): issues.append((oid, u"名称为空")) continue text = unicode(name).strip() if re.search(ur'[\\/:*?"<>|]', text): issues.append((oid, u"名称含非法字符: %s" % text)) if area is None or area == 0: issues.append((oid, u"几何为空或面积为零")) names.append(text) # 重复名称单独统计,出现次数大于 1 的就是隐患 for n, c in collections.Counter(names).items(): if c > 1: issues.append((n, u"名称重复 %d 次" % c)) del rows for item in issues: print(item) print(u"共发现 %d 处问题" % len(issues))脚本输出直接贴进 Excel,按问题类型分组,让数据那边批量改。改完之后再跑一次,输出「共发现 0 处问题」才动导出。这一步花的时间通常不到五分钟,但能省掉一次半小时的重跑,还能避免交出去的图里混着错名。
有个判断标准值得说清楚:名称重复不一定要改数据,如果业务上确实允许同名(比如同一村的两个图斑都叫「新增耕地」),那就别靠名称字段做文件名,改成「页码_P_图斑号」这种带唯一编号的组合,名称只用来做版面里的动态文本。反之,如果名称本身就是唯一的图斑号,那直接用它命名最省事。分辨这两种情况,比无脑清洗重要得多。
从那以后我每次批量出图,都是先跑体检、再落清单、最后才导出,顺序一步都不跳。图斑数据是别人给的,可靠性不由我控制,能控制的只有自己这道闸门。希望帮到你。
本文还有配套的精品资源,点击获取