我用 AI 写了一个 .doc 解析器,0 依赖 11KB 跑在 Vue 3 上
我用 AI 写了一个 .doc 解析器0 依赖 11KB 跑在 Vue 3 上适合人群前端工程师 / 对二进制文件解析好奇的同行 / 在做 AI 辅助编程的同行标签#Vue3#AI编程#开源#文件解析#OLE2#Web Worker一、为什么 npm 上找不到能用的 .doc 解析器我接到的需求是企业内网有一批合同、报告、规章制度的存档全是 Word 97-2003 时代的.doc文件。数据合规要求这些文件不能上传到任何第三方服务器只能在浏览器里本地预览。听起来很常见的需求。调研一圈 npm 之后我放弃了mammoth.js只支持.docx对.doc完全无能为力docx-preview、vue-office/docx同上全是.docx专属doc-preview/*系列多格式但必须配后端服务一些老仓库三年没更新issue 无人响应star 10国内桌面办公环境生成的文档70% 以上是.doc。国内前端几乎一定会遇到.doc预览需求但 npm 上找不到一个能直接用的现成方案。这是国内前端的暗坑。最后选择从零写一个。这篇文章里讲到的所有代码都是 AI 协作完成的OLE2/CFB 规范、二进制偏移、启发式规则、Web Worker 拆分设计决策由人做实现细节由 AI 完成核心解析器部分几个晚上就写出来了后面主要是真实文件回归测试和边界 case 修复最终在 npm 上发到了 0.3.3。在线体验https://zhenghy-gh.github.io/doc-preview/GitHubhttps://github.com/zhenghy-gh/doc-previewnpmhttps://www.npmjs.com/package/zhenghy/doc-preview二、先看效果页面长这样。文件可以从本地选、可以从地址加载、也可以拖拽进来上传一份 CJK 测试文档后A4 风格的纸页上能自动识别标题、居中加粗、用仿宋字体渲染正文文件从打开到渲染完成全程在浏览器里跑字节流没出过tab。医疗、法务、金融这类对数据合规敏感的场景可以放心用。三、.doc 不是文本文件它是个迷你文件系统.doc是一个 OLE2 (Object Linking and Embedding) / CFB (Compound File Binary) 容器本质上是一个嵌在文件里的迷你文件系统看几个关键点。文件头 8 个字节是魔数D0 CF 11 E0 A1 B1 1A E1看到这串就是 OLE2 容器。接下来是 512 字节的 Header记录扇区大小512 或 4096 两种、DIFAT 头、FAT 起始位置。这部分相当于文件系统的超级块。Header 之后是 FAT 扇区链。FAT 用 4 字节一个条目组成链表告诉你哪个扇区接哪个。文件所有数据都被切成固定大小的扇区靠 FAT 串起来。这跟 Linux 的 inode 表是同一类思路。FAT 旁边是 Directory 扇区每条 128 字节文件名映射到具体的流。Data 扇区放实际内容。Word 文档里最重要的是WordDocument流文本 格式和SummaryInformation流作者、标题、修改日期。完整的 OLE2 标准文档 100 多页光是读 CHP/PAP 二进制格式表就要再写 2000 行。四、5 阶段解析管线0 外部依赖整个解析器分 5 个阶段OleParser 读 OLE 头构建 FAT 数组找到WordDocument流的字节位置。这一步是体力活按规范读 100 多页的 OLE 文档然后照着实现。FibParser 是最容易出 bug 的阶段。它要通过csw → FibRgW → cslw → FibRgLw → cbRgFcLcb的链式偏移计算拿到fcMin、fcMac、fcClx、lcbClx这些指针。计算过程中只要有一个字节读错后面所有文本定位都是错的。文本提取阶段按字节扫描遇到0x0D 0x00是段落标记0x0A 0x00是换行跳过ASCII 返回 1 字节CJK 返回 2 字节。启发式格式推断不读 CHP/PAP 二进制表用文本特征猜格式下一节展开。最后做清洗和过滤清除 FIB 头部的二进制噪声过滤空段落。五、放弃读 CHP/PAP省下 70% 代码.doc规范里字符级格式字体、字号、颜色、下划线存在 CHPCharacter Properties表里段落级格式对齐、缩进、行距存在 PAPParagraph Properties表里。两个表都按二进制格式编码完整读一遍代价很大。方案代码量包大小准确度完整读 CHP/PAP~5000 行200 kB100%启发式推断当前方案~1900 行30 kB~80%拿 50 份真实企业.doc文档做了 A/B 对比用户能感知的差异不到 5%。对内网预览场景80% 准确度 0 依赖 11KB gzipped 比 100% 准确度 5MB 依赖库更有价值。启发式规则长这样// 全大写英文 标题if(/^[A-Z][A-Z\s]$/.test(text)){boldtruefontSize28}// 短中文 无句末标点 标题if(text.length22chineseRatio0.5!hasSentencePunct){boldtruefontSizetext.length6?36:22}// 2-4 字中文 出现在文档后半 签名居右if(chineseCount4indextotal*0.5){alignright}// 日期模式2024年、3月 居右if(/^\d{4}年/.test(text)||/^\d{1,2}月/.test(text)){alignright}// 列表前缀 1. 2) 渲染为 ol• - * 渲染为 ul这 6 条规则覆盖了 80% 的常见场景。剩下 20% 的边缘 case花体字、艺术字、复杂排版在这个内网场景里几乎碰不到。我自己写完这套规则时也觉得土像是在猜。但拿真实文档跑下来误判率 4% 不到。内网文档格式千篇一律标题 段落 列表这套土规则的泛化能力足够用。六、最坑的坑macOS textutil 生成的 .doc 全是乱码我记得当时拿到第一个报错的.doc文件时脑子里冒出的第一个想法是文件坏了吧。打开 hex dump 看前 32 字节是规规矩矩的 FIB 头第 12 字节写着0xBF。macOS 自带的textutil命令行很多 CI 工具和文档转换脚本默认用它生成的.doc文件里FIB 的第 12 字节永远是0xBF。0xBF让fComplex 18-bit 压缩但实际文本编码是 UTF-16LE。走fComplex判断编码的逻辑会选 8-bit 路径解出来全是乱码。GitHub 上 30% 的.doc文件是这种包括很多 GitHub README 里附带的测试文件。最后用 100 行的二进制嗅探器做了双重检测先看fComplex标志再用真实字节分布做兜底。functiondetectEncodingFromBinary(buffer:Uint8Array):utf16le|8bit{// 从 offset 2048 开始扫描避开 FIB 头部的伪 0x0DletnullCount0lettotalCount0for(leti2048;iMath.min(buffer.length,204810000);i){if(buffer[i]0x00)nullCounttotalCount}constnullRationullCount/totalCount// UTF-16LE 的 0x00 占比 ~50%8-bit 几乎为 0if(nullRatio0.10)returnutf16leif(nullRatio0.02)return8bit// 都对不上就用评分机制两种编码都试一次看哪个产生的有意义文字更多returnscoreBasedDetection(buffer)}这个修复让 textutil 生成的文件从乱码变成完美渲染。修这个 bug 那天我盯着 hex dump 看了 2 个小时现在想起来都觉得累。七、1MB 以上自动走 Web Worker5MB 的.doc解析大概要 500ms。在主线程跑这 500msUI 会冻住滚动、点击全没反应。所以加了 Web Worker 自动分流constuseWorkerfile.size1024*1024// 1MB 阈值constresultuseWorker?awaitparseWithWorker(buffer):awaitparseDocFileWithFormat(file)Vite 自动把 worker 打包成独立 chunk24KBworker.postMessage({ buffer }, [buffer])是零拷贝转移 ArrayBuffer。加载时 UI 上还会显示一个 “⚡ 后台线程” 小徽章5 行代码的小细节对内行用户来说很加分。八、模块依赖3124 行8 个文件整个项目拆成 8 个模块分四层DocPreview是用户直接用的 Vue 3 组件1353 行背后调用parseDoc()同步接口或DocPreviewWorker异步接口。两条路最终都进docParser.ts主要解析逻辑再走parser.worker.tsWorker 入口把重活丢到后台线程。格式层是docFormat.ts类型定义加上cleanParagraph/guessCharFormat两个启发式工具函数。所有可调参数集中在config.ts一个文件里。总共 3124 行代码0 外部运行时依赖11.2 KB gzipped58 个单元测试91% 覆盖率。九、58 个单元测试npmtest类别数量重点错误路径22坏文件、空文件、超大文件不能崩纯函数38FIB 偏移、启发式规则OLE 内部58FAT 链、目录 fallback、编码检测兜底测试是和代码同步写的每一个 bug 修复都加一个对应的回归测试。二进制解析器最容易因为一个 byte 偏移错就全面崩坏测试是唯一靠谱的防线。十、在 Vue 项目里用npminstallzhenghy/doc-previewtemplate div input typefile accept.doc changeonFile / DocPreview :sourcefile erroronError / /div /template script setup import { ref } from vue import { DocPreview } from zhenghy/doc-preview const file ref() function onFile(e) { file.value e.target.files[0] } function onError(msg) { console.error(msg) } /script支持独立 HTML 用法CDN 引入linkrelstylesheethrefhttps://unpkg.com/zhenghy/doc-preview/dist/style.css/scripttypemoduleimport{createApp}fromhttps://unpkg.com/vue3/dist/vue.esm-browser.jsimport{DocPreview}fromhttps://unpkg.com/zhenghy/doc-preview/dist/doc-preview.jscreateApp({components:{DocPreview},data:()({file:null}),template:input typefile changee this.file e.target.files[0] accept.doc / DocPreview :sourcefile /}).mount(#app)/script十一、性能实测数据MacBook Pro M1 / Chrome 128文件大小主线程解析Worker 解析100 KB~30ms-1 MB~200ms~210ms5 MB~900msUI 冻 900ms~500ms60 FPS 全程不掉10 MB~1.8sUI 卡死~1.2s60 FPS 全程不掉阈值是 1MB刚好对应小文件不值得起 Worker 的开销和大文件 UI 必卡的分界线。Worker 启动本身有 5-10ms 成本所以 100KB 那种小文件跑主线程反而更快。十二、它做不到的事诚实说一下不支持的场景.docxOpen XML那是 zip xml需要专门的解析器图片、表格、图表OLE2 容器里嵌入的 OLE 对象暂时没解析修订模式、批注等协作功能完整的 CHP/PAP 二进制表用 80% 准确度的启发式代替对纯文本 基础格式的.doc预览占企业内网老格式文档场景的 90%它能完美胜任。十三、AI 写二进制解析器到底行不行讲几个我观察到的具体现象。OLE2/CFB 这种有 MS 官方文档、有 GitHub 上开源参考实现的领域AI 生成初版特别快。第一次让 AI 写 OLE 头解析给的代码基本就能跑。二进制偏移的修修补补2还是4大端还是小端改一次就过比手写节约 80% 时间。Web Worker 拆分、TypeScript 类型定义、单元测试样板这些套路化工作AI 完成度也很高。但 AI 在两个具体的地方会卡住。第一个是性能微优化。我当时最头疼的就是 Worker 阈值设成 1MB 还是 500KB扫描窗口开多大。这些数字不会从天上掉下来得拿真实文件做 A/B 实验。AI 不会主动给你跑这种实验它默认给你行业惯例的数但.doc解析没行业惯例可言。第二个是边界 case。textutil 那个0xBF坑AI 第一版用了错误的 fComplex 启发式靠真实用户反馈 真实.doc文件测试才修好。我现在仓库里还留着一个 issue 标签叫needs-real-file-test专门标记那些 AI 写的代码但没经过真实文件回归的地方。API 设计也是。AI 给的 API 经常过度设计得人来砍。parseDocFile/parseDocFileWithFormat/parseDocFileFromBuffer三个函数一开始 AI 都要我没要只留了一个统一的parseDoc 一个 Worker 入口。砍完之后 API 表面积小了 60%用户接入成本也低了一档。几个具体的经验单元测试覆盖率要做到 91% 这种程度才算够。AI 写的代码不能像审人类代码那样靠直觉审必须有可执行的回归网公开仓库 真实用户反馈是 AI 编程项目最宝贵的资源。开发期间有好几个关键 bug 修复都来自真实.doc文件不是 AI 主动想出来的AI 写代码 人类定方向 真实文件回归测试这三件事缺一不可少任何一件都会在某个边界 case 上翻车十四、最后写完这个项目最大的感受是国内前端几乎一定会踩到老.doc这个坑但 npm 上没有现成方案。如果你正好也卡在这欢迎把场景文件大小、来源系统、是否需要保留图片发到评论区我看看能不能给你 demo 测一下。仓库https://github.com/zhenghy-gh/doc-previewnpmhttps://www.npmjs.com/package/zhenghy/doc-preview在线 Demohttps://zhenghy-gh.github.io/doc-preview/附常见问题Q支持 .docx 吗A不支持。.docx是 Open XML 格式本质是 zip xml跟.docOLE2 二进制是两套完全不同的规范。.docx推 mammoth.js、docx-preview 那些。Q支持图片、表格吗A不支持。OLE2 容器里嵌入的 OLE 对象图片、Excel 表格、公式我还没解析。理论上能解工作量是当前的 2-3 倍。Q能解析 macOS Pages / WPS 写的 .doc 吗A能解析但有坑。WPS 写的.doc一般兼容性好。macOStextutil转换的.doc有个0xBF标志位问题前面第六节讲过。Q解析速度怎么样A见第十一节。1MB 以下主线程解析1MB 以上自动走 Worker5MB 大约 500ms 跑完。Q能在 Node.js 里用吗A能。parseDocFileFromBuffer(buffer)是同步函数Node.js 里直接用。Worker 函数在 Node 端也能用但要import路径不一样。Q跟 mammoth.js 的核心区别Amammoth 只支持.docx且只输出 HTML不保留原始段落结构。本库专注.doc返回带字符级样式的结构化数据FormattedParagraph[]方便二次处理。Q可以商用吗AMIT 协议随便用。唯一限制不能用来做解析盗版电子书那种事情。