【Python爬虫实战】腾讯文档禁止复制?Python+Selenium零基础带你优雅地“绕过”限制(附完整源码)


本文为作者原创。制作不易,如果帮助到大家可以给一个免费的一键三连哦~~
完整源码请见文末,已经同步到gitcode仓库
截止至发稿2026年8月3日,代码实测可用。

一、缘起:当“只能查看”遇上数据分析的刚需

故事是这样的:前几天笔者在搜集竞赛资料时,偶然发现了一份堪称“宝藏”的腾讯文档。数据详实、分类清晰,简直是做竞品分析 (bushi) 和选赛参考的神器。于是按下 Ctrl+C (准备白嫖别人的研究成果),结果……

一直提示无法复制,甚至连右键菜单都是灰的

至暗时刻

那一刻,我的内心是崩溃的 (这人真是纯坏,白嫖不到就崩溃)

作为一名(自封的)技术博主,我当然不会轻易认输。首先尝试了截图+AI OCR识别,但表格线干扰严重,识别率感人,校对时间比手敲还长;接着翻遍了油猴脚本库,要么年久失修,要么对新版腾讯文档无效。

(此时作者的绝望感达到了顶峰,甚至开始认真考虑要不要花两小时纯手工录入)

就在我准备放弃治疗的时候,突然想起来以前自学的爬虫:既然浏览器能渲染出这些数据,那它们一定存在于DOM或内存中。 能不能用代码把数据“读”出来?于是,这个被作者命名为 QQDocRR (QQDocsCopyRestrictionRemover) 的小工具应运而生,它可以完美的将表格中的文本数据扫描下来(无格式)。接下来笔者将带你一步步自己构建这个爬虫程序

最终效果展示

最终效果展示:超劲爆!!

📌 项目地址GitCode - QQDocRR
注:本项目仅供学习交流,旨在研究Web自动化与DOM交互机制,请勿用于侵犯他人版权或违反服务条款的场景。


二、HTML分析:另辟行径

2.1 山重水复疑无路:初探源码

按下F12打开浏览器控制台,检查表格区域,结果却让人心凉半截:

<div class="operate-board" aria-hidden="true" tabindex="3">
  <div class="cell-editor-stage">
    <div class="cell-editor-container"></div>
  </div>
  <div class="col-dragger-hand" data-testid="col-dragger-hand"></div>
  <div class="row-dragger-hand"></div>
  <div class="table-corner" style="display: none; left: -1.24px; top: -1.24px; width: 0px; height: 0px;"></div>
</div>

<canvas>都没找到(气笑了)。这意味着常规的 XPath/CSS 选择器提取文本的方案并不可行…吗?

控制台

(作者甚至曾经想过要不要自动截图+图像处理…虽然蠢但好像可行……?)

2.2 柳暗花明又一村:意外的转机

可能是来运了,笔者 慌乱间 偶然间按下了Ctrl+F搜索当前选中框文字。神奇的事情发生了:居然在/html/body/div[3]/div/div/div[4]/div[3]/div/div/div/div/div/div[2]/div[1]/p[1] 这个XPath路径下找到了这段文字!

这说明什么?说明当前选中单元格的内容以明文形式存在于段落DOM中!

通过控制台顺藤摸瓜找到了这个<p>标签的主人:公式栏(Formula Bar)。

同时,在旁边 /html/body/div[3]/div/div/div[4]/div[3]/div/div/div/div/div/div[2]/input 发现了坐标输入框

让我们看看这段关键的DOM结构:

<div class="formula-bar">
  <!--这个就是坐标输入框↓-->
  <input class="bar-label" spellcheck="false">
    <div id="alloy-simple-text-editor" contenteditable="true" ...>
      <!--这个就是公示栏↓-->
      <p>......</p>
    </div>
  <div class="bar-dragger"></div>
</div>

💡 核心原理

