前阵子接了个不大不小的活儿:六个行政区图层,每个都要导出一张标准尺寸的PNG,再配一份能继续编辑的Mxd工程文件。如果手动开ArcMap,调一个图层范围、设比例尺、导图、另存Mxd,一套下来少说五六分钟,六十套就是大半天,还容易漏改标题。后来我用ArcPy写了一个循环,从读模板、替换数据源、导出图片到另存Mxd全自动,跑了不到半小时。这篇就把这套“零门槛小白版”的做法完整拆开讲,照着抄就能用。
1. 先搞清楚:这个脚本要解决什么问题
1.1 什么样的重复劳动适合交给ArcPy
开始之前,先别急着复制代码。你首先要判断一件事:手里的活到底适不适合脚本化。我总结了一个很土的标准——只要你需要连续点鼠标十次以上,并且每一步都在做“打开文档、缩放、导出、保存”这类重复动作,就值得让脚本接手。ArcPy在这类场景里有两个杀手锏:一是能用代码读写ArcMap的Mxd文档结构,二是能直接调用导出图片的底层工具。换句话说,凡是你在界面上能反复点的操作,脚本基本都能做,而且速度更快、不容易漏。
比如最常见的分乡镇出图:一个县域底图,叠加二三十个乡镇界,逐个乡镇导出PNG。手工操作需要不断切换制图范围、改图名、导出,还要防止上一次的比例尺串到下一张。用ArcPy把这些动作写进循环,再多的乡镇也只是把循环多跑几轮。再比如你手里有几十个图层需要套用同一套符号和版式时,脚本能保证每一张图的图例、比例尺、指北针、标题字体完全一致,不会因为手动操作产生细微偏差。更适合的场景,是那种“数据更新了,图要重出”的固定任务——每次只要重新跑脚本,输出目录里的图片和Mxd就整整齐齐地刷新一遍。
不过也得说清楚,ArcPy不是万能的。如果你需要每张图都做复杂的交互式调整,比如手工修图、临时改注记位置、微调制图表达,那脚本会变得非常繁琐。此时最好的方案是脚本处理大头,剩下几张特殊图再手动收尾。聪明的自动化从来不是消灭人工,而是把人工从重复劳动里解放出来。
1.2 为什么出图和生成Mxd要放在一起做
很多初学者不理解,我只要图片,为什么非要同时生成Mxd?图片导出后,成果看起来已经完成了,但真正做项目的人都知道,图片是“死”的,Mxd才是“活”的。客户看完图提一句“标题换个写法”“符号颜色再深一点”,如果只有PNG,你还得重新找数据、新建工程、重新配符号。如果当时顺手留了一份Mxd,后续修改就是打开文档改两笔再导出一遍的事。
更重要的是,Mxd保存了制图状态:数据框范围、图层顺序、符号系统、页面布局、图例位置。这些信息用脚本生成时是高度统一的,如果靠手工一个个另存,很容易出现“这张图范围没对上”“那张图比例尺还是默认的”之类的毛病。脚本循环里把出图和保存Mxd放在同一个处理单元里,保证一张图像对应一份Mxd,命名一致、内容一致,后期追溯起来极其省心。
具体到技术实现,ArcPy里并没有一个叫“CreateMapDocument”的函数能直接凭空造一个Mxd。业内常规做法,是把一个做好的模板Mxd复制出多个实例,保存时用saveACopy另存为新文件。这也是为什么我会把这一节排在环境准备之前——你要先在脑子里把“复制模板—改数据—导出—另存”这条主线立起来,后面看代码才不晕。
2. 动手前准备:版本、目录和模板
2.1 版本怎么选:ArcMap还是ArcGIS Pro
ArcPy分两大体系,容易搞混。ArcMap 10.x时代用的是arcpy.mapping模块,专门操作Mxd文档;ArcGIS Pro时代改成了arcpy.mp模块,操作的是Aprx工程文件。标题里要生成Mxd,所以必须使用ArcMap 10.x环境,而不是ArcGIS Pro。哪怕你电脑上装了Pro,也别指望这段代码直接复制过去能跑,Mxd和Aprx文件格式完全不同。
另一个需要提前确认的坑是Python版本。ArcMap 10.x自带的是Python 2.7,微软官方早就不维护了,所以脚本里尽量别写Python 3的新语法,比如f-string、pathlib这类,老老实实用format或者百分号拼接。我在代码里用的print也兼容2.7。如果你习惯用Anaconda或者系统装了Python 3,请不要直接双击py文件,ArcPy这个库是跟着ArcGIS许可证走的,必须用ArcGIS自带的Python解释器来运行。后面第四章会专门讲怎么跑最稳。
还需要注意ArcMap的版本小差异。10.2以后arcpy.mapping基本稳定,replaceDataSource、ExportToPNG这些接口都在。建议用10.4以上版本,遇到怪问题的概率更低。如果你的机器里装了多个ArcGIS版本,运行脚本时要确认调用的python.exe来自同一个版本,否则会出现“Import arcpy失败”这类最低级也最打击人的报错。
2.2 目录规划:先想好数据往哪放
批量脚本最怕路径混乱。我在实际项目里吃过亏,一开始把所有输出都扔到同一个文件夹,跑了半小时后想找某一个乡镇的图,文件名前缀还都一样,翻了好几层才找到。后来学乖了,任何批量任务都按固定结构安排目录。下面这个结构可以作为参考:
D:\project\ ├─ 00_模板 │ └─ 底图模板.mxd ├─ 01_数据 │ ├─ 区县A.shp │ ├─ 区县B.shp │ └─ 区县C.shp ├─ 02_输出 │ ├─ 图片 │ └─ 工程文件这样做的理由很直接:模板和数据分开,避免脚本在扫描数据时把模板Mxd也当成图层源;输出目录单独建,后期打包成果时直接拷02_输出整个文件夹走人。所有路径建议用英文,不要带空格,更不要带中文。虽然ArcMap对中文路径的兼容性比早期好很多,但在Python 2.7环境下,中文路径偶尔会触发UnicodeDecodeError,为了省事,英文路径最安全。
如果你的数据比较多,建议把要处理的要素类全部放进同一个文件地理数据库,或者同一个shapefile文件夹。因为后面代码里用到arcpy.ListFeatureClasses,它只会列出当前工作空间下的要素类,这样循环逻辑最简单。如果数据分散在好几个文件夹,你需要再写一层os.walk或循环拼接路径,对小白并不友好。
2.3 做一个不会出错的模板Mxd
模板是整个方案的灵魂。我做过的批量出图脚本,最后出问题几乎没有一次是代码本身有bug,全怪模板少了元素或者图层名对不上。制作模板的步骤不复杂,但每一步都有讲究。
先在ArcMap里新建一个空白地图文档,插入一个数据框,然后随便加载一个占位图层。这个占位图层建议用一个最简单的shapefile,名称必须和脚本里约定的“替换图层”完全一致,包括引号里的内容。脚本会按图层名找到它并替换数据源,这个名字一旦不一致,循环就会跳过替换,最后导出的就是一张占位图。图层名最好是纯英文或数字,尽量避免中文,因为Python 2.7的字符串编码坑很多,少碰为妙。
接下来做页面布局。在布局视图里插入图例、比例尺、指北针、标题文本框等元素。图例必须关联到占位图层上,这样后面替换数据源时,图例条目才会跟着刷新。标题文本框建议在属性里设置一个固定的元素名,比如txt_title,脚本里通过ListLayoutElements找到这个元素名来改文字。ArcMap布局元素默认名称往往又长又乱,手动改一个简短英文名,脚本代码会清爽很多。
最后,把模板另存到00_模板目录下。存好之后,最好自己手动打开一次模板,确认占位图层能正常显示,图例不是空的。模板一旦做好,后面所有的事情都变成流水线了。这也是我最想强调的一点:模板越规范,脚本越省心。
3. 核心代码:替换数据源、出图、另存Mxd
3.1 核心思路:从“一张图”推到“一批图”
写批量脚本之前,千万别直接写for循环,先把“一张图怎么完成”想清楚。我习惯先假设只有一个要素类需要出图,手工流程是什么样的:打开模板Mxd,把某个图层换成目标数据,缩放到目标数据范围,改一下标题文字,导出PNG,另存为一份新的Mxd。把这个串行流程写成代码,就是脚本的正文部分。
然后才考虑循环:这一批要处理的要素类列表是什么?用arcpy.ListFeatureClasses获取,或者用os.listdir过滤出.shp文件。每跑一个要素类,就重新读取一次模板Mxd,执行刚才的那串流程,输出文件名用要素类名称。这样一个抽象过程,既能让你写代码时思路清晰,也能在后期排查问题时快速定位每一步。
这里特别说一下“重新读取模板”这个动作。有些新手会把模板Mxd放在循环外面只读一次,然后不断saveACopy。这种做法非常危险,因为saveACopy不会改内存里那个Mxd对象,但你对数据框、图层的修改会累积在同一个对象上,第二轮循环时图层可能已经被替换过很多次,状态完全不可控。每轮重新MapDocument(template_mxd)最保险,代价就是多花一点读取时间,但对小白来说,正确性远比那零点几秒重要。
3.2 完整脚本(建议直接抄)
下面这个脚本我整理成了可直接运行的版本,路径要改成你自己的目录。它适合处理同一个文件夹下的多个shapefile,或者同一个地理数据库里的多个要素类。
# -*- coding: utf-8 -*- import arcpy import os arcpy.env.overwriteOutput = True # ===== 用户需要修改的路径 ===== template_mxd = r"D:\project\00_模板\底图模板.mxd" data_workspace = r"D:\project\01_数据" out_img_dir = r"D:\project\02_输出\图片" out_mxd_dir = r"D:\project\02_输出\工程文件" # ============================== if not os.path.exists(out_img_dir): os.makedirs(out_img_dir) if not os.path.exists(out_mxd_dir): os.makedirs(out_mxd_dir) arcpy.env.workspace = data_workspace fc_list = arcpy.ListFeatureClasses() for fc in fc_list: # 每次从模板重新读取一份新的地图文档 mxd = arcpy.mapping.MapDocument(template_mxd) df = arcpy.mapping.ListDataFrames(mxd)[0] # 找到占位图层,替换数据源 target_layer = None for lyr in arcpy.mapping.ListLayers(mxd): if lyr.name == "替换图层": lyr.replaceDataSource(data_workspace, "SHAPEFILE_WORKSPACE", fc, False) lyr.name = fc target_layer = lyr break if target_layer is None: print("模板里没找到占位图层,请检查模板!") continue # 把数据框范围缩放到当前数据范围 df.extent = target_layer.getExtent() # 修改标题文本元素 for elm in arcpy.mapping.ListLayoutElements(mxd, "TEXT_ELEMENT"): if elm.name == "txt_title": elm.text = "专题图:" + fc # 导出PNG out_png = os.path.join(out_img_dir, fc + ".png") arcpy.mapping.ExportToPNG(mxd, out_png, width=1600, height=1100, resolution=150) # 另存为新的Mxd out_mxd = os.path.join(out_mxd_dir, fc + ".mxd") mxd.saveACopy(out_mxd) print("生成完成:" + fc) # 释放内存,避免批量处理时进程占用过高 del mxd这段代码不长,但每一行都值得解释清楚。arcpy.env.overwriteOutput = True表示输出文件存在时直接覆盖,不会弹“文件已存在”的提示框。arcpy.env.workspace = data_workspace把脚本的工作空间切到数据目录,后面的ListFeatureClasses才能列出这些要素类。replaceDataSource是替换数据源的核心方法,参数分别是数据工作空间路径、工作空间类型、数据集名称、是否验证。最后那个False是不做额外验证,速度更快,如果怕出错可以改成True,能提前发现数据源填错的问题。
3.3 数据源类型参数怎么填
很多小白卡在replaceDataSource的第二个参数。这里给一个简单速查:如果你处理的是shapefile,工作空间类型填SHAPEFILE_WORKSPACE,dataset_name填不带.shp后缀的文件名;如果你处理的是文件地理数据库里的feature class,工作空间类型建议填FILEGDB_WORKSPACE,dataset_name填要素类名字;如果你处理的是个人地理数据库,需要用ACCESS_WORKSPACE。填错参数最常见的报错是ERROR 000824,提示数据源不支持,回头查一下工作空间类型往往就是答案。
还有一种更省事的写法,是把workspace_type留空字符串或者填NONE,让ArcPy根据workspace路径自动判断。这个写法在部分版本里能用,但不是百分之百稳定,为了减少不确定性,我建议还是老老实实把工作空间类型写清楚。
另外注意,如果你的数据是矢量要素类但文件名带中文,在Python 2.7下需要确保脚本顶部有# -*- coding: utf-8 -*-,并且所有字符串都用UTF-8编码。我的建议仍然是尽量用英文文件名,不给自己添堵。
3.4 三个容易踩的细节
第一个细节是图层名不会自动变成数据名。替换数据源后,图层在内容列表里的名字还是“替换图层”,如果不手动设置lyr.name = fc,导出的图例里就会显示占位名。我在脚本里已经加上了这一行,但你要理解为什么加,而不是只当作固定模板。
第二个细节是df.extent = target_layer.getExtent()。这一步相当于界面上“缩放至图层”的操作。如果不设置,导出的图会沿用模板里数据框的旧范围,可能是一片空白,也可能只显示原来的占位数据范围。如果你的模板数据框有固定比例尺需求,比如必须1:10000,那这里就不能用getExtent,而要改用数据驱动页面或者手动指定比例尺。
第三个细节是saveACopy和save的区别。saveACopy会另存为一个新文件,不影响原模板;save会直接把当前对象写回原路径。脚本里的模板在循环里被反复读取,如果你用了save,第一轮循环就把模板覆盖了,第二轮读取的可能就是已经改过的文件,整个逻辑直接崩盘。养成用saveACopy的习惯,模板永远保留一份原始版本。
4. 手把手把脚本跑起来
4.1 在ArcMap的Python窗口里运行
最省心的运行方式,是直接用ArcMap自带的Python窗口。打开ArcMap,新建一个空白地图文档,不需要做什么操作,直接调出Python窗口,把上面那段脚本整体粘贴进去,修改路径后回车。因为ArcMap进程已经带好了ArcPy环境,只要许可证没问题,基本一次跑通。
需要注意的是,脚本里的模板Mxd不能正被同一个ArcMap程序集中打开编辑,否则文件被占用,读取或另存时可能报权限错误。我习惯把模板放在单独的目录,运行脚本时ArcMap窗口保持空白地图就行。还有,Python窗口里大量输出会滚动得比较慢,如果图层数量特别多,建议临时把print注释掉,只看最后结果,速度和观感都好很多。
如果脚本中途报错,Python窗口会高亮报错行,但不会很方便地调试。小白阶段可以试试在关键位置加print输出,比如“已经找到占位图层”“正在导出XXX”,这样一眼就能看出卡在哪一步。脚本跑完后,去图片目录和工程文件目录确认有没有生成文件,文件大小是否正常。
4.2 用外部Python环境运行
如果在Python窗口里运行没问题,但你想脱离ArcMap界面,做成一个每天自动跑的任务,可以改用外部命令运行。打开命令行,用ArcGIS自带的Python解释器执行脚本,命令格式类似这样:
"C:\Python27\ArcGIS10.6\python.exe" "D:\project\批量出图.py"路径里的Python27\ArcGIS10.6要替换成你机器上实际存在的目录,版本号可能不同。这个方法的好处是一次配置好,之后可以双击bat文件或者定时任务自动执行。注意不要用系统自带的Python或者Anaconda的python.exe,否则第一行import arcpy就会报No module named arcpy。
在外部运行还有一个好处,ArcGIS的主界面不用一直开着,能节省不少内存。我之前处理三百多个图层时,ArcMap界面留在那里特别占内存,用外部Python跑,进程释放更干净,速度还快一些。如果你在公司生产环境用,建议优先考虑命令行方式。
4.3 做成ArcGIS脚本工具,让别人也能用
如果你不是只给自己用,还可以把脚本包装成ArcMap里的“脚本工具”。在ArcToolbox里右键添加工具箱,选择添加脚本,然后指定刚刚写好的py文件,把模板路径、数据工作空间、输出目录都设置成参数。脚本内部用arcpy.GetParameterAsText(0)这种方式接收参数,比硬编码路径好用得多。
做成工具的额外好处是,原来的脚本可以直接拖进ArcToolbox,同事使用时只需要在工具界面里点选文件夹,不容易因为改错代码路径而出问题。但这一步涉及工具箱设置和参数类型选择,如果你今天第一次接触ArcPy,先不用急着做,跑通上面的脚本才是第一步。
5. 常见报错与排查技巧
5.1 报错速查表
批量脚本出问题时,最怕没有头绪。我整理了一份比较常见的报错速查表,基本覆盖新手阶段会碰到的坑。
| 报错现象 | 最常见原因 | 解决办法 |
|---|---|---|
| import arcpy时提示No module named arcpy | 运行环境不是ArcGIS自带Python | 改用ArcGIS的python.exe,或在ArcMap Python窗口运行 |
| ERROR 000824: 数据源不支持 | replaceDataSource的工作空间类型填错 | 核对shapefile或gdb对应类型,填SHAPEFILE_WORKSPACE或FILEGDB_WORKSPACE |
| ERROR 000860: 图层数据源无效 | 数据集中找不到dataset_name | 检查数据文件名是否正确,shapefile不用带.shp |
| 输出PNG是空白 | 数据框范围没刷新或模板占位图层没替换 | 确认df.extent = target_layer.getExtent()已执行 |
| 图例显示“替换图层” | 没有手动改lyr.name | 替换数据源后用lyr.name = fc |
| Mxd另存失败或提示文件被占用 | 模板正在ArcMap中打开编辑 | 关闭模板文档,再重新运行脚本 |
| UnicodeDecodeError | 路径或数据名含中文,脚本编码设置不对 | 改用英文路径;脚本头加# -- coding: utf-8 -- |
| 脚本能跑但第一个图层后就停 | 某个要素数据为空或投影缺失 | 先单独处理该要素,查看getExtent是否正常 |
这张表不是金科玉律,但排查顺序我觉得很有效:先看环境,再查数据源参数,最后看模板。大多数“脚本没反应”的问题,其实都是环境没配对。
5.2 三个翻车瞬间,说给你避坑
第一次用replaceDataSource时,我把工作空间类型填成了NONE,结果第一轮循环就报错。后来才发现,对不同数据类型明确写类型关键字才是最稳的。曾经还有一次,因为模板里的占位图层名带了后缀“_1”,脚本里匹配的是“替换图层”,怎么都替换不上,导出的全是同一个占位数据。那次教训让我养成了习惯:模板里所有图层名尽量简短,并且做完模板后先手动列一次图层名,确保和脚本一致。
另一次翻车是输出图片大小差异很大。有的图层范围覆盖全县,有的只覆盖一个村,导出的图片内容面积不同,比例尺完全不同,看起来很不整齐。原因是直接用getExtent让数据框跟随每个要素全图。如果你希望所有图比例尺统一,就要在模板里固定数据框比例尺,或者按字段批量拆分到同一行政单元,而不是用全图范围。这个需求要看具体场景,没有绝对对错。
还有一个容易忽略的问题是坐标系。如果你的数据框用的是模板里的坐标系,而某个源数据的坐标系不一样,getExtent后范围可能会出现偏移。解决方法是提前把所有数据统一投影到同一个坐标系,或者在模板里为数据框设定好固定坐标系。我在批量出图前,通常会先写一小段检查脚本,把所有待处理数据的坐标系打印出来,发现有不一样的就先做投影,免得跑完才发现图位置不对。
5.3 如何验证输出成果
脚本跑完不代表万事大吉,一定要做抽样验证。我的验证习惯是:每批次随机打开三到五张PNG,先看图层内容是否完整,再看图例文字、标题是否正确,最后把对应的Mxd拖进ArcMap看一眼,确认数据框范围和图片一致。如果脚本处理了几百个文件,至少要抽1%到5%,量少的话我甚至每张都过一遍。
验证Mxd还有一个快捷方式,不用一个个打开。可以写一个小循环,用arcpy.mapping.MapDocument读取每个Mxd,然后打印出数据框范围和图层列表,快速比对有没有异常。如果你不会写这个检查脚本,先用ArcMap手动抽查几份也够用。关键是不要“跑完就认为成功”,文件生成成功和内容正确是两回事。
6. 进阶一点:把图集按字段拆出来
6.1 按字段批量拆图的思路
前面处理的是“一个要素类出一张图”,但更常见的业务是“一个要素类里有二十个乡镇,每个乡镇各出一张图”。这种需求就变成按字段拆分。思路是先用arcpy.da.SearchCursor遍历某个行政区字段的唯一值,然后对每个值设置lyr.definitionQuery,把数据框范围缩放到被过滤后的图层范围,再导出图片。
这里有一个关键转换:同样一个要素类,不再需要replaceDataSource,因为数据源始终是同一个。你在每轮循环里只要修改definitionQuery,然后重新获取一次extent就行。脚本框架和前面很像,只是最外层的循环对象从要素类列表变成了字段值列表。实际跑的时候要注意字段值可能有空格或特殊字符,拼接查询条件时用双引号转义。零门槛阶段先把3.2的脚本吃透,再往这个方向扩展,会顺手得多。
6.2 如果以后换到ArcGIS Pro怎么办
很多朋友可能会问,现在新项目是不是都该用Pro?如果哪天你换到ArcGIS Pro,这套基于arcpy.mapping的代码无法直接运行,要用arcpy.mp模块,生成的文件也从Mxd变成Aprx。两者的对象命名和接口有不少差异,比如MapDocument变成了ArcGISProject,saveACopy变成了saveAsCopy。思路是相通的,都是“模板+循环+导出”,但函数名要重新学。
我对小白的建议是:如果你所在单位还在用ArcMap,就先把这套Mxd方案用熟;如果有新项目从零开始,可以直接学Pro方案。不用惊慌,两个版本切换时,重点是把这里教的“替换数据源、设置范围、导出、另存”四个动作迁移过去,剩下的是查文档的问题。
6.3 我的两点使用体会
最后分享两点我自己的经验。第一,模板值得花一小时去打磨,版式、符号、字体全都定好,脚本只是让模板复制得更多。模板里一个线宽设置错了,批量出图后就是几十张统一错,前期多花的时间会在后期成倍省回来。第二,第一次跑脚本不要一口气跑全量,先指定一个图层试跑,确认PNG和Mxd都没问题后再放开全量。不然等跑到第十张发现问题,前面九张都得返工。这套思路我救过好几次急,希望你下次遇到批量出图也能更从容。