Playwright自动化进阶:网络拦截、模拟登录与文件上传实战 1. 项目概述从基础到进阶的自动化实战如果你已经用Playwright写过一些简单的页面点击、表单填充脚本可能会觉得它“不过如此”。但当你真正尝试去自动化一个复杂的业务流程比如需要处理动态令牌的登录、拦截并修改特定网络请求、或者上传一个需要特定处理流程的文件时挑战才真正开始。我最近在为一个内部系统构建端到端测试和批量操作工具时就深度用到了网络拦截、模拟登录和文件上传这三个高级特性。它们不再是锦上添花的功能而是解决实际阻塞性问题的核心工具。网络拦截让你从被动的“页面观察者”变为主动的“流量操控者”可以模拟后端响应、屏蔽无用资源、甚至篡改请求数据这对于测试异常场景和提升脚本稳定性至关重要。模拟登录则远不止是填充用户名密码它涉及到会话保持、令牌管理、跨域认证等一整套状态维持机制。而文件上传看似一个简单的input[type“file”]操作在实际的Web应用中却可能遇到隐藏的input、复杂的JS上传组件、甚至是非标准的上传协议需要更精细的操作。这篇文章我就结合这几个月的实战踩坑经验把这三大高级技巧掰开揉碎了讲清楚。我会假设你已经了解Playwright的基本用法如启动浏览器、定位元素等然后直接切入这些进阶场景的核心实现逻辑、避坑指南和性能优化技巧。目标是让你看完后能立刻将这些方案应用到自己的项目中解决那些令你头疼的自动化难题。2. 网络拦截从监听者到操控者的进阶很多人在使用Playwright时只把它当作一个更快的、能处理动态内容的“升级版Selenium”。但实际上它的网络API才是其真正的威力所在。page.route()这个方法允许你在请求发出前或响应返回前进行拦截和修改这为自动化测试和爬虫开发打开了新世界的大门。2.1 核心原理与应用场景解析page.route()的工作原理类似于一个在浏览器和网络之间设置的代理层。当页面发起一个符合你设定规则的请求通过URL模式匹配时这个请求不会直接发往服务器而是先被Playwright截获。此时你可以选择继续请求让请求正常发出但可以在途中修改请求头、请求体。模拟响应直接返回一个你构造的响应完全绕过服务器。中止请求直接丢弃该请求常用于屏蔽广告、追踪脚本或不需要的资源。为什么这个功能如此重要我举几个亲身经历的场景测试后端异常你想测试前端在“服务器返回500错误”或“特定API返回空数据”时的表现。难道要去折腾后端服务吗不需要。直接拦截该API的请求返回一个自定义的错误响应即可。提升测试速度与稳定性页面加载慢往往是因为第三方字体、分析脚本、大图等资源。在测试环境中这些资源非必需。拦截并中止对这些资源的请求可以极大加快页面加载速度同时减少因外部资源不可用导致的测试失败。模拟复杂数据一个列表页的渲染依赖于一个返回复杂JSON的API。在自动化脚本中你想固定测试数据避免因数据库变化导致断言失败。拦截该API返回一个预设的、结构稳定的JSON文件内容。请求篡改测试提交表单时验证前端是否对某些敏感字段如密码做了加密。你可以拦截提交请求检查其请求体是否明文包含密码。2.2 实战拦截、修改与模拟响应理论说再多不如一行代码。我们直接看最常用的几种操作模式。基础拦截与请求修改假设我们需要测试一个搜索功能但想验证前端是否在请求头中正确携带了认证令牌。import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) context await browser.new_context() page await context.new_page() # 关键使用 route 方法拦截所有请求 await page.route(**/api/search**, lambda route: handle_route(route)) await page.goto(https://your-test-site.com) # ... 执行搜索操作 await browser.close() async def handle_route(route): # 获取被拦截的请求对象 request route.request print(f拦截到请求: {request.url}) print(f请求方法: {request.method}) print(f请求头: {request.headers}) # 示例修改请求头添加一个自定义头 headers request.headers headers[x-custom-test-header] playwright-intercept # 继续请求但使用修改后的请求头 await route.continue_(headersheaders) asyncio.run(main())注意**/api/search**是一个通配符模式**匹配任何路径包括多级*匹配单段路径。这非常灵活。route.continue_()是让请求继续发送到服务器的关键不调用它请求就会挂起。模拟API响应Fulfill这是更强大的功能直接“伪造”服务器响应。比如我们要测试一个商品列表页在空状态下的UI展示。async def handle_route_mock(route): # 构造一个模拟的JSON响应 mock_response { code: 200, message: success, data: { items: [], # 模拟空列表 total: 0 } } # 使用 route.fulfill 直接返回响应不发送真实请求 await route.fulfill( status200, headers{Content-Type: application/json}, bodyjson.dumps(mock_response) ) # 在page.route时使用这个处理函数 await page.route(**/api/products/list**, handle_route_mock)这样做的好处是极致的速度和稳定性。你的测试不再依赖后端API的可用性和数据状态。中止不必要的请求对于提升性能立竿见影。通常我们会拦截图片、样式表、字体或特定的分析脚本。# 中止所有图片请求加速页面加载 def should_abort_request(request): abort_resource_types [image, stylesheet, font, media] if request.resource_type in abort_resource_types: return True # 也可以根据URL匹配来中止比如广告域名 if google-analytics.com in request.url: return True return False # 在page上设置请求监听符合条件的直接中止 page.on(request, lambda request: ( request.abort() if should_abort_request(request) else None )) # 或者使用route方式更精确 await page.route(**/*.{png,jpg,jpeg,gif,svg,ico,css,woff,woff2}, lambda route: route.abort())实操心得不要盲目拦截所有资源。有些CSS或JS可能是页面功能所必需的拦截后会导致页面功能异常。最好根据实际测试需求逐步添加拦截规则。可以先从最耗时的图片和视频开始。2.3 高级模式动态响应与请求链处理有时我们需要更复杂的逻辑比如先发起一个请求获取令牌再用这个令牌去修改后续的请求。这就需要结合多个route和异步处理。场景一个页面加载后会先请求一个/api/config接口获取CSRF令牌后续的所有POST请求都需要在头里带上这个令牌。csrf_token None async def main(): # ... 初始化 browser, page # 首先拦截配置接口获取token并存储 async def intercept_config(route): response await route.fetch() # 关键先发起真实请求获取响应 body await response.text() global csrf_token # 假设响应体是JSON且token在 data.token 字段 try: data json.loads(body) csrf_token data.get(data, {}).get(token) print(f获取到CSRF Token: {csrf_token}) except: pass # 将真实的响应返回给页面 await route.fulfill(responseresponse) await page.route(**/api/config**, intercept_config) # 然后拦截所有POST请求动态添加token到头信息 async def modify_post_requests(route): request route.request if request.method POST and csrf_token: headers request.headers headers[x-csrf-token] csrf_token await route.continue_(headersheaders) else: await route.continue_() await page.route(**/api/**, modify_post_requests) # ... 后续操作这里用到了route.fetch()它允许你获取该请求的“真实”响应然后你可以处理这个响应再通过route.fulfill(responseresponse)将其原样或修改后返回给页面。这是一种“先请求后处理”的模式非常适合需要依赖真实响应数据的场景。常见问题与排查拦截不生效首先检查URL匹配模式是否正确。使用page.on(‘request’, lambda req: print(req.url))监听所有请求确认目标请求的URL是否按预期发出。模式中的*和**要使用正确。route.continue_()被遗忘如果你在处理函数中既没有调用continue_()也没有调用fulfill()或abort()请求将会一直挂起导致页面加载超时。这是最常见的错误。修改请求体route.continue_()的post_data参数可以修改POST数据但要注意如果你修改了数据可能需要同步更新Content-Length请求头否则可能导致服务器解析错误。对于JSON数据通常直接传递新的字符串即可Playwright会处理长度。性能影响拦截逻辑本身有开销。如果为每一个请求都设置复杂的拦截处理函数可能会降低脚本执行速度。尽量让拦截条件精确避免使用**匹配所有请求。3. 模拟登录超越用户名和密码自动化中最常见的需求就是登录。但现代Web应用的登录机制五花八门除了传统的表单提交还有OAuth2、SSO、图形验证码、动态令牌如TOTP、人机验证等。Playwright提供了多种策略来应对。3.1 策略选择状态复用 vs. 流程模拟面对登录我们通常有两条路状态复用推荐手动登录一次然后将浏览器上下文Context的存储状态cookies, localStorage等保存下来。后续脚本直接加载这个状态就相当于已经登录了。这速度快稳定且避免了处理复杂登录逻辑如验证码。适用于测试需要频繁登录的场景。流程模拟用脚本完全模拟用户输入用户名、密码、点击登录按钮的完整过程。这更贴近真实用户行为适合测试登录功能本身或者当登录状态无法持久化保存时使用。对于大多数自动化任务如日常爬虫、定时测试状态复用是首选。它的核心是browser_context.storage_state()方法。3.2 状态复用实战保存与加载会话让我们一步步实现一个可靠的登录状态管理方案。第一步获取并保存登录状态import asyncio import json from playwright.async_api import async_playwright async def save_login_state(): async with async_playwright() as p: # 使用 persistent_context 可以更好地保存cookies等数据 user_data_dir ./playwright_user_data # 指定一个用户数据目录 browser await p.chromium.launch_persistent_context( user_data_dir, headlessFalse # 首次登录建议非无头方便处理可能的验证码 ) page await browser.new_page() await page.goto(https://your-app.com/login) # 模拟登录过程如果自动化可行 await page.fill(#username, your_username) await page.fill(#password, your_password) # 如果有验证码这里可能需要手动干预或使用第三方服务 await page.click(button[typesubmit]) # 等待登录成功跳转到首页或出现登录成功元素 await page.wait_for_url(**/dashboard**) # 或者 await page.wait_for_selector(#user-avatar, statevisible) # 关键步骤保存当前上下文的状态到文件 state await browser.storage_state(path./auth_state.json) print(登录状态已保存至 auth_state.json) await browser.close() # 运行一次这个函数获取状态文件 # asyncio.run(save_login_state())执行上述脚本在浏览器中完成一次手动登录如果需要的话状态就会被保存到auth_state.json文件中。这个文件包含了该站点所有的cookies、localStorage、sessionStorage。第二步在自动化脚本中加载状态async def run_with_saved_state(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) # 后续可以无头运行 # 加载之前保存的状态创建一个已登录的上下文 context await browser.new_context(storage_state./auth_state.json) page await context.new_page() # 直接打开需要登录的页面 await page.goto(https://your-app.com/dashboard) # 此时页面应该已经是登录状态无需再次登录 # 验证登录成功例如检查用户菜单是否存在 avatar page.locator(#user-avatar) if await avatar.is_visible(): print(成功通过加载状态登录) # ... 执行你的后续自动化操作 await browser.close() asyncio.run(run_with_saved_state())注意事项状态过期Cookies有有效期Session或持久化。如果状态文件过期你需要重新运行save_login_state来更新它。可以写一个判断逻辑定期更新状态文件。用户数据目录launch_persistent_context使用的目录包含了更完整的浏览器数据如缓存、历史记录有时比单纯的storage_state更稳定但文件更大。你可以根据情况选择。多账号如果需要管理多个账号可以为每个账号创建不同的context并保存不同的storage_state文件实现账号隔离。3.3 处理复杂登录流程OAuth与验证码当状态复用不可行如每次登录需要动态短信验证码或者你需要测试登录流程时就必须模拟完整登录。处理OAuth登录如GitHub, GoogleOAuth流程通常涉及跳转到第三方授权页面。自动化处理的关键是监听弹出窗口popup。async def login_with_oauth(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) context await browser.new_context() page await context.new_page() await page.goto(https://your-app.com/login) # 点击“通过GitHub登录”按钮 async with page.expect_popup() as popup_info: await page.click(button:has-text(Login with GitHub)) popup await popup_info.value # 现在 popup 是第三方授权页面 await popup.wait_for_load_state() # 在授权页面上填写信息注意这可能需要你有测试账号 await popup.fill(#login_field, github_username) await popup.fill(#password, github_password) await popup.click(input[namecommit]) # 授权页面可能会询问权限点击授权按钮 # 注意选择器的变化可能需要等待 auth_button popup.locator(button:has-text(Authorize)) if await auth_button.is_visible(): await auth_button.click() # 授权成功后popup会自动关闭控制权回到原页面 # 等待原页面跳转到登录成功后的页面 await page.wait_for_url(**/dashboard**) print(OAuth登录成功) # ... 后续操作应对验证码这是自动化登录的终极难题。完全通用的解决方案不存在但有几种策略绕过在测试环境中让开发提供一个“万能验证码”或直接禁用验证码。手动干预在脚本中设置一个暂停点当遇到验证码时弹出提示让用户手动输入。这适用于执行频率不高的任务。# 假设验证码图片在一个img元素里输入框是 #captcha-input captcha_img page.locator(#captcha-image) if await captcha_img.is_visible(): print(请查看浏览器中的验证码并输入) # 这里可以甚至将图片保存到本地供查看 await captcha_img.screenshot(pathcaptcha.png) manual_code input(请输入验证码: ) await page.fill(#captcha-input, manual_code) await page.click(#submit-login)第三方服务使用付费的验证码识别API如2Captcha, DeathByCaptcha。这些服务提供API你上传验证码图片它们返回识别结果。这需要额外集成和成本。机器学习对于固定样式的简单验证码可以尝试训练一个OCR模型。这成本高仅适用于特定场景。实操心得对于需要长期稳定运行的自动化任务尽一切可能获取并复用登录状态这是最稳定、最快速的方式。将模拟完整登录作为备选方案。4. 文件上传破解各种前端组件的桎梏文件上传是Web自动化中的另一个经典难题。问题不在于input[type“file”]本身而在于前端开发者们为了美化或增加功能用各种方式把它隐藏或包装了起来。4.1 标准文件上传与隐藏Input处理标准情况页面上有一个可见的input[type“file”]元素。这是最简单的。# 定位到文件输入框 file_input page.locator(input[typefile]) # 使用 set_input_files 方法传入文件路径 await file_input.set_input_files(/path/to/your/file.pdf) # 如果是多个文件传入列表 await file_input.set_input_files([/path/to/file1.jpg, /path/to/file2.jpg])set_input_files方法非常高效它直接设置input的值不会触发文件选择器的打开因此速度极快。隐藏的Input很多UI库如Ant Design, Element UI会将原生的input隐藏display: none或visibility: hidden然后用一个漂亮的按钮来触发点击。如果你直接用set_input_files定位到那个隐藏的input通常也能成功因为Playwright可以直接与隐藏元素交互除非被前端框架特殊处理。# 即使这个input是隐藏的以下代码通常也有效 hidden_input page.locator(input[typefile][style*display: none]) await hidden_input.set_input_files(myfile.txt)如果上述方法无效可能是因为元素被放置在Shadow DOM中或者有更复杂的事件监听。这时需要尝试触发那个“代理”按钮的点击事件但最终文件数据还是要设置到隐藏的input上。4.2 复杂组件与事件触发上传有些上传组件不是简单的input而是通过JavaScript监听拖放事件或粘贴事件。Playwright同样可以模拟。模拟拖放上传# 首先定位到接收拖放的区域通常是一个div drop_zone page.locator(.upload-drop-zone) # 准备要上传的文件信息 file_to_upload { name: report.pdf, mimeType: application/pdf, buffer: open(/path/to/report.pdf, rb).read() # 读取文件为二进制 } # 触发拖放事件 await drop_zone.dispatch_event(dragenter) # 拖入 await drop_zone.dispatch_event(dragover) # 悬停 # 最关键的一步触发 drop 事件并传递文件数据 await drop_zone.dispatch_event(drop, { dataTransfer: { files: [file_to_upload] } })注意模拟拖放的成功率高度依赖于前端组件的具体实现。有些组件可能依赖于DataTransfer对象的其他属性。如果上述方法不工作你可能需要查看页面源码看组件监听了哪些事件和数据格式。模拟“点击上传”但触发复杂事件对于某些封装过的上传组件直接设置隐藏input可能不触发组件的内部状态更新如文件列表预览。这时可能需要模拟完整的用户操作链点击“上传按钮”触发文件选择器。但是Playwright无法与操作系统原生文件选择器交互。所以我们需要在点击按钮之前先监听即将打开的filechooser事件并直接设置文件。# 监听文件选择器事件 async with page.expect_file_chooser() as fc_info: await page.click(button:has-text(选择文件)) # 点击触发选择器的按钮 file_chooser await fc_info.value # 通过文件选择器对象设置文件 await file_chooser.set_files(/path/to/file.txt)这种方法更贴近真实用户操作能触发组件内部更完整的事件流兼容性更好。4.3 监听上传进度与处理后端响应上传文件后我们通常需要知道是否成功。这需要监听网络请求。场景上传一个文件并等待后端返回包含文件URL的响应。import asyncio async def upload_and_wait(): # ... 初始化 page # 在点击上传前先监听特定的上传API请求 async with page.expect_response(**/api/upload**) as response_info: # 执行上传操作无论用set_input_files还是file_chooser await page.locator(input[typefile]).set_input_files(test.zip) # 可能还需要点击一个“开始上传”按钮 await page.click(#upload-submit-button) # 等待响应返回 response await response_info.value print(f上传请求状态: {response.status}) if response.ok: response_body await response.json() file_url response_body.get(data, {}).get(url) print(f文件上传成功访问地址: {file_url}) else: print(f上传失败: {await response.text()})page.expect_response()是一个非常有用的方法它等待一个匹配特定URL模式的响应完成。这对于需要根据上传结果进行后续操作的场景至关重要。处理大文件上传与进度模拟对于大文件有些前端会显示上传进度条。这个进度条的数据通常来自于对XMLHttpRequest或Fetch API的upload事件的监听。Playwright无法直接模拟这个进度因为它依赖于浏览器内部的网络层。但是你可以通过拦截请求人为地减慢请求速度来测试进度条UI是否正常工作但这通常不是自动化测试的重点。常见问题排查set_input_files无效首先检查定位器是否正确找到了input元素。使用page.locator(‘input[type“file”]’).count()看看找到了几个。如果多个可能需要更精确的选择器。其次确认文件路径是绝对路径且脚本有读取权限。上传后页面无反应很可能是因为你没有触发上传的“提交”动作。set_input_files只是选择了文件就像用户在文件选择器里选了文件一样。通常还需要点击一个“上传”或“提交”按钮来触发真正的HTTP请求。记得用expect_response来等待这个请求完成。跨域问题如果你的脚本在一个域名下运行而上传目标是另一个域名可能会遇到CORS限制。这在Playwright控制的浏览器中通常不是问题因为你可以通过browser.new_context()设置忽略HTTPS错误或自定义权限但需要留意。5. 综合实战构建一个带登录和文件上传的自动化工作流现在我们把网络拦截、模拟登录和文件上传组合起来完成一个真实的自动化任务自动登录一个内部CMS拦截并修改文章列表API的返回数据以创建稳定的测试环境然后上传一篇新的文章草稿包含图片。5.1 场景搭建与架构设计假设我们有一个内部内容管理系统CMS具有以下特点登录采用表单提交成功后会在localStorage中存入一个auth_token。文章列表页/dashboard/articles会调用GET /api/articles接口。创建文章页面有一个富文本编辑器支持从本地上传图片图片上传接口是POST /api/upload/image。我们的目标是每天凌晨自动上传一篇包含固定内容的文章草稿用于测试发布流程。我们的脚本设计思路如下登录采用状态复用策略。首次运行手动登录保存状态后续运行直接加载状态。网络拦截在打开文章列表页时拦截GET /api/articles返回一个固定的、简化的文章列表数据确保测试环境的一致性避免受其他用户操作干扰。文件上传在富文本编辑器中通过定位隐藏的input[type“file”]或监听filechooser事件来上传文章封面图。流程整合将以上步骤串联成一个完整的async函数。5.2 分步实现与代码详解第一步环境准备与状态管理我们创建一个auth_manager.py模块来专门处理登录状态。# auth_manager.py import json import os from playwright.async_api import async_playwright AUTH_STATE_FILE ./cms_auth_state.json async def ensure_login_state(): 确保有效的登录状态存在如果不存在或过期则引导用户手动登录 if os.path.exists(AUTH_STATE_FILE): print(检测到已保存的登录状态文件。) # 这里可以添加简单的状态有效性检查如检查文件修改时间 # 为了简单起见我们假设文件有效 return AUTH_STATE_FILE else: print(未找到登录状态文件开始创建...) return await create_login_state() async def create_login_state(): 打开浏览器引导用户手动登录并保存状态 async with async_playwright() as p: # 使用持久化上下文方便处理可能的复杂登录流程如验证码 browser await p.chromium.launch_persistent_context( user_data_dir./playwright_cms_data, headlessFalse, args[--start-maximized] ) page await browser.new_page() await page.goto(https://internal-cms.example.com/login) print(请在打开的浏览器窗口中完成登录操作...) print(登录成功后脚本会自动保存状态。) print(等待登录成功检测URL跳转到/dashboard...) # 等待登录成功这里用URL变化作为标志 try: await page.wait_for_url(**/dashboard**, timeout120000) # 等待2分钟 except Exception as e: print(f等待登录超时或失败: {e}) await browser.close() raise print(登录成功正在保存状态...) # 保存状态 await browser.storage_state(pathAUTH_STATE_FILE) await browser.close() print(f状态已保存至: {AUTH_STATE_FILE}) return AUTH_STATE_FILE第二步主脚本集成网络拦截与上传# main_automation.py import asyncio import json from pathlib import Path from auth_manager import ensure_login_state from playwright.async_api import async_playwright async def mock_articles_api(route): 拦截文章列表API返回固定的测试数据 mock_data { code: 0, message: success, data: { articles: [ {id: 1, title: [测试]固定文章A, status: published}, {id: 2, title: [测试]固定文章B, status: draft}, {id: 99, title: 这是由自动化脚本拦截返回的固定数据, status: draft} ], total: 3 } } await route.fulfill( status200, headers{Content-Type: application/json}, bodyjson.dumps(mock_data) ) async def upload_article_with_image(page, title, content, image_path): 执行创建文章和上传图片的完整流程 # 1. 导航到创建文章页面 await page.goto(https://internal-cms.example.com/article/create) # 2. 填写文章标题和内容 await page.fill(input[nametitle], title) # 假设富文本编辑器有一个可编辑的div editor_frame page.frame_locator(#editor-iframe).locator(body) await editor_frame.click() # 点击获得焦点 await editor_frame.press(ControlA) # 全选如果有默认内容 await editor_frame.press(Backspace) await editor_frame.type(content) # 3. 上传封面图片 - 使用 file chooser 监听方式兼容性更好 print(f准备上传图片: {image_path}) # 先点击“上传图片”按钮这个按钮会触发文件选择器 async with page.expect_file_chooser() as fc_info: await page.click(button:has-text(上传封面图)) file_chooser await fc_info.value await file_chooser.set_files(image_path) # 4. 等待图片上传完成监听上传接口的响应 async with page.expect_response(**/api/upload/image**) as resp_info: # 文件选择器设置文件后有些组件会自动上传有些需要再点确定。 # 这里假设是自动上传如果需要点击在这里添加。 # await page.click(#confirm-upload) pass upload_response await resp_info.value if upload_response.ok: resp_json await upload_response.json() image_url resp_json.get(data, {}).get(url) print(f图片上传成功URL: {image_url}) # 可能需要在表单的某个隐藏input里填入这个URL取决于前端实现 # await page.fill(input[namecover_image], image_url) else: print(f图片上传失败: {await upload_response.text()}) return False # 5. 保存草稿 async with page.expect_response(**/api/article/save**) as save_info: await page.click(button:has-text(保存草稿)) save_response await save_info.value if save_response.ok: print(文章草稿保存成功) return True else: print(f文章保存失败: {await save_response.text()}) return False async def main(): # 确保有登录状态 state_file await ensure_login_state() async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) # 生产环境用无头 # 加载登录状态创建上下文 context await browser.new_context( storage_statestate_file, viewport{width: 1920, height: 1080} ) page await context.new_page() # 设置请求拦截在访问文章列表页时拦截API await page.route(**/api/articles**, mock_articles_api) # 第一步访问文章列表页此时API已被拦截看到的是模拟数据 await page.goto(https://internal-cms.example.com/dashboard/articles) await page.wait_for_load_state(networkidle) print(已访问文章列表页列表数据已被模拟。) # 第二步执行上传新文章流程 article_title f自动化测试文章-{datetime.now().strftime(%Y%m%d-%H%M%S)} article_content 这是一篇由Playwright自动化脚本创建的文章内容。**加粗测试**。\n\n![图片描述](图片URL将由上传接口返回) image_path Path(__file__).parent / assets / test_cover.jpg success await upload_article_with_image( page, article_title, article_content, str(image_path) ) if success: print(自动化工作流执行完毕) else: print(自动化工作流执行过程中出现错误。) # 可选截图保存结果 await page.screenshot(pathf./result_{datetime.now().strftime(%Y%m%d_%H%M%S)}.png) await browser.close() if __name__ __main__: asyncio.run(main())5.3 错误处理、日志与稳定性优化一个用于生产环境的脚本必须健壮。我们需要添加超时控制为关键操作如等待元素、等待响应设置合理的超时时间并使用try...except捕获超时异常。try: await page.wait_for_selector(#success-message, statevisible, timeout30000) # 30秒 except TimeoutError: print(错误未在30秒内找到成功消息元素。) # 可以在这里截图帮助调试 await page.screenshot(patherror_timeout.png) raise重试机制对于网络不稳定的操作如文件上传可以加入重试逻辑。max_retries 3 for attempt in range(max_retries): try: async with page.expect_response(**/api/upload/**, timeout60000) as resp_info: await file_chooser.set_files(file_path) response await resp_info.value if response.ok: break # 成功则跳出循环 else: print(f上传请求失败状态码{response.status}第{attempt1}次重试...) except Exception as e: print(f上传过程发生异常: {e}第{attempt1}次重试...) if attempt max_retries - 1: raise Exception(f文件上传失败已重试{max_retries}次。) await asyncio.sleep(2) # 重试前等待2秒详细日志使用Python的logging模块替代print可以输出不同级别INFO, WARNING, ERROR的日志到文件和控制台方便事后排查。资源清理确保脚本无论成功还是失败都能正确关闭浏览器和上下文避免残留进程。async with async_playwright() as p: browser None try: browser await p.chromium.launch() # ... 主要逻辑 except Exception as e: logging.error(f脚本执行失败: {e}) finally: if browser: await browser.close()通过这个综合案例你将网络拦截用于创造稳定的测试数据环境用状态复用实现了无缝登录并用高级的文件上传技巧处理了真实的Web组件。这三大技巧的组合足以让你应对绝大多数Web自动化中的复杂场景。记住关键不是记住所有API而是理解其背后的原理拦截、状态、事件这样你就能根据具体的页面实现灵活组合出解决方案。

本周精选

本月热点