☰
Python 读写 DXF 实战:ezdxf 从入门到批量处理
2026/10/13 14:34:22 网站建设 项目流程

简介:ezdxf 是一个与 DXF 版本无关的 Python 程序包,面向需要以编程方式创建、读取和修改 DXF 图纸的开发者,尤其适合 CAD 二次开发、工程图纸自动化处理等场景。它支持 R12 至 R2018 的读写与新建,未知标签会被忽略但保留以便保存,因此也能打开含第三方应用数据的图纸,且与操作系统无关,已在 CPython 和 pypy3 上测试。资源包共 1250 个文件,约 20.12MB,其中 717 个 py 源码文件构成核心实现,178 个 rst 文档与 10 个 md 说明提供使用指引,131 个 png 与 8 个 jpeg 为示例配图,63 个 dxf 样例便于对照测试,另有 ttf 字体、pyx/py 等 Cython 扩展源码及少量 dwg、svg 等辅助文件。目前已有 2418 人学习下载。借助完整源码、文档与示例图纸,读者可快速掌握 DXF 文件读写、实体创建与版本兼容处理,并参考底层数学类的 Cython 实现优化性能。

1. 用 Python 读写 DXF:为什么 ezdxf 是绕不开的那一层

如果你手头有一堆 CAD 图纸要批量改图层、提坐标、生成料表,或者后端服务需要按参数吐出一张标准 DXF 给下游机床用,那你迟早会撞上「DXF 的 Python 接口」这件事。DXF 是 AutoCAD 的交换格式,本质是一堆组码(group code)加值的文本流,手写解析器不是不行,但版本差异、实体嵌套、块引用、扩展数据这些坑足够让人掉一层皮。ezdxf 就是在这个位置上出现的:它把 DXF 的读写、实体模型、块、图层、标注、三维网格都封装成 Python 对象,让你用msp.add_line()这种直觉写法生成图纸,也能反过来遍历一张几万实体的图纸做统计。它适合三类人:做 CAD 二次开发的、做工程数据批处理的、以及需要把几何数据接进自己算法流水线的。这篇不吹概念,直接讲怎么装、怎么建、怎么读、参数怎么调、哪里会翻车。

2. 环境准备与最小可运行闭环:先把一张 DXF 写出来再读回来

2.1 安装与版本选择:别在版本上省事

ezdxf 是纯 Python 包,没有二进制依赖,安装本身不复杂,但版本选择有讲究。它对新版 DXF 格式的支持是随版本推进的,如果你要处理 R2018 及以上格式,或者用到ACIS实体、MLEADER这类较新的对象,就得用较新的 ezdxf。反过来,如果你的运行环境锁死在某个老 Python 上,那只能退到支持该 Python 的最后一个 ezdxf 版本。

# 建议在虚拟环境里装,避免和系统里的 CAD 相关包冲突 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装最新稳定版 pip install ezdxf # 如果需要画图预览(把 DXF 渲染成 PNG 看效果),加绘图后端 pip install "ezdxf[draw]" # 确认版本,后面排查问题时第一件事就是看这个 python -c "import ezdxf; print(ezdxf.__version__)"

逻辑说明:ezdxf[draw]会拉入 matplotlib 等可选依赖,用于ezdxf.addons.drawing模块把图纸渲染成图片,调试几何位置时非常有用。参数上,如果你只是做数据提取,不装 draw 也完全能跑。注意不要用pip install --pre去装预发布版,除非你在追某个刚修的问题,预发布版的 API 可能和文档不一致。

2.2 新建图纸:doc、msp、图层三件套

最小闭环的第一步是新建一个文档对象,拿到模型空间,然后往里塞实体。这里的关键认知是:ezdxf 里doc是整张图纸,msp是模型空间(你画的东西默认都在这),psp是图纸空间(布局)。新手最容易搞混的就是往 psp 里画了东西却在模型空间找不到。

