Playwright实战:自动化下载夸克网盘分享文件
1. 为什么是 Playwright绕开接口签名的一种思路前一阵子我需要把一个分享目录里的几百个文件批量同步到本地第一反应是直接用 requests 打开分享页、解析文件 ID、拼下载接口。结果半天就败下阵来分享页的文件列表和下载凭证都带前端签名参数凭证有效期又短等我把列表爬完前面的凭证早就失效了。反复补参数、追接口的状态写起来像在解一道随时会变条件的方程。后来我换了个思路与其逆向接口不如让浏览器自己去完成整套交互。签名、Cookie、凭证刷新这些麻烦事浏览器内核全都处理掉了我只需要像真人一样操作页面打开链接、填提取码、点下载。Playwright 就是干这个最顺手的工具。用 Playwright 而不是 Selenium我主要看中三点。第一是自动等待机制click()会等元素可交互后再点省掉了一大堆time.sleep()。第二是下载事件处理得干净expect_download()能精确捕获一次下载并拿到文件流不像 Selenium 那样经常要自己去下载目录里找文件。第三是调试体验脚本跑挂了可以直接切到有头模式看页面现场也可以用 inspect 工具逐步检查元素。如果你只是偶尔下一次两个文件没必要上这套东西。但当需求变成“定期同步整个分享目录”“按文件名过滤下载”“批量归档”的时候方案性价比就完全不同了。而且只要页面 UI 不变脚本就能长期用接口方案遇到前端一次改版可能就全废了。下图是我对两种方案的对比方案稳定性开发成本维护成本适用场景直接请求接口低接口字段常变中需要逆向前端逻辑高随时可能挂临时拉一次数据Playwright 自动化高UI 稳定则脚本稳中熟悉 API 后上手快低主要改选择器批量、周期、复杂交互方案稳定性开发成本维护成本适用场景直接请求接口低接口字段常变中需要逆向前端逻辑高随时可能挂临时拉一次数据Playwright 自动化高UI 稳定则脚本稳中熟悉 API 后上手快低主要改选择器批量、周期、复杂交互2. 登录态怎么“存”storage_state 让扫码只发生一次分享页面多数情况不需要登录也能预览下载但登录之后更稳定下载大文件、访问部分加密分享时也不容易被风控拦。所以我给脚本加了一套登录态持久化机制保证扫码只扫一次后续运行直接复用。核心 API 是context.storage_state(path...)。Playwright 会把当前上下文里的 Cookies、localStorage、sessionStorage 全部序列化成一个 JSON 文件下次创建浏览器上下文时把这个 JSON 传回去浏览器就自动恢复了登录状态效果等同于用户带着上次登录的会话重新打开页面。2.1 首次扫码与状态保存先看创建上下文的代码。状态文件不存在时我刻意把浏览器设置为有头模式让扫码窗口正常弹出来import os import time from playwright.sync_api import sync_playwright STATE_FILE quark_state.json SHARE_ID 分享链接中的ID EXTRACT_CODE 1234 # 无提取码就留空 SAVE_DIR /path/to/your/download def create_context(playwright): browser playwright.chromium.launch(headlessFalse) context_options { accept_downloads: True, viewport: {width: 1440, height: 900}, user_agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ), } if os.path.exists(STATE_FILE): context_options[storage_state] STATE_FILE context browser.new_context(**context_options) return browser, context打开网盘首页后判断登录态。为了稳妥我不用某个具体文字判断登录而是探测“登录按钮”是否存在存在说明未登录不存在说明已带态。这个逻辑在页面改版时不容易误判。def ensure_login(context, page): page.goto(https://pan.quark.cn/, wait_untildomcontentloaded) if page.locator(button:has-text(登录)).count() 0: input(请在浏览器中完成扫码登录成功后回到终端按回车...) # 多等两秒等前端把最新 token 写入本地存储 page.wait_for_timeout(2000) context.storage_state(pathSTATE_FILE) print(登录态已保存到, STATE_FILE) else: print(已处于登录态无需重复扫码)这里有两个细节值得展开。第一扫码成功后的两三秒内前端还会发一两个请求刷新用户信息和权限 token如果立刻保存状态很容易漏掉刚写入的某条关键数据。实测下来多等两秒再 dump登录态的“寿命”明显更长。第二状态文件本质上是浏览器会话里所有敏感凭据的完整导出绝不能当普通配置文件提交到仓库。你应该把它加进.gitignore只在本地或受控服务器上保存。2.2 状态文件复用时的注意点状态文件不是永久有效的。网盘的会话通常几天后过期或者账号在别处登录会把旧会话顶掉。所以每次脚本启动我都重新走一次ensure_login而不是拿到文件就直接用。判断未登录后我会顺手把旧文件备份成带时间戳的路径再触发重新扫码避免写文件写到一半把原来的状态弄坏。import time def backup_expired_state(): if os.path.exists(STATE_FILE): backup fquark_state_expired_{int(time.time())}.json os.rename(STATE_FILE, backup) print(旧登录态已备份为, backup)3. 提取码与分享链接两种入口一起处理分享链接的形态通常是下面这两种# 无提取码 https://pan.quark.cn/s/AbCdEf # 带提取码 https://pan.quark.cn/s/AbCdEf#/list/share?pwd1234注意提取码不是普通查询参数它挂在#后面的 hash 片段里。这个片段不会出现在发往服务器的 HTTP 请求里纯粹是给前端 JavaScript 读取用的。浏览器加载完页面后前端脚本再根据 hash 中的pwd参数去请求真实的文件列表接口。明白这一点就有了两条处理路径。一条是把带pwd的完整 URL 交给 Playwright 打开。理论上页面加载完会自动读取 hash 并拉取列表但实测偶尔会出现前端没解析 hash 的情况画面停在空白或提取码输入框。另一条是等页面出现提取码输入框后用 Playwright 模拟人工输入和确认。我两条都做互为兜底。def open_share_page(page, share_id: str, pwd: str ): url fhttps://pan.quark.cn/s/{share_id}#/list/share if pwd: url f?pwd{pwd} page.goto(url, wait_untildomcontentloaded) # 兜底如果出现提取码输入框手动填写 pwd_input page.locator( input[placeholder*提取码], input[placeholder*请输入提取码] ).first if pwd_input.count() 0: pwd_input.fill(pwd) page.locator( button:has-text(确认), button:has-text(提取文件) ).first.click() page.wait_for_timeout(2000) page.wait_for_selector(文件列表根元素的选择器, timeout30000) print(分享页加载完成)选择器会随前端版本更新而变化所以我在脚本里给同一种元素写了多个候选表达式用逗号分隔传给locator()Playwright 会按顺序命中第一个可用元素。这一招在写任何“以页面自动化为主”的工具时都适用能显著降低页面改版带来的维护成本。还有一个反直觉的细节如果页面弹出了提取码输入框并且输入框里已经有值——这种诡异情况我遇到过前端把 hash 中的pwd自动回填到了输入框但没触发确认请求。此时你只需要找确认按钮点击不要重复fill()。重复输入会触发前端的校验事件反而容易把原本自动回填的值弄脏。如果提取码是错的页面通常会提示“提取码错误”输入框被清空。脚本里我把这个提示文案当作错误分支处理记录日志后跳过当前分享链接而不是无脑重试。反复提交错误提取码可能触发临时风控会把整个脚本锁死一段时间。4. 下载链路定位文件、触发按钮、落到本地文件列表加载出来后就要把目标文件找出来再触发下载。4.1 海量文件场景下的滚动加载分享目录就几个文件时列表一次渲染完直接定位文件名就行。但文件一多前端基本都会改成懒加载只有滚动到附近才渲染对应行。我的做法是逐屏滚动每滚一次就检查目标文件名是否出现def find_file(page, name: str): for _ in range(30): matched page.locator(ftext{name}) if matched.count() 0: return matched.first page.evaluate(window.scrollBy(0, 1000)) page.wait_for_timeout(600) raise TimeoutError(f在列表中找不到文件{name})这里有一个新手容易踩的坑page.locator()本身不会去页面里找元素它只是创建一个懒加载的定位器。真正触发查询的是.count()、.click()这些动作。所以我用.count()判断是否出现比直接调.click()再捕获异常干净得多。4.2 悬停后出现的下载按钮文件行平时只显示文件名、大小、日期下载按钮是鼠标悬停到该行时才出现的。Playwright 的.hover()能模拟悬停但按钮浮现需要一点渲染时间所以点击前先wait_for_selector等它出现import os from urllib.parse import unquote def download_file(page, save_dir: str, name: str): target find_file(page, name) target.hover() page.wait_for_selector(text下载, timeout5000) with page.expect_download(timeout120000) as dl_info: target.locator(text下载).click() download dl_info.value failure download.failure() if failure: raise RuntimeError(f下载失败: {failure}) filename download.suggested_filename or name filename unquote(filename) save_path os.path.join(save_dir, filename) download.save_as(save_path) print(f文件已保存{save_path})target.locator(text下载)表示只在目标文件行内部查找“下载”按钮不会误点其他悬浮工具条里的同名字段。这一点在页面布局复杂时会救命早期版本我就是因为用了全局的text下载经常点到不相干的行。4.3 文件名解码与文件夹打包download.suggested_filename一般来自服务器返回的content-disposition头比手工拼接可靠。但偶尔会遇到 URL 编码的字符串所以保存前我做了一次unquote()解码。如果下载目标是整个文件夹页面会先发起打包任务出现“正在生成压缩包”之类的进度提示之后才真正产生下载事件。Playwright 的expect_download会一直阻塞等待下载事件发生所以这类场景要把超时时间拉长到两三分钟。生成的文件名也可能与目录名不完全一致可能是系统随机拼出来的压缩包名不要指望它等于你点击的目录名。4.4 批量下载的节奏控制批量下载时尽量别让页面在短时间内触发几十个下载任务。浏览器的并发下载数是有上限的太多任务积压会让浏览器直接拦住后续下载。我每下载完一个文件就在循环里等 3 到 5 秒整体耗时增加一点但稳定度提升非常明显。实测下来批量模式下“多个文件同时卡住”的场景大多不是网盘限速而是浏览器下载队列爆了。5. 生产级兜底会话失效、超时重试与无人值守本地手动跑通脚本只是第一步真正难的是让它变成能反复执行、出错能自救的工具。5.1 会话失效的检测与人工介入登录态保存到文件不代表一劳永逸。我遇到的实际情况是会话可能几天后就失效账号在别处登录也会把旧会话顶掉。如果脚本不检查直接下载往往要等到下载事件超时才意识到问题。所以我在每次下载前都先确认会话有效发现登录页跳转就进入重新登录流程。这个流程我做成半人工方式二维码截图存到本地人工扫码后脚本继续。虽然不能全自动但比每次手动打开浏览器强太多。我还养成了一个习惯——重新登录前先把旧状态文件备份成带时间戳的名字等确认登录成功后再覆盖正式文件避免状态文件写一半损坏。def ensure_valid_session(page, context): page.goto(https://pan.quark.cn/, wait_untildomcontentloaded) if page.locator(button:has-text(登录)).count() 0: backup_expired_state() input(检测到登录态失效请扫码登录完成后按回车...) page.wait_for_timeout(2000) context.storage_state(pathSTATE_FILE) page.reload()5.2 超时重试与临时风控分享页偶尔响应很慢或者接口临时限流。我观察到的典型场景是第一次点击下载后长时间没有产生下载事件页面也没有报错完全卡住。针对这种情况我封装了一个简单的重试装饰器把打开分享页和下载文件包起来def retry(times: int 3, interval: int 10): def decorator(func): def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception as e: print(f第 {i 1} 次尝试失败{e}) time.sleep(interval) raise RuntimeError(重试次数耗尽) return wrapper return decorator重试时的关键不是原地再点一次下载而是先刷新页面、重新走一遍开页逻辑把整个页面恢复到一个干净状态。否则残留的弹窗和异常状态会让第二次点击继续失败。还有一个真实经验如果某次下载触发了风控接下来几个文件大概率都会卡住。我遇到这种情况就停止整个任务等 10 到 15 分钟再重新拉全量列表而不是继续往下试。硬顶着下载大概率只会被拦得更久。5.3 headless 模式下的行为差异脚本要丢到服务器上跑时浏览器不能每次弹窗口。Playwright 支持 headless 模式但有两个注意事项必须先处理。第一首次扫码保存状态必须在有头模式下完成。无头模式看不到页面也没法扫码。我通常本地有头模式跑一次拿到状态文件再传到服务器上用无头模式跑。第二无头模式对页面滚动行为的表现有差异。我在无头模式下遇到过mouse.wheel()滚动不生效后来统一改用page.evaluate(window.scrollBy(0, 1000))直接操作原生滚动搭配前面find_file里的轮询逻辑两种模式都稳定。5.4 下载完整性校验下载完成只代表 Playwright 没报错不代表文件是完好的。我会额外做一层大小校验把本地文件大小和页面展示的期望值比对不一致的删掉重下。这样能避免一个半截文件混进归档目录后续同步时还以为它已经存在。夸克网盘页面显示的文件大小是带单位的文本比如“12.5 MB”转成字节数再比对允许 5% 的误差因为前端展示时做了四舍五入严格相等比较容易误杀正常文件。def parse_size(text: str) - float: value, unit text.split( ) value float(value) if unit KB: return value * 1024 if unit MB: return value * 1024 * 1024 if unit GB: return value * 1024 * 1024 * 1024 return value def verify_size(save_path: str, expected_size_text: str | None) - bool: if not expected_size_text: return True expected parse_size(expected_size_text) actual os.path.getsize(save_path) return abs(actual - expected) / expected 0.05最后再分享一个我自己一直在用的技巧维护一个本地文件索引清单每次同步前先扫描本地已存在的文件只下载缺失或大小不匹配的那部分。一开始这样做只是为了省流量后来发现它对增量同步的稳定性帮助极大——下载任务中途断了重新跑一遍脚本只会补没完成的文件不会重复下载几百个已有文件。如果你需要的是定期同步某个分享目录这个思路可以直接抄过去用。

