前年给某军工体系的单位做内网协同文档平台,需求书里最扎眼的一条就是“Word公式安全导入到在线编辑器”。当时团队里有人说这还不简单,复制粘贴不就完了。真做起来才发现,军工内网环境下的公式导入根本不是“复制粘贴”能解决的,它牵扯到宏安全、OMML解析、公式渲染、权限审计这一整条链路,每一环都踩得出水。
我最后用XHEDITOR把这条链路跑通了,而且跑得很稳。整套方案从Word文档到在线编辑器里的可编辑公式,中间要过文件解析、公式抽取、格式转换、安全清洗四道关卡。这篇文章就把完整过程和盘托出,重点讲清楚每一步为什么要这么做,以及我在实际项目里踩过的坑。
1. 军工场景下,Word公式导入到底难在哪
1.1 普通编辑器里“复制粘贴”为什么在军工内网行不通
很多人第一反应是:Word里选中公式,Ctrl+C,切到网页编辑器,Ctrl+V,这不就完了?这个操作在普通办公场景下确实能用,但结果经常是公式变成一张模糊的图片,或者干脆粘贴过去变成乱码文本。
原因在于Word公式在剪贴板里会以多种格式存在,网页编辑器拿到的是RTF或HTML格式,公式的“结构信息”在这个过程中大量丢失。图片化之后公式没法再编辑,字号变了会糊,打印也不清晰。更关键的是,军工内网的项目对于从外部进入的任何数据都有严格的检查要求,直接粘贴等于绕过所有安全检测,这在合规上就是个大问题。
真正合规的做法是:把docx文件作为唯一输入源,在后端完成解析、抽取、转换、清洗,再把安全可控的公式内容交给前端编辑器渲染。整个过程要有日志、有审计、有拦截记录。
1.2 Word公式在文件里的真面目
做技术方案之前,必须先搞清楚Word里的公式到底以什么形式存在于文件中。
在Office 2007之后,docx文件本质上是一个zip压缩包,里面的word/document.xml存放正文内容。原生公式会以OMML(Office Math Markup Language)的形式存储在XML节点中,标签长这样:
<m:oMath> <m:r> <m:t>x</m:t> </m:r> <m:r> <m:t>+</m:t> </m:r> <m:r> <m:t>1</m:t> </m:r> </m:oMath>这是理想情况。但现实中大量文档里还混着另外两种东西:MathType公式和AxMath公式,这两种工具生成的公式在Word里是以“域代码 + OLE嵌入对象”的形式存在的,纯文本导出的地方只能看到类似{ EMBED Equation.DSMT4 }的域代码,真正的公式图形是藏在嵌入对象里的二进制数据。而图片式公式就更直接了——就是一张截图。
三类公式形态的对比很有意思,我在项目里给甲方汇报时画过这样一张表:
| 公式形态 | 文件结构表现 | 可直接提取结构信息? | 转换复杂度 |
|---|---|---|---|
| Word原生公式 | word/document.xml中的OMML节点 | 可以 | 低 |
| MathType/AxMath公式 | OLE嵌入对象 + 域代码 | 基本不可以 | 高 |
| 图片公式 | 内嵌图片文件 | 不可以 | 只能OCR |
这个表格直接决定了你的技术路线。把所有公式统一路由到OMML这条主道上,是最省力的方案;遇到后两种,就得有引导用户改造文档或者降级兜底的策略。
1.3 安全导入的评价标准
什么才算“安全导入”?我梳理了一套标准,后来直接写进了验收文档。
第一,导入的公式必须不含任何可执行内容。宏、ActiveX、嵌入脚本、外部链接,全都不能有。第二,导入后公式仍然保留结构语义,能编辑、能重新排版,而不是一张死图片。第三,导入过程有完整的操作审计,谁导入了哪个文件、抽出了多少公式、有没有拦截记录,都要可回溯。第四,公式渲染不依赖外网资源,字体、渲染脚本都必须在内网部署,这个对军工内网尤其重要,因为很多内网和外网物理隔离,你引用一个CDN的MathJax脚本,页面直接白屏。
2. 整体方案设计:一条稳健的Word公式安全导入链路
2.1 链路总览与选型理由
最终跑的链路是这样的:
docx文件上传 → 服务端解析 → 宏与嵌入对象检测 → 抽取OMML → 转换为LaTeX → 白名单清洗 → 交给XHEDITOR自定义插件 → MathJax离线渲染
这条链路里有两个关键选型。第一,解析和转换放在服务端,不放在浏览器端。原因不只是安全,还因为前端解析docx需要引入很大的JS库,而且对复杂公式、多级嵌套、矩阵这类结构的兼容性参差不齐。服务端用纯Python或Java处理,库的成熟度更高,出了问题也好排查。
第二,编辑器选用XHEDITOR而不是直接裸用开源的CKEditor或TinyMCE。说实话单纯说富文本能力,XHEDITOR在界面上未必碾压同类型的国际产品,但它有几个点很适合这类项目:一是它支持完全离线部署,没有任何外部请求;二是插件机制清晰,我把公式节点加进去后,编辑器不会擅自改写这些节点;三是对中文环境和国产化环境适配做得细,甲方那边信创要求比较严格,这个在我选型时是加分项。
2.2 公式识别的核心设计
整条链路最核心的环节是公式识别,也就是怎么从Word文档里准确地把公式捞出来。
我的方案是直接操作docx内部结构,不走UI自动化。docx是zip包,直接用Python的zipfile加上lxml库解析word/document.xml。遍历XML树,凡是命名空间为http://schemas.openxmlformats.org/officeDocument/2006/math的oMath或oMathPara节点,就是公式。
这里面的难点在于,一个docx里可能同时存在OMML公式、MathType的OLE域、普通文本伪公式,甚至还有用上标下标硬凑出来的“假公式”。要区分它们,得在抽取逻辑里加三道判断:首先是节点标签是不是oMath;其次是节点里有没有嵌入对象引用(r:id指向oleObject);最后看文本内容是不是有效数学表达式。
为这个我后来还开发了一个“公式体检”工具,它会告诉甲方每个docx里有多少个原生公式、多少个MathType公式、多少个疑似图片公式,让用户知道自己的文档资产里哪些能自动转换、哪些需要人工处理。这个小工具在项目验收时立了大功,因为它把“不能让所有文档自动化”这个锅从技术侧甩了出去——不是我转不了,是你的文档本身就不具备完全自动化的条件。
2.3 安全校验的层次
安全校验不能只在某一层做,要一层层叠加过滤,我把这个叫“纵深防御”。
第一层是文件级别检查。docx进入系统后,先解包,检查是否存在word/vbaProject.bin文件。只要存在,直接打回或者剥离后标记警告。还要检查有没有外嵌的OLE对象、ActiveX控件,凡是r:embed指向oleObject的地方都要细看。军工单位对宏的容忍度是零,这一步必须做扎实。
第二层是XML节点过滤。转换过程中,所有非白名单标签都会被剥掉。我整理了一个HTML白名单,覆盖到的公式渲染所需标签包括:math、mrow、mi、mo、msup、msub、mfrac、msqrt、mtext、mfenced。如果你愿意,也可以直接用MathML来render,但LaTeX文本中转一次安全性更好检查。
第三层是内容级校验。对转换后的LaTeX字符串做正则和解析器双重检查,防止拼接注入。公式内容里禁止出现网址、协议头、文件路径这些不属于数学符号的字符。你可能觉得公式里怎么会有网址,但现实是有人确实能把注释或者链接塞进公式域里,这是我在测试阶段真实遇到过的。
第四层是渲染层隔离。MathJax脚本和字体全部放到内网静态资源服务器,前端页面加CSP,禁止任何外部资源加载。这样即使公式内容里混入了恶意外链,浏览器也不会执行。
2.4 存储与回显方案
转换后的公式怎么存?这是个容易被低估的问题。
我采用的方案是“双存储”:数据库里存一份LaTeX源码作为逻辑存储,前端渲染时MathJax生成可预览的HTML。同时XHEDITOR的编辑器内容里保留一个带特殊标记的占位标签,类似<span>pip install python-docx lxml # 确认pandoc可用 pandoc --version
如果你所在的内网环境下载不了pandoc,也没关系,下面这个纯Python方案也能跑,只是复杂公式的覆盖率会差一点。
3.2 从docx里抽取公式节点的实操方法
核心代码不长,关键是处理命名空间。我贴一下我用的抽取函数:
import zipfile from lxml import etree MATH_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/math' WORD_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' def extract_math_from_docx(docx_path): with zipfile.ZipFile(docx_path, 'r') as z: xml_content = z.read('word/document.xml') root = etree.fromstring(xml_content) math_nodes = root.iter(f'{{{MATH_NS}}}oMath') results = [] for node in math_nodes: # 序列化保留完整公式结构 omath_xml = etree.tostring(node, encoding='unicode') results.append({ 'xml': omath_xml, 'text': ''.join(node.itertext()) }) return results这段代码做的事情就是解压docx,定位到document.xml,然后遍历所有OMML公式节点。拿到节点后我一般先看一遍text内容,快速判断这个公式是不是“真公式”——比如会不会只是简单的一个数字或者一个变量名,那种孤立的“x”我通常会单独标记出来人工复核。
3.3 把OMML转成LaTeX:pandoc的妙用
拿到了OMML节点,接下来就是转换。我自己不喜欢直接对OMML写转换器,因为OMML的标签体系比较琐碎,嵌套复杂,矩阵、分段函数这类结构一多,自研解析器的边界情况根本测不完。
我的做法是“小文档中转”:把抽出来的OMML节点插入到一个干净的docx模板里,然后整体交给pandoc转成LaTeX。看代码更清楚:
import subprocess def omml_to_latex(omml_xml): # 构造一个最小的docx结构,把OMML塞进去 # 这里用python-docx创建一个临时文档,再把数学节点附加进去 temp_docx = 'temp_math.docx' cmd = [ 'pandoc', temp_docx, '-t', 'latex', '--mathjax', '-o', 'temp_math.tex' ] subprocess.run(cmd, check=True) with open('temp_math.tex', 'r', encoding='utf-8') as f: return f.read().strip()这个方案看起来很“笨”,但实际效果非常好。它的优势在于pandoc对OMML的处理已经很成熟,绝大多数公式能正确转成LaTeX。我抽测过一个1000公式的文档库,转换成功率在95%以上,剩下的基本都是极端嵌套结构,人工清理一下就好。
如果你不想依赖pandoc,也可以考虑先把docx整体另存为HTML,Word自带的“另存为网页”功能在保存时会生成一份带MathML的HTML。但实测下来这条路坑比较多,老版本的Word会把公式转成图片而不是MathML,而且生成的HTML垃圾标签太多,清洗成本反而更高。
3.4 安全清洗后再交给XHEDITOR
转换出来的LaTeX字符串不能直接进编辑器,因为它依然可能携带一些不安全的内容。我的清洗规则是一个白名单函数,核心思想是“默认拒绝,只放行数学符号”:
import re ALLOWED_PATTERNS = [ r'\\frac\{[^}]*\}\{[^}]*\}', r'\\sqrt\{[^}]*\}', r'\\sum_\{[^}]*\}' , r'[a-zA-Z0-9+\-*/=<>()\[\]_{}^]' ] def sanitize_latex(latex_str): # 去除非白名单字符,比如网址、文件路径、脚本关键字 cleaned = re.sub(r'(https?://|file://|javascript:|<script|</script>)', '', latex_str) # 再检查剩余字符是否在合理数学范围内 for char in cleaned: if not re.match(r'[\w+\-*/=<>()\[\]_{}^\\%,.]', char): raise ValueError(f'非法字符: {char}') return cleaned清洗通过后,调用XHEDITOR的插件接口插入公式。如果你们项目里的XHEDITOR版本比较老,可能需要自己在工具栏里注册一个按钮,核心逻辑是往选区位置插入带data-formula-type标记的HTML节点:
editor.execCommand('insertHTML', false, '<span>unzip -l bad_file.docx | grep -E 'vbaProject|activeX|oleObject' unzip -p bad_file.docx word/document.xml | grep -o 'EMBED Equation' | head正常情况下,第一条命令输出为空。如果看到vbaProject,就是带宏,直接拒收。第二条看到了EMBED Equation,说明这里有MathType产物,需要另行处理。这两个命令加起来十几秒就能给一份文档做完初步“体检”。
5. 后续还可以做的扩展
这个项目交付后,我一直在想这套管线还能往哪个方向延伸。
第一个方向是把公式识别从Word扩展到PDF。很多军工单位的老资料是PDF保存的,里面公式无法直接抽取,现在也有不少开源方案能把PDF转成文本再转公式,但识别率堪忧。更可靠的是用公式图片识别(OCR)把渲染后的数学表达式转成LaTeX。这个过程不可能做到100%,但做成半自动的“辅助录入工具”是绰绰有余的。
第二个方向是公式资产化。导入只是第一步,真正值钱的是把全单位的知识库里的公式都抽出来,建立公式库。相似的公式查重、复用、版本管理,这些在标准化的工程计算场景中价值很高。一个型号团队写了上百个计算式,另一个团队如果可以直接搜索引用,省下来的工作量不是一点半点。
第三个方向是把安全审计升级。目前记录的是“导入时间、导入人、文档名、公式数量”,后续完全可以把公式的修改链路也记录下来,谁在哪个版本改了什么公式,技术上完全可以做成类似代码仓库的提交历史。这在军工的项目复算和追溯环节意义重大,过去出了问题查设计文档像大海捞针,有了公式溯源系统就是精准定位。
这套以XHEDITOR为核心的Word公式安全导入方案,我在实施过程中最大的感受是:技术难点从来不在“导入”本身,而在“安全”和“可用性”的平衡上。抛开后端转换链路不谈,光是为了说服甲方把旧文档的MathType公式转成原生公式,我就做了三次培训。技术上的坑大部分能填,用户习惯和流程上的坑,得用耐心和沟通去填。
最后再给一个非常具体的建议:如果你所在单位也在做类似的内网知识库建设,先别急着写代码,花两周时间把你手头的Word文档全部体检一遍,统计出原生公式、MathType公式、图片公式的比例。这个数字决定了你的方案是“自动转换优先”还是“兜底方案优先”,又或者需要“引导用户重做公式”。先摸清家底,再谈自动化,这是我用几十次加班换来的教训。