腾讯文档虽然用了一些魔术渲染表格以优化性能并防止复制,但为了支持编辑功能,仍然保留了传统的公式栏和坐标栏组件。而这就给了我们可乘之机! 当我们选中某个单元格时:

  1. 坐标栏 (input.bar-label) 会更新为当前单元格引用(如 B5)或合并区域(如 A1:C3
  2. 公式栏 (#alloy-simple-text-editor) 会同步显示该单元格的纯文本内容

我们的策略就是:向坐标栏写入目标单元格地址 → 触发回车跳转 → 再从公式栏读取文本。本质上就是模拟人类“看坐标→定位→读内容”的过程,只不过我们用代码把这个过程自动化了。


三、实战:一步步构建爬虫

3.1 技术选型:Requests vs Selenium

正式开始前,先向刚开始接触Python爬虫的同学们申明一下:

很多爬虫教程上来就用 requests + BeautifulSoup,但在这个场景下并非最好的解决方案:

  • 腾讯文档是 SPA 应用,数据通过 JSONP/WebSocket 动态加载
  • 需要登录态 Cookie,且 Token 有时效性
  • Canvas 渲染的数据无法通过 HTTP 响应直接获取

如果使用requests库,可能就需要进行 逆向 找到承载数据对应的网络请求。这种直接找到对应API再进行请求的方式相当于直接和腾讯文档的反爬虫机制硬碰硬!这样虽然数据会更加完整,但仅仅为了 白嫖 复制文本就大花心思去做逆向…你懂的。

因此我们选择 Selenium —— 让真实浏览器替我们完成渲染和交互,我们只需要“读屏”即可。因此,我们可以使用通过JavaScript脚本向坐标栏写入目标单元格地址 → 模拟按下回车 → 从公式栏读取文本 → 保存

依赖安装:

pip install selenium openpyxl

接下来就可以准备开工了!


3.2 核心功能实现——爬取表格内容

细心的读者可能发现了一个问题:既然已经用了 Selenium,为什么还要自己写 JavaScript 脚本?Selenium 不是已经提供了 find_elementget_attribute 等方法吗?

的确,Selenium 封装了丰富的 API 来操作 DOM,但在这个场景下它们并不够用:

  • 我们需要向输入框注入值并通过触发键盘事件(模拟回车)来触发input/change事件,而 Selenium 的 send_keys('content') 在面对 React/Vue 等现代框架时,往往无法正确触发框架绑定的事件处理器,导致值被“吞掉”。
  • 我们需要读取动态更新的文本节点,但公式栏的内容可能被框架以特殊方式管理,单纯用 get_attribute('textContent') 可能取不到最新值。

因此,这里我们选择自己编写JS脚本并通过Selenium进行JS注入来达成我们想要的效果。

💡 什么是 JS 注入?

JS 注入 (JavaScript Injection) 是指通过 driver.execute_script() 方法,将一段自定义的 JavaScript 代码发送到浏览器端执行。这相当于在目标页面的上下文中“植入”我们自己的逻辑,可以直接操作 DOM、调用原生 API,甚至劫持事件流。

有趣的是,Selenium 的许多底层操作本身也是通过 JS 注入实现的——例如 click() 方法在元素不可见时会调用 JS 的 click()get_attribute 在某些驱动实现中也依赖 JS 计算。所以我们并不是在“绕开” Selenium,而是在更深一层利用它的能力,实现那些高级 API 无法直接做到的事情。

思路简单粗暴:逐个格子进行扫描,然后将扫描的内容保存下来。因此,接下来我们将制作扫描脚本,先行后列将所有格子扫描一遍。

接下来,我们将具体实现三段核心 JS 脚本,分别负责跳转单元格读取公式栏读取坐标栏


脚本一:跳转单元格 (jumpToCell)

作用:向坐标输入框写入目标单元格引用(如 B5),并模拟回车键,触发页面跳转。

这里的关键是绕过框架的值拦截——直接使用 HTMLInputElement.prototype.value 的原始 setter,而不是简单地 el.value = ref,否则 React 等框架可能不会感知变化。

// jumpToCell.js
const sels = arguments[0];  // CSS选择器列表(由Python传入)
const ref = arguments[1];   // 目标单元格引用

let el = null;
for (const s of sels) {
    const e = document.querySelector(s);
    if (e) { el = e; break; }
}
if (!el) return false;

el.focus();

// 使用原生setter绕过框架拦截
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
    const setter = Object.getOwnPropertyDescriptor(
        window.HTMLInputElement.prototype, 'value'
    ).set;
    try { setter.call(el, ref); } catch(_) { el.value = ref; }
} else {
    try { el.textContent = ref; } catch(_) {}
}

// 触发input/change事件
el.dispatchEvent(new Event('input', {bubbles: true}));
el.dispatchEvent(new Event('change', {bubbles: true}));

// 模拟回车键
const o = {key:'Enter', code:'Enter', keyCode:13, which:13, bubbles:true};
el.dispatchEvent(new KeyboardEvent('keydown', o));
el.dispatchEvent(new KeyboardEvent('keypress', o));
el.dispatchEvent(new KeyboardEvent('keyup', o));

return true;

脚本二:读取公式栏文本 (readFormulaText)

作用:从公式栏 DOM 中提取当前选中单元格的纯文本内容。

我们使用多个候选选择器(按优先级排列),以兼容腾讯文档的不同版本。脚本会遍历这些选择器,返回第一个非空文本。

// readFormulaText.js
const sels = arguments[0];  // CSS选择器列表

for (const s of sels) {
    const e = document.querySelector(s);
    if (!e) continue;
    const t = (e.innerText || e.textContent || '').trim();
    if (t) return t;
}
return '';

脚本三:读取坐标栏 (readNameBox)

作用:获取坐标栏当前显示的单元格引用或合并区域范围(如 A1A1:C3),用于判断当前单元格是否属于合并区域,避免内容被重复读取。

// readNameBox.js
const sels = arguments[0];

for (const s of sels) {
    const e = document.querySelector(s);
    if (!e) continue;
    let v = '';
    if (e.tagName === 'INPUT' || e.tagName === 'TEXTAREA') {
        v = e.value || '';
    } else {
        v = e.innerText || e.textContent || '';
    }
    v = (v || '').trim().toUpperCase();
    // 检查是否有效的单元格引用或区域
    if (/^[A-Z]+\d+$/.test(v) || /^[A-Z]+\d+:[A-Z]+\d+$/.test(v)) return v;
}
return '';

有了这三段 JS,我们就可以在 Python 中通过 driver.execute_script() 调用它们,实现“跳转→读取”的扫描循环。

Python 扫描引擎雏形(核心循环):
class QQDocRR:
    NAME_BOX_SELECTORS = [
        "input.bar-label",
        ".formula-bar input.bar-label",
        "input[class*='bar-label']",
    ]
    FORMULA_BAR_SELECTORS = [
        "#alloy-simple-text-editor",
        ".formula-input",
        "[role='combobox']",
    ]

    def _jump_to(self, ref: str) -> bool:
        return bool(self.driver.execute_script(
            self.JS_JUMP_TO_CELL,   # 脚本一
            self.NAME_BOX_SELECTORS,
            ref
        ))

    def _read_formula_text(self) -> str:
        return self.driver.execute_script(
            self.JS_READ_FORMULA_TEXT,  # 脚本二
            self.FORMULA_BAR_SELECTORS
        ) or ""

    def _scan(self, max_row=5, max_col=7):
        from openpyxl.utils import get_column_letter
        for r in range(1, max_row + 1):
            for c in range(1, max_col + 1):
                ref = f"{get_column_letter(c)}{r}"
                self._jump_to(ref)
                time.sleep(0.01)  # 等待DOM更新
                text = self._read_formula_text()
                print(f"[{ref}] {text[:50]}")

⚠️ 关键细节

_jump_to 之后必须加 time.sleep(0.01),因为 DOM 更新是异步的。如果不等待,公式栏可能还没刷新就被读取,导致读到上一个单元格的内容。这个 10 10 10ms 是经过实测可行的安全间隔。

当然,这只是核心区域的代码,并非完整的代码。完整版代码已经投放在GitCode仓库中。

这个图片前几秒是纯黑的,当时录的时候卡了一下效果


四、后续可选改良:让它变成真正的“软件”

4.1 更通用一点!——命令行参数

硬编码的行列数显然不够灵活。我们把配置项抽取为命令行参数,并引入 argparse 模块。

📘 argparse 简介

argparse 是 Python 标准库中的命令行参数解析器,可以轻松定义位置参数、可选参数、互斥组等。它自动生成帮助信息,并对非法输入给出友好提示。我们用它来支持用户指定浏览器类型、文档URL、输出路径、扫描行列范围等。

def _parse_args(self) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="腾讯文档「在线表格」纯文本导出工具"
    )
    # 浏览器类型互斥组(必须选择其一)
    bg = parser.add_mutually_exclusive_group(required=True)
    bg.add_argument("--edge", action="store_const", const="edge", dest="browser")
    bg.add_argument("--chrome", action="store_const", const="chrome", dest="browser")
    bg.add_argument("--firefox", action="store_const", const="firefox", dest="browser")

    parser.add_argument("-u", "--doc-url", type=str, default="", help="腾讯文档URL")
    parser.add_argument("-o", "--out", type=str, default="./tencent_doc.xlsx")
    parser.add_argument("-c", "--max-col", type=str, default="G", help="最大扫描列(如 'G' 或 7)")
    parser.add_argument("-r", "--max-row", type=int, default=5, help="最大扫描行数")
    parser.add_argument("-e", "--max-empty", type=int, default=3, help="连续空行停止阈值")
    return parser.parse_args()