import ezdxf # 新建一个 R2010 格式的文档,版本用字符串常量,别写魔法数字 doc = ezdxf.new(dxfversion="R2010", setup=True) # setup=True 会预置一些标准线型和文字样式,省得后面自己建 msp = doc.modelspace() # 建一个自己的图层,颜色用 ACI 索引色(1=红,2=黄,3=绿,4=青,5=蓝,7=白/黑) doc.layers.add(name="WALL", color=1) doc.layers.add(name="DIM", color=3) # 在 WALL 图层上画一条线,坐标是 (x, y) 或 (x, y, z) msp.add_line((0, 0), (1000, 0), dxfattribs={"layer": "WALL"}) # 画一个圆,半径 50,圆心在 (500, 200) msp.add_circle((500, 200), radius=50, dxfattribs={"layer": "WALL"}) # 加一段单行文字 msp.add_text("测试标注", height=25, dxfattribs={"layer": "DIM", "insert": (500, 300)}) doc.saveas("demo_min.dxf") print("实体数量:", len(msp))

逻辑说明:dxfattribs是 ezdxf 里给实体挂 DXF 属性的统一入口,layer、color、linetype、lineweight都从这里传。参数上,height是文字高度,单位跟随图纸单位;insert是文字插入点。setup=True这个参数值得单独说:它会在文档里预置标准线型(CONTINUOUS、DASHED 等)和文字样式(Standard),如果你不设,后面用linetype="DASHED"时会报找不到线型。很多人第一次用 ezdxf 画虚线失败,就是栽在这。

2.3 读回图纸:遍历实体与按类型过滤

写出去只是热身,真正的日常是读别人的图纸。读的时候不要一上来就for e in msp全遍历,几万实体的图纸这样跑又慢又难处理,正确做法是按类型查询。

import ezdxf doc = ezdxf.readfile("demo_min.dxf") msp = doc.modelspace() # 按类型查询,比全遍历快,也更清晰 for line in msp.query("LINE"): start = line.dxf.start end = line.dxf.end print(f"线: {start} -> {end}, 图层={line.dxf.layer}") # 查询多个类型用空格分隔 for e in msp.query("CIRCLE TEXT"): print(e.dxftype(), e.dxf.layer) # 按图层过滤,注意图层名大小写敏感 for e in msp.query('*[layer=="WALL"]'): print("WALL 上的实体:", e.dxftype()) # 统计各类型数量,做图纸体检时常用 from collections import Counter counter = Counter(e.dxftype() for e in msp) print(counter)

逻辑说明:msp.query()支持类似 SQL 的字符串语法,*[layer=="WALL"]是属性过滤,LINE CIRCLE是类型并集。参数上,line.dxf.start返回的是Vec3对象,可以直接当元组用,也能做向量运算。这里有个性能点:query内部会走索引,比手写if e.dxftype() == "LINE"快,尤其是大图纸。如果你要反复按图层查,可以先把实体按图层分桶缓存起来。

3. 实体操作进阶:块、标注、坐标变换怎么用对

3.1 块定义与插入:别把块当普通实体

块(BLOCK)是 DXF 里复用几何的核心机制,但它的模型和普通实体不一样:块定义存在doc.blocks里,插入到模型空间的是INSERT实体,它引用块定义并带自己的插入点和缩放。很多人直接改块定义里的实体,结果发现图纸里所有引用都变了,这就是块引用共享定义的特性。

import ezdxf doc = ezdxf.new("R2010", setup=True) msp = doc.modelspace() # 定义一个块,基点是 (0,0) blk = doc.blocks.new(name="BOLT_M8") # 往块定义里加几何:一个圆加两条线表示螺栓俯视 blk.add_circle((0, 0), radius=4) blk.add_line((-6, 0), (6, 0)) blk.add_line((0, -6), (0, 6)) # 插入块,指定插入点、缩放、旋转角度(度) msp.add_blockref("BOLT_M8", insert=(100, 100), dxfattribs={"xscale": 1.0, "yscale": 1.0, "rotation": 0}) msp.add_blockref("BOLT_M8", insert=(200, 100), dxfattribs={"rotation": 45}) doc.saveas("demo_block.dxf")

逻辑说明:doc.blocks.new()创建块定义,add_blockref()创建 INSERT 实体。参数上,rotation单位是度不是弧度,xscale/yscale控制缩放,负值可以镜像。注意块定义里的坐标是相对于块基点的,插入点才是它在模型空间的位置。如果你要改某个插入实例而不影响其他实例,得用INSERT实体的virtual_entities()拿到变换后的实体,或者直接炸开(explode)。