相关新闻

选后相邻关键牌:锚点偏移模型在扑克牌魔术中的数学与应用

选后相邻关键牌:锚点偏移模型在扑克牌魔术中的数学与应用

玩牌的人基本都绕不过“关键牌”这三个字。大量基础找牌魔术的背后,其实都藏着一张不被人注意的牌:它混在整副牌里,只有你知道它的位置。观众选走一张牌之后,你只要想办法让手里这张关键牌与选牌“靠到一起”,接下来所…

2026/10/12 19:14:54 阅读更多 →
吉林大学SNL编译器课设实战:从词法分析到三地址码生成

吉林大学SNL编译器课设实战:从词法分析到三地址码生成

简介:本资源是吉林大学计算机学院《编译原理》课程设计的完整实现——SNL语言编译器,面向高校计算机专业高年级本科生及编译技术初学者,用于实践词法分析、LL(1)语法分析与语义分析等核心编译流程。项目采用C开发,代码全部自主完成…

2026/10/12 19:14:54 阅读更多 →
基于THS终端的Python量化交易全链路实战指南

基于THS终端的Python量化交易全链路实战指南

量化交易圈子里,THS是一个绕不开的名字。很多人是从手工看盘开始接触它的,后来慢慢发现:与其每天盯盘手动下单,不如让程序替我们完成数据读取、策略计算、信号生成和交易执行这一整套闭环。这篇文章我不打算讲那些悬在空中的理论&…

