☰
Python+Playwright自动化下载夸克网盘:登录态保存、提取码填写与批量下载实战
2026/10/9 10:34:18 网站建设 项目流程

如果你也跟我一样,经常要从夸克网盘的分享链接里整批下载资料,就一定懂这套流程有多费手指:打开链接、输入提取码、扫码登录、等列表加载、再一个一个点下载按钮。单个文件还好,几十个文件就是纯粹的体力劳动。这篇文章就把“用 Python 加 Playwright 自动化完成下载”的完整方案拆给你看,包含登录态持久化、提取码自动填写、下载事件捕获这几个关键环节,以及我在真实环境下反复试出来的坑。适合有一定 Python 基础、想省事的人直接抄作业。

1. 为什么是 Playwright,而不是 requests 或 Selenium

1.1 这个项目要解决的核心问题

夸克网盘的分享页面和很多国内网盘一样,不是一个简单的静态网页。文件列表、下载按钮、提取码校验都是由前端 JavaScript 动态渲染出来的,页面里充斥着各种加签参数、加密字段和临时 token。如果用 requests 去模拟,你不仅要先分析接口,还要复现它的签名算法,遇到登录态过期还得重新算一遍,维护成本极高。

我一开始也想走接口路线,后来仔细看了一遍请求就知道这条路要花的时间跟手动下载差不多。分享页面把文件列表都渲染在前端,浏览器自己也拿不到所谓的“纯接口”,所有请求都是自动帮你带好的。于是思路就变成了:与其逆向接口,不如直接驱动浏览器,让页面自己完成所有鉴权,我不碰那些签名逻辑,只负责定位按钮、点击、接收下载文件。

1.2 技术选型的三个关键理由

为什么不用 Selenium,也放下自己拼 HTTP 包的念头?有三个现实原因。

第一,Playwright 对浏览器下载事件的支持非常干净。在 Selenium 里要配置浏览器 profile、设置下载目录、处理 MIME 类型,稍不注意文件就静默下载到了默认目录,你根本不知道它什么时候结束。而 Playwright 提供了一个expect_download()上下文管理器,你可以在点击下载按钮之前就声明“我要捕获下一个下载事件”,然后拿到下载对象,调用save_as()把文件搬到目标位置,逻辑非常直观。

第二,登录态持久化有官方原生接口。Playwright 的context.storage_state(path=...)可以把当前上下文里的 cookie、localStorage、sessionStorage 一次性导出成 JSON 文件。下次创建 context 时直接传入storage_state="xxx.json",登录状态自动恢复,完全不需要手动去浏览器里一个个复制 cookie。

第三,Playwright 的自动等待机制非常省心。页面元素还没渲染完时,普通脚本很容易点在空位置上。Playwright 的 locator 默认会等待元素可见、可点击,配合超时参数,脚本写得比 Selenium 短不少,也不容易出现竞态问题。

1.3 整体架构:登录、提取码、下载三层

整个项目我拆成了三个独立功能模块,对应三种不同的场景。

  • 第一层是登录态管理。单独写一个save_state.py,打开浏览器让你扫码登录,登录完成后自动保存会话数据到本地 JSON。这个脚本只在首次或会话过期时运行。
  • 第二层是分享链接解析。打开链接后先判断页面是否需要输入提取码,如果需要,就自动填入并点击确认,然后等待文件列表加载完成。
  • 第三层是下载处理。进入文件列表后,要么全选后打包下载,要么逐项点击下载按钮,利用 Playwright 的下载事件把文件保到本地目录。

这三层彼此独立,实际运行时只需执行第三层,前两层作为前置依赖。这样设计的好处是职责清晰——登录过期了只重跑第一层,不需要动下载代码;页面结构变了只改第二层的选择器,不至于牵一发动全身。

2. 环境准备与登录态持久化

2.1 安装 Python 与 Playwright,附一个镜像坑

假设你已经装好了 Python 3.8+。安装 Playwright 只需要两条命令:

