别再死记硬背async/await了用PlaywrightPython写自动化脚本这3个坑我帮你踩过了第一次用Playwright写自动化测试脚本时我对着文档里的async/await关键字发呆了半小时。明明照着示例代码敲了一遍运行时却总是报错。后来才发现问题出在我对Python异步编程的理解太肤浅——以为加上await就能自动实现并行结果连最基本的执行顺序都控制不了。如果你也在Playwright的异步世界里跌跌撞撞这篇文章就是为你准备的。我会用三个真实踩过的坑带你避开新手最常见的误区。这些经验都是用深夜调试的咖啡换来的保证你看完就能写出更健壮的自动化脚本。1. 同步上下文中的await陷阱为什么你的脚本突然崩溃去年给电商项目写爬虫时我遇到了一个诡异的问题在Jupyter Notebook里运行完美的Playwright脚本移植到Django项目里就报错。错误信息显示SyntaxError: await outside async function可明明代码里到处都是async def。1.1 同步与异步的边界混淆问题出在我混用了同步和异步上下文。看这段典型错误代码from playwright.sync_api import sync_playwright def test_login(): with sync_playwright() as p: browser p.chromium.launch() page browser.new_page() await page.goto(https://example.com) # 这里会报错关键问题sync_playwright()创建的是同步上下文而await只能在异步函数中使用。这种错误在新手中特别常见因为Playwright的文档同时提供了同步和异步两种API。1.2 两种修正方案对比方案A统一使用同步API适合简单脚本from playwright.sync_api import sync_playwright def test_login(): with sync_playwright() as p: browser p.chromium.launch() page browser.new_page() page.goto(https://example.com) # 去掉await方案B全异步写法推荐复杂场景from playwright.async_api import async_playwright import asyncio async def test_login(): async with async_playwright() as p: browser await p.chromium.launch() page await browser.new_page() await page.goto(https://example.com) asyncio.run(test_login())提示如果你在Django等同步框架中调用异步代码需要使用sync_to_async装饰器转换。但最佳实践是保持整个调用链的一致性。2. 时间控制的误区time.sleep如何毁掉你的并行效率在调试一个需要等待页面加载的脚本时我习惯性地写下了time.sleep(5)。结果发现整个脚本的执行时间比预期长了3倍——异步的优势完全消失了。2.1 阻塞式睡眠的代价看这个对比实验import time import asyncio async def demo_blocking(): print(开始任务) time.sleep(3) # 同步阻塞 print(结束任务) async def demo_non_blocking(): print(开始任务) await asyncio.sleep(3) # 异步挂起 print(结束任务)运行两个函数的区别特性time.sleepasyncio.sleep是否阻塞事件循环是否适合场景同步代码异步代码资源占用占用线程释放线程2.2 Playwright中的正确等待方式在自动化测试中硬性等待无论time还是asyncio.sleep都是次优选择。Playwright提供了更智能的等待机制await page.goto(url, wait_untilnetworkidle) # 等待网络空闲 await page.wait_for_selector(#submit-btn) # 等待元素出现 await page.wait_for_function(window.readyState complete) # 自定义条件这些方法比固定时长等待更可靠还能自动适应不同网络环境。我在实际项目中测得用智能等待平均能节省40%的执行时间。3. 事件循环管理为什么你的async代码有时不执行最让我抓狂的一次调试经历是明明所有语法都正确但异步函数就是不被执行。最后发现是因为事件循环没有正确启动。3.1 事件循环的三种启动方式错误示范新手常见async def scrape_data(): # ...Playwright操作... scrape_data() # 这样调用不会执行正确方法一Python 3.7推荐asyncio.run(scrape_data()) # 创建并运行新事件循环正确方法二需要精细控制时loop asyncio.get_event_loop() try: loop.run_until_complete(scrape_data()) finally: loop.close()正确方法三在已有事件循环中运行async def main(): await scrape_data() await other_task() asyncio.run(main())3.2 Playwright与事件循环的配合当使用Playwright的异步API时特别要注意浏览器启动和关闭的时机。这是我总结的最佳实践async def run_playwright(): async with async_playwright() as p: browser await p.chromium.launch() try: page await browser.new_page() # 主要操作逻辑... finally: await browser.close() # 确保浏览器被关闭 # 在Jupyter等特殊环境中可能需要这样调用 import nest_asyncio nest_asyncio.apply() asyncio.run(run_playwright())注意如果在Jupyter Notebook中运行遇到事件循环错误需要先安装nest-asyncio包。这是因为Jupyter本身已经运行了一个事件循环。4. 实战构建健壮的异步测试框架掌握了这些避坑技巧后我们可以设计更可靠的测试架构。以下是我在多个项目中验证过的模式4.1 分层设计架构tests/ ├── __init__.py ├── conftest.py # 全局fixture ├── fixtures/ # 设备管理 │ ├── browser.py # 浏览器生命周期 │ └── pages.py # 页面对象管理 ├── utils/ │ └── async_utils.py # 异步工具函数 └── test_*.py # 实际测试用例关键文件fixtures/browser.py示例import pytest from playwright.async_api import async_playwright pytest.fixture(scopesession) async def browser(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) yield browser await browser.close()4.2 错误处理模板async def safe_click(element, timeout5000): try: await element.click(timeouttimeout) except Exception as e: await page.screenshot(patherror.png) raise AssertionError(f点击元素失败: {e}) from e4.3 性能优化技巧并行执行使用asyncio.gather同时运行多个测试上下文复用通过fixture共享浏览器实例智能等待结合Playwright的auto-waiting机制资源监控用browser.contexts跟踪内存泄漏async def run_tests_parallel(): results await asyncio.gather( test_login(), test_search(), test_checkout(), return_exceptionsTrue ) for r in results: if isinstance(r, Exception): print(f测试失败: {r})第一次完整跑通这套架构时我们的测试套件执行时间从原来的12分钟降到了3分钟。更棒的是那些偶发的超时错误几乎消失了——因为正确的异步处理让资源竞争变得可控。