乐于分享
好东西不私藏

WPS官方文档一万两千篇文档,四分钟采完

WPS官方文档一万两千篇文档,四分钟采完
准备开发WPS的JSA宏和WPSJS加载项的skill,先把文档采集下,之前采集过的文档过时了,结果还不错,尝试用了下WPS灵犀来搞,全程AI帮我做好了,包括以下的文章帮我总结好过程分享。

一万两篇文档,四分钟采完

—— WPS 开放平台文档逆向采集实战手记

从浏览器自动化到 JSON API 逆向,一场「笨办法」到「聪明办法」的进化之旅。


引子:一个「简单」的需求

最近在研究 WPS 的 JSA(JavaScript API)开发,需要频繁查阅 WPS 开放平台的官方文档。文档地址是 open.wps.cn,内容很全——加载项 API、宏编辑器 API、表格 API、文字 API、演示 API……应有尽有。

但问题来了:

  • 每次查资料都要联网,网络不稳定时就抓瞎
  • 文档没有离线版本,想本地全文搜索 impossible
  • 侧边栏导航层层嵌套,找一个具体方法要点好几下
  • 想批量分析 API 命名规律、参数模式,只能手动一页页复制

于是我萌生了一个想法:把整棵文档树的页面全部采集下来,存成本地文件,想怎么搜就怎么搜。

听起来不难,对吧?但实际操作中,我经历了一段从「笨办法」到「聪明办法」的完整进化。今天就把这个过程分享出来,希望能帮到有类似需求的朋友。


一、笨办法:浏览器自动化采集 HTML

最直觉的方案是模拟浏览器操作:打开网页,展开侧边栏导航树,逐个点击叶子节点,提取页面正文内容保存。我用的工具是 CDP(Chrome DevTools Protocol)浏览器自动化。

1.1 打开页面,分析结构

首先导航到文档首页,用 JavaScript 提取侧边栏 DOM 结构。很快发现这是一个 Vue SPA(单页应用),侧边栏导航是动态渲染的。

1.2 展开 12000+ 个折叠节点

真正的噩梦开始了。侧边栏是一棵多级树,默认只展开第一层。每个父节点都需要点击才能展开子节点,而子节点展开后又会冒出新的折叠节点。

我写了个循环:每轮找到所有折叠的三角形图标(CSS class 包含 -rotate-90),全部点击展开,等 0.5 秒渲染完,再找下一批。

执行过程大概是这样的:

轮次
点击展开数
已发现链接数
第 1 轮
12 个
2 个
第 50 轮
20 个
316 个
第 200 轮
20 个
1,267 个
第 400 轮
36 个
2,284 个
……
……
……

问题在于:每次点击后 Vue 会异步渲染,新展开的节点需要时间才能出现在 DOM 中。而且展开过的节点有时会自动折叠回去(可能是 Vue 的响应式更新导致的状态丢失)。

我跑了 200 多轮循环,花了十几分钟,才展开到 2000 多个链接。而根据估算,整棵树有超过 12000 个叶子节点。照这个速度,光展开导航树就要一个小时。

1.3 提取到的链接也不完整

更让人崩溃的是,即使节点展开了,由于 Vue 的虚拟 DOM 机制,折叠状态下的子节点链接根本不在 DOM 中——它们是懒加载的,只有展开时才会创建。

这意味着:不把所有节点展开到叶子层级,就拿不到完整的链接列表。而展开所有节点又极其耗时且不稳定。

到了这一步,我意识到:浏览器自动化方案行不通。 不是为了采集内容,光是把导航树完整展开就要花费不可接受的时间。


二、转折:一条 Network 请求改变了一切

正当我准备放弃的时候,灵光一闪:既然页面是通过 AJAX 请求加载数据的,那为什么不直接看它请求了什么?

我用 Performance API 列出了页面加载时的所有网络请求,过滤掉 JS、CSS、图片等静态资源后,发现了两个关键的 JSON API 请求:

请求 URL
作用
/docs/api/collections/client?lang=zh
返回完整侧边栏导航树
/docs/api/doc/{doc_id}?lang=zh&source=local
返回单个页面的 Markdown 原文

用浏览器直接访问第一个 API,返回了一个 8MB 的 JSON。里面就是整棵导航树——所有节点、所有层级、所有链接,一次性全部返回。