3.2 标注与尺寸:样式不建好,标注全是错的

标注(DIMENSION)是 DXF 里最容易被低估的部分。它不是一个简单实体,而是由标注样式(DIMSTYLE)驱动的一组几何。你不建样式直接用默认的,出来的标注可能文字高度不对、箭头大小不对、单位精度不对。

import ezdxf doc = ezdxf.new("R2010", setup=True) msp = doc.modelspace() # 新建标注样式,基于 Standard 改 dimstyle = doc.dimstyles.duplicate_entry("Standard", "MY_DIM") dimstyle.dxf.dimtxt = 25 # 标注文字高度 dimstyle.dxf.dimasz = 15 # 箭头大小 dimstyle.dxf.dimdec = 2 # 小数位数 dimstyle.dxf.dimscale = 1.0 # 全局比例,出图比例不同时要调 # 加一个线性标注,测量 (0,0) 到 (1000,0) 的水平距离 dim = msp.add_linear_dim( base=(500, -100), # 标注线位置 p1=(0, 0), # 第一测量点 p2=(1000, 0), # 第二测量点 dimstyle="MY_DIM", ) dim.render() # 必须调用,才会生成标注几何块 doc.saveas("demo_dim.dxf")

逻辑说明:add_linear_dim()只是创建了标注定义,render()才会根据样式生成实际的几何块,漏掉render()是标注不显示的常见原因。参数上,dimscale是全局比例,如果你按 1:100 出图,这里要设成 100,否则标注文字会小到看不见。dimdec控制小数位数,工程图里通常 0 或 2。注意标注样式是文档级的,改一次影响所有用该样式的标注。

3.3 坐标变换与 UCS:别在错误的坐标系里算坐标

DXF 里的坐标默认是 WCS(世界坐标系),但用户图纸经常有 UCS(用户坐标系)。ezdxf 提供了ezdxf.math里的矩阵和向量工具,做旋转、平移、缩放时不要手算三角函数,用现成的。

from ezdxf.math import Matrix44, Vec3 # 构造一个绕 Z 轴旋转 30 度、再平移 (100, 200) 的变换矩阵 m = Matrix44.z_rotate(30 * 3.1415926 / 180) @ Matrix44.translate(100, 200, 0) # 对一个点做变换 p = Vec3(10, 0, 0) p2 = m.transform(p) print(p2) # 对实体做变换(会原地修改实体) import ezdxf doc = ezdxf.new("R2010") msp = doc.modelspace() line = msp.add_line((0, 0), (100, 0)) line.transform(m)

逻辑说明:Matrix44支持矩阵乘法组合变换,顺序是从右往左应用。参数上,旋转角度用弧度,z_rotate是绕 Z 轴。transform()会直接改实体的控制点,对 LINE、CIRCLE、LWPOLYLINE 都有效,但对 INSERT 实体的变换要小心,它改的是插入点而不是块内几何。如果你要批量变换,先把矩阵算好,再循环调用,别在循环里反复构造矩阵。

4. 避坑与排查:那些让图纸打不开或数据对不上的时刻

4.1 保存后 CAD 提示「文件无效或损坏」

现象:ezdxf 保存的 DXF 用 AutoCAD 打开报错,或者某些国产 CAD 直接打不开。原因通常有三类:一是用了目标版本不支持的实体或属性,比如在 R12 文档里用了LWPOLYLINE的某些扩展属性;二是手动改了doc.header里的版本变量导致不一致;三是往实体上挂了非法的扩展数据(XDATA)。解决:先确认dxfversion和你要用的实体匹配,R12 尽量只用 LINE、CIRCLE、ARC、TEXT 这些基础实体;不要手动改$ACADVER;XDATA 的组码要按规范来。排查时可以用doc.audit()做一次自检。

import ezdxf doc = ezdxf.readfile("problem.dxf") auditor = doc.audit() print("错误数:", len(auditor.errors)) print("修复数:", len(auditor.fixes)) for e in auditor.errors: print(e)

4.2 中文文字变成问号或乱码

