Python 自动化打通 CAD 工程图纸读取通道:ODA File Converter + ezdxf 从 DWG 转 DXF 到文字层全量落地
施工图纸到了交付阶段,DWG 是母版,PDF 是分发件。可一旦你想让程序去读这批图——抽图号、抽设备表、抽回路数、抽技术说明——第一道墙就立在面前:DWG 读不了。
不是\”读不准\”,是压根读不了。Python 生态里最成熟的 CAD 库 ezdxf,名字里那个 dxf 就是答案的一部分:它实现的是 DXF 的读写,不是 DWG。你在没装商业 CAD 的机器上敲下 ezdxf.readfile(\”某平面图.dwg\”),得到的不是一个警告,而是一条干脆的拒绝。
这篇文章要讲的,就是怎么在没有商业 CAD 环境的前提下,用 ODA File Converter + ezdxf 搭出一条能跑通的通道,把一批施工图从\”打不开的二进制\”变成\”可检索、可对账、可留痕的文字层数据\”。全文围绕通道搭建的全貌展开:环境怎么钉、转换怎么封、文字怎么抽、产物怎么验、坑在哪里。
一、背景:DWG 为什么是一座数据孤岛
先把问题的性质说清楚,否则后面所有动作都像是玄学。
DWG 是 Autodesk 的私有二进制格式,结构未公开,且随 AutoCAD 版本持续演进。社区里对它的逆向解析一直在做,但覆盖度、稳定性、对新版本的跟进速度,都不足以支撑工程级的批量生产。相比之下,DXF 是 Autodesk 自己公开的交换格式,有完整的组码(group code)规范,文本形态甚至可以用记事本打开逐行核对。
于是行业里形成了一条默认路径:DWG → DXF → 程序解析。看起来只是加了一步格式转换,但这\”一步\”恰恰是全部难点所在。
难点有三个。
**第一,转换器的可获得性。**最正统的转换方式当然是 AutoCAD 自己:命令行导出、COM 接口、或者随安装包附带的其他执行文件`。但商业授权不是每台机器都有,尤其在 CI 环境、临时分析机、或者纯交付侧的办公机器上。这时就需要一个不依赖商业授权的独立转换器,ODA File Converter 是目前工程上最常用的选择——它由 Open Design Alliance 维护,免费提供 DWG/DXF/DWF 之间的批量互转,带命令行接口,可脚本化。
**第二,版本地雷。**DXF 不是只有一个版本。AutoCAD R12 是 AC1009,R2000 是 AC1015,R2004 是 AC1018,R2007 是 AC1021,R2010 是 AC1024,R2013 是 AC1027,R2018 是 AC1032。同一个 DWG 导出成不同版本,实体完整性、代理实体处理、表格对象的表现都可能不同。你不能用\”导出成功\”当终点,必须用\”ezdxf 能读、实体数合理、文字层非空\”当终点。
**第三,读出来之后的分寸。**这是最容易被跳过的一层。抽到文字只是第一步,抽到的文字里混着 MTEXT 格式码(\\C3;、\\Fsimsun|b0|i0;、\\P、%%C)是常态;只扫模型空间会漏掉图框、标题栏、图例表——这些东西大量存在于图纸空间;块属性(ATTRIB)里写的参数,图面上未必显示,拿它当数量依据会直接翻车。这些都不是转换器的锅,而是\”通道后半段\”的工程纪律。
所以本文要搭的不是\”一个转换命令\”,而是一条从文件到可核验产物的完整链路。
二、环境准备:先把版本号钉死
这一节的所有版本号都请照抄,不要凭印象填。格式转换类的问题,八成是环境问题,而环境问题里八成是路径问题。
转换器:ODA File Converter 27.1.0,本机安装路径为 D:\\Program Files\\ODA\\ODAFileConverter 27.1.0\\,可执行文件为同目录下 ODAFileConverter.exe。注意是可执行文件绝对路径,不是安装目录。这是 ezdxf 的 odafc 附加模块唯一认的配置项。
Python 解释器:C:/Users/admin/.workbuddy/binaries/python/envs/default/Scripts/python.exe。这个环境里 ezdxf、python-docx、olefile、openpyxl 齐备。如果你另有一个只装了 ezdxf 的精简环境,跑纯转换够用,但一旦涉及表格还原与 Word 产物就会缺件。
库:ezdxf(提供 addons.odafc 与 DXF 解析)、openpyxl(表格产物)、olefile(OLE 容器判别,本文不展开,仅作依赖登记)。
目录约定:源图目录只读,任何中间产物一律落在独立的工作区,禁止回写源目录。这一条在多人共享图纸服务器的场景里是硬要求——源图被改动属于事故,不是失误。
导出版本:默认 ACAD2018(即 DXF AC1032),回退阶梯为 [None, R2010, R2000]。None 表示\”让转换器自己决定\”,通常是最贴近原图的版本;R2010(AC1024)与 R2000(AC1015)依次下探,用于绕过新版 DXF 在个别代理实体上的解析异常。
审计开关:audit=True。ODA 在导出时会跑一次图形审计,修复部分破坏实体。代价是耗时增加,收益是少一批\”转换成功但内容残缺\”的假成功。
路由阈值:这不是本文通道的参数,但必须写进设计里——单张图超过 20MB 时,ODA 通道的失败率显著上升,且失败形态是静默的(不报错、不产出,或者产出空壳)。这类图应当改走 accoreconsole 通道;另外,若需要读取布局视口的语义(视口比例、视口边界、模型空间与图纸空间的映射关系),则应走 AutoCAD COM 通道。本文只覆盖中小体量图纸的 ODA 路线,大图的换道决策另有专门篇幅。
关于版本,还需要记住一条对应关系:DXF 的版本代号与 AutoCAD 的发行代号不是一一对应的字面关系,填错版本字符串时 ODA 通常不会报错,而是产出一个内容异常的 DXF。所以版本字符串建议集中在一张映射表里维护(本文代码里的 CLI_VERSION_MAP 就是干这个的),不要在调用处散落硬编码。
关于 audit 也有个常见误解:开启审计不等于\”修好所有坏图\”。它修复的是结构层面的部分破坏实体,修不了逻辑层面的内容缺失。审计通过之后的 DXF 依然可能是空壳,这就是为什么后文要把\”实体数非零\”单独列为成功判据。
最后一条准备事项,也是最容易被跳过的:先跑探针再跑流程。ODA 路径写错、解释器选错、库没装,这些错误在批量任务里会伪装成\”转换失败\”,让你在几十张图上排查一个本可以在三秒内暴露的问题。下面这段代码就是干这件事的。
三、步骤拆解:一条通道上的六个关卡
把整条链路拆成六步,每一步都有明确输入、明确产出、明确卡点。任何一步不过,不允许进入下一步——这不是流程洁癖,是被反复教育出来的结果。
**S0 登记。**拿到一批图先建清单:图号、专业、文件路径、文件大小、DWG 内部版本号。产出 _图纸清单.csv。卡点是图号与文件一一对应,无遗漏。这里有个真实教训:曾经把一册 81 页的 DWG 当成一张图登记,后续全线漏读——所以登记表里必须有\”页数/布局数\”一列,不能用文件数代替图纸数。
**S1 通道判定。**按 C1 到 C5 的顺序试可用通道,逐个记录\”可用/不可用 + 原因\”。产出 _通道判定.md。卡点是:判\”可用\”必须有实跑证据,判\”不可用\”必须附失败证据(空目录、超时记录、异常栈),不接受口头结论。
**S2 转中间文件。**DWG 转 DXF,带版本回退与 audit。产出 <图号>.dxf。卡点有三条:DXF 存在且非零字节、ezdxf 能打开、实体数不为零。另外必须核对导出文件的修改时间是否为本次运行——见过因为输出名固定、旧文件未被覆盖,导致\”明明成功了却读的是上周的产物\”这种鬼故事。
**S3 模型空间全量抽取。**遍历模型空间,抽图面文字、块清单、实体直方图三类。产出 <图号>_text.txt、<图号>_blocks.csv、_实体直方图.txt。卡点是三类产物齐备且文字已清格式码。
**S4 图纸空间补全。**遍历所有布局(含各图纸空间),做块属性递归。产出 <图号>_layout.txt。卡点极硬:图号数为 0 一律退回重来。图框、标题栏、图例表大量存在于图纸空间,只扫模型空间就下\”无图号\”\”无表格\”的结论,是这套流程里最常见也最致命的误判。
**S5 对账与留痕。**把抽取结果与预期做断言:文字条数、块数、图号数、产物文件存在性与非空性全部核对。产出 _核查表.md。卡点是每一项都能指到具体文件与具体行——“读过了\”不等于\”留东西了”,没有产物的读取等于没读。
这六步看着繁琐,但它们对应的是六类真实的失效模式。跳过其中任何一步,你省下的时间都会在后面对账时加倍还回来。
再补一句关于\”卡点\”的工程设计。卡点不是打印一行日志提醒自己注意,而是让程序自己拒绝往下走。图号数为 0 时,settle_artifact_ledger 应该返回 REJECT 并让入口函数以非零退出码结束,而不是打印一句\”警告:图号为空\”然后继续。前者是可判定的闸门,后者是可被忽略的提示——在批量跑几十张图的时候,被忽略的提示等于没有提示。
四、代码实现:从绑定运行时到产物断言
下面四段代码构成本文的主体,外加一段命令行入口把它们串起来。函数命名刻意避开 convert_dwg_to_dxf、extract_text、clean_text 这类通用名,原因是这批脚本会与团队里其他读图脚本长期共存于同一台机器,重名导致误引用已经发生过不止一次——把 bind_oda_runtime、relay_dwg_to_dxf、harvest_text_spans、settle_artifact_ledger 这类带前缀语义的名字固定下来,检索和排错都会轻松很多。
代码按职责拆成四个模块文件:stage_cad_channel.py(环境与绑定)、stage_cad_relay.py(转换与版本回退)、stage_cad_textlayer.py(遍历与清洗)、stage_cad_inventory.py(直方图),对账与入口分别放在 stage_cad_settle.py 与 run_textlayer_pipeline.py。拆开的原因很实际:转换与抽取的失败性质完全不同,混在一个文件里排查时,你分不清\”没转出来\”和\”转出来了没抽到\”。
4.1 ODA 运行时绑定与环境预检
第一段解决两件事:把 ODA 可执行文件路径写进 ezdxf 的全局选项,以及在开跑之前把环境缺陷一次性暴露出来。
# -*- coding: utf-8 -*-
\”\”\”stage_cad_channel.py —— ODA 通道绑定与转换前环境预检\”\”\”
from __future__ import annotations
import os
import sys
from pathlib import Path
ODA_HOME = Path(r\”D:/Program Files/ODA/ODAFileConverter 27.1.0\”)
ODA_CLI = ODA_HOME / \”ODAFileConverter.exe\”
ACCORE_CONSOLE = Path(r\”D:/Autodesk/AutoCAD 2023/accoreconsole.exe\”)
def bind_oda_runtime(oda_cli: Path = ODA_CLI) –> str:
\”\”\”把 ODA 可执行文件绝对路径写进 ezdxf 全局选项,并返回实际写入值。
注意:win_exec_path 必须是 .exe 文件的绝对路径,指向所在目录会静默失效。
\”\”\”
import ezdxf
from ezdxf import options
resolved = str(Path(oda_cli).resolve())
if not os.path.isfile(resolved):
raise FileNotFoundError(f\”ODA 可执行文件不存在或不是文件: {
resolved}\”)
options.set(\”odafc-addon\”, \”win_exec_path\”, resolved)
return resolved
def probe_runtime(oda_cli: Path = ODA_CLI) –> dict:
\”\”\”转换前跑一遍环境探针:缺什么当场报,别等三小时。\”\”\”
report: dict = {
\”python\”: sys.version.split()[0],
\”oda_exec\”: str(oda_cli),
\”oda_ok\”: False,
\”libs\”: {
},
\”fallback_channel\”: \”none\”,
}
for name in (\”ezdxf\”, \”docx\”, \”olefile\”, \”openpyxl\”):
try:
mod = __import__(name)
report[\”libs\”][name] = getattr(mod, \”__version__\”, \”ok\”)
except ImportError as exc:
report[\”libs\”][name] = f\”MISSING ({
exc.__class__.__name__})\”
report[\”oda_ok\”] = os.path.isfile(str(oda_cli))
if ACCORE_CONSOLE.is_file():
report[\”fallback_channel\”] = \”accoreconsole\”
return report
def render_probe(report: dict) –> str:
lines = [
f\”python : {
report[\’python\’]}\”,
f\”oda : {
report[\’oda_exec\’]} -> {
\’OK\’ if report[\’oda_ok\’] else \’MISSING\’}\”,
f\”fallback : {
report[\’fallback_channel\’]}\”,
]
for k, v in report[\”libs\”].items():
lines.append(f\”{
k:<9} : {
v}\”)
return \”\\n\”.join(lines)
if __name__ == \”__main__\”:
rep = probe_runtime()
print(render_probe(rep))
if rep[\”oda_ok\”]:
print(\”bound ->\”, bind_oda_runtime())
else:
raise SystemExit(\”ODA 不可用,先修环境再谈流程\”)
这段代码里有个值得单独说的细节:options.set(\”odafc-addon\”, \”win_exec_path\”, …) 这一行,是整个 ODA 通道的唯一正确入口。很多人绕过它,直接 ezdxf.readfile() 去读 DWG,得到 UnsupportedFileType 或者一个空文档,然后判定\”这张图有问题\”。实际上图没问题,是调用姿势错了——ezdxf 不知道你的 ODA 装在哪里,它不会去注册表里猜。
4.2 转换封装:版本回退阶梯 + audit
第二段是转换本体。这里给出两条调用路径:一条是直接调用 ODA 命令行(产出可见的 .dxf 文件,便于留痕与复查),一条是走 ezdxf 的 odafc.readfile(临时文件中转,代码更短)。生产环境推荐前者,因为产物落盘本身就是留痕。
ODA File Converter 的命令行参数顺序是固定的:输入目录、输出目录、输出版本、输出类型、是否递归、是否审计。
# -*- coding: utf-8 -*-
\”\”\”stage_cad_relay.py —— DWG→DXF 转换封装:版本回退阶梯 + audit + 超时换道\”\”\”
from __future__ import annotations
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Sequence
DEFAULT_LADDER: Sequence[str | None] = (None, \”R2010\”, \”R2000\”)
CLI_VERSION_MAP = {
None: \”ACAD2018\”, # DXF AC1032
\”R2010\”: \”ACAD2010\”,
\”R2000\”: \”ACAD2000\”,
}
@dataclass
class




