

一万两篇文档,四分钟采完
—— 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 秒渲染完,再找下一批。
执行过程大概是这样的:
问题在于:每次点击后 Vue 会异步渲染,新展开的节点需要时间才能出现在 DOM 中。而且展开过的节点有时会自动折叠回去(可能是 Vue 的响应式更新导致的状态丢失)。
我跑了 200 多轮循环,花了十几分钟,才展开到 2000 多个链接。而根据估算,整棵树有超过 12000 个叶子节点。照这个速度,光展开导航树就要一个小时。
1.3 提取到的链接也不完整
更让人崩溃的是,即使节点展开了,由于 Vue 的虚拟 DOM 机制,折叠状态下的子节点链接根本不在 DOM 中——它们是懒加载的,只有展开时才会创建。
这意味着:不把所有节点展开到叶子层级,就拿不到完整的链接列表。而展开所有节点又极其耗时且不稳定。
到了这一步,我意识到:浏览器自动化方案行不通。 不是为了采集内容,光是把导航树完整展开就要花费不可接受的时间。
二、转折:一条 Network 请求改变了一切
正当我准备放弃的时候,灵光一闪:既然页面是通过 AJAX 请求加载数据的,那为什么不直接看它请求了什么?
我用 Performance API 列出了页面加载时的所有网络请求,过滤掉 JS、CSS、图片等静态资源后,发现了两个关键的 JSON API 请求:
/docs/api/collections/client?lang=zh | |
/docs/api/doc/{doc_id}?lang=zh&source=local |
用浏览器直接访问第一个 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 += saved3.2 文件命名与目录结构
为了让采集结果方便检索,我按导航树的第二级节点创建子文件夹,文件名包含完整树路径。
例如面包屑为 ["WPS客户端二次开发", "WPS Office 基础接口", "加载项 API 参考", "自定义功能区", "自定义功能区概述"] 的页面,保存为:
WPSOffice 基础接口/加载项 API 参考/加载项 API 参考_自定义功能区_自定义功能区概述.md这样想找表格相关的内容,直接进"表格 API 参考"文件夹即可。
3.3 Windows 文件名大小写陷阱
采集过程中遇到一个小坑:Windows 文件系统不区分大小写,而 WPS 文档中存在大小写不同的同名节点(如 ffi 和 FFI)。两个文件名在 Linux 上是不同的文件,在 Windows 上会冲突覆盖。
解决方案很简单:检测到文件名(转小写后)重复时,自动在文件名后加序号后缀。
3.4 最终成果
采集结果汇总:
| 合计 | 12,183 | 100% |
"WPS Office 基础接口"下的各子模块分布:
全部 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采集」获取完整代码。
夜雨聆风