add_mutually_exclusive_group 确保用户只能选择一种浏览器,避免冲突。为了方便使用,--max-col 支持列字母(如 G)和数字两种输入方式,内部统一转换为整数列号。


4.2 更加健壮的程序——生命周期管理和日志系统

为了 装逼 让工具更专业,我们添加了日志系统和浏览器生命周期管理。这部分代码封装在 BrowserManager 类中,并实现了上下文管理器协议。

class BrowserManager:
    def __enter__(self) -> Self:
        """进入with块: 关闭旧进程 → 启动新进程"""
        self.kill_browser()
        self.start_browser()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """退出with块: 打印结束状态日志"""
        logging.info("流程结束。浏览器保持打开状态供用户查看。") #也可以选择关闭浏览器

BrowserManager 配合 with 语句使用,确保浏览器进程的正确启停。kill_browser 在启动前清理残留进程,避免端口占用;start_browser 通过 --remote-debugging-port 启动 CDP 调试模式,使 Selenium 能够连接到已有浏览器实例(保留用户的登录态)。


日志系统使用 logging 模块而非 print,支持级别控制和格式化输出。扫描进度每10行打印一次,包含非空格计数和连续空行计数,方便监控长时间运行的任务。


4.3 精益求精——合并单元格与智能停止

最后两个实用特性大幅提升了工具的鲁棒性:

  • 合并单元格处理:当跳转到某个单元格时,坐标栏可能显示一个区域(如 A1:C3),说明该单元格属于合并区域。此时应将文本写入左上角,并在最终 xlsx 中记录合并信息。
  • 智能停止:连续 N N N 行(默认 3 3 3行)全部为空时,认为有效数据已结束,提前终止扫描,避免浪费时间扫描大量空白区域。