2026/10/12 19:13:53 阅读更多 →

最新新闻

差分约束与SPFA最长路:从糖果题掌握建图与正环判定

差分约束与SPFA最长路:从糖果题掌握建图与正环判定

第一次做 SCOI2011 糖果这道题时,我的第一反应是这题跟“差分约束”有什么关系?n 个小朋友分糖果,无非就是比较大小、算个数,贪心排个序好像就完了。但仔细读题会发现,约束条件不是一串连续的大小关系,而是…

2026/10/12 20:05:20 阅读更多 →
PDT团队KPI指标库深度拆解:三维度19项指标与落地避坑指南

PDT团队KPI指标库深度拆解:三维度19项指标与落地避坑指南

简介:《PDT团队KPI指标库.pdf》是一份面向产品开发团队管理者、项目经理及研发人员的KPI考核体系参考资料,系统梳理了财务、客户、内部业务三大维度共二十余项关键绩效指标,并清晰界定各指标的适用范围与数据来源。资源为单个PDF文件&#xf…

2026/10/12 20:05:20 阅读更多 →
Axure 9负坐标空间全解析:从元件失踪到实战应用

Axure 9负坐标空间全解析:从元件失踪到实战应用

1. 负坐标空间到底是什么?先搞清楚 Axure 9 的画布逻辑做原型设计的人应该都遇到过这么一幕:某天打开一个同事交接的 Axure 9 文件,页面首页空空荡荡,可是右侧大纲面板里明明躺着十几个元件,选中其中一个,画…

