
简介面向需要处理加密CAD图纸的设计人员与绘图学习者这份文档聚焦多重插入引用这类常见图纸加密方式及其炸开思路适合具备一定AutoCAD基础、希望理解块参照与AutoLISP脚本应用的读者参考也适合需要自行排查图纸锁定问题的读者。压缩包仅含1个doc文件大小约18KB属单一文档型资料篇幅精炼便于检索与留存。内容围绕wjjm、CAD加密插件及lsp脚本展开并串联AutoLISP编程、ENTSEL与ENTGET获取块信息、DEFUN定义exm与lockb函数等知识点涉及图纸知识产权保护与加密解密逻辑对多重插入块的特性、命令调用和脚本定义给出较集中说明帮助读者建立从识别加密块、分析块参照属性到转换普通块的排查框架。已有457人学习可作为补充脚本处理经验与排错方向的轻量资料。1. 图纸被做成多重插入引用先弄清它为什么炸不开一张从别处流出来的 CAD 图纸块选得中、能移动、能复制唯独 EXPLODE 一敲就提示“无法分解”。不少人第一反应是版本不对、软件坏了换台机器再试结果一模一样。这种情况十有八九不是加密而是建块时用了多重插入引用——看着是块本质是“块参照 行列阵列参数”的组合实体平台从设计上就不允许直接分解它。这份资源围绕的就是这件事怎么识别、怎么降级成普通块、怎么再炸开以及降级后丢掉的行列副本怎么补回。图纸在互联网上转手几道之后这类处理手法比图纸本身更值钱。它适合手上压着一批外来图纸、需要二次编辑的绘图与深化人员也适合想把这套动作批量化的人。下面按看清身份、单块降级、插件与命令行、踩坑、验证的顺序走步骤都能直接抄。2. 识别与判定用 entget 把 100 子类标记打出来在动手改任何东西之前先确认你选中的到底是什么。AutoLISP 的entget会把一个图元的所有组码原样吐出来这是判断块类型最直接的黑匣子。很多人一上来就找插件、找脚本结果对象根本不是多重插入块白折腾半天。2.1 一条 entget 看清块的真实身份把下面这段存成whatis.lsp加载后输入WHATIS点一下目标块命令行会打印实体类型、全部子类标记和行列参数。这段代码只读不写随便点不会改图。(defun c:whatis (/ e ed v70 v71) (setq e (car (entsel \n点一个块参照: ))) (if e (progn (setq ed (entget e)) ;; 实体类型普通块和多重插入块这里都是 INSERT (princ (strcat \n实体类型 0 (cdr (assoc 0 ed)))) ;; 100 组码可能出现多次必须遍历assoc 只拿第一个 (foreach x ed (if ( (car x) 100) (princ (strcat \n子类标记 100 (cdr x))) ) ) ;; 70/71 缺省时按 1 处理避免 nil 参与比较 (setq v70 (cond ((assoc 70 ed) (cdr (assoc 70 ed))) (t 1))) (setq v71 (cond ((assoc 71 ed) (cdr (assoc 71 ed))) (t 1))) (princ (strcat \n列数 70 (itoa v70))) (princ (strcat \n行数 71 (itoa v71))) (if (assoc 44 ed) (princ (strcat \n列间距 44 (rtos (cdr (assoc 44 ed)) 2 3)))) (if (assoc 45 ed) (princ (strcat \n行间距 45 (rtos (cdr (assoc 45 ed)) 2 3)))) (princ) ) (princ \n没选中对象。) ) (princ) )逻辑上分三步entsel拿到图元名entget拿到组码表然后分别取0、100、70、71、44、45。关键点在100这一项——assoc只返回列表里第一个匹配项而一个块参照的组码表里通常有AcDbEntity和AcDbBlockReference或AcDbMInsertBlock两个子类标记用assoc会漏掉后面那个。所以这里用foreach遍历整张表把每个100都打出来。参数上没什么可调的itoa只接受整数70、71本来就是整数44、45是实数用rtos转字符串第二参数2表示十进制第三参数3是小数位数按你的图纸精度改就行。如果打印出来100里出现AcDbMInsertBlock基本可以锁定是多重插入引用。2.2 组码 70/71/44/45 各管什么同样是INSERT普通块参照和多重插入引用的差别全在这几个组码上。把下面这张表对着entget的输出看一眼就能对上号。组码含义普通块参照多重插入引用0实体类型INSERTINSERT2块名有有10/20/30插入基点有有41/42/43X/Y/Z 缩放有有50旋转角有有70列数1 或缺省通常大于 171行数1 或缺省通常大于 144列间距无或 0有实际值45行间距无或 0有实际值100子类标记AcDbBlockReferenceAcDbMInsertBlock需要留意两个容易误判的情况。一种是70、71都被改成 1但100仍然是AcDbMInsertBlock这种图表面上只有一个块EXPLODE 照样拒绝光看行列数会漏判另一种是某些 R12 格式导出的 DXF压根没有100子类标记只能靠70、71是否大于 1 来判断。所以判定条件要写成“子类标记命中 或 行列数大于 1”两个条件取或不要只认一个。2.3 用 ssget 过滤批量筛出可疑块单张图纸点到手软不现实尤其是几十张一套的施工图。ssget的X模式可以按组码条件全图筛选先把候选集缩小再逐个判定。(defun c:scanm (/ ss i e ed v70 v71 hit) (setvar CMDECHO 0) (setq ss (ssget X ((0 . INSERT)))) ; 全图所有块参照含外部参照里的块 (setq i 0 hit 0) (if ss (repeat (sslength ss) (setq e (ssname ss i)) (setq ed (entget e)) (setq v70 (cond ((assoc 70 ed) (cdr (assoc 70 ed))) (t 1))) (setq v71 (cond ((assoc 71 ed) (cdr (assoc 71 ed))) (t 1))) (if (or ( v70 1) ( v71 1) (member (100 . AcDbMInsertBlock) ed)) (progn (setq hit (1 hit)) ;; 把可疑对象放到当前选择集里方便后续批量处理 (ssadd e (ssadd)) (princ (strcat \n可疑块: (cdr (assoc 2 ed)) 行列 (itoa v70) x (itoa v71) 句柄 (cdr (assoc 5 ed)))) ) ) (setq i (1 i)) ) ) (princ (strcat \n共扫描 (itoa (if ss (sslength ss) 0)) 个块参照可疑 (itoa hit) 个。)) (setvar CMDECHO 1) (princ) )ssget X的过滤表用点对(0 . INSERT)注意点对中间是空格加点写成列表会被当成别的意思。ssname按索引取图元名索引从 0 开始所以循环里先取后自增。判定用了or连接三个条件member对点对列表做的是equal比较能正确匹配(100 . AcDbMInsertBlock)。这里有个细节值得说(ssadd e (ssadd))这一行实际上是新建空选择集再加对象写在这里只是演示如何把命中项收集起来做后续批处理实际跑的时候如果只想看报告删掉这行不影响结果。另外ssget X会连冻结图层和外部参照里的块一起扫到如果图纸很大扫描会慢几秒属于正常现象别以为是脚本卡死了。3. 降级路线对比entmod 改四个组码还是 entmake 重建看清楚身份之后真正要做的只有一件事把一个AcDbMInsertBlock的块参照变成 AutoCAD 认得的普通AcDbBlockReference。原始资料里给的是entmake重建实体的写法能用但不是唯一写法也不是保真度最高的写法。这里把两条路线摆在一起对比。3.1 entmod 路线只动四个组码保真度最高entmod是直接修改已有图元的组码表改完图元还是原来那个图元句柄、图层、颜色、线宽、扩展数据、属性跟随标志全部原地保留。对多重插入块来说只要把行列参数打回原形就行。(defun c:exm (/ e ed) (setvar CMDECHO 0) (setq e (car (entsel \n点取多重插入块: ))) (if (and e ( (cdr (assoc 0 (entget e))) INSERT)) (progn (setq ed (entget e)) ;; 行列数改回 1阵列参数失效 (setq ed (subst (70 . 1) (assoc 70 ed) ed)) (setq ed (subst (71 . 1) (assoc 71 ed) ed)) ;; 间距清零避免个别版本仍按旧间距渲染 (if (assoc 44 ed) (setq ed (subst (44 . 0.0) (assoc 44 ed) ed))) (if (assoc 45 ed) (setq ed (subst (45 . 0.0) (assoc 45 ed) ed))) (entmod ed) (entupd e) ; 刷新显示不改这一行图形可能不重画 (princ \n已降级为普通块参照可直接 EXPLODE。) ) (princ \n选中的不是块参照。) ) (setvar CMDECHO 1) (princ) )subst的用法是“新元素、旧元素、表”它替换表里第一个equal匹配的元素所以(assoc 70 ed)必须存在否则subst拿到 nil 会把整张表搞坏。这就是为什么要先assoc再subst不能直接写(subst (70 . 1) (70 . 3) ed)赌它一定存在。entupd传入图元名作用是让屏幕重画少了这一步命令行提示成功但图形看着没变容易以为没生效。参数上70、71我写成 1 而不是 0。原始资料里的写法是置 0部分版本能过但 0 行 0 列在语义上不合法个别版本会报“无效的块参照”或者显示异常。设成 1 表示单行单列语义正确EXPLODE 也就放行了。44、45 清零是可选的行列都为 1 时间距本来就不起作用清掉只是让组码表干净一点。3.2 entmake 路线原文档的思路与它的代价原始资料里那段exm走的是另一条路entget取出全部组码改掉 70、71、44、45把子类标记换成AcDbBlockReference然后entmake新建一个图元、entdel删掉旧的。逻辑上等价但代价明确——新建的图元会拿到新句柄挂在原实体上的扩展数据XDATA、扩展字典、部分反应器会丢。(defun exm-strip (ed codes / out) ;; 过滤掉不能带进 entmake 的组码 (foreach x ed (if (not (member (car x) codes)) (setq out (cons x out)) ) ) (reverse out) ) (defun c:exm2 (/ e ed) (setvar CMDECHO 0) (setq e (car (entsel \n点取多重插入块: ))) (if (and e ( (cdr (assoc 0 (entget e))) INSERT)) (progn (setq ed (entget e)) (setq ed (subst (70 . 1) (assoc 70 ed) ed)) (setq ed (subst (71 . 1) (assoc 71 ed) ed)) (if (assoc 44 ed) (setq ed (subst (44 . 0.0) (assoc 44 ed) ed))) (if (assoc 45 ed) (setq ed (subst (45 . 0.0) (assoc 45 ed) ed))) ;; 逐项替换子类标记mapcar 能覆盖所有出现位置 (setq ed (mapcar (lambda (x) (if (equal x (100 . AcDbMInsertBlock)) (100 . AcDbBlockReference) x ) ) ed ) ) ;; 句柄、所有者、反应器组不能原样带给 entmake (setq ed (exm-strip ed (5 330 360 102))) (if (entmake ed) (progn (entdel e) (princ \n已重建为普通块参照。) ) (princ \nentmake 失败检查组码表。) ) ) (princ \n选中的不是块参照。) ) (setvar CMDECHO 1) (princ) )这段代码最值得注意的是mapcar那一块。原始资料里用的是subst替换(list 100 AcDbMInsertBlock)写法上依赖“表里只有一处子类标记”这个假设用mapcar遍历全表逐项比对不管出现几次都能换干净也不会误伤前面的AcDbEntity。exm-strip过滤5、330、360、102四类组码是因为句柄由系统分配、所有者指向块表记录、扩展字典和反应器组在entmake时容易触发“无效组码”报错带上反而添乱。两条路线怎么选图纸是自家产出的、没有挂扩展数据entmake更省心一步到位图纸来自外部、图层和属性可能挂了东西优先entmod改完还能用entget复查出问题也好回溯。我一般先用entmod只有在某些老版本上entmod改完 EXPLODE 仍不放行时才退到entmake重建。3.3 加载运行APPLOAD、拖拽、命令行三条路径脚本写好了得让 CAD 认识它。三种加载方式适用场景不一样。命令行输入APPLOAD回车弹对话框选文件这是最稳的方式加载成功会提示“已成功加载”失败会明确告诉你哪一行出错。缺点是每次开图都要点一遍适合调试阶段。把.lsp文件直接拖进绘图区CAD 会自动加载命令行回显加载路径。这是原始资料里推荐的方式快但有两个前提文件扩展名必须是.lsp而且拖拽时不能落在工具栏或对话框上否则只是把文件路径插进了命令行。拖拽加载失败时命令行一般只闪一行提示很容易错过所以调试期我不用拖拽。想让脚本每次启动都在用APPLOAD对话框右下角的“启动组”把文件加进去。代价是启动变慢一点而且如果文件被移动了位置每次启动都会弹一次找不到文件的提示。团队协作时把脚本放在固定路径下再进启动组别放桌面。加载成功后命令行输入EXM回车按提示点块。命令名来自脚本里(defun c:exm ...)中c:后面那串大小写不敏感但不能带空格和特殊符号。原始资料里正文写“输入 wjmm”脚本里定义的却是wjjm这种前后不一致在流传的资料里很常见遇到“未知命令”先回去看defun c:后面到底写的是什么。3.4 反向锁定lockb 把普通块还原成多重插入原始资料里还给了反向函数lockb作用是把普通块转回多重插入块。这个功能听起来奇怪实际用途是给图纸做轻度保护——转成多重插入后别人直接 EXPLODE 会被拒绝得先知道有降级这回事才能继续。(defun c:lockb (/ e ed) (setvar CMDECHO 0) (setq e (car (entsel \n点取普通块: ))) (if (and e ( (cdr (assoc 0 (entget e))) INSERT)) (progn (setq ed (entget e)) (setq ed (subst (70 . 2) (assoc 70 ed) ed)) ; 2 列 (setq ed (subst (71 . 2) (assoc 71 ed) ed)) ; 2 行 (setq ed (mapcar (lambda (x) (if (equal x (100 . AcDbBlockReference)) (100 . AcDbMInsertBlock) x ) ) ed ) ) (entmod ed) (entupd e) (princ \n已转为多重插入块EXPLODE 将被拒绝。) ) (princ \n选中的不是块参照。) ) (setvar CMDECHO 1) (princ) )注意70、71必须设成大于 1 的数只改子类标记不改行列数多数版本不会认。设成 2 行 2 列之后图形上会立刻多出三个副本这是预期行为不是画错了。这个函数我只建议在自己的测试图上用别拿去处理正式图纸——万一忘了原状态后面的人接手会很麻烦。4. 插件与命令行wjjm、CYN- 这类快捷方案能不能长期用原始资料里提到两条“捷径”一个是拖进 CAD 就能用的wjjm另一个是输入CYN-前缀命令的加密插件。这两类方案在应急时确实快但它们的边界得提前说清楚否则图纸一多、版本一换就会翻车。4.1 拖拽加载能成功为什么 APPLOAD 反而报错同一份.lsp拖进窗口能跑APPLOAD加载却报错最常见的原因是文件本身带了 BOM 或者用了 UTF-8 编码保存。AutoLISP 解释器对文件头很敏感带 BOM 的文件第一行往往解析失败而拖拽路径在某些版本上容错更好于是出现“能跑但加载报错”的怪现象。处理办法很简单用记事本打开另存为 ANSI 编码或者用支持切换编码的编辑器存成无 BOM 的 UTF-8。另一个常见原因是文件里混进了中文全角标点尤其是从网页复制粘贴的代码括号、引号、分号经常被自动替换成全角解释器会直接报“输入中含有多余的右括号”。这类错误定位靠肉眼看很难我一般把代码贴进编辑器开显示不可见字符全角字符一眼就能挑出来。还有一种情况是文件扩展名被系统藏起来了实际文件名是wjjm.lsp.txt拖进去 CAD 不认改扩展名即可。4.2 CYN- 这类前缀命令的本质原始资料里写“输入 CYN—将多重插入块转换为普通块”这里有个典型的坑正文里那个横杠是全角破折号命令行里必须输入半角连字符-。全角字符在命令行里会被当成未知字符回车后提示“未知命令”很多人卡在这一步以为是插件没装好。带前缀的命令本质上是插件作者为了避免和系统命令重名在自己定义的命令前加了一段命名空间。加载插件后用!加变量名、或者直接敲前缀再按 Tab 看补全能确认插件到底注册了哪些命令。如果按 Tab 没有任何补全说明插件根本没加载成功跟命令名写错是两回事。这类加密插件还有一个隐患它们经常在加载时改注册表、写启动项卸载后残留。我处理外来插件一律先在虚拟机或者不重要的机器上跑一遍确认行为正常再放到主力机上。图纸是别人的机器是自己的这个顺序不能反。4.3 不装插件也能批量DXF 文本级替换如果手头图纸多到几十上百张逐张打开点块不现实可以走 DXF 文本替换这条路。DXF 是纯文本格式组码和值一行一个AcDbMInsertBlock就是一行普通字符串用脚本批量替换比在 CAD 里点快得多而且不依赖任何插件。# 将 DXF 中的多重插入块批量降级为普通块参照 # 用法: python dxf_unminsert.py in.dxf out.dxf import sys def convert(src, dst): # DXF 可能含本地编码的中文块名用 latin-1 读写保证字节不变 with open(src, r, encodinglatin-1, errorsignore) as f: data f.read().splitlines() out, i, in_minsert, fixed [], 0, False, 0 while i 1 len(data): code data[i].strip() val data[i 1] if code 0: in_minsert False # 遇到新实体重置状态 if code 100 and val.strip() AcDbMInsertBlock: out [100, AcDbBlockReference] in_minsert True fixed 1 i 2 continue if in_minsert and code in (70, 71): out [code, 1] # 行列数改回 1 i 2 continue out [data[i], data[i 1]] i 2 with open(dst, w, encodinglatin-1) as f: f.write(\n.join(out)) print(处理完成替换 %d 处 % fixed) if __name__ __main__: convert(sys.argv[1], sys.argv[2])逻辑是一个极简状态机按“组码行 值行”成对读取遇到0组码说明进入新实体把in_minsert复位遇到100且值等于AcDbMInsertBlock就成对写入替换后的两行并把状态置为真状态为真期间遇到70、71一律改写成 1。其余内容原样透传。参数上输入输出都用latin-1是关键DXF 里的中文块名可能是 GBK 字节用 UTF-8 读写会破坏字节导致文件打不开。R12 格式的 DXF 没有100子类标记这段脚本对它无效得改成只按70、71大于 1 判定属于另一套逻辑。处理完务必在 CAD 里打开验证DXF 替换属于“改文件”而不是“改模型”出错不会给你撤销机会。5. 避坑与排查五条在现场反复出现的翻车记录这套动作看着简单真正上手时翻车点集中得很。下面五条是我在不同图纸上反复遇到的按“现象—原因—解决”记下来遇到对得上的直接照方抓药。5.1 现象entmod 执行成功EXPLODE 仍提示无法分解现象很迷惑命令行明明打印了“已降级”entget再看70、71也变成 1 了一敲 EXPLODE 还是那句“无法分解”。原因通常有两个。一是块定义本身被设成了“不允许分解”这个开关不在块参照上在块定义里组码280的某一位控制。二是这个块是匿名块或者动态块动态块在降级之后仍带匿名块特性EXPLODE 会被拒绝。解决办法是先确认块名以*开头的匿名块要先EXPLODE一次外层再处理内层动态块要用BEDIT进去看块定义或者干脆用WBLOCK把块内容导出成独立图纸再插回来。原始资料里那种“点一下就能炸”的说法对普通多重插入块成立对带动态特性的块不成立这点要提前有预期。5.2 现象炸完只剩一个块阵列副本全丢这是最容易被忽略、后果也最严重的一条。多重插入块在图形上看着有 6 行 4 列共 24 个副本降级成普通块之后图上只剩 1 个剩下 23 个凭空消失图面立刻缺一大片。原因不复杂那 23 个副本从来就不是独立实体它们只是同一个块参照按照行列间距渲染出来的视觉结果文件里只存了一份几何数据。降级等于把渲染参数抹掉副本自然跟着没。解决思路是把行列“展开”成真正的独立块参照再删原实体。核心是读出70、71、44、45按旋转角把行列偏移换算到世界坐标逐个插入。(defun c:exmexpand (/ e ed bn p ang sx sy n m dc dr i j np) (setvar CMDECHO 0) (setq e (car (entsel \n点取多重插入块: ))) (setq ed (entget e)) (setq bn (cdr (assoc 2 ed))) (setq p (cdr (assoc 10 ed))) (setq ang (cond ((assoc 50 ed) (cdr (assoc 50 ed))) (t 0.0))) (setq sx (cond ((assoc 41 ed) (cdr (assoc 41 ed))) (t 1.0))) (setq sy (cond ((assoc 42 ed) (cdr (assoc 42 ed))) (t 1.0))) (setq n (cond ((assoc 70 ed) (cdr (assoc 70 ed))) (t 1))) (setq m (cond ((assoc 71 ed) (cdr (assoc 71 ed))) (t 1))) (setq dc (cond ((assoc 44 ed) (cdr (assoc 44 ed))) (t 0.0))) (setq dr (cond ((assoc 45 ed) (cdr (assoc 45 ed))) (t 0.0))) (setq i 0) (repeat n (setq j 0) (repeat m ;; 列方向沿块 X 轴行方向沿 Y 轴polar 的角度用弧度 (setq np (polar p ( ang (/ pi 2.0)) (* j dr sy))) (setq np (polar np ang (* i dc sx))) (command _.-INSERT bn np sx sy ang) (setq j (1 j)) ) (setq i (1 i)) ) (entdel e) (setvar CMDECHO 1) (princ) )polar的三个参数是基点、角度弧度、距离所以(/ pi 2.0)是 90 度。行列间距乘了sx本文还有配套的精品资源点击获取