#_scan函数中
#...
for r in ...:
    for c in ...:
        #...
        namebox_val = self._read_namebox()
        cell_range = _CellUtils.parse_cell_range(namebox_val)

        if cell_range:
             # 记录合并区域(去重)
             if cell_range not in self._seen_merges:
                 self._seen_merges.add(cell_range)
                 self.merges.append(cell_range)
             target_row, target_col = cell_range[0], cell_range[1]
         else:
             target_row, target_col = r, c

         # 连续空行检测
         if row_has_content:
             empty_streak = 0
         else:
             empty_streak += 1
        if empty_streak >= self.args.max_empty:
             logging.info(f"连续 {self.args.max_empty} 空行, 提前结束扫描。")
        break

合并单元格的处理逻辑:当跳转到 B2 但名称框返回 A1:C3 时,说明 B2 属于一个合并区域。此时应将文本写入 A1(左上角),并在最终 xlsx 中记录合并信息。_seen_merges 集合用于去重,避免同一合并区域被重复记录。


智能停止机制:当连续 N N N 行(默认 3 3 3行)全部为空时,认为有效数据已结束,提前终止扫描。这对于不确定表格大小的场景非常实用,避免无意义地扫描大量空白区域。


五、完整代码与使用说明

完整的 QQDocRR 源码已开源至 GitCode,包含上述所有功能及更多细节优化:

