ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

不用Selenium!Python直接调腾讯文档接口批量导出实战

不用Selenium!Python直接调腾讯文档接口批量导出实战 先说结论如果你有一堆腾讯文档需要定期备份或者想把在线表格里的数据批量同步到本地做分析直接在浏览器里点“导出”逐个下载是最笨的办法。用 Selenium 模拟人去点短期跑几个还行一旦文档数量上来了各种等待超时、元素定位失败、验证码弹窗就能把你折磨到怀疑人生。我这次做的东西说直白点就是不去碰浏览器 UI直接分析网页版腾讯文档自己调用的后端接口用 Python 模拟请求把文档内容拉下来。整个过程不涉及任何“破解”或“越权”只是把浏览器开发者工具里能看到的东西换成代码去执行而已。这篇文章会把完整的分析思路、抓包方法、关键接口长什么样、以及批量导出时的性能与稳定性问题都讲清楚适合被重复性导出工作折磨过、想彻底解放双手的开发者参考。1. 项目整体思路为什么必须绕过 Selenium1.1 需求场景到底长什么样先描述一下我遇到的真实场景。我在团队里负责维护一批运营文档分布在好几个腾讯文档文件夹里包含项目计划表、排期表、复盘记录、周报汇总加起来大概有两百多篇。每周都要做一次全量备份把内容同步到公司的文件服务器里做归档。一开始的做法很朴素登录腾讯文档网页版挨个点开文档点右上角菜单找导出按钮选择格式下载到本地。两百篇文档就算每篇只花 30 秒也得将近两个小时。更别提鼠标手滑点错格式、下载到一半断网、某个文档权限异常导致页面打不开这类随机事件每周光做备份就得占掉我小半天。后来我想过用 Selenium 做自动化写一个脚本驱动浏览器去逐个点按钮。实验结果证明这条路能跑通但维护成本和运行效率都极其感人。1.2 Selenium 作为备选方案的致命伤先说速度。Selenium 本质上是“遥控”一个真实的浏览器每一步操作都有完整的浏览器渲染开销。打开一个文档页面从发起请求到 DOM 渲染完毕再到工具栏按钮真正可点击通常要等 3 到 5 秒这还是网络状况好的时候。两百个文档光是等待页面加载的纯时间就超过 15 分钟。再说稳定性。浏览器自动化最怕的就是页面结构变化。腾讯文档是典型的前后端分离应用前端会不停迭代按钮的class属性、弹窗的 DOM 层级、导出选项的排列顺序随时可能调整。我经历过一次前端改版导出菜单从鼠标悬浮弹出改成了点击弹出所有基于hover的定位代码全部失效脚本直接罢工。最要命的还是风控问题。短时间内在同一个浏览器会话里高频打开、操作大量文档很容易触发登录态异常校验。一旦弹出滑块验证或者要求重新登录整个脚本就卡死了。Selenium 能做的事情本质上和你手动操作一样只是把点击换成了代码它并没有降低操作频率反而因为执行速度快更容易触发防护机制。1.3 换个思路网页前端也是一个“客户端”想通了 Selenium 的局限之后我开始重新观察腾讯文档网页版的工作原理。打开浏览器开发者工具的网络面板清空记录然后随便点开一篇文档你会发现页面在加载文档内容之前会先发出若干条XHR或Fetch请求返回的是 JSON 数据页面靠这些 JSON 渲染出文字和表格。这里有一个被很多人忽略的事实网页版腾讯文档本质上就是一个“HTML 壳子 JavaScript 客户端”真正的内容数据全部来自后端接口。前端通过什么接口拿列表、通过什么接口拿文档内容、通过什么接口导出文件全部可以通过开发者工具看得一清二楚。那就好办了。既然前端能用这些接口拿到数据那说明这些接口就是设计给“登录用户”用的。我只要保持登录状态用代码直接调用这些接口拿到 JSON 响应再解析出内容整个过程绕开了浏览器渲染速度快到飞起也不会因为页面结构变化而挂掉。这就是我说“不用 Selenium 也能批量导出”的核心逻辑UI 自动化是模拟人点击接口分析是模拟数据请求。数据源头本来就摆在那里直接从源头取数据永远比模拟人去搬砖靠谱。1.4 先明确技术边界避免跑偏这里必须先说清楚“逆向分析”的范畴。我做的是分析浏览器端正常发出的网络请求属于前端开发者和爬虫工程师都熟悉的常规操作目的仅仅是提高自己账号下文档的导出效率。整个过程不涉及对腾讯文档客户端代码的篡改、不涉及破解加密协议、不涉及绕过登录授权也不涉及获取任何未授权数据。有些人会把“逆向”理解为“破解”然后开始琢磨怎么绕过滑块验证、怎么模拟登录态、怎么扫别人文档。这类内容我不想碰也不会在这篇文章里出现。你拿代码去调自己的账号、自己的文档这叫效率工具你去调别人的数据那就是另外一回事了。技术分析本身是安全的用法才决定风险。2. 抓包定位核心接口的完整过程2.1 准备工作一个浏览器和它的开发者工具要用接口方式操作腾讯文档第一步是搞清楚网页版到底调用了哪些接口。不需要什么特殊工具Chrome 的开发者工具就够用了我自己用的是 ChromeFirefox 和 Edge 的开发者工具功能也完全足够。我建议准备工作做两步浏览器里确保已经登录了腾讯文档并且测试账号里有至少几篇不同类型的文档表格、文字、幻灯片都来一个方便对比接口差异。打开开发者工具F12切到 Network 面板。Network 面板里会持续记录浏览器发出的所有请求包括图片、CSS、JS 文件、XHR 请求等我们需要的是其中的XHR/Fetch类型。这里有一个小技巧操作前先点击 Network 面板左上角的清除按钮图标即“Clear”把之前的请求记录全部清掉。这样待会儿产生的网络请求就干净多了不会混入大量无关内容。2.2 动手抓包从文档列表到打开文档准备工作完成之后开始操作并观察请求在腾讯文档首页停留在“最近查看”或者某个文件夹列表页面。注意看 Network 面板有没有新的XHR请求出现如果有逐个点开看看它们的 URL 和响应内容。你会发现某些请求的响应里包含了文档的标题、更新时间、文档类型、访问链接等信息。这一步定位的就是“文档列表接口”。接着随便点开一篇文档观察页面加载过程中新增的 XHR 请求。响应里包含了这篇文档的正文内容、表格数据、样式信息等。再操作一次“导出为 Word/Excel”观察导出时是不是也有对应请求发出。这一步的核心目标是建立直觉列表页有列表接口文档页有内容接口导出工具有导出接口。大部分在线办公类产品都是这个套路只是接口的路径、参数、返回格式各有不同。2.3 如何从一堆请求里认出关键接口一个正常的文档页面打开网络面板里可能会有十几到几十个请求不可能每个都去仔细看要有针对性地筛选。我习惯的做法是点击 Network 面板上的Fetch/XHR筛选按钮直接把图片、脚本、样式全部过滤掉。按请求响应体积排序重点看响应体积大的请求。文档内容通常都在大体积的 JSON 响应里。逐个点击请求切换到Preview标签页看 JSON 的层级结构。如果看到title、content、sheet、cell这类关键词基本就锁定了。腾讯文档的接口域名主要是docs.qq.com子域名下的一系列/cgi-bin/路径具体是哪个路径不同功能模块可能不一样。你在自己的抓包结果里能看到我在这篇文章里不会把具体路径写死因为前端会有版本更迭接口路径和参数也可能调整。以你自己抓包拿到的实际请求为准是这套方法能长期生效的关键。2.4 登录态的获取与保持在线接口的共性要求是必须带着登录凭证才能返回数据。腾讯文档也不例外。浏览器里登录之后所有请求的请求头里都会自动带上Cookie字段。Cookie 里包含了会话凭证后端通过它识别当前用户是谁、有没有权限访问某篇文档。所以要在代码里调用接口第一步就是把 Cookie 保存下来。方法很简单在 Network 面板里随便点开一个请求。在Headers标签页里找到Request Headers下的Cookie字段。把整个 Cookie 字符串复制出来保存到本地配置文件里后续代码要用。注意Cookie 其实就是你的登录凭证泄露 Cookie 等于泄露账号。写代码的时候不要把 Cookie 硬编码到脚本里再传到公开仓库建议用环境变量或者本地配置文件保存。Cookie 的时效性也需要注意。正常登录状态下腾讯文档的 Cookie 可能可以持续比较长的时间但如果你退出了登录、换了网络环境、或者触发了风控Cookie 就可能失效。那时候脚本会报 401 或者返回登录跳转的 HTML而不是 JSON 数据。遇到这种情况重新去浏览器里登录再复制一份新 Cookie 替换即可。3. 腾讯文档 API 调用实战3.1 请求协议长什么样把抓包结果整理一下腾讯文档网页版的接口调用方式可以归纳为大部分核心接口是POST请求。请求地址是https://docs.qq.com/...下的某个/cgi-bin/路径。请求头需要带Cookie有的接口还会校验Referer或User-Agent建议把抓包里看到的请求头信息尽量带全。请求体一般是application/json或表单格式里面包含文档 ID、分页信息、操作类型等参数。响应体通常是一个 JSON 对象外层有ret/code/msg之类的状态字段内层是实际数据。这里我不贴死某个接口的完整参数表因为腾讯文档前端更新后接口参数极有可能调整。我提供的是一个经过验证的“请求模板”你用自己的抓包结果去替换参数名和路径就能在几分钟内跑通。用 Python 的requests库做一个示例先设置好会话import requests SESSION requests.Session() COOKIE 粘贴你的Cookie BASE_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Referer: https://docs.qq.com/, Cookie: COOKIE, Content-Type: application/json, }把SESSION.headers设置成BASE_HEADERS后续所有请求都会自动带上这些请求头。3.2 文档列表接口拿到全部文档的索引批量导出的前提是知道要导出哪些文档。我抓包时发现在文档列表页面滚动加载的过程中会不断有请求发出请求的响应里包含了文档的title、url、doc_type、edit_time等字段。这个接口就是列表接口。列表接口一般支持分页请求体里有类似offset、page_size、folder_id或doc_list之类的参数。我的建议是先用小参数比如page_size20试探拿到响应后观察返回的 JSON 结构再调整参数。一次接口返回的数据结构大致长这样这是我抓包后整理出来的通用形态具体字段名以你实际抓包为准{ ret: 0, data: { list: [ { docId: xxxx, title: 2024年度运营计划, url: https://docs.qq.com/doc/xxxx, type: doc, editTime: 1710000000 } ], total: 234 } }解析逻辑可以写成一个函数不断按分页参数往下翻直到取完total数量为止。这个列表就是后续批量导出的“任务队列”。3.3 文档内容接口拉取正文字符串拿到文档 ID 之后下一步是获取文档内容。在浏览器里点开一篇文档网络面板里会有一个请求的响应里包含大段的正文内容这就是文档内容接口。不同文档类型在线文档、表格、幻灯片的内容接口返回格式差别比较大在线文档返回的一般是结构化 JSON包含段落、标题、文本样式、图片资源引用。在线表格返回的多是单元格的字典结构键是行列坐标值是文本内容。幻灯片返回的则是每一页的对象列表包含文本框、图形、图片信息。解析时先别急着写完美方案我建议先打印出原始 JSON 的 key 层级再用jsonpath或直接遍历的方式把文本内容提取出来。对于在线文档最常见的方式是遍历结构化块里的text字段并拼接。示例代码def extract_doc_text(content_json): # 伪代码根据实际返回结构调整 blocks content_json.get(data, {}).get(blocks, []) lines [] for block in blocks: text_parts block.get(text, {}).get(elements, []) line .join(el.get(text, ) for el in text_parts) if line: lines.append(line) return \n.join(lines)这一步是整个项目最“脏活累活”的部分因为不同模板、不同编辑历史产生的 JSON 结构可能略有不同。我的建议是先做一个“调试模式”把原始 JSON 输出到本地文件人工观察结构再针对性写解析代码。完全靠猜是猜不出来的。3.4 保持会话Cookie 过期与自动重试接口调用过程中最常见的问题就是请求返回登录跳转。这里的表现很有意思用requests直接请求接口如果 Cookie 失效接口可能不会返回 401 错误码而是返回一段 HTML登录页或者一个要求重定向的 JSON。写代码时一定要做一层判断检查响应的Content-Type和首字节如果发现不是 JSON就立刻中止并提示重新登录。我用的统一封装函数大致是这样import requests from requests.exceptions import RequestException def api_post(url, payload): try: resp SESSION.post(url, jsonpayload, timeout15) content_type resp.headers.get(Content-Type, ) if json not in content_type: raise RuntimeError(接口未返回JSON可能登录态失效请刷新Cookie后重试) data resp.json() if data.get(ret) ! 0: raise RuntimeError(f接口业务错误{data.get(msg)}) return data except RequestException as e: raise RuntimeError(f网络请求失败{e})这个函数把“网络异常”和“业务异常”分开处理外层调用时统一捕获RuntimeError并做重试或记录日志稳定性会好很多。3.5 参数中的签名与加密字段怎么处理抓包时你可能会发现某些请求的请求体里带了一两个看起来像加密串的参数比如sign、token、nonce、biz_id。很多初学者一看到这种字段就头大觉得必须去逆向 JS 才能做出来。其实不用慌先看清楚这些参数到底是怎么生成的。我把参数分成三类固定参数抓包里是多少就是多少直接写死。跟登录态绑定Cookie 变了它才变代码里直接沿用 Cookie。前端计算生成比如把当前时间戳、文档 ID 做某种组合再哈希需要读 JS 才能复现。我的经验是腾讯文档网页端的核心接口大部分用的是前两类只要带着合法 Cookie 和正确的文档 ID请求就能成功。只有极少数特定场景比如某些导出操作用到前端预计算内容才会需要去看第三类参数。即便遇到第三种也建议优先考虑“这个接口是不是有替代方案”而不是一头扎进 JS 逆向里。能通过抓包复制参数解决的问题就别去写 JS 逆向代码这是时间性价比最高的工作方式。4. 批量导出落地的工程细节4.1 导出为本地文件从 Markdown 到 PDF接口拿到的原始内容是 JSON 结构不是最终的 Word 或 PDF 文件。所以“批量导出”本质上要分成两步先从接口拿内容再把内容转换成目标格式。我选择了两条技术路线文本类文档统一转成 Markdown转成一个带格式的.md文件。好处是纯文本体积小、易归档、后续要转成 PDF 或 Word 也方便用pandoc一条命令就能搞定。表格类文档保留原始单元格数据导出为 CSV 或 Excel用openpyxl写入.xlsx。如果你确实需要导出成官方 Word 或 PDF 格式理论上可以继续分析网页版的“导出”按钮对应的接口。但我试下来这个接口对参数要求更繁琐而且返回的文件可能需要请求另一个下载 URL链路更长。对于备份场景Markdown 和 Excel 已经足够没必要为了格式一致性去硬啃导出接口。Markdown 转换的逻辑其实很直白拿到结构化文本块之后根据块的类型判断标题层级h1、h2、正文、列表拼成 Markdown 字符串。4.2 并发控制快但不能过于快批量导出的话题绕不开并发。文档数量多的时候串行请求会比较慢但无脑开多线程也不是好主意。我测试过几种策略策略速度稳定性适用场景全串行最慢最稳文档量少不追求速度固定线程池3~5线程较快较稳文档量中等无限制异步最快极易触发风控不推荐我最终选了线程池ThreadPoolExecutor设置 4 个工作线程的方案。4 个线程同时跑每个文档的平均耗时大约从串行的 0.8 秒降到 0.25 秒左右两百个文档一分钟内能跑完。再往上加线程速度提升有限反而容易因为请求频率过高触发接口限流。代码结构和伪代码如下from concurrent.futures import ThreadPoolExecutor, as_completed def export_one_doc(doc): doc_id doc[docId] title sanitize_filename(doc[title]) try: content fetch_document_content(doc_id) save_to_markdown(title, content) return (title, ok, ) except Exception as e: return (title, failed, str(e)) with ThreadPoolExecutor(max_workers4) as executor: futures {executor.submit(export_one_doc, doc): doc for doc in doc_list} for future in as_completed(futures): title, status, err future.result() print(f{title} {status})4.3 增量备份与断点续传处理重复导出的问题时批量导出的一个经典问题是我每周都要跑一次如果只是重新拉取所有文档不仅慢还可能把之前已经改好的本地文件覆盖。所以要做一个增量判断。我的做法是在本地为每篇文档保存一个元数据文件或直接写到 SQLite 里记录文档 ID 对应的edit_time和本地文件路径。每次批量导出前先调列表接口把所有文档的edit_time拉下来与本地记录对比只有更新时间比本地新时才重新拉取内容。这样实现了“断点续传”和“增量更新”两种能力。即使某一次运行到一半挂了下次跑的时候已经成功的文档会直接跳过只有没跑过的、以及更新过的文档才会重新拉取。这个设计带来的体验提升极其明显——从每周全量跑 2 分钟变成了通常只需要跑十几秒。4.4 日志与告警跑挂了能立刻知道脚本跑到一半失败最怕的是默默失败没有任何提示等发现的时候数据已经缺了一大片。所以我专门加了一个轻量日志模块把每次导出的结果、失败原因、耗时统一登记到日志文件和 CSV 表格里。日志记录的核心字段文档 ID、标题、文档类型开始时间和结束时间状态成功/失败失败原因超时/解析异常/登录失效/业务错误重试次数有了日志排查问题会变得非常高效。比如某个文档一直失败你能看到失败原因是“权限异常”那大概率是这篇文档被移除了分享权限或者已经删除人工确认一下就好。如果全是“登录失效”那就是 Cookie 过期重新登录即可。如果全是“请求超时”那就是网络环境的问题。批量任务最怕的就是黑盒运行日志是维护这类脚本的必修课。5. 常见问题与排查技巧实录5.1 问题速查表问题表现可能原因解决方案接口返回 HTML 而非 JSONCookie 过期或登录态失效重新登录刷新 Cookie请求返回 403缺少 Referer 或请求头不全补全从抓包中复制的完整请求头返回 JSON 里没有内容字段该文档类型与预期解析结构不匹配打印原始 JSON调整解析逻辑响应很慢或超时单线程串行网络波动增加超时时间用线程池并发拉取数量有限翻页不全分页参数没带对检查列表接口的分页字段逐步调试某些文档导不出来文档权限异常、文档被删除、或被移入回收站日志标记人工复核5.2 最容易被忽视的小坑说几个我踩过的、不太会遇到但也可能栽跟头的细节第一个是文件名非法字符。Windows 文件名里不能有\ / : * ? |而文档标题里面出现/和:的概率非常高。不处理直接写入本地文件会直接抛 OSError。我写了一个清洗函数把这些字符替换成全角字符或者下划线。第二个是图片资源的处理。文档内容接口返回的文本一般没问题但图片往往是一个 URL 或者一个资源 ID。如果导出的 Markdown 里直接引用外链图片本地归档后图片可能因为外链失效而变成死图。比较稳妥的做法是同时下载图片到本地然后把 Markdown 里的图片路径改为本地相对路径。这一步会让代码复杂一些但对长期归档很重要。第三个是接口内部的默认参数调整。列表接口有的会按“最近查看”排序有的会按“创建时间”排序如果业务上依赖某个固定顺序比如按文件夹分组导出需要在请求参数里显式指定不能依赖接口默认值否则导出的文件顺序会和你预期不一致。5.3 合规边界与长期使用建议最后认真说一句接口分析类工具最大的风险不是“跑不通”而是“用错地方”。我一直强调这套方案只能用来操作自己账号下有权限的文档。判断标准很简单你在浏览器里能正常打开、能正常导出的文档才属于可以用接口去批量操作的文档。凡是你在浏览器里都看不到、没权限访问的内容接口也不可能正常返回强行去绕权限就是个错误方向。合规层面的另一个注意事项是个人开发脚本用于自动化操作属于个人效率工具范畴但如果要在公司内部大规模推广建议提前与所在组织的信息安全负责人确认。毕竟批量请求对服务端会有额外负载也需要确认与平台服务条款的兼容性。从长期使用角度有几个建议建议原因不要把接口路径和参数写死在代码里前端更新后很容易失效集中维护抓包结果更省心定期检查 Cookie 有效期被踢下线时及时发现而不是等到大规模失败后才察觉保持中低并发减少对服务端的压力也降低触发风控的概率关键数据先跑存量备份再搞增量同步保证零丢失的底线下再去追求效率平时多观察接口返回的字段变化早发现问题早调整解析代码避免精神内耗最后再分享一个小技巧我在实际使用中发现真正让“接口分析”比“Selenium 自动化”体验好一大截的不仅仅是速度而是排查问题的颗粒度。Selenium 的脚本一旦报错你要去截图看浏览器到底停在哪个页面、哪个按钮没点着通常得反复调试好几分钟才能定位。而接口调用的报错返回结果里直接写着失败原因要么是参数不对要么是没权限要么是登录态失效一眼就能盯出来。另外一个花小钱办大事的建议把“列表接口拉取文档元数据”和“内容接口拉取正文”这两个环节彻底拆开。前者非常快可以在任何时间高频执行用于监控文档是否更新后者比较重只在检测到更新时执行。这样一来即使内容接口因为某些原因挂了你也依然有一份最新的文档索引和更新时间表可以非常从容地去排障而不是像以前一样对着一个秃掉的文件列表干瞪眼。这套方案我已经稳定运行了几个月。最初的动机只是不想再每周花两个小时做机械备份后来慢慢整理成了结构清晰的脚本中间也换过几次解析逻辑但核心思路始终没变浏览器只是数据入口的后台直接和后台对话永远是最短路径。如果你手里也有一堆在线文档需要定期处理不妨按这个思路先把接口抓出来你会发现原来让你烦躁的“批量导出”其实几分钟就能搞定。
返回列表