告别Selenium弹窗噩梦:Playwright实现无头浏览器文件自动下载实战
1. 项目概述为什么我们要告别Selenium如果你做过Web自动化测试或者数据抓取尤其是涉及到文件下载的场景那你大概率经历过“弹窗噩梦”。浏览器原生的“另存为”对话框就像一堵无法逾越的高墙横亘在你的自动化脚本和本地文件之间。Selenium作为老牌自动化工具在处理这类交互式系统弹窗时显得力不从心。你不得不借助AutoIt、PyAutoGUI这类基于坐标模拟的桌面自动化工具脚本变得脆弱、跨平台性差维护成本直线上升。这个项目要解决的正是这个痛点。它的核心是利用Playwright这一现代浏览器自动化库结合Python实现无需人工干预、稳定可靠的无头浏览器文件自动下载。这里的“无头”指的是没有图形界面的浏览器它运行在后台更节省资源更适合自动化任务。而“告别Selenium弹窗噩梦”则直击了传统方案的软肋。我最近在一个数据报表自动归档的项目中就遇到了这个问题。每天需要从几十个内部系统页面下载Excel和PDF报表用Selenium写了一半全卡在下载弹窗上。后来切换到Playwright配合其强大的下载事件监听和路径控制API整个流程变得异常丝滑。这篇文章我就把这个从“踩坑”到“填坑”的完整实战经验分享出来并附上一个用pytest框架组织的、可直接复用的下载测试实战案例。无论你是做自动化测试的QA工程师还是需要定时抓取文件的数据工程师或者是任何被浏览器下载弹窗困扰的开发者这套方案都能让你彻底解脱。接下来我们从为什么选择Playwright开始一步步拆解实现细节。2. 核心工具选型为什么是Playwright而非Selenium在开始动手之前我们必须搞清楚工具选型背后的逻辑。为什么在这个场景下Playwright是比Selenium更优的选择这不仅仅是“新”与“旧”的问题而是架构和设计理念的差异。2.1 Selenium的瓶颈弹窗处理的“阿喀琉斯之踵”Selenium通过WebDriver协议与浏览器通信。这个协议设计之初主要目标是模拟用户对网页内容的操作比如点击、输入、获取元素等。浏览器原生的“另存为”对话框是操作系统级别的组件而非网页DOM的一部分。WebDriver协议无法直接与之交互。这就是问题的根源。传统的Selenium解决方案通常是修改浏览器首选项在启动浏览器时通过ChromeOptions设置默认下载路径并禁用下载提示。这招对部分简单场景有效但存在明显缺陷无法获取下载状态你只知道浏览器“应该”在下载但不知道它何时开始、何时结束、是否成功。文件名不可控下载的文件名通常是服务器返回的原始文件名如果你想根据页面内容重命名会非常麻烦。兼容性问题不同浏览器Chrome, Firefox的设置方式差异很大且浏览器版本升级可能导致设置失效。借助第三方工具集成AutoIt或PyAutoGUI来识别并操作系统弹窗。这是下下策因为它极度脆弱依赖于固定的窗口标题、按钮坐标。系统主题、分辨率、语言设置的改变都会导致脚本失败。破坏跨平台性Windows上的脚本无法在Linux或macOS上运行。阻塞脚本操作弹窗时脚本必须等待无法进行其他异步任务。2.2 Playwright的破局之道原生API支持与事件驱动Playwright由微软团队开发它采用了不同的思路。它不仅仅是一个WebDriver客户端而是直接通过DevTools Protocol等底层协议与浏览器内核对话提供了更强大、更底层的控制能力。对于文件下载它提供了原生的、一等公民级别的支持。其核心优势体现在page.on(‘download’)事件监听器这是关键。当页面触发下载时无论是通过点击一个带download属性的链接还是通过JavaScript触发的文件流Playwright会抛出一个download事件。你的脚本可以监听这个事件并立即获取到一个Download对象完全绕过了系统弹窗。Download对象这个对象包含了下载的所有信息文件的网络请求URL、建议的文件名、下载状态等。最重要的是它提供了save_as(path)方法让你可以自由指定文件保存的完整路径和名称。等待下载完成Download对象还有path()方法等待下载完成并返回临时路径和failure()方法获取失败原因让你能精确控制下载流程判断成功与否。简单来说Selenium试图“绕过”或“模拟”弹窗而Playwright直接从浏览器内核层面“拦截”了下载请求并提供了编程接口。这是一种降维打击。注意Playwright需要安装特定的浏览器版本它自带一个经过优化的Chromium、Firefox和WebKit。这看似增加了部署复杂度但实际上保证了环境的一致性避免了因用户本地浏览器版本差异导致的问题对于自动化任务反而是优点。3. 环境搭建与基础配置理论说清楚了我们开始动手。第一步是把环境搭起来。我会以macOS/Linux的命令行示例为主Windows用户只需将pip3和python3命令替换为pip和python即可。3.1 安装Playwright Python包打开你的终端执行以下命令。建议使用虚拟环境如venv或conda来管理依赖避免包冲突。# 安装Playwright的Python客户端库 pip3 install playwright # 安装Playwright自带的浏览器Chromium, Firefox, WebKit。这一步会下载浏览器时间稍长。 playwright installplaywright install这个命令非常关键它会下载Playwright维护的、保证兼容性的浏览器版本。如果你只想安装Chromium以节省空间和時間可以使用playwright install chromium。3.2 验证安装与录制工具可选但推荐安装完成后可以写一个最简单的脚本来验证# test_install.py import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: # 启动无头Chromium浏览器 browser await p.chromium.launch(headlessTrue) page await browser.new_page() await page.goto(https://example.com) print(await page.title()) await browser.close() asyncio.run(main())运行python3 test_install.py如果输出“Example Domain”说明环境配置成功。另外Playwright提供了一个强大的代码生成器对于初学者快速上手或探索页面操作序列非常有帮助# 启动代码生成器会打开一个有界面的浏览器 playwright codegen https://example.com这个工具能记录你的点击、输入操作并实时生成对应的Python代码。虽然我们最终的项目不依赖它但在分析目标网站下载触发逻辑时它是一个绝佳的辅助工具。4. 核心实现监听、拦截与保存下载文件现在进入最核心的部分。我们将实现一个完整的、健壮的文件自动下载函数。我会先给出一个基础版本然后逐步添加错误处理、状态等待等工业级特性。4.1 基础版本事件监听与文件保存假设我们要从一个网站下载文件这个网站有一个按钮点击后会触发文件下载。我们的脚本需要导航到页面。点击下载按钮。拦截下载事件并将文件保存到指定位置。import asyncio from pathlib import Path from playwright.async_api import async_playwright, Download async def download_file(url: str, download_selector: str, save_dir: Path): 基础版文件下载函数 :param url: 目标网页地址 :param download_selector: 触发下载的按钮/链接的CSS选择器 :param save_dir: 文件保存目录 async with async_playwright() as p: # 启动浏览器设置headlessTrue为无头模式 browser await p.chromium.launch(headlessTrue) # 创建新页面上下文可以统一设置下载行为 context await browser.new_context(accept_downloadsTrue) # 必须设置为True page await context.new_page() # 用于存储下载对象的Future download_future asyncio.Future() # 核心注册下载事件监听器 def on_download(download: Download): # 当下载事件触发时设置future的结果为这个download对象 if not download_future.done(): download_future.set_result(download) page.on(download, on_download) try: # 导航到目标页面 await page.goto(url) # 点击触发下载的元素 await page.click(download_selector) # 等待下载事件被触发获取download对象最多等待30秒 download await asyncio.wait_for(download_future, timeout30.0) # 构建保存路径使用下载的建议文件名保存在指定目录 suggested_filename download.suggested_filename if not suggested_filename: suggested_filename fdownloaded_file_{int(time.time())} save_path save_dir / suggested_filename # 将文件保存到指定路径 await download.save_as(save_path) print(f文件已下载并保存至: {save_path}) # 可选等待下载在后台完成save_as内部已包含等待 # download.path() # 这会等待下载完成并返回临时文件路径 except asyncio.TimeoutError: print(错误在指定时间内未触发下载。) except Exception as e: print(f下载过程中发生错误: {e}) finally: await browser.close() # 使用示例 async def main(): target_url https://example.com/download-page download_button_selector a#download-link # 替换为实际的选择器 save_directory Path(./downloads) save_directory.mkdir(parentsTrue, exist_okTrue) # 确保目录存在 await download_file(target_url, download_button_selector, save_directory) if __name__ __main__: asyncio.run(main())代码解析与注意事项accept_downloadsTrue在创建浏览器上下文(new_context)时这个参数必须设置为True否则浏览器会拒绝所有下载监听器也不会生效。page.on(‘download’, on_download)这是核心魔法。我们将一个回调函数on_download绑定到页面的download事件上。一旦页面内有下载触发这个函数就会被调用并接收到一个已经初始化好的Download对象。asyncio.Future()由于下载事件是异步触发的我们使用一个Future对象来“等待”这个事件的发生。这是一种在异步编程中协调不同回调的常见模式。download.suggested_filename浏览器从服务器响应头通常是Content-Disposition中获取的建议文件名。强烈建议优先使用这个名称因为它通常是最准确的。如果为空则需要自己生成一个。download.save_as(path)该方法执行两个操作a) 等待下载数据完全传输完毕b) 将文件移动到你指定的path。这是一个阻塞异步等待操作所以你不必再额外调用download.path()来等待完成。4.2 进阶版本添加状态检查、重命名与超时控制基础版本能工作但不够健壮。在实际项目中我们需要考虑下载可能失败网络错误、服务器返回404、我们需要根据页面内容自定义文件名、需要更精细的超时控制。import asyncio import time from pathlib import Path from typing import Optional from playwright.async_api import async_playwright, Download, Page, TimeoutError as PlaywrightTimeoutError async def robust_download( page: Page, trigger_action: callable, # 一个执行触发下载操作的异步函数 save_dir: Path, custom_filename: Optional[str] None, download_timeout: float 120.0, action_timeout: float 30.0 ) - Optional[Path]: 健壮版下载函数 :param page: 已创建的Playwright页面对象 :param trigger_action: 异步函数执行触发下载的操作如 page.click() :param save_dir: 保存目录 :param custom_filename: 自定义文件名不含路径若为None则使用建议文件名 :param download_timeout: 下载过程总超时秒 :param action_timeout: 触发操作后的等待超时秒 :return: 成功则返回保存的Path对象失败返回None download_future asyncio.Future() def on_download(download: Download): if not download_future.done(): download_future.set_result(download) # 注册监听器 page.on(download, on_download) try: # 执行用户定义的触发操作 await trigger_action() # 等待下载事件被触发 download: Download await asyncio.wait_for(download_future, timeoutaction_timeout) print(f下载已开始建议文件名: {download.suggested_filename}) # 确定最终文件名 if custom_filename: final_filename custom_filename else: final_filename download.suggested_filename or fdownload_{int(time.time())} save_path save_dir / final_filename # 保存文件并设置下载过程超时 await asyncio.wait_for(download.save_as(save_path), timeoutdownload_timeout) print(f文件下载成功: {save_path}) # 检查下载是否真的成功例如服务器可能返回错误页面而不是文件 if download.failure(): print(f下载失败原因: {download.failure()}) save_path.unlink(missing_okTrue) # 删除可能已存在的损坏文件 return None # 验证文件是否确实存在且大小不为0可选 if save_path.exists() and save_path.stat().st_size 0: return save_path else: print(错误下载的文件为空或不存在。) return None except asyncio.TimeoutError: print(f错误操作或下载在{action_timeout}/{download_timeout}秒内未完成。) return None except Exception as e: print(f下载过程中发生未预期错误: {e}) return None finally: # 重要移除事件监听器避免影响后续操作或内存泄漏 page.remove_listener(download, on_download) # 使用示例 async def main_advanced(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) context await browser.new_context(accept_downloadsTrue) page await context.new_page() await page.goto(https://httpbin.org/response-headers?Content-Dispositionattachment%3B%20filename%3D%22example.json%22) save_dir Path(./advanced_downloads) save_dir.mkdir(exist_okTrue) # 定义一个触发动作点击一个按钮这里用goto模拟实际可能是click async def trigger_dl(): # 假设页面上有一个按钮其id是download-btn # await page.click(#download-btn) # 由于httpbin示例直接返回文件我们直接触发这里用重新加载模拟 pass # 在这个特定例子中导航即触发 # 调用健壮下载函数并尝试自定义文件名 saved_file_path await robust_download( pagepage, trigger_actiontrigger_dl, save_dirsave_dir, custom_filenamemy_custom_data.json, # 覆盖服务器建议的文件名 download_timeout60, action_timeout10 ) if saved_file_path: print(f文件最终保存于: {saved_file_path}) else: print(下载失败。) await browser.close()这个进阶版本的提升点参数化触发动作将trigger_action抽象为一个可调用对象使得函数更加通用。你可以传入任何异步函数比如先填写表单再点击或者等待某个条件后再触发。双重超时控制action_timeout控制从触发操作到下载事件发生的时间download_timeout控制整个文件数据传输和保存的时间。这对于下载大文件非常有用。失败检查调用download.failure()来检查下载过程是否在网络层面失败如连接中断。这是Playwright提供的原生状态检查。文件验证下载完成后检查文件是否存在以及文件大小防止下载到空的或损坏的文件例如服务器返回了一个错误HTML页面但状态码是200。清理监听器在finally块中使用page.remove_listener移除事件监听。这是一个好习惯能避免在长时间运行的脚本中监听器累积导致的内存泄漏或意外行为。自定义文件名逻辑提供了灵活的命名策略优先使用自定义名其次用服务器建议名最后用时间戳兜底。5. 实战集成pytest构建可复用的下载测试套件单元测试是保证自动化脚本长期稳定运行的关键。我们将使用pytest框架把下载功能封装成易于测试的组件。这里会展示如何组织测试用例、使用fixture管理浏览器生命周期以及如何处理异步测试。5.1 项目结构规划一个清晰的项目结构有助于维护。建议如下playwright_download_project/ ├── conftest.py # pytest全局配置和fixture定义 ├── downloader/ # 核心功能模块 │ ├── __init__.py │ └── core.py # 包含 robust_download 等核心函数 ├── tests/ # 测试目录 │ ├── __init__.py │ ├── conftest.py # 测试专用的fixture可选 │ ├── test_basic_download.py │ └── test_advanced_scenarios.py ├── requirements.txt # 项目依赖 └── downloads/ # 默认下载目录.gitignore忽略5.2 创建核心功能模块与全局Fixture首先将我们之前写好的健壮下载函数放到downloader/core.py中。然后创建顶层的conftest.py用于定义pytest fixture管理Playwright浏览器实例。这是pytest的魔法文件其中的fixture可以被所有测试文件使用。# conftest.py import pytest import asyncio from pathlib import Path from playwright.async_api import async_playwright, Browser, BrowserContext, Page # 定义一个事件循环fixture用于运行异步测试 pytest.fixture(scopesession) def event_loop(): 为整个测试会话创建一个事件循环。 loop asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() # 浏览器实例fixture会话级别所有测试共用节省启动时间 pytest.fixture(scopesession) async def browser(event_loop): async with async_playwright() as p: # 启动浏览器可根据环境变量决定是否无头 headless True # 测试环境通常为无头 browser await p.chromium.launch(headlessheadless, slow_mo100) # slow_mo让操作变慢便于观察 yield browser # 测试结束后关闭浏览器 await browser.close() # 浏览器上下文fixture函数级别每个测试独立避免状态污染 pytest.fixture async def browser_context(browser): context await browser.new_context(accept_downloadsTrue) yield context await context.close() # 页面fixture函数级别每个测试获得一个干净的页面 pytest.fixture async def page(browser_context): page await browser_context.new_page() yield page await page.close() # 临时下载目录fixture函数级别每个测试有自己的干净目录 pytest.fixture def temp_download_dir(tmp_path): download_dir tmp_path / test_downloads download_dir.mkdir() return download_dirFixture设计解析event_loop:pytest-asyncio插件需要这个fixture来运行异步测试函数。我们创建了一个会话级别的事件循环。browser:会话级别(scopesession”)。启动和关闭浏览器开销很大让所有测试用例共享同一个浏览器实例可以极大提升测试速度。browser_context:函数级别(scope”function”, 默认)。每个测试用例获得一个独立的上下文这相当于一个独立的“隐身会话”拥有独立的cookie、本地存储和下载设置。这确保了测试之间的隔离性一个测试的下载不会影响另一个。page:函数级别。每个测试用例获得一个来自其独立上下文的新页面保证页面状态干净。temp_download_dir:函数级别。使用pytest内置的tmp_pathfixture为每个测试创建一个唯一的临时目录来存放下载文件。测试结束后该目录会被自动清理确保没有残留文件影响下一次测试。5.3 编写具体的测试用例现在我们来编写测试文件。我们测试两种常见场景1) 直接下载一个已知的文件2) 在一个需要交互的页面上触发下载。# tests/test_basic_download.py import pytest from pathlib import Path from downloader.core import robust_download pytest.mark.asyncio async def test_download_from_static_link(page, temp_download_dir): 测试场景页面有一个直接指向文件的链接点击即下载。 使用 httpbin.org 的 /stream-bytes 端点模拟文件下载。 # 导航到一个能触发下载的测试页面 # 这里我们使用 httpbin它返回一个包含特定字节流的“文件” await page.goto(https://httpbin.org/stream-bytes/1024?seedtest) # 定义触发动作在这个简单例子中导航本身就会触发下载因为httpbin返回的是流 # 但在真实场景中可能是点击一个链接。我们这里模拟点击一个不存在的元素来“触发”下载事件。 # 实际上对于httpbin /stream-bytes访问URL就会开始下载。 # 我们需要先设置监听器再执行导航。但 robust_download 要求先有page对象。 # 因此更合理的测试是使用一个真正需要点击的页面。我们换一个端点。 await page.goto(about:blank) # 先清空页面 # 使用一个能通过点击触发下载的测试URL例如一个设置了下行头 attachment 的端点 # 我们通过 evaluate 在页面注入一个下载链接并点击它 download_url https://httpbin.org/response-headers?Content-Dispositionattachment%3B%20filename%3D%22testfile.bin%22Content-Typeapplication%2Foctet-stream await page.evaluate(f() {{ const link document.createElement(a); link.href {download_url}; link.download testfile.bin; link.textContent Download Test File; document.body.appendChild(link); link.click(); }}) async def trigger_action(): # 上面的 evaluate 已经执行了点击所以这里不需要再做任何事。 # 这个函数的存在是为了满足 robust_download 的接口。 pass saved_path await robust_download( pagepage, trigger_actiontrigger_action, save_dirtemp_download_dir, custom_filenamehttpbin_test_file.bin ) # 断言文件应该被成功下载并保存 assert saved_path is not None assert saved_path.exists() # 检查文件大小httpbin返回的流大小是随机的但我们知道它不为空 assert saved_path.stat().st_size 0 # 检查文件名是否符合我们的自定义 assert saved_path.name httpbin_test_file.bin pytest.mark.asyncio async def test_download_with_form_submission(page, temp_download_dir): 测试场景需要先填写表单提交后生成并下载文件。 这里用一个模拟的本地HTML文件来演示。 # 创建一个临时的HTML表单页面提交后会触发文件下载 # 由于安全限制真实的文件下载需要服务器配合。这里我们简化用一段JavaScript模拟下载行为。 html_content !DOCTYPE html html body form iddownloadForm input typetext namedata valueSample Data button typesubmitGenerate Report/button /form script document.getElementById(downloadForm).addEventListener(submit, function(e) { e.preventDefault(); // 模拟创建一个Blob并触发下载 const blob new Blob([This is the report content for: this.data.value], {type: text/plain}); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download report_ Date.now() .txt; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); /script /body /html # 将HTML内容设置到当前页面 await page.set_content(html_content) async def trigger_action(): # 触发动作点击提交按钮 await page.click(button[typesubmit]) saved_path await robust_download( pagepage, trigger_actiontrigger_action, save_dirtemp_download_dir, download_timeout5.0 ) assert saved_path is not None assert saved_path.exists() # 检查文件内容是否包含我们提交的数据 file_content saved_path.read_text() assert Sample Data in file_content pytest.mark.asyncio async def test_download_timeout_handling(page, temp_download_dir): 测试异常场景下载超时或未触发。 await page.goto(about:blank) async def trigger_action_that_never_downloads(): # 一个永远不会触发下载的动作比如点击一个普通链接 await page.evaluate(() { const link document.createElement(a); link.href https://example.com; link.textContent Go to Example; document.body.appendChild(link); link.click(); }) saved_path await robust_download( pagepage, trigger_actiontrigger_action_that_never_downloads, save_dirtemp_download_dir, action_timeout2.0, # 设置很短的超时期望它超时 download_timeout2.0 ) # 断言由于没有下载触发 robust_download 应该返回 None assert saved_path is None # 断言下载目录是空的或者只有我们可能创建的占位文件但不应有目标文件 # 注意由于触发动作可能导航到其他页面可能会产生其他下载这个断言在复杂场景下可能不稳定。 # 更稳定的做法是检查特定文件名不存在。 # assert len(list(temp_download_dir.iterdir())) 0测试要点与技巧pytest.mark.asyncio这个装饰器是必须的它告诉pytest这个测试函数是异步的需要用asyncio事件循环来运行。使用依赖注入测试函数通过参数声明它需要的fixture如page,temp_download_dirpytest会自动注入。这使得测试代码非常干净且易于复用fixture提供的资源。测试用例隔离得益于browser_context和pagefixture的函数级别作用域每个测试都在一个全新的浏览器上下文和页面中运行互不干扰。测试正面与负面场景我们不仅测试了正常的下载流程(test_download_from_static_link,test_download_with_form_submission)还测试了异常情况(test_download_timeout_handling)确保我们的robust_download函数在出错时行为符合预期。模拟与真实在测试中我们混合使用了真实的网络服务httpbin.org和客户端模拟page.set_content。对于核心逻辑的单元测试应尽量使用可控的模拟对于集成测试则使用真实的测试环境。5.4 运行测试与查看报告在项目根目录下运行以下命令# 运行所有测试 pytest -v # 运行特定测试文件 pytest tests/test_basic_download.py -v # 运行并生成HTML报告需要安装 pytest-html pytest --htmlreport.html --self-contained-html-v参数会输出更详细的信息方便查看每个测试用例的执行情况。如果测试失败pytest会给出清晰的错误回溯帮助你快速定位问题。6. 常见问题排查与实战心得在实际使用Playwright进行自动化下载的过程中你肯定会遇到一些坑。下面是我总结的一些典型问题及其解决方案以及一些从实战中得来的经验技巧。6.1 下载监听器不触发问题现象点击了下载按钮但page.on(‘download’)事件监听器里的回调函数从未被调用。可能原因与排查步骤accept_downloads未设置或为False这是最常见的原因。确保在创建浏览器上下文(browser.new_context)时传入了accept_downloadsTrue。注意这个设置是上下文级别的不是页面级别的。下载被浏览器拦截或由新窗口/tab处理有些下载链接设置了target”_blank”或者网站逻辑是在新窗口中开始下载。Playwright的page对象只监听当前页面的下载事件。你需要监听整个上下文的下载使用context.on(‘download’, …)而不是page.on(‘download’, …)。这样该上下文下所有页面的下载都会被捕获。async with async_playwright() as p: browser await p.chromium.launch() context await browser.new_context(accept_downloadsTrue) download_future asyncio.Future() def handle_download(download): if not download_future.done(): download_future.set_result(download) context.on(download, handle_download) # 监听上下文 page await context.new_page() # ... 你的操作等待新页面并监听它如果下载确实在新标签页触发你需要使用page.wait_for_event(‘popup’)来等待新页面然后在新页面上设置监听器。点击操作未正确触发下载可能元素不是真正的下载链接例如是通过JavaScript异步请求文件。使用Playwright的代码生成器(playwright codegen)重新录制一遍操作确认点击的选择器和步骤是否正确。有时可能需要等待某个元素出现或网络请求完成后再点击。浏览器或网站使用了不同的下载机制极少数情况下网站可能通过iframe、Worker或其他非常规方式触发下载。这时需要更仔细地分析网络请求。打开浏览器的开发者工具在Playwright中可以通过await page.pause()暂停脚本然后手动操作在Network标签页查看点击下载按钮时产生了什么类型的请求通常是带有Content-Disposition: attachment头的请求。6.2 下载的文件损坏或为空问题现象文件成功保存但打开时是空的或者内容不是预期的二进制文件可能是HTML错误页面。解决方案检查download.failure()在调用save_as之后立即检查download.failure()的返回值。如果不为None则说明下载过程本身失败了如网络错误、服务器返回4xx/5xx状态码。验证文件头和大小即使failure()为None服务器也可能返回一个状态码为200的错误页面。在保存后可以读取文件的前几个字节或检查文件大小。saved_path await download.save_as(‘file.zip’) if saved_path.stat().st_size 1024: # 假设文件至少1KB print(“警告下载的文件可能过小或为空。”) # 可以读取前500字节检查是否是文本/HTML with open(saved_path, ‘rb’) as f: header f.read(500) if b’html’ in header.lower() or b’error’ in header.lower(): print(“文件内容疑似错误页面。”)等待下载真正完成download.save_as()方法内部会等待下载完成。但如果你在调用它之前就关闭了浏览器上下文或页面下载可能会中断。确保你的脚本逻辑在save_as完成前保持相关的page和context处于打开状态。6.3 异步编程中的竞态条件问题现象偶尔会错过下载事件或者Future对象已经被设置过了。根本原因在异步环境中事件触发和代码执行顺序是不确定的。如果下载在注册监听器之前就触发了或者同一个页面短时间内触发了多次下载我们的简单Future模式可能会出错。健壮化方案使用队列asyncio.Queue对于可能连续触发多个下载的场景使用asyncio.Queue是更安全的选择。import asyncio from asyncio import Queue from playwright.async_api import Page, Download async def handle_multiple_downloads(page: Page, save_dir: Path): 处理一个页面内可能发生的多次下载。 download_queue Queue() def on_download(download: Download): # 将每个下载对象放入队列 download_queue.put_nowait(download) page.on(‘download’, on_download) # ... 执行会触发下载的操作 ... # 在一个循环中处理队列中的所有下载 saved_paths [] try: while True: # 设置一个超时避免无限等待 download await asyncio.wait_for(download_queue.get(), timeout10.0) filename download.suggested_filename or f”download_{len(saved_paths)}.bin” save_path save_dir / filename await download.save_as(save_path) saved_paths.append(save_path) print(f”已处理下载: {filename}”) download_queue.task_done() # 通知队列任务完成 except asyncio.TimeoutError: print(“等待新下载超时可能所有下载已完成。”) finally: page.remove_listener(‘download’, on_download) return saved_paths6.4 实战心得与技巧优先使用browser.new_context管理状态每个测试或任务都应该在独立的context中运行。这不仅能隔离下载还能隔离cookies、localStorage等使脚本行为更可预测也更容易实现并行执行。利用slow_mo参数调试在浏览器启动时设置slow_mo500单位毫秒会让Playwright的每个操作都放慢。这在调试复杂的交互流程时非常有用你可以清楚地看到页面是如何一步步变化的。结合page.wait_for_event进行更精细的控制除了监听download事件你还可以等待其他事件比如request或response来精确判断下载请求何时发出、何时完成。async with page.expect_download() as download_info: await page.click(‘#download-button’) download await download_info.value # 这种方式更简洁适用于你知道点击后必然触发一次下载的场景处理需要认证的下载如果下载链接需要登录务必在同一个context内完成登录操作以保持会话状态。Playwright可以很方便地保存和加载登录状态context.storage_state()。无头模式下的资源节省对于生产环境的定时任务务必使用headlessTrue。你还可以通过args参数传递更多Chromium启动参数来优化资源占用例如--disable-gpu,--no-sandbox注意安全考量--single-process等。