GitCode仓库网址~~

最终效果展示

最终效果展示:超劲爆!!

快速上手

# 1. 安装依赖
pip install selenium openpyxl

# 2. 在浏览器中登录腾讯文档并打开目标表格

# 3. 运行(以Edge为例)
python QQDocRR.py --edge

# 4. 指定URL和输出路径
python QQDocRR.py --chrome -u "https://docs.qq.com/sheet/DXXXXXX" -o output.xlsx

# 5. 自定义扫描范围
python QQDocRR.py --edge -r 500 -c Z -e 5

完整参数

usage: QQDocRR.py [-h] (--edge | --chrome | --firefox) [--config-path PATH] [--cdp-host HOST:PORT] [-u URL] [-o FILE] [-c COL] [-r N]
                  [-e N] [--dump-dom] [--no-dump-dom] [--dump-n N]

腾讯文档「在线表格」纯文本导出工具

options:
  -h, --help            show this help message and exit
  --edge                使用 Microsoft Edge 浏览器
  --chrome              使用 Google Chrome 浏览器
  --firefox             使用 Mozilla Firefox 浏览器
  --config-path PATH    浏览器可执行文件路径(指定后保存到 browsers_paths.json)
  --cdp-host HOST:PORT  CDP 调试地址(默认: 127.0.0.1:9222)
  -u URL, --doc-url URL
                        腾讯文档 URL(为空则查找已打开的标签)
  -o FILE, --out FILE   输出 xlsx 文件路径(默认: ./tencent_doc.xlsx)
  -c COL, --max-col COL
                        最大扫描列(列字母如 G 或数字, 默认: 7)
  -r N, --max-row N     最大扫描行数(默认: 5)
  -e N, --max-empty N   连续 N 个空行后停止扫描(默认: 3)
  --dump-dom            输出 DOM 调试样本(默认开启)
  --no-dump-dom         禁用 DOM 调试输出
  --dump-n N            最多记录 DOM 样本数(默认: 8)

前置条件

  • Python 3.10+
  • 对应浏览器的 WebDriver(Chrome/Edge 由 Selenium Manager 自动管理,Firefox 需手动安装 geckodriver)
  • 浏览器中登录腾讯文档账号(按需),工具复用浏览器的登录态,不会存储任何凭证

六、写在最后の碎碎念

笔者先要对各位跃跃欲试的同学们说:这个方法并不好,只是笔者 没事找事 本着研究学习的目的制作的。本意是在实在不方便联系作者并且很需要文档数据的情况下想出的下下策。

推荐大家 不要当懒狗 最好还是优先联系文档所有者获取授权或直接请求分享可编辑副本,因为直接抓取可能违背了作者的意愿,也存在法律风险!!

⚠️ 本工具仅作为 Web 自动化技术的学习案例,作者不对任何因使用本工具而产生的纠纷负责。请尊重知识产权,合理使用技术。

如果你也对 Web 自动化、DOM 逆向或爬虫技术感兴趣,欢迎在评论区交流讨论!觉得有用的话,别忘了点赞收藏加关注再走哦~~

项目完整源码下方自取↓

声明:本项目为作者原创,采用 MIT 开源协议,欢迎 Star、Fork 和二次开发。
📦 源码仓库https://gitcode.com/weixin_68744568/QQDocumentRestrictionRemover/tree/main
✉️ 作者:Gary | 联系邮箱:gary_beijing@126.com

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