那一刻,我的心情可以用四个字形容:豁然开朗。

2.1 导航树 API

请求地址:

GEThttps://open.wps.cn/docs/api/collections/client?lang=zh

返回的是一个嵌套的 JSON 树结构,每个节点包含:

  • name:节点显示名称(如"表格 API 参考")
  • type:节点类型(folder 或 file)
  • path:页面路径
  • children:子节点数组(folder 类型才有)
  • docInfo:文档元信息(file 类型才有),其中 id 字段是内容 API 的关键参数

整棵树长这样(简化版):

{"name": "WPS Office 基础接口",  "type": "folder",  "children": [    {      "name": "表格 API 参考",      "type": "folder",      "children": [        {          "name": "Workbook 对象",          "type": "file",          "docInfo": {            "id": "app-integration-dev_..._Workbook_obj",            "breadcrumb": ["WPS客户端二次开发", "WPS Office 基础接口", "表格 API 参考", "Workbook 对象"]}}]}]}

2.2 内容 API

拿到每个叶子节点的 docInfo.id 后,就可以请求内容了:

GEThttps://open.wps.cn/docs/api/doc/{doc_id}?lang=zh&source=local

返回内容更让人惊喜:data.content 字段直接就是 Markdown 原文!不需要解析 HTML,不需要正则提取正文,拿到的就是干净的 Markdown。

这意味着:我最初计划先采集 HTML 再转换格式的方案完全是多此一举。API 返回的就是最原始的 Markdown,直接保存即可。


三、秒杀:从一小时到四分钟

有了两个 API,采集逻辑变得极其简洁:

1. 调用导航树 API,递归遍历 JSON 树,提取所有 type=file 的叶子节点

2. 对每个叶子节点的 docInfo.id,调用内容 API 获取 Markdown

3. 按导航树层级创建文件夹,保存 .md 文件

3.1 多线程并发下载

单线程逐个请求太慢(12000 个页面,每个请求约 0.2 秒,串行需要 40 分钟)。用 Python 的 ThreadPoolExecutor 开 20 个线程并发请求,整体耗时降到 4 分钟左右。

核心代码逻辑(简化版):

from concurrent.futures importThreadPoolExecutorwithThreadPoolExecutor(max_workers=20)asexecutor:    futures ={executor.submit(fetch_and_save, doc_id): doc_idfor doc_id in all_doc_ids}for future inas_completed(futures):        doc_id, saved, errors = future.result()        total_success += saved

3.2 文件命名与目录结构

为了让采集结果方便检索,我按导航树的第二级节点创建子文件夹,文件名包含完整树路径。

例如面包屑为 ["WPS客户端二次开发", "WPS Office 基础接口", "加载项 API 参考", "自定义功能区", "自定义功能区概述"] 的页面,保存为:

WPSOffice 基础接口/加载项 API 参考/加载项 API 参考_自定义功能区_自定义功能区概述.md

这样想找表格相关的内容,直接进"表格 API 参考"文件夹即可。

3.3 Windows 文件名大小写陷阱

采集过程中遇到一个小坑:Windows 文件系统不区分大小写,而 WPS 文档中存在大小写不同的同名节点(如 ffi 和 FFI)。两个文件名在 Linux 上是不同的文件,在 Windows 上会冲突覆盖。

解决方案很简单:检测到文件名(转小写后)重复时,自动在文件名后加序号后缀。

3.4 最终成果

采集结果汇总:

顶级节点
文件数
占比
WPS Office 集成模式
43
0.4%
WPS Office 基础接口
12,140
99.6%
合计12,183100%

"WPS Office 基础接口"下的各子模块分布:

子模块
文件数
占比
文字 API 参考
4,267
35.2%
表格 API 参考
4,095
33.8%
演示 API 参考
1,761
14.5%
通用 API 参考
861
7.1%
宏编辑器控件 API 参考
397
3.3%
PDF API 参考
275
2.3%
加载项 API 参考
267
2.2%
宏编辑器 API 参考
144
1.2%
WPS 扩展 API
69
0.6%
其他
104
0.9%

全部 12,183 个文件,总大小约 70MB,采集耗时 216 秒,0 错误。


四、原理:为什么 API 比浏览器自动化快 100 倍

4.1 浏览器自动化的瓶颈