2026/10/12 20:05:20 阅读更多 →
IPD开发阶段活动说明:从PDCP到ADCP的36个关键动作与落地指南

IPD开发阶段活动说明:从PDCP到ADCP的36个关键动作与落地指南

简介:这份PDF是一份IPD(集成产品开发)体系中开发阶段活动说明的专用资料,面向产品经理、项目经理、研发管理及职能部门负责人,用于明确项目从开发到验证、发布阶段的关键动作与角色分工。内容围绕外围组成员确定、项目…

2026/10/12 20:05:20 阅读更多 →
豆瓣电影Top250数据分析系统:Python爬虫+pandas+Flask+ECharts全流程实战

豆瓣电影Top250数据分析系统:Python爬虫+pandas+Flask+ECharts全流程实战

简介:本资源为基于Python的豆瓣电影数据分析与可视化系统完整毕业设计包,面向计算机相关专业学生及需要数据分析实战项目的开发者。项目采用Flask搭建后端、ECharts完成图表渲染,结合爬虫采集豆瓣电影数据并用pandas做清洗处理,数…

2026/10/12 20:05:20 阅读更多 →
从生产事故到审查系统:open-code-review如何让代码审查真正落地

从生产事故到审查系统:open-code-review如何让代码审查真正落地

1. 我为什么会在代码审查这件事上栽跟头——项目起点先坦白说,我并不是一开始就想搞什么代码审查工具的。真正让我下决心动手的,是某次上线事故。那是一个数据迁移项目,项目不大,三个人改了差不多两周。代码过了两轮 review&#…

2026/10/12 20:04:19 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →