Playwright实战教程公众号文章

81MB 录屏 + 1 小时逐字稿,WorkBuddy用Playwright帮我全部拿下

腾讯会议,AI 纪要和逐字稿质量都不错,怎么保存到本地? 本文给你一条能抄的命令和一句能抄的提示词,数据均来自真实执行。

Playwright实战教程

公众号 · 11 篇文章

腾讯会议,AI 纪要和逐字稿质量都不错,怎么保存到本地? 本文给你一条能抄的命令和一句能抄的提示词,数据均来自真实执行。


一、先说痛点:看得见,拿不走

上周我进行了一次线上交流,腾讯会议自动生成了 AI 纪要和逐字稿。但我想存下来,发现三件事:

① 逐字稿没有导出按钮只能一屏一屏选中、复制。

② 滚动条能拉到底,但那是假的鼠标往下滚,滚到一半,上面的内容就"消失"了——DOM 被回收。

③ Ctrl+A全选只有十几条数据是 React 运行时渲染的,静态 HTML 里一个字都没有。

怎么办呢?毕竟会议链接访问地址过了有效期就不能访问了。


二、先别急着写代码:任务是怎么被"说清楚"的

虽然我是Playwright博主,但也不想自己从头写代码呀。我给了WorkBuddy提问:

结果还行,比我预期要快的就搞定了。

别急着划走,下面的内容可能更重要。

很多人用 AI 干活,第一句是"帮我写个爬虫下载会议录制",然后拿到一堆跑不通的代码。 

问题不在 AI,在于任务没被定义清楚。

我这次给 AI 的提示词,是一份标准的任务委托书,四层信息:

① 目标把链接里的内容固化下来。 → 定义的是"做完"的标准,而不是"写段代码"。

② 首选路径你如果有浏览器自动化能力,就用 Playwright 干。 → 给出技术手段,缩小 AI 的搜索空间。

③ 降级路径(Plan B)⭐ 最值钱干不了就引导我手动下载。 → 给 AI 一个失败出口。

④ 输出约束文件名、保存目录写死。 → 产物可直接落地,不用二次整理。

为什么第 ③ 条最值钱?

没有 Plan B 的提示词,AI 遇到 403 会开始编造:假装下载成功、给你一堆"理论上可行"的代码。 有了 Plan B,AI 知道自己"可以承认干不了",反而会更诚实去试、去报错。

实测中还有个细节值得说: 我提示词里写的是"请模拟登录",但 AI 探测后发现根本不需要登录,就直接跳过了。

好的 AI 执行者会修正你提示词里的错误假设,而不是盲从。 如果你把"必须模拟登录"写死,它就会去研究扫码、Cookie 注入,白扔两小时。


三、拆解三段核心技术

翻看workbuddy实现过程,看看用到哪些Playwright技巧,以及值得我们学习的地方。

3.1 免登录:为什么 /crm/链接能直接打开

录制回放有两种链接形态:

/crm/—— 创建者分享的公开链接,带一次性访问令牌,无需登录

其他形态—— 需要企业/个人账号登录

有个坑:请求 /crm/后会 302 跳转到 /cw/,标题变成「录制文件」。 这个跳转是内部的,别被吓到,以为要登录了。

代码上必须用持久化上下文,不是普通 launch():

python
ctx = p.chromium.launch_persistent_context(
    user_data_dir="~/.wemeet-profile",
    headless=False,
    args=["--disable-blink-features=AutomationControlled"],
    viewport={"width": 1440, "height": 900},
)

两个理由:页面下发的 am_tokenCookie 后面下载 MP4 全靠它; --disable-blink-features=AutomationControlled干掉 navigator.webdriver指纹。

3.2 虚拟滚动:为什么 innerText只给你 6%

这是最硬的一块。

长列表的经典优化:浏览器不把 500 条数据全挂到 DOM 上,而是——

  • 只渲染可视区 + 上下缓冲区(约 1.5 屏)的节点
  • 用一个占位高度撑出完整滚动条(所以你能拉到底,但那是假的)
  • 滚动时复用同一批 DOM 节点,只改里面的文字

实测数据:

容器: .minutes-module-list
scrollHeight = 18,354 px   ← 理论总高度(撑出来的,刚加载时只有 10,825)
clientHeight =   669 px    ← 可视高度
可视文本     = 1,078 字符   ← 真实挂载在 DOM 上的量

scrollHeight和 innerText之间的巨大差值,就是虚拟滚动的指纹。

破解思路:每次只有一屏是真的,那就滚一段、抓一段、拼起来; 相邻两段必然重叠,按最长「后缀-前缀」匹配去重。

两个关键参数:

滚动步长 = 0.6 屏小于一屏才不会跳过未渲染的部分;0.6 保证约 40% 重叠。

settle_ms= 700ms复用节点后重渲染要时间,太快会抓到上一屏残影。

⚠️ 这里我踩了个坑,值得单独讲

第一版我给重叠窗口设了个"看起来很合理"的上限:

python
merge_chunks(chunks, max_overlap=2000)   # ❌ 错了

