一次复杂习题册排版系统的重构实录
自动套用 Word 模板,听起来像是一个格式复制问题:读取参考文档的字体、字号、行距和页边距,再把这些参数写到目标文档中。 如果文档只有几段文字,这个理解大致成立。但当输入变成一份真实习题册——横向页面、双栏、页眉页脚、二维码、浮动图片、制表位、上千个可编辑 MathType 公式——“复制格式”很快会变成“如何在不破坏文档的前提下重写它”。 我们为此尝试的word-format skill,最初走的是一条直观路线:让 Word 把参考文档导出为 HTML,替换文字,再通过浏览器和剪贴板粘回 Word。它在少量内容生成上工作得很好,却在完整习题册上暴露了根本缺陷:页面看起来仍有公式,但原来的 Equation.DSMT4 OLE 对象已经退化成 PNG;页眉、关系、双栏结构和嵌入对象也无法得到可靠保证。
最终,系统的主线从“重建一个看起来相似的文档”,转向“保留源 DOCX 容器,只修改经过确认的 OOXML 节点”。
这次重构带来的最大认识是:
复杂 Word 排版不是视觉参数的集合,而是一次受约束的文档结构迁移。本文讲清楚这个结论是如何得到的、当前方案怎样工作,以及为什么“结构审计 + 全页渲染”缺一不可。
第一版为什么看起来合理
Word 自己能够把 DOCX 导出为 Filtered HTML。导出的页面保留了大量段落标签、类名和行内样式,因此最初的处理链路非常自然:一个仍能打开的 DOCX,也可能已经坏了
第一次处理含大量 MathType 公式的习题册时,输出文件可以打开,页面上也能看到公式。只看截图,转换似乎成功。 但双击公式后,MathType 无法再编辑它。检查 DOCX 内部结构才发现,Word 导出的 HTML 只保留了公式的预览图,浏览器剪贴板传回的也是图片。原来的 OLE 二进制对象没有穿过这条转换链。 这类失败危险之处在于,它不是明显的文件损坏,而是能力的静默退化:- 公式仍然可见,但已经失去可编辑性;
- 标题看起来在正确位置,实际可能依赖错误的分页符;
- 页面样式大致相似,母题间距和选项对齐却被直接格式覆盖;
- OLE 数量可能没有显著异常,公式仍可能在页面中截断或重叠。
DOCX 不是一份文本,而是一张关系网
一个典型 DOCX 可以简化为下面的结构:word/document.xml,样式定义位于 word/styles.xml。图片和公式预览位于 word/media,可编辑 MathType 对象位于 word/embeddings,XML 中的 r:id 再通过 .rels 文件指向这些部件。
这意味着一个公式不是单独的 <w:object> 标签。它同时依赖正文中的对象节点、关系条目、OLE 二进制和预览资源。只复制 XML 标签而没有复制关系与部件,或让 HTML 把对象扁平化为图片,都会破坏完整性。
样式也比想象中复杂。Word 的最终视觉效果通常由两层叠加:
styles.xml中的可复用样式;- 具体段落或 run 上的直接格式。
从模板截图到结构合同
在 M1 纯享版模板上,我们没有继续凭肉眼抄录几个字号,而是做了一次只读“模板蒸馏”:把模板拆成可解释、可测量、可执行的规则。 提取范围包括:- 页面方向、尺寸、页边距和文档网格;
- 双栏数量、栏宽与栏间距;
- 页眉、页脚、页码域和其中的图片关系;
- “章节名称”“题型”“母题”“子题”“解析”等语义样式;
- 样式之外的段落间距、缩进和制表位覆盖;
- 图片、二维码、MathType/OLE 和关系部件;
- 页面级与结构级验收条件。
Equation.DSMT4 对象。这个数字并不是排版参数,却是非常重要的结构基线:任何全量格式迁移,都不能让这些对象在不知情的情况下变成图片或消失。
蒸馏后的模板不再只是一个“看起来正确的 Word 文件”,而是一份结构合同:规定页面应该如何组织、段落语义如何映射、哪些直接格式需要保留或清理,以及输出必须通过哪些检查。
这一步也改变了实现方式。程序不再试图复制模板中的全部内容,而是从模板读取规则,把这些规则应用到源文档自己的内容和容器上。
当前架构:保留源容器,只改明确目标
当前 OOXML 主线可以概括为四个阶段:检查、迁移、审计和渲染。1. 检查:记录处理前的结构基线
系统先读取模板中的自定义样式和公式对象数量,同时检查源文档的Equation.DSMT4、OLE、媒体和主要段落结构。
这个阶段不修改文件。它要回答的是:模板实际提供了哪些语义样式?源文档包含多少需要保留的对象?当前文档是否符合现有分类规则?
如果模板没有预期的“题型、母题、解析”等语义,继续批量执行只会让错误扩大。自动化的第一道安全门不是“脚本能不能跑”,而是输入是否属于它理解的文档类型。
2. 迁移:修改 document.xml 和 styles.xml
apply_exercise_template_ooxml.ps1 打开模板和源 DOCX 的 ZIP 包,读取双方的 word/document.xml 与 word/styles.xml,然后执行几类受控变更:
- 把模板的页面几何与自定义样式克隆到源文档;
- 为克隆样式分配稳定的
m1_*ID,并修复basedOn、next和link引用; - 根据文本和旧样式特征,将段落识别为章节、题型、母题、子题、解析或正文;
- 清理会压过模板的直接缩进、间距和加粗;
- 删除错误的显式分页符,并在规定的语义边界插入分栏符;
- 标准化独立公式段的 Tab、间距、缩进和对齐。
word/document.xml 和 word/styles.xml 写入新内容。word/embeddings、word/media、关系文件、页眉页脚和其他部件继续来自源文档。
这不是“所有非目标字节都不变”的强哈希证明,因为 ZIP 条目会被重新压缩;它保证的是非目标部件的解压内容来自源包、目标 XML 的修改范围明确,并通过后续计数和结构审计检查关键对象是否保留。
写入先落到临时文件,完成后再移动为正式输出,避免失败时留下一个名称正确、内容不完整的 DOCX。
3. 审计:把排版规则变成可判定条件
脚本执行成功并不代表排版正确。audit_exercise_template_ooxml.ps1 会重新打开输出包,将关键要求转换为机器可以判断的条件,例如:
- 显式分页符应被清除;
- 分栏符数量应与题型、检测题和反馈通道边界匹配;
- 解析正文和
Step n后续内容不能残留直接加粗; - 普通正文不能继续携带异常的直接缩进;
- 独立公式段不能保留错误的定位 Tab;
- 可删除的真空段不能继续制造版面间隙;
- 与源文档相比,公式标记、OLE 部件和媒体部件数量不能异常变化。
4. 渲染:结构正确仍不等于页面正确
审计通过后,系统用 Word COM 将 DOCX 导出为 PDF,再通过 Windows PDF 渲染 API生成逐页 PNG、检查页和 JSON 报告。 页面检查关注的是机器计数很难发现的问题:- 题型是否真正从下一栏顶部开始;
- 母题是否被不必要地拆开;
- 公式是否缺失、截断、重叠或偏移;
- 二维码和图片是否完整;
- 是否出现异常空白页;
- 解析正文是否因为 run 级粗体残留而整段变黑。
PASS 只表示 PDF 和页面图片完整生成,不能代替逐页视觉检查。结构审计证明“文档内部符合规则”,全页渲染证明“Word 实际把它画对了”。两者回答的是不同问题。
三个看似局部、实际决定可靠性的细节
分页符和分栏符不是同一件事
双栏习题册中,新题型通常应从下一栏开始,而不是跳到下一页。源文档却可能使用 page break 强行制造位置,甚至把分页符藏在一个非空标题段的 run 中。 早期清理逻辑只删除“空白分页段”,因此漏掉了这些隐藏节点。当前实现直接查找所有显式<w:br w:type="page"/>,删除断点但保留所在段落和文字;随后再根据“题型、检测题、检测题答案、反馈通道”等语义插入真正的 column break。
这个修复说明,Word 自动化不能只按段落外观推断结构。一个段落有文字,不代表其中没有控制页面流的隐藏节点。
粗体必须在 run 层处理
“解析:D”在人眼中是一段连续文字,在 OOXML 中却可能被拆成多个<w:r>。如果直接给整段取消粗体,会把“解析:”标签的强调也删掉;如果只改段落样式,后续 run 上的直接 w:b 和 w:bCs 仍会压过样式。
当前实现先识别解析标签所在的 run,再只清理标签之后的直接粗体,并将处理范围延续到下一个语义标题。15 份解析版文档的批量纠偏,正是由这个细节推动完成的。
公式段不能只依赖样式名
MTDisplayEquation 表示独立公式段,但源样式可能带居中制表位,公式前还可能存在定位 Tab。直接复制样式会把错误位置一起保留下来。
当前规则在保留公式 OLE 本身的前提下,删除公式前的定位 Tab,将段前段后和缩进归零,并显式左对齐。与此同时,它不会全局删除 xml:space="preserve",因为行内公式前后的保留空格可能正是正文与对象之间所需的分隔。
这里的原则是:按语义缩小修改范围。只处理确认属于独立公式段的几何属性,不对所有 Tab 和空格进行粗暴清洗。
两条路线,而不是一个万能转换器
OOXML 主线并没有让 HTML 路线失去价值。它只是把两类不同任务分开处理。
为了防止误用,HTML 新建模式检测到公式数量标记后会停止,而不是静默输出一个“看起来正常、公式已经不可编辑”的文件。
从单文件试验到批量验证
架构是否可靠,最终要看它在真实文档上的表现。 M1 模板的只读提取记录了 1,569 个Equation.DSMT4 对象,并形成了人工可读规范与机器可读规格。M18 源文档记录了 1,433 个公式对象,用于验证版式迁移前后的对象保留边界。
在后续批量任务中:
- 15 份解析版文档完成了 run 级粗体纠偏和容器结构核验;
- 9 份待续排文档逐文件执行结构审计,9/9 通过;
- 这 9 份文档共渲染 307 页并完成逐页检查,未发现公式缺失、截断、重叠或异常空白页。
这次重构留下的工程原则
第一,先定义不变量,再讨论如何修改。对于习题册,不变量包括正文内容、公式可编辑性、OLE 与媒体部件、关系网络和未授权的用户文件。没有这些边界,“格式迁移”很容易变成不可控的文档重建。 第二,模板应该被蒸馏为规则,而不是只作为视觉参考。样式、直接格式、页面几何和语义槽位都应被记录;否则每一次套版都要重新猜测。 第三,最小修改面比万能转换更可信。保留源容器,只修改document.xml 和 styles.xml 中确认过的节点,虽然需要理解 OOXML,却显著降低了公式、图片和关系被转换的风险。
第四,自动审计与视觉检查必须并存。对象计数、断点和直接格式适合机器验证;重叠、截断、错栏和页面节奏仍需要完整渲染来发现。
第五,把失败经验固化成门禁。公式退化为 PNG 后,系统增加了 HTML 模式保护;隐藏分页符导致空白页后,清理范围扩展到 run 内部;解析正文误加粗后,处理粒度下沉到 run。可靠性不是一次设计出来的,而是把每个真实故障转化为规则、测试和拒绝条件。
结语
word-format 最初解决的是“怎样把参考 Word 的格式复制过来”。真实文档迫使我们换了一个问题:怎样在改变版式的同时,证明复杂 DOCX 中不该改变的东西仍然存在?
这个问题的答案不再是一条 HTML 转换命令,而是一套分层方法:把模板蒸馏成结构合同,以源 DOCX 为容器做定点 OOXML 修改,用自动审计证明关键约束,再用全页渲染检查 Word 的真实输出。
自动化处理复杂文档,最有价值的能力不是“生成一个能打开的文件”,而是能够清楚说明:修改发生在哪里,哪些结构被保留,以及结果通过了什么证据。