相关新闻

从光学到产品:护眼钢化膜的技术原理与实现路径深度解析(以悟赫德 scinique 技术为例)

从光学到产品:护眼钢化膜的技术原理与实现路径深度解析(以悟赫德 scinique 技术为例)

1. 引言:为什么我们需要 "护眼" 的手机膜?随着 OLED 屏幕在智能手机中的全面普及,以及用户日均用屏时长的不断增加(据统计,2026 年国内用户日均手机使用时长已超过 6.5 小时),视疲劳正…

2026/7/5 0:39:55 阅读更多 →
ASM330LHH与PIC18F25K80的工业级运动跟踪系统设计

ASM330LHH与PIC18F25K80的工业级运动跟踪系统设计

1. 从传感器到系统:ASM330LHH与PIC18F25K80的硬件搭档当我在工业自动化项目中第一次接触到ASM330LHH这颗6DoF惯性测量单元(IMU)时,立刻被它的性能参数所震撼。作为意法半导体MEMS传感器家族的重要成员,它在一个3x2.5x0.83mm的封装内集成了三轴…

2026/7/5 0:35:54 阅读更多 →
Python3与Java Hutool实现SM2国密算法跨语言加解密互通方案

Python3与Java Hutool实现SM2国密算法跨语言加解密互通方案