结果:18,844 字符的稿子被拼成 50,328 字符——2.67 倍。

为什么?某些长段落(3,000+ 字符)的真实重叠超过了 2000, 窗口太小 → 匹配失败 → best = 0→ 整段原样追加。

这个坑的狠处在于:它不报错,只静默产出一份"看起来很完整"的废数据。

所以我加了一道质量门禁——时间戳单调性自检:

python
ts = re.findall(r"(?m)^\d{1,2}:\d{2}$", transcript)
regress = sum(1for a, b inzip(ts, ts[1:]) if b < a)

真实输出对比:

# 错误版(max_overlap=2000)
[step 3] 逐字稿 50328 字符
[check] 时间戳 291 个,回退 13 处     ← 一眼看出重复了

# 正确版(不设上限)
[step 3] 逐字稿 18844 字符
[check] 时间戳 131 个,回退 0 处 -> 时序正常   ✅

这就是测试思维在爬虫里的价值:给数据加一道断言,让错误自己跳出来。

3.3 网络拦截:403 的真相

拿到 MP4 直链后我以为稳了,结果连挂三次:

直接 urlopen(video.src)      → ❌ HTTP 403
用 page.request.get()        → ❌ HTTP 403(很多人以为它会带浏览器 Cookie,实测不会)
补 Referer 和 UA             → ❌ HTTP 403

403 的原因不是 token 过期,是缺请求头。

既然猜不出来,就让浏览器自己发一次请求,我拦下来照抄:

python
defon_route(route, request):
if".mp4"in request.url:
        captured["url"] = request.url
        captured["headers"] = dict(request.headers)   # 原样抄走
    route.continue_()

page.route("**/*", on_route)
page.evaluate("() => document.querySelector('video').play()")

拦截到的两个关键头:

Range: bytes=0-媒体流的标准起始请求。缺了直接 403,正常应返回 206 Partial Content。

Cookie: am_token=...页面下发的访问令牌。缺了鉴权失败。

用这两个头回放请求:

[info] HTTP 206 | video/mp4 | 80.9 MB
  已下载 20/81 MB (1s)
  已下载 40/81 MB (3s)
  已下载 60/81 MB (4s)
  已下载 80/81 MB (5s)

81 MB,5 秒。因为绕开了浏览器的解码/缓存层,直接走裸 HTTP 流。


四、合规边界(这一段请先看)

技术本身是中性的,用法不是。四条边界,别越线:

✅ 可以做固化你自己创建的会议;固化客户明确授权留档的会议。

❌ 不做破解验证码;绕过他人站点的访问控制;抓取未授权内容。

❌ 不外流会议号、链接、token、Cookie——任何可识别信息。

❌ 不进对外产物逐字稿正文属于敏感内容,不出现在任何对外交付物里。

一句话:这个工具只是把你本来就能看到的页面内容自动存下来, 不提供任何绕过权限控制的能力。请遵守平台服务条款。


五、建议收藏:判断口诀卡片

看到什么 → 该怎么干

滚动条很长,DOM 节点很少→ 虚拟滚动。分段滚(0.6 屏步长)+ 重叠拼接。

数据拿不到,但浏览器能显示→ 网络拦截。别猜接口,用 page.route让浏览器告诉你。

拼接完字符数明显偏大→ 先数时间戳。回退 > 0就是拼重复了,别交付。

下载媒体 403→ 缺 Range+ Cookie,不是 token 过期。

进阶:拦截到 XHR/Fetch 后往往能直接找到后端 JSON 接口—— 别再爬 DOM 了,直接请求接口,稳得多也快一个数量级。


六、写在最后

三个技术点:

① 免登录— /crm/分享链接自带令牌,持久化上下文保住 Cookie。 ② 虚拟滚动— 0.6 屏步长分段滚 → 最长重叠拼接 → 时间戳自检(救了我一次)。 ③ 网络拦截— 不猜请求头,用 page.route原样抄下来回放。

但最让我有收获的不是这三段代码,而是第二节那件事:

我把任务写成了「目标 + 首选路径 + 降级路径 + 输出约束」的委托书。 AI 在里面自己修正了我"必须模拟登录"的错误假设,自己发现了虚拟滚动,自己加了数据自检。

会写提示词的人和不会写的人,用的是同一个 AI,拿到的却是两种结果。前者拿到能跑的产物,后者拿到一堆看起来很专业的代码片段。


建议收藏。下次遇到"看得见拿不走"的页面,直接翻这张口诀卡。

评论区聊聊:你手上最想固化的一场会议,卡在哪一步?是滚动抓不全,还是视频 403?说出来,我逐条回。

📦 源码包:wemeet_recorder.py(单文件工具)+ requirements.txt+ README.md+ run_guide.md+ FAQ.md+ 真实运行日志(含失败版对照)+ SKILL.md(可封装成 Agent 技能)+ 零依赖 demo,按包内 run_guide 五步即可复现。

后台回复「会议固化」,获取完整配套源码包。

前往微信阅读全文

内容来自公众号,可前往微信查看原文。

查看作者的更多文章 →