pip install playwright playwright install chromium

第一条是装 Python 库,第二条是下载 Chromium 浏览器内核。很多人在第二步卡住,因为默认要从官方 CDN 拉一个一两百 MB 的浏览器包,国内网络环境下经常超时。解决办法是设置环境变量,使用国内镜像源:

set PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright/builds playwright install chromium

macOS 或 Linux 下把set换成export即可。这里我只装了 Chromium,不用 WebKit 和 Firefox,因为夸克网页版对 Chromium 系的兼容性最好,而且一个内核已经足够跑完整套流程。

装完之后可以用一个最小脚本验证是否可用:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://pan.quark.cn/") print(page.title()) browser.close()

能打印出页面标题,说明环境就绪。

2.2 第一步:把登录会话先存起来

我设计了一个独立脚本save_state.py,它只做一件事:打开夸克网盘首页,让你扫码登录,登录完成后保存全部会话数据。

from playwright.sync_api import sync_playwright STATE_FILE = "quark_state.json" with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context( 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", viewport={"width": 1400, "height": 900}, ) page = context.new_page() page.goto("https://pan.quark.cn/", wait_until="networkidle") print("请在打开的浏览器里扫码登录,登录完成后回到命令行按回车") input() context.storage_state(path=STATE_FILE) print("会话已保存到", STATE_FILE) browser.close()

这里有几个细节值得说明。headless=False是必须的,因为扫码需要人眼参与。登录完成后的回车确认是因为 Playwright 无法感知“你什么时候登录完”,用input()阻塞脚本是最朴素也最可靠的方式。保存下来的quark_state.json是一个 JSON 文件,里面包含所有 cookie 和本地存储数据,大小通常只有几十 KB。

2.3 为什么 storage_state 比单纯存 Cookie 更省心

有人可能会问,为什么不从浏览器开发者工具里复制 cookie 存成字典,然后在 Playwright 里 add_cookie?

主要原因是现代网站的登录态不只依赖 cookie。夸克网盘这类产品会把部分会话信息放在 localStorage 里,里面可能存着用户唯一标识、签名值等数据。你手动复制 cookie 容易漏掉关键字段,而且 cookie 的 Domain、Path、Expires 属性一旦对不上,登录态照样失效。

storage_state把整个上下文里的存储状态完整导出,加载时也是整体恢复,不存在字段遗漏的问题。使用时就在创建 context 时传入:

context = browser.new_context( storage_state=STATE_FILE if os.path.exists(STATE_FILE) else None, )

如果文件不存在,就传None,Playwright 会按空白会话启动。这也是代码健壮性的一部分:首次运行没有会话文件时,可以退化为手动登录流程。

需要注意,storage_state 和浏览器指纹是绑定的。如果你的下载脚本使用了不同的 user_agent 或 viewport,网站可能认为这是新设备,要求重新登录。所以我建议在保存状态和下载脚本里用同一份 UA 和视口参数,减少这种不必要的风控误判。

3. 核心功能实现:解析分享链接、提取码与文件列表

3.1 提取码到底在哪里填

分享链接分两种:带提取码和不带提取码。带提取码的链接打开后,页面上通常会弹出一个输入框,要求输入四位或六位提取码。但现实是,这个弹层不一定马上出现,它可能在页面加载完成后的一两秒才通过异步接口渲染出来,所以不能用“打开页面后立刻定位”的思路。

我写了一个专门的处理函数,它会先等待输入框出现,填码后尝试回车,再兜底点击确认按钮:

def ensure_extract_code(page, code): if not code: return try: input_box = page.locator( "input[placeholder*='提取码'], input[placeholder*='提取密码']" ).first input_box.wait_for(state="visible", timeout=8000) input_box.fill(code) page.keyboard.press("Enter") except Exception: pass page.wait_for_timeout(1500) try: page.locator("button:has-text('提取文件')").first.click(timeout=3000) except Exception: pass

还有个很容易踩的坑:如果分享链接本身带pwd参数,夸克会自动解析并跳过提取码输入层,此时页面上根本没有输入框,所以上面这段代码里用了 try/except 静默跳过。脚本逻辑应当是“有输入框就填,没有输入框就认为页面已经直接放行”,而不是强制要求输入框存在。

填码之后最好检查一下是否出错。如果提取码错误,页面会提示“提取码不正确”或类似文案。可以在填码后加一段校验:

if page.locator("text=提取码错误").count() > 0 or page.locator("text=提取码不正确").count() > 0: raise RuntimeError("提取码错误或已失效")

别小看这一步,它能把错误暴露在早期,而不是等到文件列表定位失败时再排查。

3.2 文件列表的识别与选择器定位

进入分享页面后,文件列表是前端渲染的 SPA 组件,文件名、选中框、下载按钮都在一个列表容器里。不同时期的夸克页面类名不一样,所以我不会给你一个写死的 CSS 选择器让你永久使用,而是提供一套定位方法。

先用一段探测代码打印出列表条目的文本和属性,让你确认当前页面结构:

rows = page.locator("div.list-item, .file-list-item, [class*='fileItem']") print("识别到条目数量:", rows.count()) for i in range(min(rows.count(), 10)): print(i, rows.nth(i).get_attribute("title") or rows.nth(i).inner_text()[:30])

运行后你会看到每个列表项的主体内容。如果输出为空,说明选择器没匹配上,这时打开 DevTools,点击左上角的元素选择器,再点击页面上任意文件名,观察它所在的 DOM 节点,把对应的类名替换进去即可。

实战中我倾向于用相对宽松的规则:文件名单元格的文本就是文件名,下载按钮的文字就是“下载”,顶部工具栏有“全选”和“下载”两个按钮。与其依赖某个版本独有的类名,不如用文本定位,兼容性更好。

3.3 全选打包下载 vs 逐项下载

分享页面最省事的下载方式是全选后打包。如果分享内容是一个包含几十个文件的文件夹,直接在页面顶部的工具栏里点击“全选”,再点“下载”按钮,夸克会自动把所有文件打包成一个 zip 文件,并触发浏览器下载。这个方式对脚本最友好:只需要捕获一次下载事件,保存一个文件。

对应代码:

with page.expect_download() as dl_info: page.locator("text=全选").first.click(timeout=5000) page.locator("text=下载").first.click(timeout=5000) download = dl_info.value download.save_as(os.path.join(DOWNLOAD_DIR, download.suggested_filename))

不过要注意,Playwright 的expect_download()必须写在点击动作之前。很多人第一次写会先点击再声明捕获,结果永远拿不到下载事件。原因很简单:Playwright 的事件机制要求你提前注册等待,点击动作触发下载事件时,等待器已经在监听,才能接住这个对象。

如果你想按文件逐个下载,逻辑就变成遍历列表,对每个条目悬停并点击其下载按钮:

for idx in range(rows.count()): row = rows.nth(idx) row.hover() with page.expect_download() as dl_info: page.locator("button:has-text('下载')").first.click(timeout=5000) dl = dl_info.value file_path = os.path.join(DOWNLOAD_DIR, dl.suggested_filename) dl.save_as(file_path) print("已下载:", dl.suggested_filename)

这段代码的问题在于,当列表条目很多时,浏览器下载队列会被塞满,页面可能出现“同时下载文件数过多”的提示。我的做法是在每两个文件之间加 3 秒左右的间隔,既给页面反应时间,也降低触发风控的概率。

4. 自动化下载的完整代码与运行效果

4.1 完整脚本结构:从参数解析到文件落盘

标题里的“完整实现”不能只是零碎函数。我把项目整合成了一个可直接运行的入口脚本,逻辑包括参数解析、登录态加载、提取码处理、下载执行、异常处理五步。

import os import sys from playwright.sync_api import sync_playwright STATE_FILE = "quark_state.json" DOWNLOAD_DIR = os.path.abspath("downloads") UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36") def ensure_extract_code(page, code): if not code: return try: input_box = page.locator( "input[placeholder*='提取码'], input[placeholder*='提取密码']" ).first input_box.wait_for(state="visible", timeout=8000) input_box.fill(code) page.keyboard.press("Enter") except Exception: pass page.wait_for_timeout(1500) try: page.locator("button:has-text('提取文件')").first.click(timeout=3000) except Exception: pass if page.locator("text=提取码错误").count() > 0: raise RuntimeError("提取码错误或已失效") def download_share(share_url, extract_code): os.makedirs(DOWNLOAD_DIR, exist_ok=True) with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context( user_agent=UA, viewport={"width": 1400, "height": 900}, accept_downloads=True, storage_state=STATE_FILE if os.path.exists(STATE_FILE) else None, ) page = context.new_page() page.goto(share_url, wait_until="domcontentloaded") ensure_extract_code(page, extract_code) page.wait_for_timeout(3000) try: with page.expect_download(timeout=60000) as dl_info: page.locator("text=全选").first.click(timeout=5000) page.locator("text=下载").first.click(timeout=5000) download = dl_info.value target = os.path.join(DOWNLOAD_DIR, download.suggested_filename) download.save_as(target) print("打包下载完成:", target) except Exception as e: print("全选打包失败,尝试逐项下载:", e) rows = page.locator("div.list-item, .file-list-item, [class*='fileItem']") for idx in range(rows.count()): row = rows.nth(idx) row.hover() with page.expect_download(timeout=60000) as dl_info: page.locator("button:has-text('下载')").first.click(timeout=5000) dl = dl_info.value target = os.path.join(DOWNLOAD_DIR, dl.suggested_filename) dl.save_as(target) print("已下载:", dl.suggested_filename) page.wait_for_timeout(3000) browser.close() if __name__ == "__main__": if len(sys.argv) < 2: print("用法:python download.py 分享链接 [提取码]") sys.exit(1) url = sys.argv[1] code = sys.argv[2] if len(sys.argv) >= 3 else "" download_share(url, code)

这个脚本选用了wait_until="domcontentloaded"而不是"networkidle"。原理是夸克页面加载完所有静态资源要很久,但实际可交互的文件列表并不需要等全部网络请求结束。用networkidle会拖慢执行速度,还可能出现超时反而更不可靠的情况。domcontentloaded搭配后续的定位等待,是效率与稳妥之间更好的折中。

4.2 命令行封装与批量任务处理

把脚本保存为download.py后,日常使用方式就是两条命令:

python save_state.py python download.py "https://pan.quark.cn/s/xxxx" "1234"

如果没有提取码,第二个参数留空字符串即可。如果你想一次性下载多个分享链接,也很简单,不用改脚本,写一个外层循环:

任务清单.csv: 链接,提取码 https://pan.quark.cn/s/aaaa,1234 https://pan.quark.cn/s/bbbb,

然后用一个小脚本逐行调用主脚本,每次调用之间 sleep 十秒左右,避免浏览器还没完全退出时下一个任务就把系统资源占满。

实际运行时你会发现,Playwright 启动浏览器到页面加载完成,通常需要 5 到 10 秒的时间,真正下载大文件的时间另算。所以这套方案适合批量分享下载和需要离线整理资料的场景,不太适合只想下载一个几 MB 小文件的临时需求。

5. 常见问题排查与避坑实录

5.1 高频问题速查表

以下这些坑都是我在实际运行中碰到的,不是从文档里抄出来的。整理成表格,方便你对照排查。

现象可能原因解决方案
页面提示“文件已被删除”或“分享已失效”分享链接过期或分享者主动取消分享联系分享者换新链接,检查 URL 是否完整
打开页面一直没有提取码输入框链接本身带提取码参数,页面已自动解析不做处理,直接看文件列表是否出现
输入提取码后提示提取码错误复制时带入了空格或错误字符手动打开链接验证提取码,脚本里先 strip 再填
点击下载后没有下载事件expect_download()放在了点击之后必须提前声明下载事件监听
保存的文件是.crdownload或只有几十字节下载没完成时就进行了保存操作用download.save_as()等待完成流复制
定位列表条目数量为 0页面改版,类名变化用 DevTools 检查实际 DOM,更新选择器
登录态突然失效storage_state 过期或浏览器指纹改变重新运行save_state.py扫码登录一次
脚本执行到下载时卡住不动等待时间过于激进或页面弹出额外验证调大 timeout,或者改用手动处理该链接

5.2 防止页面自动校验干扰

分享页面有时在点击下载后会弹出一个滑块验证,或者要求你输入图片验证码才能继续。这类机制无法通过脚本逻辑彻底消除,但可以通过几个操作习惯把触发概率降到很低。

第一,使用真实的 user_agent,尽量和你日常浏览器保持一致。第二,保持headless=False,有头模式在页面看来更像真实用户操作。第三,在启动参数里关闭自动化控制标记:

browser = p.chromium.launch( headless=False, args=["--disable-blink-features=AutomationControlled"], )

这个参数能减轻页面通过navigator.webdriver属性做自动化识别的干扰,但不能保证百分之百不被识别。更有效的办法是控制操作节奏:不要以毫秒级速度连续点击、不要一上来就同时下载十几个文件,每步操作之间留出自然间隔,让页面有时间完成自身的逻辑校验。

如果最终还是触发了滑块验证,我的建议是不要在脚本里硬扛。滑块验证明文图片识别加轨迹模拟的工程量非常大,而且网盘风控每天可能变化,花几个小时去逆向不如让脚本暂停,人工滑一下再继续。实际使用中,合理控速下触发的概率很低。

5.3 我踩过才明白的几个细节

选择器要写宽,不要写死。第一次写脚本时我照着当时页面结构把类名写得很精确,没过两个星期页面一改版脚本就整段失效。后来我改成了文本定位加宽泛类名组合的方案,比如找“下载”按钮就用button:has-text('下载'),找列表就用多个备选类名让 Playwright 去匹配第一个存在的结果。这样即使页面微调,脚本仍然能跑。

下载大文件时要考虑磁盘空间和文件名冲突。save_as的目标路径如果已有同名文件,Playwright 会直接覆盖,不会额外提示。批量下载时我加了时间戳前缀来避免误覆盖,但这个逻辑需要你按自己的需求调整。

不要迷信“下载全部”按钮。分享文件夹特别大时,点击下载全部后夸克需要在服务端打包,可能几分钟后才返回 zip 文件,浏览器长时间处于等待状态,容易造成误判。如果分享内容只是单个视频、单个文档,直接全选打包最快;如果是几百个文件的大目录,我倾向于拆成多个批次,或者只下载自己真正需要的子文件夹。

5.4 我的真实使用经验

这套脚本我自己一直保留着,最常用的场景是每周整理素材资源。我并没有把它做成后台常驻服务,而是每次需要时手动跑一次。原因很简单:网盘页面改版频率不可控,完全无人值守的自动化方案需要持续维护,性价比不高。当前这个方案里,登录状态持久化解决了“每次都要扫码”的烦恼,提取码处理解决了“每个链接都弹窗”的重复操作,批量下载解决了“几十个文件戳到手酸”的核心痛点,已经覆盖了 80% 的真实需求。

最后说一个容易被忽略的小技巧:运行下载脚本之前,先把浏览器的下载目录清空,或者在脚本里给文件名加日期前缀。这样你不需要盯着日志,只看下载目录里新增了哪些文件,就能快速判断哪些任务成功了、哪些需要重跑。自动化下载这件事,关键不在于脚本本身多高大上,而在于它能不能真的让你从重复劳动里脱身。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询