1. 项目概述与核心价值最近在做一个需要跨语言数据交换的项目,后端是Java,用到了Hutool这个“瑞士军刀”库来处理SM2国密算法的加解密,而另一个数据处理服务是用Python3写的。这就引出了一个很实际的问题:Java这边用Hutool加密的数…

2026/7/5 0:33:53 阅读更多 →

最新新闻

Python异步代理池实战:从requests阻塞到httpx.AsyncClient,爬虫效率翻倍的踩坑记录

Python异步代理池实战:从requests阻塞到httpx.AsyncClient,爬虫效率翻倍的踩坑记录

一、起因:代理验证拖垮了整个采集系统先交代一下背景。我在一家电商公司做数据采集,核心系统是竞品价格监控——每天爬天猫、京东、拼多多的商品价格,日采集量在几十万到百万级。刚开始做的时候,代理管理这块是比较粗糙的——抓了…

2026/7/5 1:36:20 阅读更多 →
因为刷短视频导致流量费用每个月暴涨5块钱

因为刷短视频导致流量费用每个月暴涨5块钱

上个月有一天流量使用了10G,这几乎不太可能,但是也不是完全不可能。如果120K/s 9个小时不停下载--------------目前就是这个状态。然后就会有4G/天 流量花费一个月下来就是120G,本身流量只有20G,虽然剩下流量不限量,但…