浏览器自动化(Selenium、Puppeteer、CDP 等)的本质是模拟人类操作:点击、等待渲染、读取 DOM。每一次操作都有固定开销:

  • 渲染延迟:Vue/React 等框架收到状态变更后,需要异步执行虚拟 DOM diff 和 patch,通常需要 100-300ms
  • 网络往返:每次导航到新页面都要重新加载 HTML、CSS、JS,执行框架初始化
  • 状态管理:SPA 的导航状态(展开/折叠)保存在内存中,操作过快会导致状态丢失或回滚
  • DOM 查询:在复杂页面中通过 CSS 选择器定位元素也有性能成本

对于几十个页面来说,这些开销可以忽略。但当页面数量达到上万级别时,每个页面多花 1 秒,总量就是好几个小时。

4.2 API 直调的优势

API 直调绕过了所有中间环节:

  • 导航树 API 一次返回全部 12000+ 个节点的完整结构,不需要逐层展开
  • 内容 API 直接返回 Markdown 原文,不需要渲染 HTML 再解析提取
  • HTTP 请求可以轻松并发(20 线程),而浏览器自动化受限于单线程操作序列
  • 没有渲染开销,没有状态管理问题,纯粹的请求-响应模式

打个比方:浏览器自动化像是从北京坐绿皮火车去广州,每站都停;API 直调像是坐直达高铁。目的地一样,但效率天差地别。

4.3 如何发现 API

很多人觉得「逆向 API」听起来很高深,其实方法非常朴素:打开浏览器 DevTools,看 Network 面板。

具体步骤:

1. 用 Chrome 打开目标页面,按 F12 打开 DevTools

2. 切换到 Network 面板,勾选 Fetch/XHR 过滤器

3. 刷新页面,观察加载时的请求列表

4. 在侧边栏点击任意导航项,观察新触发的请求

5. 寻找返回 JSON 且包含文档内容的请求

6. 用 Python requests 直接调用该 API 验证

90% 的现代 Web 应用都有类似的 JSON API。学会这个方法,采集效率可以提升一到两个数量级。


五、经验总结

5.1 先观察,再动手

这次采集最大的教训是:动手写代码之前,先花 5 分钟观察目标网站的请求模式。我一开始直接上浏览器自动化,折腾了好几个小时才发现根本不需要这么复杂。如果先看 Network 面板,5 分钟就能发现 API。

5.2 SPA 网站的采集策略

现代 Web 应用几乎都是 SPA(单页应用),直接爬 HTML 拿不到完整内容。但 SPA 必然依赖后端 API 提供数据,找到 API 就找到了金矿。判断一个网站是否值得用 API 采集,看两点:

  • Network 面板中是否有返回 JSON 的 XHR 请求
  • 这些 JSON 是否包含你需要的数据(文本、链接、结构等)

5.3 并发是关键

Python 的 requests 库本身是同步的,但配合 ThreadPoolExecutor 可以轻松实现并发。对于 IO 密集型任务(网络请求),线程池的效果非常显著。20 个并发线程可以将 12000 个页面的采集时间从 40 分钟压缩到 4 分钟。

5.4 文件名设计很重要

采集上万个文件时,文件命名策略直接决定了后续的检索效率。我的做法是:

  • 按导航树第二级分类建子文件夹(表格、文字、演示等)
  • 文件名包含完整面包屑路径,用下划线连接
  • 这样即使在文件管理器中浏览,也能一眼看出文档的层级位置

5.5 留下可复用的脚本

网站改版是常态,API 可能随时失效。我把采集脚本和 API 发现指南一起沉淀成了可复用的工具,以后网站改版时只需要重新排查 API 地址,更新脚本中的 URL 常量即可。


写在最后

这次采集经历让我深刻体会到:解决问题的最高境界不是把笨办法优化到极致,而是找到那个让笨办法变得不必要的聪明办法。

12000 个文档,如果用浏览器自动化逐个采集,可能需要好几个小时甚至一天。而找到 API 后,4 分钟全部搞定。这中间的差距不在于代码能力,而在于对问题本质的理解。

下次遇到类似的采集需求,不妨先问自己一个问题:这个网站的数据,背后是什么 API 在支撑?

找到它,你就赢了一半。


本文涉及的采集脚本已开源,关注公众号回复「WPS采集」获取完整代码。