现象:写入的中文在 CAD 里显示为???。原因:DXF 的文字编码依赖文档版本和$DWGCODEPAGE头变量,R2007 以前用 ANSI 编码,之后用 UTF-8。如果你在 R12 文档里写中文,而系统代码页不匹配,就会乱码。解决:优先用 R2007 及以上版本,它们原生支持 Unicode;如果必须用老版本,确保doc.header["$DWGCODEPAGE"]设置正确,并且文字样式里指定的字体支持中文。另一个常见原因是文字样式用了txt.shx这种不含中文字形的字体,换成支持中文的字体即可。

4.3 遍历大图纸时内存暴涨

现象:读一张几十万实体的图纸,内存直接几个 G。原因:msp.query()返回的是实体对象列表,ezdxf 会为每个实体建 Python 对象,实体多的时候开销很大。解决:用迭代器而不是列表,msp.query("LINE")本身返回的是可迭代的,但如果你写成list(msp.query("LINE"))就全展开了。另外,只提取你需要的属性,不要保留实体引用。如果只是统计,可以用doc.entitydb直接遍历底层数据库,跳过对象包装。

4.4 块引用炸开后坐标不对

现象:INSERT实体explode()后,得到的实体坐标和视觉位置对不上。原因:explode()返回的实体是在块定义坐标系里的,没有应用插入点的变换。解决:用insert.virtual_entities()拿到已经应用变换的实体,或者自己用insert.matrix44()拿到变换矩阵再手动应用。注意virtual_entities()返回的实体是临时的,要复制到模型空间才能保存。

4.5 标注 render 后重复生成

现象:反复调用dim.render()导致标注几何重复叠加。原因:render()每次都会往标注的几何块里追加内容,不是幂等的。解决:渲染一次后不要再调,或者先清空标注的几何块再渲染。如果你在循环里改标注再渲染,记得每次重建标注对象,而不是复用。

5. 把 ezdxf 接进自动化流水线:批量处理与几何提取的实战技巧

走到这一步,单张图纸的读写应该没问题了。真正体现价值的是批量场景:一个目录几百张 DXF,要统一改图层颜色、提取所有多段线的顶点做面积计算、或者按规则生成一批标准图框。我一般会写一个处理函数,把「读入 → 过滤 → 变换 → 写出」四步固定下来,中间用日志记录每张图的实体数和异常。

import ezdxf from pathlib import Path from ezdxf.math import area def process_dxf(src: Path, dst: Path): try: doc = ezdxf.readfile(src) except Exception as e: print(f"[跳过] {src.name}: {e}") return msp = doc.modelspace() # 统一把所有 LINE 的颜色改成 ByLayer,避免硬编码颜色 for line in msp.query("LINE"): line.dxf.color = 256 # 256 = ByLayer # 提取所有闭合 LWPOLYLINE 的面积 total_area = 0.0 for pl in msp.query("LWPOLYLINE"): if pl.closed: pts = [(p[0], p[1]) for p in pl.get_points()] total_area += abs(area(pts)) print(f"{src.name}: 实体 {len(msp)}, 闭合多段线面积合计 {total_area:.2f}") doc.saveas(dst) # 批量跑一个目录 src_dir = Path("./input") dst_dir = Path("./output") dst_dir.mkdir(exist_ok=True) for f in src_dir.glob("*.dxf"): process_dxf(f, dst_dir / f.name)

逻辑说明:line.dxf.color = 256是 DXF 里 ByLayer 的约定值,设成 256 后实体颜色跟随图层,这是图纸规范化的常见操作。area()来自ezdxf.math,接受顶点列表返回多边形面积,注意顶点顺序会影响正负,所以取绝对值。参数上,pl.get_points()返回的是(x, y, start_width, end_width, bulge)五元组,取前两个就是坐标。这个流水线的关键点是异常隔离:单张图读失败不能中断整批,所以readfile包在 try 里。

再补一个我常用的技巧:如果你要对比两张图纸的差异,不要逐实体比对,先把实体转成「类型 + 图层 + 关键坐标」的元组集合,再做集合差。这样几万实体也能秒级出结果。另外,ezdxf 的doc.header里有很多图纸级设置,比如单位$INSUNITS、角度方向$ANGDIR,批量处理前先读一遍这些,能避免很多「为什么这张图比例不对」的玄学问题。最后一句血泪经验:任何批量写操作,先在小样本上跑通并人工打开确认,再全量跑,DXF 的坑往往在打开那一刻才暴露。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询