多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python+Playwright自动化下载夸克网盘:登录态保存、提取码填写与批量下载实战

Python+Playwright自动化下载夸克网盘:登录态保存、提取码填写与批量下载实战 如果你也跟我一样经常要从夸克网盘的分享链接里整批下载资料就一定懂这套流程有多费手指打开链接、输入提取码、扫码登录、等列表加载、再一个一个点下载按钮。单个文件还好几十个文件就是纯粹的体力劳动。这篇文章就把“用 Python 加 Playwright 自动化完成下载”的完整方案拆给你看包含登录态持久化、提取码自动填写、下载事件捕获这几个关键环节以及我在真实环境下反复试出来的坑。适合有一定 Python 基础、想省事的人直接抄作业。1. 为什么是 Playwright而不是 requests 或 Selenium1.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_statexxx.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_HOSThttps://npmmirror.com/mirrors/playwright/builds playwright install chromiummacOS 或 Linux 下把set换成export即可。这里我只装了 Chromium不用 WebKit 和 Firefox因为夸克网页版对 Chromium 系的兼容性最好而且一个内核已经足够跑完整套流程。装完之后可以用一个最小脚本验证是否可用from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) 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(headlessFalse) context browser.new_context( user_agentMozilla/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_untilnetworkidle) print(请在打开的浏览器里扫码登录登录完成后回到命令行按回车) input() context.storage_state(pathSTATE_FILE) print(会话已保存到, STATE_FILE) browser.close()这里有几个细节值得说明。headlessFalse是必须的因为扫码需要人眼参与。登录完成后的回车确认是因为 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_stateSTATE_FILE if os.path.exists(STATE_FILE) else None, )如果文件不存在就传NonePlaywright 会按空白会话启动。这也是代码健壮性的一部分首次运行没有会话文件时可以退化为手动登录流程。需要注意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(statevisible, timeout8000) input_box.fill(code) page.keyboard.press(Enter) except Exception: pass page.wait_for_timeout(1500) try: page.locator(button:has-text(提取文件)).first.click(timeout3000) 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(timeout5000) page.locator(text下载).first.click(timeout5000) 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(timeout5000) 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(statevisible, timeout8000) input_box.fill(code) page.keyboard.press(Enter) except Exception: pass page.wait_for_timeout(1500) try: page.locator(button:has-text(提取文件)).first.click(timeout3000) except Exception: pass if page.locator(text提取码错误).count() 0: raise RuntimeError(提取码错误或已失效) def download_share(share_url, extract_code): os.makedirs(DOWNLOAD_DIR, exist_okTrue) with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context( user_agentUA, viewport{width: 1400, height: 900}, accept_downloadsTrue, storage_stateSTATE_FILE if os.path.exists(STATE_FILE) else None, ) page context.new_page() page.goto(share_url, wait_untildomcontentloaded) ensure_extract_code(page, extract_code) page.wait_for_timeout(3000) try: with page.expect_download(timeout60000) as dl_info: page.locator(text全选).first.click(timeout5000) page.locator(text下载).first.click(timeout5000) 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(timeout60000) as dl_info: page.locator(button:has-text(下载)).first.click(timeout5000) 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_untildomcontentloaded而不是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尽量和你日常浏览器保持一致。第二保持headlessFalse有头模式在页面看来更像真实用户操作。第三在启动参数里关闭自动化控制标记browser p.chromium.launch( headlessFalse, args[--disable-blink-featuresAutomationControlled], )这个参数能减轻页面通过navigator.webdriver属性做自动化识别的干扰但不能保证百分之百不被识别。更有效的办法是控制操作节奏不要以毫秒级速度连续点击、不要一上来就同时下载十几个文件每步操作之间留出自然间隔让页面有时间完成自身的逻辑校验。如果最终还是触发了滑块验证我的建议是不要在脚本里硬扛。滑块验证明文图片识别加轨迹模拟的工程量非常大而且网盘风控每天可能变化花几个小时去逆向不如让脚本暂停人工滑一下再继续。实际使用中合理控速下触发的概率很低。5.3 我踩过才明白的几个细节选择器要写宽不要写死。第一次写脚本时我照着当时页面结构把类名写得很精确没过两个星期页面一改版脚本就整段失效。后来我改成了文本定位加宽泛类名组合的方案比如找“下载”按钮就用button:has-text(下载)找列表就用多个备选类名让 Playwright 去匹配第一个存在的结果。这样即使页面微调脚本仍然能跑。下载大文件时要考虑磁盘空间和文件名冲突。save_as的目标路径如果已有同名文件Playwright 会直接覆盖不会额外提示。批量下载时我加了时间戳前缀来避免误覆盖但这个逻辑需要你按自己的需求调整。不要迷信“下载全部”按钮。分享文件夹特别大时点击下载全部后夸克需要在服务端打包可能几分钟后才返回 zip 文件浏览器长时间处于等待状态容易造成误判。如果分享内容只是单个视频、单个文档直接全选打包最快如果是几百个文件的大目录我倾向于拆成多个批次或者只下载自己真正需要的子文件夹。5.4 我的真实使用经验这套脚本我自己一直保留着最常用的场景是每周整理素材资源。我并没有把它做成后台常驻服务而是每次需要时手动跑一次。原因很简单网盘页面改版频率不可控完全无人值守的自动化方案需要持续维护性价比不高。当前这个方案里登录状态持久化解决了“每次都要扫码”的烦恼提取码处理解决了“每个链接都弹窗”的重复操作批量下载解决了“几十个文件戳到手酸”的核心痛点已经覆盖了 80% 的真实需求。最后说一个容易被忽略的小技巧运行下载脚本之前先把浏览器的下载目录清空或者在脚本里给文件名加日期前缀。这样你不需要盯着日志只看下载目录里新增了哪些文件就能快速判断哪些任务成功了、哪些需要重跑。自动化下载这件事关键不在于脚本本身多高大上而在于它能不能真的让你从重复劳动里脱身。
返回列表