读完一万段要两分钟:我们把标书读取从 WPS 对象模型搬到了 docx 解析

WPS 加载项逐段读文档,一千段要 14.5 秒,一万段直接卡死两分钟。我们最后把「读」整个搬出 WPS,直接解 docx,读一遍降到一秒级。这篇讲清楚为什么慢、怎么改、哪些场景还是不灵。## 点一下「通读」,WPS 就不动了

早期版本里我们做了个「通读」按钮,点下去界面直接失去响应。用户以为崩了,再点一次,卡得更久。

我们一开始的判断是加载项和主线程抢资源,于是加异步、加进度条、把回调拆开,全都没用。后来在几台机器上埋点计时,才发现时间几乎全花在读段落上,跟渲染、跟主线程调度关系不大。

实测的三个数字:Paragraphs.Item(i) 读 1000 段 14.5 秒;Paragraph.Next() 读 2000 段 22.6 秒;Tables.Item(i) 读 231 张表 14.7 秒。一册标书一万多段,逐段扫一遍两三分钟,WPS 全程被占着。

这不是调参数能救的,是路选错了。

Paragraphs.Item(i) 为什么越读越慢

WPS 的 JS 加载项走的是 COM 风格的对象模型,每次调用都是一次跨进程请求。Paragraphs.Item(i) 的问题在于集合没有游标:取第 i 个元素,它要从头数过去。读第 1 段很快,读第 9000 段就要先数 8999 次。整体是 O(n²),段落越多越难看。

Paragraph.Next() 看起来像顺着指针走,实测 2000 段 22.6 秒,也不便宜。每次 Next 仍然是一次跨进程往返,而且拿到的对象还要逐个读属性------样式名、文本、范围,每个属性又是一次往返。表格同理,Tables.Item(i) 取 231 张表 14.7 秒。

所以结论很直接:对象模型适合点对点操作,定位一个标题、替换一处文字、往某个格子里写值。它不适合扫全篇。我们之前把「扫描」和「编辑」混在同一套 API 里做,卡死是必然的。

读的活搬出去,直接拆 docx

.docx 本质是个 zip,正文在 word/document.xml。我们的加载项本来就配了一个本机 Node 服务,于是把「读」整个搬过去:服务端直接打开盘上的 .docx,解压、解析 XML,一次扫出标题树、每张表的每个格子、图名、词频,按文件修改时间缓存。

实测一册约 6MB 的文档,读文件加解压约 1 秒,扫一遍几十毫秒。对比对象模型的两三分钟,不是一个量级。而且这个过程完全不碰 WPS,界面不卡,用户可以继续翻文档。

几个踩过的工程细节:

  • WPS 开着文件时也能读。docx 文件本身允许共享读,我们只读不写,不会和 WPS 抢锁。
  • 取索引前先让 WPS 执行一次 Save。用户改完没保存就点检查,读到旧版本会得出错误结论,这个坑我们踩过,结论错得还很像那么回事。
  • 缓存键用文件修改时间,不用内容哈希。哈希要整读一遍文件,修改时间的代价几乎为零,够用。
  • 解析器只取我们需要的节点,不建完整 DOM。一册一万多段全建树,内存和 GC 都不划算。

思路用伪代码大概是这样:

js 复制代码
// 服务端:只读解析,不经过 WPS
const zip = await openZip(docxPath);        // 共享读,WPS 开着也能打开
const xml = await zip.read('word/document.xml');
const tree = parseHeadingsAndTables(xml);   // 标题树 + 表格格子 + 图名
cache.set(docxPath, { mtime, tree });       // 按修改时间失效

这条路不是我们独创。docx4j、python-docx、Aspose 这些库都是直接处理文件的路数,只是我们一开始被「加载项就该用加载项 API」的惯性带偏了,没往这个方向想。

写的时候再回到 WPS

索引建好之后,所有「写」的操作仍然走 WPS 对象模型,因为只有它能保证改动进入 WPS 的编辑状态、能被撤销、能触发重排。

用法都是点对点的:按样式 Find 定位标题,约 0.6 秒一次;在指定范围里 Find 替换;往表格格子里写值。这些操作次数有限------一册文档几十个标题、若干处替换,加起来是秒级,用户能接受。

取舍就一句话:读走文件,写走对象模型。读是 O(全篇),写是 O(改动点),把重的那头放到不依赖 WPS 的一侧。

顺带一个好处:索引在服务端,检查逻辑可以纯函数化------标题层级是否完整、表格是否有空单元格、图名编号是否连续,这些都不依赖 WPS 是否在前台,也方便单独跑测试。

这个办法的边界

不是所有场景都能这么干,说清楚几条:

文档没保存过就不行。 我们依赖盘上的文件,用户新建文档还没落盘,服务端读不到。目前是提示先保存,没做内存态同步。

只认标准 .docx。 WPS 的私有格式不行。老 .doc 要先转换,转换本身有损,公式、域代码、复杂图形可能变形。

修订和批注要单独处理。 document.xml 里修订痕迹是带标记的节点,直接按文本扫会把删除的内容也算进去。我们目前对带修订的文档给提示,不做自动判断。

写操作仍然受对象模型约束。 如果某次改动是「全篇批量替换几百处」,还是会慢。我们限制单次检查的写操作数量,超出的部分让用户分批做。

缓存一致性靠 Save 保证,不是强一致。 用户在 WPS 里改了但没保存,索引就是旧的。这是有意的取舍,换来的是读路径足够快。

还没解决的部分:图片和嵌入对象只解析到图名和位置,没做内容识别;跨文档的样式继承没完全覆盖,个别自定义样式在标题树里会漏。这些不影响主流程,但会在边界文档上出偏差。

回头看,最初的错误不是性能优化没做好,是把「读」和「写」当成了同一类操作。分开之后,卡死的问题自然就消失了。

我们做的是投标文件编制方向的工具,上面这套读写的拆分是产品里真实在跑的做法。同行如果也在做 WPS 加载项处理长文档,欢迎交流各位怎么处理修订和私有格式这两个坑。


以上是我们开发 WPS 加载项「标书神器」时的实际做法,欢迎交流。

相关推荐
打工仔折腾 AI5 小时前
Changedetection.io 网页变动监控:Docker 部署、通知配置与公网暴露实践
运维·人工智能·后端·python·docker·容器·性能优化
某哈压力大8 小时前
SSH 突然 Connection reset?不是密钥坏了,是 OpenSSL 把 sshd 干趴了
前端·node.js·命令行
回眸&啤酒鸭9 小时前
【回眸】Node.js 新手从零到一实战指南
node.js
浪遏17 小时前
recruitment-cli:让 AI 接手求职招聘,从架构到一次发送超时
架构·typescript·node.js
熊猫钓鱼>_>1 天前
普通用户视角的端侧智能体验手记【鸿蒙心迹】
开发语言·人工智能·华为·智能手机·性能优化·ai编程·harmonyos
2601_967987371 天前
Python 教程:默认字典(defaultdict)全维度深度指南
python·性能优化·数据处理·collections·defaultdict
MyBili1 天前
Chrome性能优化实战:让老旧浏览器重获流畅体验
chrome·性能优化·谷歌浏览器·内存管理·浏览器提速·老旧设备优化·卡顿解决
疯华整猫2 天前
删掉 LeakCanary 那天,我用 Trae 自己撸了一套:最难的不是分析堆,是决定什么时候 dump
android·人工智能·性能优化
沉默小管2 天前
我用 NestJS + Nuxt 3 做了个导航站,聊几个踩过的坑
node.js·seo