简介:ezdxf 是一个与 DXF 版本无关的 Python 程序包,面向需要以编程方式创建、读取和修改 DXF 图纸的开发者,尤其适合 CAD 二次开发、工程图纸自动化处理等场景。它支持 R12 至 R2018 各版本 DXF 的读写与新建,能完整保留未知标签,即使图纸包含第三方应用数据也可安全打开与保存,且跨 Windows、Linux、macOS 平台,兼容 CPython 与 pypy3。资源包共 1250 个文件,约 20.12MB,以 717 个 py 源码为主体,辅以 rst 文档、png 示意图、dxf 示例图纸、ttf 字体及少量 pyx、pxd 等 Cython 扩展文件,结构完整,便于查阅源码与文档。目前已有 2418 人学习下载。借助该包,读者可快速掌握 DXF 文件读写、实体创建与修改、版本兼容处理等核心用法,并参考示例图纸与文档搭建自己的 CAD 自动化脚本,是学习 Python 操作 DXF 的实用工具库。
1. 用 Python 读写 DXF:为什么我最后留了 ezdxf 在环境里
如果你做过 CAD 相关的自动化,大概率遇到过这种局面:甲方丢来一堆 DXF 图纸,要求批量改图层颜色、提取所有标注文字、或者把几千个图元按规则重新归类。手动开 CAD 点一整天不现实,用别的库要么读不全、要么写出来的文件打不开。我最早用过一个只能读不能写的库,改完还得回 CAD 里手动另存,等于白干。后来换成 ezdxf,才算把「读—改—写」这条链路跑通。
ezdxf 是一个纯 Python 的 DXF 读写库,不需要装 AutoCAD,不依赖任何商业组件,pip 装完就能用。它覆盖了从 R12 到 R2018 的 DXF 版本,能操作图元、图层、块、标注、文字样式这些核心对象。适合谁?做批量图纸处理的、搞 CAD 数据提取的、需要程序化生成图纸的,以及想把 DXF 接进自己 Python 工具链的人。这篇不是 API 文档翻译,是我自己踩过坑之后整理的一条可复现路径。
2. 环境搭建与第一个可运行脚本:从安装到读出图元
2.1 安装与版本选择
安装本身没什么好说的,但版本选择有个坑值得先讲。ezdxf 的 API 在不同大版本之间有过调整,尤其是add_text和add_mtext的参数、以及doc.header的访问方式。我一般会锁一个近期稳定版,而不是无脑装最新。
# 建议在虚拟环境里装,避免和系统里的 CAD 相关包冲突 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 装 ezdxf,锁一个稳定版本 pip install "ezdxf>=1.1,<2.0" # 验证安装 python -c "import ezdxf; print(ezdxf.__version__)"逻辑说明:用虚拟环境隔离是基本操作,重点在版本约束。>=1.1,<2.0这个范围能避开早期 0.x 版本里一些已经废弃的接口,又不会跳到还没稳定的 2.x。参数上,如果你团队里有人用旧脚本,先确认他们用的哪个版本,再决定要不要统一。
装完之后,先别急着写复杂逻辑,拿一个最小 DXF 跑通读取流程。如果没有现成文件,ezdxf 可以自己创建一个。
import ezdxf # 创建一个新的 DXF 文档,版本用 R2010,兼容性比较好 doc = ezdxf.new(dxfversion="R2010") # 新建一个图层,颜色设为红色(颜色号 1) doc.layers.add(name="MY_LAYER", color=1) # 拿到模型空间,往里加图元 msp = doc.modelspace() msp.add_line((0, 0), (100, 0), dxfattribs={"layer": "MY_LAYER"}) msp.add_circle((50, 50), radius=25, dxfattribs={"layer": "MY_LAYER"}) msp.add_text("测试文字", dxfattribs={"height": 5, "layer": "MY_LAYER"}).set_placement((10, 10)) # 保存 doc.saveas("demo_output.dxf") print("已生成 demo_output.dxf")逻辑说明:ezdxf.new()的dxfversion参数决定输出文件的 DXF 版本,R2010 是很多下游软件都能读的折中选项。doc.layers.add()里color=1是 AutoCAD 颜色索引(ACI),1 代表红。dxfattribs这个字典是 ezdxf 里非常核心的一个参数,几乎所有add_*方法都靠它来指定图层、颜色、线型等属性。set_placement是给文字定位的,比直接传insert更直观。
2.2 读取已有 DXF 并遍历图元
生成只是热身,实际工作里更多是读别人的文件。下面这段遍历所有图元、按类型统计并提取文字内容。
import ezdxf from collections import Counter doc = ezdxf.readfile("input.dxf") msp = doc.modelspace() type_counter = Counter() texts = [] for entity in msp: dxftype = entity.dxftype() type_counter[dxftype] += 1 # 提取 TEXT 和 MTEXT 的文字内容 if dxftype == "TEXT": texts.append(entity.dxf.text) elif dxftype == "MTEXT": texts.append(entity.text) # MTEXT 用 .text 属性 print("图元类型统计:", dict(type_counter)) print("提取到的文字条数:", len(texts)) for t in texts[:10]: print(repr(t))逻辑说明:ezdxf.readfile()会自动识别 DXF 版本。遍历msp得到的每个entity,用dxftype()判断类型。注意 TEXT 和 MTEXT 取文字的方式不一样:TEXT 用entity.dxf.text,MTEXT 用entity.text,这个差异我第一次写的时候搞混了,结果 MTEXT 全取到空字符串。Counter用来快速统计图元构成,排查文件里到底有什么东西时很好用。
参数上,readfile还有个encoding参数,遇到中文乱码时可以显式指定,但大多数情况让它自动检测就行。如果文件很大,遍历时可以考虑用msp.query("TEXT MTEXT")只取需要的类型,比全量遍历快。
3. 图元操作与批量修改:图层、文字、块的实际处理
3.1 图层批量改名与颜色调整
批量改图层是高频需求。比如甲方要求把所有以OLD_开头的图层统一改成NEW_前缀,同时把颜色改成指定值。
import ezdxf doc = ezdxf.readfile("input.dxf") # 遍历所有图层,按规则改名和改色 rename_map = {} for layer in doc.layers: old_name = layer.dxf.name if old_name.startswith("OLD_"): new_name = "NEW_" + old_name[4:] rename_map[old_name] = new_name layer.dxf.name = new_name layer.dxf.color = 3 # 绿色 # 关键一步:图元上引用的图层名也要同步更新 for entity in doc.modelspace(): layer_name = entity.dxf.get("layer", None) if layer_name in rename_map: entity.dxf.layer = rename_map[layer_name] doc.saveas("output_renamed.dxf") print("改名映射:", rename_map)逻辑说明:这里有个容易翻车的地方——改了doc.layers里的图层名,并不自动更新图元上引用的图层名。图元通过dxf.layer存的是图层名字符串,不是对象引用。所以必须再遍历一遍图元,把旧名字替换成新名字。我第一次只改了图层表,结果打开文件发现图元全跑到默认图层 0 上去了,血泪经验。
参数上,layer.dxf.color用的是 ACI 颜色号,常见值:1 红、2 黄、3 绿、4 青、5 蓝、6 品红、7 白/黑。如果要真彩色,得用layer.rgb属性,但兼容性不如 ACI。
3.2 文字内容批量替换与块内图元处理
文字替换看起来简单,但块(BLOCK)里的文字是另一个坑。块定义里的图元不在模型空间直接遍历到,得单独处理。
import ezdxf doc = ezdxf.readfile("input.dxf") def replace_text_in_entities(entities, old, new): """在给定图元集合里替换文字内容""" count = 0 for e in entities: if e.dxftype() == "TEXT": if old in e.dxf.text: e.dxf.text = e.dxf.text.replace(old, new) count += 1 elif e.dxftype() == "MTEXT": if old in e.text: e.text = e.text.replace(old, new) count += 1 return count # 1. 处理模型空间 n1 = replace_text_in_entities(doc.modelspace(), "旧项目名", "新项目名") # 2. 处理所有块定义内部的图元 n2 = 0 for block in doc.blocks: n2 += replace_text_in_entities(block, "旧项目名", "新项目名") # 3. 处理布局(图纸空间) n3 = 0 for layout in doc.layouts: n3 += replace_text_in_entities(layout, "旧项目名", "新项目名") doc.saveas("output_replaced.dxf") print(f"模型空间替换 {n1} 处,块内替换 {n2} 处,布局替换 {n3} 处")逻辑说明:doc.blocks遍历的是所有块定义,块定义本身也是可迭代的图元集合。doc.layouts包含模型空间和各个图纸空间布局。三处都处理完才算干净。很多人只处理模型空间,结果图纸边框里的标题栏文字没换掉,交付时被退回来。
参数上,替换逻辑用的是 Python 字符串的replace,区分大小写。如果要不区分大小写,得先lower()比较再处理,但那样会丢失原始大小写,需要额外记录。块内文字如果属性是ATTRIB(块属性),处理方式又不一样,得用entity.dxf.tag和entity.dxf.text配合,这个后面进阶部分再说。
4. 避坑与排查:那些让我重跑脚本的瞬间
4.1 中文乱码:现象是文字变成问号或方块
现象:读进来的中文文字显示为???或者乱码方块。原因:DXF 文件的编码声明和实际内容不一致,早期 R12 版本尤其常见,有些文件头写的是 ANSI 但内容实际是 GBK。解决:ezdxf.readfile()加encoding参数显式指定,比如encoding="gbk"。如果还不确定,先用二进制方式读文件头几行看$DWGCODEPAGE的值。
4.2 保存后文件打不开:现象是 CAD 提示文件损坏
现象:脚本跑完没报错,但用 CAD 打开提示文件损坏或版本不兼容。原因:多半是dxfversion设得太老或太新,或者写入了一些该版本不支持的图元属性。解决:统一用R2010或R2013,这两个版本兼容性最稳。如果必须输出 R12,注意 R12 不支持 MTEXT、LWPOLYLINE 等图元,得先转换。
4.3 图元丢失:现象是遍历数量对不上
现象:明明文件里有很多图元,遍历modelspace()却只拿到一部分。原因:图元可能在块定义里、在图纸空间布局里,或者被放在了paperspace而不是modelspace。解决:分别遍历doc.blocks、doc.layouts,用Counter统计各处的图元数量,定位到底在哪。另外,msp.query()支持按类型过滤,比手动判断更可靠。
4.4 性能问题:现象是处理大文件时内存暴涨
现象:处理几万个图元的 DXF 时,内存占用飙升甚至卡死。原因:一次性把所有图元读进列表,或者频繁saveas。解决:用生成器方式遍历,不要list(msp);批量修改完再统一保存一次,不要每改一个图元存一次。如果文件特别大,可以考虑用ezdxf.addons里的drawing模块做流式处理。
4.5 块属性取不到:现象是 ATTRIB 文字为空
现象:块参照里的属性文字用普通 TEXT 方式取不到。原因:块属性是ATTRIB类型,挂在INSERT图元下面,不是独立的 TEXT。解决:先找到INSERT图元,再遍历它的attribs,用attrib.dxf.text取值。如果要改,改完还得调attrib.dxf.text = new_value,并且确保块定义里有对应的ATTDEF。
5. 进阶技巧:块属性操作与 DXF 版本转换的验证方法
块属性这块值得单独拎出来讲,因为它是自动化改图纸标题栏的核心。假设你有一个带标题栏的图框块,里面有「项目名称」「图号」「日期」几个属性,要批量替换。
import ezdxf doc = ezdxf.readfile("input.dxf") msp = doc.modelspace() # 找到所有块参照(INSERT),按块名筛选 target_block_name = "TITLE_BLOCK" updated = 0 for insert in msp.query(f'INSERT[name=="{target_block_name}"]'): for attrib in insert.attribs: tag = attrib.dxf.tag if tag == "PROJECT_NAME": attrib.dxf.text = "新项目名称" updated += 1 elif tag == "DRAWING_NO": attrib.dxf.text = "DWG-2024-001" updated += 1 doc.saveas("output_titleblock.dxf") print(f"更新了 {updated} 个属性")逻辑说明:msp.query('INSERT[name=="TITLE_BLOCK"]')是 ezdxf 的查询语法,按图元类型和属性过滤,比手动遍历快也准。insert.attribs拿到的是该块参照的所有属性,每个attrib有dxf.tag(属性标签)和dxf.text(属性值)。改完直接保存即可,不需要额外同步块定义,因为属性值是存在块参照上的。
参数上,查询语法里的name是块名,区分大小写。如果块名不确定,可以先遍历所有 INSERT,打印insert.dxf.name看看。
版本转换的验证方法:转换完不要只看脚本有没有报错,要实际验证。我一般会做三步——第一步,用 ezdxf 重新读一遍输出文件,确认图元数量和类型统计和预期一致;第二步,用doc.dxfversion确认版本号;第三步,如果条件允许,用目标 CAD 软件实际打开一次,看有没有警告。
import ezdxf # 验证输出文件 doc = ezdxf.readfile("output_titleblock.dxf") print("DXF 版本:", doc.dxfversion) print("图层数:", len(doc.layers)) print("块定义数:", len(doc.blocks)) msp = doc.modelspace() from collections import Counter counter = Counter(e.dxftype() for e in msp) print("模型空间图元统计:", dict(counter)) # 检查是否有图元落在不存在的图层上 layer_names = {layer.dxf.name for layer in doc.layers} orphans = [e.dxf.layer for e in msp if e.dxf.get("layer") and e.dxf.layer not in layer_names] print("孤立图层引用:", set(orphans) if orphans else "无")逻辑说明:这段验证脚本是我每次批量处理完都会跑的。doc.dxfversion确认版本,Counter确认图元构成,最后那个孤立图层检查能发现「图元引用了不存在的图层」这种隐蔽问题——这种文件在 CAD 里打开可能不报错,但图元显示会异常。
从那以后我每次批量改完 DXF,都强制走一遍「重读—统计—孤立图层检查」这三步,宁可多花三十秒,也不想交付后被退回来重跑。希望帮到你。
本文还有配套的精品资源,点击获取