2026/7/5 1:34:19 阅读更多 →
【无人机】基于玻尔兹曼引导的 Q 学习用于在受洪水影响的无线网络中优化 3D 无人机部署附matlab代码

【无人机】基于玻尔兹曼引导的 Q 学习用于在受洪水影响的无线网络中优化 3D 无人机部署附matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、算法改进、程序设计科研仿真。🍎完整代码获取 定制创新 论文复现私信🍊个人信条:做科研,博学之、审问之、慎思之、明辨之、…

2026/7/5 1:34:19 阅读更多 →
【无人机动态避障】基于金豺优化算法GJO融合动态窗口法DWA的无人机三维动态避障方法研究MATLAB代码

【无人机动态避障】基于金豺优化算法GJO融合动态窗口法DWA的无人机三维动态避障方法研究MATLAB代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、算法改进、程序设计科研仿真。 🍎完整代码获取 定制创新 论文复现私信 🍊个人信条:做科研,博学之、审问之、慎思之、明辨…

2026/7/5 1:30:17 阅读更多 →
Anthropic Fable 5 Cyber Jailbreak Severity:AI越狱统一评级体系深度解析

Anthropic Fable 5 Cyber Jailbreak Severity:AI越狱统一评级体系深度解析

引言:AI安全的"CVSS时刻" 2026年7月3日,Anthropic正式发布了**Cyber Jailbreak Severity(CJS)**评级体系——这是全球首个针对AI模型"越狱"行为严重程度的标准化评估框架。同一天,Fable 5在经历18天出口管制后重新上线,搭载了一套全新的多层级安全防…

2026/7/5 1:30:17 阅读更多 →
AI 压测数据回放:让模型读报告之前先校准口径

AI 压测数据回放:让模型读报告之前先校准口径

AI 压测数据回放:让模型读报告之前先校准口径 一、压测报告不能直接丢给模型 AI 可以帮助分析压测结果,但前提是输入数据口径清楚。很多压测报告里混着预热阶段、限流阶段、错误重试、下游故障和业务噪声。如果直接让模型总结,很容易得到一段…

2026/7/5 1:22:14 阅读更多 →

日新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

月新闻