Python自动化神器Playwright:让浏览器听话的全面指南

1- Python 自动化神器 Playwright:让浏览器听话的全面指南

想象一下:深夜还在加班,你面前有成堆的表单需要填写,手都酸了…突然灵光一闪,有没有工具能自动完成这些重复劳动?答案就是 Playwright!这个微软开源的自动化神器,比 Selenium 更快、更强大,让你的浏览器变成听话的小助手!

1.1- Playwright 是什么?

Playwright 是微软在 2020 年初开源的新一代浏览器自动化测试工具,其功能与 Selenium 类似,能够驱动浏览器执行各种自动化操作。它支持所有主流浏览器,包括 Chrome、Firefox、Safari、Edge,并能在有头或无头模式下运行。

1.1.1- 为什么选择 Playwright?

Playwright 相比 Selenium 的优势:

  • 原生支持多浏览器(Selenium 需要 WebDriver)
  • 默认无头模式(Selenium 需要手动设置)
  • 自动等待元素加载(Selenium 不需要再用 time.sleep()
  • 支持 API 测试、手机模拟、文件上传下载等高级功能

1.2- 快速安装

只需两行命令,3 秒搞定 Playwright 安装:

pip install playwright
playwright install

第一个命令安装 Playwright 库,第二个命令下载浏览器内核(必需步骤,否则无法控制浏览器)。

1.3- 基本概念

在使用 Playwright 之前,我们需要了解几个核心概念:

1.3.1- Browser(浏览器)

浏览器实例,支持多种浏览器引擎:

# 启动Chrome/Edge (Chromium引擎)
browser = playwright.chromium.launch(headless=False)  # headless=False表示有头模式

# 启动Firefox
browser = playwright.firefox.launch()

# 启动Safari (WebKit引擎)
browser = playwright.webkit.launch()

1.3.2- Context(上下文)

一个浏览器实例下可以有多个 context,将浏览器分割成不同的上下文,实现会话分离:

context = browser.new_context()

这在需要多用户同时登录同一网站时非常有用,不需要创建多个浏览器实例。

1.3.3- Page(页面)

一个 context 下可以有多个 page,代表一个浏览器标签页或弹出窗口:

page = context.new_page()

Page 是我们进行大多数操作的主要对象。

1.4- Hello Playwright!让浏览器跑起来

来看一个最简单的示例,使用 Playwright 打开百度并截图:

from playwright.sync_api import sync_playwright

def run():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto("https://www.baidu.com")
        page.screenshot(path="baidu.png")
        browser.close()

run()

代码解析:

  1. sync_playwright():启动 Playwright,支持 with 语法,确保程序结束后资源释放
  2. p.chromium.launch():启动 Chrome 浏览器
  3. page.goto("https://www.baidu.com"):访问百度
  4. page.screenshot(path="baidu.png"):截图保存
  5. browser.close():关闭浏览器

1.5- 元素定位与操作

1.5.1- 如何找到网页元素

在使用 Playwright 自动化之前,需要了解如何定位网页元素:

  1. 打开 Chrome 浏览器,导航到目标网站(如 https://www.baidu.com
  2. F12 打开开发者工具
  3. 点击开发者工具左上角的 " 选择元素 " 按钮
  4. 将鼠标悬停在想查看的网页元素上,就能看到对应的 id、class、类型等信息

图片

1.5.2- 基础定位方法

Playwright 提供多种定位元素的方法,推荐优先使用语义化定位器而非传统 CSS/XPath,以提高代码可读性和稳定性。

1.5.2.1- CSS 选择器

# 定位ID为"username"的输入框
page.locator("#username").fill("admin")

# 定位类名为"submit-btn"的按钮
page.locator("button.submit-btn").click()

1.5.2.2- XPath 表达式

# 定位name属性为"email"的输入框
page.locator("//input[@name='email']").fill("[email protected]")

# 定位包含特定文本的按钮
page.locator("//button[contains(text(), '提交')]").click()

1.5.2.3- 按角色定位(Role)

# 定位名称为"登录"的按钮
page.get_by_role("button", name="登录").click()

# 定位角色为输入框且名称为"用户名"
page.get_by_role("textbox", name="用户名").fill("user123")

1.5.2.4- 按文本内容定位

# 精确匹配文本
page.get_by_text("欢迎回来").click()

# 正则表达式模糊匹配
page.get_by_text(re.compile(r"订单编号\d+")).hover()

1.5.2.5- 按标签关联定位

# 通过关联的<label>文本定位输入框
page.get_by_label("密码:").fill("secret")

# 通过占位符定位
page.get_by_placeholder("请输入手机号").type("13800138000")

1.6- 自动化表单填写示例

让我们看一个实际例子,自动打开百度,输入搜索词并点击搜索:

from playwright.sync_api import sync_playwright
import time

def run():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)  # 有头模式,可以看到浏览器操作
        page = browser.new_page()
        page.goto("https://www.baidu.com")
        
        page.fill(".s_ipt", "Python")  # 输入搜索词
        page.click("input[type='submit']")  # 点击搜索按钮
        page.screenshot(path="baidu_search.png")  # 截图保存结果
        time.sleep(3)  # 等待3秒,便于查看结果
        browser.close()

run()

执行后,你将看到浏览器自动打开百度,输入 “Python” 并进行搜索,整个过程行云流水!

图片

1.7- 模拟用户操作

Playwright 可以模拟各种用户操作,包括点击、悬停、滚动、拖拽等:

# 点击按钮
page.click("button.submit") 

# 鼠标悬停
page.hover("#menu") 

# 模拟滚动鼠标(向下滚500像素)
page.mouse.wheel(0, 500) 

# 拖拽元素
page.drag_and_drop("#source", "#target") 

1.8- 等待与网络控制

1.8.1- 等待机制

Playwright 提供智能等待机制,避免了使用硬编码延时:

# 等待元素出现
page.wait_for_selector('.result')

# 等待元素消失
page.wait_for_selector('.loading', state='hidden')

# 等待网络请求完成
page.wait_for_load_state('networkidle')

# 自定义等待条件
page.wait_for_function('document.querySelector(".price").innerText.includes("¥")')

1.8.2- 网络控制

Playwright 可以监听、拦截和修改网络请求:

# 监听网络请求
page.on('request', lambda request: print(request.url))
page.on('response', lambda response: print(response.status))

# 拦截图片请求(提高加载速度)
page.route('**/*.{png,jpg,jpeg}', lambda route: route.abort())

# 修改API响应
page.route('**/api/data', lambda route: route.fulfill(
    status=200,
    body='{"message": "被我改了吧"}'
))

1.9- 截图与录制

Playwright 可轻松实现截图和录制功能:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    # 启动浏览器
    browser = p.chromium.launch(headless=True)
    # 开始录制视频
    context = browser.new_context(record_video_dir="videos/")
    page = context.new_page()
    page.goto("https://www.baidu.com/")
    
    # 全页面截图
    page.screenshot(path='screenshot.png')
    
    # 某个元素的截图
    page.locator('#result_logo').screenshot(path='logo.png')
    
    # PDF导出(只在无头模式下支持)
    page.pdf(path='page.pdf')

1.10- 自动生成脚本

如果你不擅长编程,Playwright 提供了代码录制功能,可以通过录制浏览器操作自动生成代码:

playwright codegen -o script.py

执行上述命令后,会打开一个浏览器窗口和代码生成面板,你在浏览器中的所有操作都会被转换为 Playwright 代码:

图片

1.11- Playwright vs Selenium

功能 Playwright Selenium
速度 更快 较慢
多浏览器支持 内置 需驱动
无头模式 默认支持 需手动设置
等待元素加载 自动等待 需手动 sleep
API 自动化 支持 不支持
移动端模拟 支持 需插件

如果你要做自动化测试、爬虫或网页填表,Playwright 绝对是优选!但如果项目已经基于 Selenium,也不必急着替换。

1.12- 动态元素处理技巧

1.12.1- 显式等待元素加载

# 等待元素可见后再操作
page.locator(".loading").wait_for(state="visible")

# 等待元素消失
page.wait_for_selector(".spinner", state="hidden")

1.12.2- 处理 iframe 嵌套

# 定位到iframe内的元素
iframe = page.frame(name="payment-iframe")
iframe.get_by_text("确认支付").click()

1.12.3- Shadow DOM 穿透

# 通过>>符号穿透Shadow DOM层级
page.locator("div#shadow-host >> input.custom-input").fill("data")

1.13- 复杂场景定位策略

1.13.1- 多重条件筛选

# 定位类名为"item"且包含文本"特价"的元素
page.locator(".item", has_text="特价").click()

# 组合角色和文本过滤
page.get_by_role("listitem").filter(has_text="待付款").nth(0).click()

1.13.2- 相对定位

# 父子关系定位
parent = page.locator("div.parent")
child = parent.locator("span.child")

# 兄弟元素定位
second_item = page.locator("ul > li").nth(1)

1.13.3- 动态列表处理

# 遍历商品列表并点击第三个商品
items = page.locator(".product-list > li")
items.nth(2).click()

# 根据文本动态定位最新添加的条目
new_item = page.locator("tr:has-text('2024-03-18')").last

1.14- 实际应用场景

1.14.1- 自动化登录与数据提取

from playwright.sync_api import sync_playwright
import csv

def extract_product_data():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()
        
        # 1. 登录系统
        page.goto("https://example.com/login")
        page.fill("#username", "your_username")
        page.fill("#password", "your_password")
        page.click("button[type=submit]")
        
        # 等待登录成功
        page.wait_for_selector(".dashboard")
        
        # 2. 导航到产品页面
        page.click("text=产品列表")
        page.wait_for_selector(".product-grid")
        
        # 3. 提取产品数据
        products = []
        product_cards = page.locator(".product-card").all()
        
        for card in product_cards:
            name = card.locator(".name").text_content()
            price = card.locator(".price").text_content()
            stock = card.locator(".stock").text_content()
            products.append({"name": name, "price": price, "stock": stock})
            
        # 4. 导出为CSV
        with open("products.csv", "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=["name", "price", "stock"])
            writer.writeheader()
            writer.writerows(products)
            
        print(f"已提取 {len(products)} 条产品数据")
        browser.close()

if __name__ == "__main__":
    extract_product_data()

1.14.2- 定时监控网站变化

import schedule
import time
from playwright.sync_api import sync_playwright

def check_website_status():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        response = page.goto("https://example.com")
        status = response.status
        
        if status != 200:
            # 发送告警通知(邮件、短信等)
            print(f"网站异常!状态码: {status}")
        else:
            # 检查关键元素是否存在
            if page.locator(".important-element").count() == 0:
                print("关键元素缺失!")
            else:
                print("网站正常运行中")
                
        browser.close()

# 设置定时任务,每小时检查一次
schedule.every(1).hour.do(check_website_status)

while True:
    schedule.run_pending()
    time.sleep(60)

1.14.3- 自动化表单提交

from playwright.sync_api import sync_playwright
import pandas as pd

def batch_submit_forms(data_file):
    # 读取Excel数据
    df = pd.read_excel(data_file)
    
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()
        
        for index, row in df.iterrows():
            # 打开表单页面
            page.goto("https://example.com/form")
            
            # 填写表单字段
            page.fill("#name", row["姓名"])
            page.fill("#phone", row["电话"])
            page.fill("#email", row["邮箱"])
            page.fill("#address", row["地址"])
            
            # 选择下拉菜单
            page.select_option("select#category", value=row["类别"])
            
            # 点击提交按钮
            page.click("button[type=submit]")
            
            # 等待提交成功
            page.wait_for_selector(".success-message")
            print(f"已成功提交第 {index+1} 条数据")
            
        browser.close()

if __name__ == "__main__":
    batch_submit_forms("data.xlsx")

1.15- 常见问题与故障排除

1.15.1- 浏览器无法启动

问题:运行脚本时出现 “Browser.launch() failed” 或类似错误。

解决方法:

  1. 确保已运行 playwright install 命令安装浏览器

  2. 检查系统依赖是否完整:playwright install-deps

  3. 尝试指定可执行浏览器路径:

    browser = playwright.chromium.launch(
        executable_path="/path/to/chrome"
    )
    
  4. 如果在 Docker 或无界面环境中运行,确保使用无头模式并安装必要依赖

1.15.2- 元素定位失败

问题:page.locator(selector) 找不到元素或点击失败。

解决方法:

  1. 使用开发者工具确认选择器是否正确

  2. 检查元素是否在 iframe 中,需要先切换 frame

  3. 增加等待时间或条件:

    page.wait_for_selector("your-selector", state="visible", timeout=10000)
    
  4. 使用更可靠的选择器,优先使用 ID 或数据属性

  5. 尝试不同的定位策略(如 role、text 而非 CSS)

1.15.3- 处理动态内容和 AJAX

问题:页面内容通过 JavaScript 动态加载,导致定位不到元素。

解决方法:

  1. 等待网络空闲:

    page.goto("https://example.com", wait_until="networkidle")
    
  2. 等待特定请求完成:

    with page.expect_response("**/api/data") as response_info:
        page.click(".load-data-button")
    response = response_info.value
    
  3. 使用显式等待特定元素出现:

    page.wait_for_selector(".dynamic-content", state="visible")
    

1.15.4- 绕过验证和检测

问题:网站检测到自动化工具,阻止访问。

解决方法:

  1. 修改浏览器指纹:

    browser = p.chromium.launch(
        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36"
    )
    
  2. 降低操作速度,模拟人类行为:

    page.fill("#username", "test", delay=100)  # 每个字符间隔100ms
    
  3. 随机化操作间隔:

    import random
    import time
    
    time.sleep(random.uniform(1, 3))  # 随机等待1-3秒
    
  4. 使用无痕模式并禁用 WebDriver 标志:

    context = browser.new_context(is_private=True)
    

1.16- 高级技术

1.16.1- 处理验证码

1.16.1.1- 图像验证码

import pytesseract
from PIL import Image
import io
import base64

def solve_image_captcha(page):
    # 截取验证码图片
    captcha_elem = page.locator(".captcha-image")
    captcha_screenshot = captcha_elem.screenshot()
    
    # 使用OCR识别验证码
    image = Image.open(io.BytesIO(captcha_screenshot))
    captcha_text = pytesseract.image_to_string(image).strip()
    
    # 输入验证码
    page.fill(".captcha-input", captcha_text)
    print(f"识别的验证码: {captcha_text}")

1.16.1.2- 滑动验证码

def solve_slider_captcha(page):
    # 获取滑块元素
    slider = page.locator(".slider-handle")
    
    # 获取滑块轨道
    track = page.locator(".slider-track")
    
    # 获取位置信息
    slider_box = slider.bounding_box()
    track_box = track.bounding_box()
    
    # 计算移动距离
    distance = track_box["width"] - slider_box["width"]
    
    # 执行滑动操作
    page.mouse.move(slider_box["x"] + slider_box["width"]/2, 
                   slider_box["y"] + slider_box["height"]/2)
    page.mouse.down()
    
    # 模拟人类滑动轨迹(先快后慢)
    steps = 30
    for i in range(steps):
        if i < steps * 0.7:
            # 加速阶段
            x_offset = distance * (i / steps) * 1.5
        else:
            # 减速阶段
            x_offset = distance * (i / steps) * 0.5
        
        page.mouse.move(slider_box["x"] + slider_box["width"]/2 + x_offset, 
                       slider_box["y"] + slider_box["height"]/2)
        page.wait_for_timeout(10)  # 短暂停顿
        
    page.mouse.up()

1.16.2- 多标签页和窗口处理

from playwright.sync_api import sync_playwright

def handle_multiple_tabs():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()
        page.goto("https://example.com")
        
        # 创建新标签页并切换到它
        page2 = browser.new_page()
        page2.goto("https://example.org")
        
        # 在第一个标签页点击会打开新标签的链接
        with page.expect_popup() as popup_info:
            page.click("a[target='_blank']")
        popup = popup_info.value  # 获取新打开的页面
        
        # 在新打开的页面上操作
        popup.wait_for_load_state()
        print(f"新标签页标题: {popup.title()}")
        
        # 在多个标签页之间切换
        page.bring_to_front()  # 切换回第一个标签
        
        # 关闭特定标签页
        popup.close()
        
        browser.close()

1.16.3- 文件上传与下载

def handle_file_operations():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        
        # 设置下载路径
        download_path = "./downloads"
        context = browser.new_context(accept_downloads=True)
        page = context.new_page()
        
        # 文件上传
        page.goto("https://example.com/upload")
        page.set_input_files("input[type='file']", [
            "path/to/file1.pdf",
            "path/to/file2.jpg"
        ])
        page.click("button[type='submit']")
        
        # 文件下载
        page.goto("https://example.com/download")
        
        with page.expect_download() as download_info:
            page.click(".download-button")
        
        download = download_info.value
        # 保存下载文件到指定位置
        download.save_as(f"{download_path}/{download.suggested_filename}")
        print(f"文件已下载: {download.suggested_filename}")
        
        browser.close()

1.16.4- 性能监控与分析

def measure_performance():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        context = browser.new_context()
        page = context.new_page()
        
        # 启用性能指标收集
        client = page.context.new_cdp_session(page)
        client.send("Performance.enable")
        
        # 访问页面
        page.goto("https://example.com")
        
        # 获取性能指标
        metrics = client.send("Performance.getMetrics")
        
        # 提取关键指标
        for metric in metrics["metrics"]:
            if metric["name"] in ["DOMContentLoaded", "FirstPaint", "FirstContentfulPaint"]:
                print(f"{metric['name']}: {metric['value']} ms")
        
        # 获取网络性能
        navigation_timing = page.evaluate("""() => {
            const timing = performance.timing;
            return {
                dnsLookup: timing.domainLookupEnd - timing.domainLookupStart,
                tcpConnection: timing.connectEnd - timing.connectStart,
                requestResponse: timing.responseEnd - timing.requestStart,
                domProcessing: timing.domComplete - timing.domLoading,
                pageLoad: timing.loadEventEnd - timing.navigationStart
            }
        }""")
        
        for key, value in navigation_timing.items():
            print(f"{key}: {value} ms")
            
        browser.close()

1.17- 实战项目:爬取图片并保存

下面是一个完整的实战项目,用于爬取 Unsplash 免费图片网站的图片并保存到本地:

from playwright.sync_api import sync_playwright
import os
import requests
import time
import random

def download_image(url, folder, filename):
    """下载图片并保存到指定文件夹"""
    os.makedirs(folder, exist_ok=True)
    response = requests.get(url)
    if response.status_code == 200:
        with open(f"{folder}/{filename}", "wb") as f:
            f.write(response.content)
        return True
    return False

def scrape_unsplash_photos(search_term, num_images=10):
    """爬取Unsplash上的图片并下载"""
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()
        
        # 访问Unsplash搜索页面
        page.goto(f"https://unsplash.com/s/photos/{search_term}")
        page.wait_for_load_state("networkidle")
        
        # 创建保存文件夹
        folder = f"unsplash_{search_term}"
        
        # 已下载的图片数量
        downloaded = 0
        
        # 滚动加载更多图片
        while downloaded < num_images:
            # 获取所有图片元素
            image_elements = page.locator("figure a.rEAWd img").all()
            
            for img in image_elements:
                if downloaded >= num_images:
                    break
                    
                try:
                    # 获取图片URL和ID
                    img_url = img.get_attribute("src")
                    
                    if img_url and "images.unsplash.com" in img_url:
                        # 提取图片ID作为文件名
                        img_id = img_url.split("/")[-1].split("?")[0]
                        filename = f"{img_id}.jpg"
                        
                        # 下载高质量版本
                        hq_url = img_url.split("?")[0]
                        
                        print(f"正在下载图片 {downloaded+1}/{num_images}: {filename}")
                        if download_image(hq_url, folder, filename):
                            downloaded += 1
                            # 随机间隔,避免被封
                            time.sleep(random.uniform(0.5, 2.0))
                except Exception as e:
                    print(f"下载图片时出错: {e}")
            
            if downloaded < num_images:
                # 滚动到页面底部加载更多图片
                page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
                page.wait_for_timeout(2000)  # 等待新内容加载
        
        print(f"已成功下载 {downloaded} 张关于 '{search_term}' 的图片到 '{folder}' 文件夹")
        browser.close()

if __name__ == "__main__":
    search_term = input("请输入要搜索的图片关键词: ")
    num_images = int(input("要下载的图片数量: "))
    scrape_unsplash_photos(search_term, num_images)

1.18- 异步操作与并发

Playwright 支持同步和异步两种 API。对于需要高性能的场景,异步 API 可以大大提升效率:

1.18.1- 基本异步操作

import asyncio
from playwright.async_api import async_playwright

async def async_example():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://www.baidu.com")
        await page.screenshot(path="async_example.png")
        await browser.close()

# 运行异步函数
asyncio.run(async_example())

1.18.2- 并发爬取多个页面

import asyncio
from playwright.async_api import async_playwright

async def scrape_page(url, browser):
    """爬取单个页面的函数"""
    page = await browser.new_page()
    await page.goto(url)
    title = await page.title()
    
    # 提取页面信息
    content = await page.text_content("main")
    await page.close()
    return {"url": url, "title": title, "content_length": len(content)}

async def scrape_multiple_pages(urls):
    """并发爬取多个页面"""
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        
        # 创建任务列表
        tasks = [scrape_page(url, browser) for url in urls]
        
        # 并发执行所有任务
        results = await asyncio.gather(*tasks)
        
        await browser.close()
        return results

async def main():
    urls = [
        "https://www.example.com",
        "https://www.wikipedia.org",
        "https://www.python.org",
        "https://www.github.com",
        "https://www.stackoverflow.com"
    ]
    
    results = await scrape_multiple_pages(urls)
    for result in results:
        print(f"URL: {result['url']}")
        print(f"标题: {result['title']}")
        print(f"内容长度: {result['content_length']} 字符")
        print("-" * 50)

# 运行异步主函数
asyncio.run(main())

1.18.3- 限制并发数量

import asyncio
from playwright.async_api import async_playwright

async def scrape_with_semaphore(urls, max_concurrency=3):
    """使用信号量限制并发数量"""
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        
        # 创建信号量限制并发
        semaphore = asyncio.Semaphore(max_concurrency)
        
        async def scrape_with_limit(url):
            async with semaphore:  # 使用信号量控制并发访问
                page = await browser.new_page()
                await page.goto(url, timeout=60000)
                title = await page.title()
                await page.close()
                return {"url": url, "title": title}
        
        # 创建任务
        tasks = [scrape_with_limit(url) for url in urls]
        results = await asyncio.gather(*tasks)
        
        await browser.close()
        return results

# 使用示例
async def main():
    urls = ["https://example.com"] * 10  # 10个URL
    results = await scrape_with_semaphore(urls, max_concurrency=3)
    print(f"已完成 {len(results)} 个页面的爬取")

asyncio.run(main())

2- 高级应用与最佳实践

2.1- 与 CI/CD 系统集成

将 Playwright 集成到持续集成流程中可以实现自动化测试的全流程覆盖:

# .github/workflows/playwright.yml 示例
name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-python@v4
      with:
        python-version: '3.10'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install playwright pytest pytest-playwright
        playwright install
    - name: Run tests
      run: pytest

2.2- 移动设备模拟

Playwright 支持模拟各种移动设备,包括触摸操作和设备方向:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    # 使用iPhone 13设备模拟
    iphone_13 = p.devices['iPhone 13']
    browser = p.webkit.launch(headless=False)
    context = browser.new_context(**iphone_13)
    page = context.new_page()
    
    # 访问网站
    page.goto("https://m.jd.com")
    
    # 模拟触摸滑动
    page.touchscreen.tap(200, 300)
    
    # 模拟设备旋转
    context.set_viewport_size({"width": 844, "height": 390})  # 横屏
    
    # 截图
    page.screenshot(path="mobile_view.png")

2.3- 性能监控与优化

2.3.1- 页面性能指标收集

def collect_performance_metrics():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        # 监听性能事件
        performance_metrics = {}
        
        # 在导航前开始监控
        page.on("console", lambda msg: 
            performance_metrics.update({msg.text.split(":")[0]: msg.text.split(":")[1]}) 
            if "Performance" in msg.text else None
        )
        
        # 注入性能监控代码
        page.add_init_script("""
        window.addEventListener('load', () => {
            const paint = performance.getEntriesByType('paint');
            for (const entry of paint) {
                console.log(`Performance:${entry.name}:${entry.startTime}`);
            }
            
            // 核心Web性能指标
            setTimeout(() => {
                const lcp = performance.getEntriesByType('largest-contentful-paint').pop();
                console.log(`Performance:LCP:${lcp ? lcp.startTime : 0}`);
                
                // 首次输入延迟
                const fid = performance.getEntriesByType('first-input').pop();
                console.log(`Performance:FID:${fid ? fid.processingStart - fid.startTime : 0}`);
            }, 3000);
        });
        """)
        
        # 访问页面
        page.goto("https://example.com", wait_until="networkidle")
        page.wait_for_timeout(5000)  # 等待性能数据收集
        
        print("页面性能指标:")
        for metric, value in performance_metrics.items():
            print(f"  {metric}: {value}ms")
        
        browser.close()

2.3.2- 资源优化

def analyze_page_resources():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        # 收集资源信息
        resources = []
        page.on("request", lambda request: 
            resources.append({
                "url": request.url,
                "resourceType": request.resource_type,
                "method": request.method,
                "size": -1  # 稍后更新
            })
        )
        
        page.on("response", lambda response: 
            update_resource_size(resources, response.url, response.headers.get("content-length", "-1"))
        )
        
        # 访问页面
        page.goto("https://example.com", wait_until="networkidle")
        
        # 分析资源
        resource_summary = {
            "image": {"count": 0, "size": 0},
            "script": {"count": 0, "size": 0},
            "stylesheet": {"count": 0, "size": 0},
            "other": {"count": 0, "size": 0}
        }
        
        for res in resources:
            res_type = res["resourceType"]
            if res_type not in resource_summary:
                res_type = "other"
            
            resource_summary[res_type]["count"] += 1
            if res["size"] != -1:
                resource_summary[res_type]["size"] += int(res["size"])
        
        # 输出分析结果
        print("资源加载分析:")
        for res_type, info in resource_summary.items():
            print(f"  {res_type}: {info['count']}个文件, 总大小: {info['size']/1024:.2f}KB")
        
        browser.close()

def update_resource_size(resources, url, size):
    for res in resources:
        if res["url"] == url:
            res["size"] = size
            break

2.4.1- 保存和复用登录状态

import json
from pathlib import Path

def save_authentication_state():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        context = browser.new_context()
        page = context.new_page()
        
        # 访问登录页面
        page.goto("https://example.com/login")
        
        # 执行登录
        page.fill("#username", "your_username")
        page.fill("#password", "your_password")
        page.click("button[type=submit]")
        
        # 等待登录成功
        page.wait_for_selector(".dashboard", timeout=10000)
        
        # 保存认证状态
        storage_state = context.storage_state()
        Path("auth").mkdir(exist_ok=True)
        with open("auth/state.json", "w") as f:
            f.write(json.dumps(storage_state))
        
        print("登录状态已保存")
        browser.close()

def use_saved_authentication():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        
        # 使用保存的认证状态创建上下文
        context = browser.new_context(storage_state="auth/state.json")
        page = context.new_page()
        
        # 直接访问需要登录的页面
        page.goto("https://example.com/dashboard")
        
        # 验证是否已登录
        logged_in = page.locator(".user-profile").is_visible()
        print(f"是否已登录: {logged_in}")
        
        browser.close()

2.5- 数据驱动测试

使用 Playwright 结合 pytest 实现数据驱动的自动化测试:

import pytest
from playwright.sync_api import Page, expect

# 测试数据
test_users = [
    {"username": "user1", "password": "pass1", "expected_name": "User One"},
    {"username": "user2", "password": "pass2", "expected_name": "User Two"},
    {"username": "user3", "password": "pass3", "expected_name": "User Three"}
]

@pytest.mark.parametrize("user_data", test_users)
def test_login_multiple_users(page: Page, user_data):
    # 访问登录页
    page.goto("https://example.com/login")
    
    # 填写登录表单
    page.fill("#username", user_data["username"])
    page.fill("#password", user_data["password"])
    page.click("button[type=submit]")
    
    # 验证登录结果
    expect(page.locator(".user-name")).to_contain_text(user_data["expected_name"])
    expect(page.locator(".logout-button")).to_be_visible()

2.6- API 和 UI 测试结合

Playwright 不仅可以测试 UI,还能同时处理 API 请求,实现端到端测试:

def test_api_and_ui_integration():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        context = browser.new_context()
        page = context.new_page()
        
        # 先通过API创建测试数据
        api_context = browser.new_context()
        api_page = api_context.new_page()
        
        response = api_page.request.post(
            "https://api.example.com/items", 
            data={
                "name": "测试商品",
                "price": 99.99,
                "description": "这是一个测试商品"
            },
            headers={"Authorization": "Bearer your_token"}
        )
        
        # 检查API响应
        assert response.ok
        item_data = response.json()
        item_id = item_data["id"]
        
        # 然后通过UI验证数据是否正确显示
        page.goto(f"https://example.com/items/{item_id}")
        
        # 验证UI显示的数据
        assert page.locator(".item-name").text_content() == "测试商品"
        assert page.locator(".item-price").text_content() == "¥99.99"
        
        # 清理测试数据
        api_page.request.delete(f"https://api.example.com/items/{item_id}")
        
        browser.close()

2.7- 智能等待策略

设计更智能的等待策略,避免测试不稳定:

def smart_waiting_example():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        # 访问页面
        page.goto("https://example.com")
        
        # 等待页面准备好交互 - 组合策略
        page.wait_for_load_state("domcontentloaded")
        page.wait_for_function("""
            () => {
                // 检查关键元素是否可交互
                const button = document.querySelector('.action-button');
                // 检查加载动画是否消失
                const spinner = document.querySelector('.loading-spinner');
                // 检查JS是否完全初始化
                const appInitialized = window.appReady === true;
                
                return button && !spinner && appInitialized;
            }
        """)
        
        # 现在页面已完全准备好,可以进行交互
        page.click(".action-button")
        
        browser.close()

2.8- 无障碍测试

使用 Playwright 测试网站的无障碍性:

def accessibility_testing():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        # 访问页面
        page.goto("https://example.com")
        
        # 运行无障碍性检查
        accessibility_snapshot = page.evaluate("""() => {
            // 使用内置的axe-core库进行检查
            return new Promise(resolve => {
                // 动态加载axe-core
                const script = document.createElement('script');
                script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/axe.min.js';
                script.onload = function() {
                    axe.run().then(results => {
                        resolve(results);
                    });
                };
                document.head.appendChild(script);
            });
        }""")
        
        # 分析结果
        violations = accessibility_snapshot["violations"]
        if violations:
            print(f"发现{len(violations)}个无障碍性问题:")
            for violation in violations:
                print(f"  - {violation['id']}: {violation['description']}")
                print(f"    影响{len(violation['nodes'])}个元素")
        else:
            print("未发现无障碍性问题")
        
        browser.close()

2.9- 国际化测试

测试应用在不同语言和区域设置下的表现:

def test_internationalization():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        
        # 测试不同语言设置
        locales = ["zh-CN", "en-US", "ja-JP", "fr-FR"]
        
        for locale in locales:
            print(f"测试语言: {locale}")
            
            # 创建带有语言设置的上下文
            context = browser.new_context(locale=locale)
            page = context.new_page()
            
            # 访问网站
            page.goto("https://example.com")
            
            # 检查关键UI元素是否正确翻译
            submit_button_text = page.locator("button[type=submit]").text_content()
            welcome_text = page.locator(".welcome-message").text_content()
            
            print(f"  提交按钮文本: {submit_button_text}")
            print(f"  欢迎信息: {welcome_text}")
            
            # 测试日期格式
            date_display = page.locator(".current-date").text_content()
            print(f"  日期显示: {date_display}")
            
            # 测试货币格式
            price_display = page.locator(".product-price").text_content()
            print(f"  价格显示: {price_display}")
            
            # 截图保存
            page.screenshot(path=f"i18n_{locale}.png")
            
            context.close()
        
        browser.close()

2.10- 视觉回归测试

使用截图比较功能进行视觉回归测试:

from PIL import Image, ImageChops
import math
import io

def visual_regression_test():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        # 设置固定视口大小以确保一致性
        page.set_viewport_size({"width": 1280, "height": 800})
        
        # 访问要测试的页面
        page.goto("https://example.com")
        
        # 等待页面完全加载
        page.wait_for_load_state("networkidle")
        
        # 截取当前页面截图
        current_screenshot = page.screenshot()
        
        # 加载基准截图(假设已存在)
        try:
            baseline_image = Image.open("baseline.png")
            current_image = Image.open(io.BytesIO(current_screenshot))
            
            # 计算图像差异
            diff_image = ImageChops.difference(baseline_image, current_image)
            diff_box = diff_image.getbbox()
            
            if diff_box:
                # 有差异,计算差异百分比
                diff_percent = calculate_image_diff_percent(baseline_image, current_image)
                print(f"检测到视觉差异: {diff_percent:.2f}%")
                
                # 保存差异图像
                diff_image.save("diff.png")
                current_image.save("current.png")
                
                # 如果差异超过阈值则测试失败
                assert diff_percent < 1.0, f"视觉差异 ({diff_percent:.2f}%) 超过阈值"
            else:
                print("没有检测到视觉差异")
        except FileNotFoundError:
            # 如果基准图像不存在,则创建一个
            with open("baseline.png", "wb") as f:
                f.write(current_screenshot)
            print("已创建基准截图")
        
        browser.close()

def calculate_image_diff_percent(img1, img2):
    # 确保图像尺寸相同
    if img1.size != img2.size:
        img2 = img2.resize(img1.size)
    
    # 计算差异
    diff = ImageChops.difference(img1, img2)
    stat = diff.convert("L").getdata()
    diff_pixels = sum(1 for p in stat if p > 10)  # 差异阈值
    total_pixels = img1.size[0] * img1.size[1]
    
    return (diff_pixels / total_pixels) * 100

2.11- 安全性与隐私保护

2.11.1- 敏感信息处理

def secure_automation_practices():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        context = browser.new_context()
        page = context.new_page()
        
        # 从环境变量或安全存储中获取凭据
        import os
        username = os.environ.get("TEST_USERNAME")
        password = os.environ.get("TEST_PASSWORD")
        
        if not username or not password:
            raise ValueError("必须设置环境变量TEST_USERNAME和TEST_PASSWORD")
        
        # 禁用网络日志以防泄露敏感信息
        context.set_extra_http_headers({"playwright-log": "false"})
        
        # 登录过程
        page.goto("https://example.com/login")
        page.fill("#username", username)
        
        # 使用安全方式输入密码
        page.fill("#password", password)
        
        # 截图时遮盖敏感信息
        page.evaluate("""() => {
            const passwordField = document.querySelector('#password');
            if (passwordField) {
                passwordField.style.filter = 'blur(5px)';
            }
        }""")
        
        # 现在截图是安全的
        page.screenshot(path="login_safe.png")
        
        page.click("button[type=submit]")
        
        # 清除会话
        context.clear_cookies()
        
        browser.close()

2.11.2- 安全风险评估

def security_scanning():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        # 访问目标网站
        page.goto("https://example.com")
        
        # 收集所有表单,检查是否使用HTTPS
        forms = page.locator("form").all()
        for i, form in enumerate(forms):
            action = form.get_attribute("action") or ""
            method = form.get_attribute("method") or "get"
            
            # 检查表单是否提交到HTTPS端点
            if action.startswith("http:") or (not action.startswith("https:") and page.url.startswith("http:")):
                print(f"安全风险: 表单 #{i+1} 使用不安全的HTTP协议提交")
            
            # 检查敏感表单是否使用POST方法
            has_password = form.locator("input[type=password]").count() > 0
            if has_password and method.lower() != "post":
                print(f"安全风险: 包含密码的表单 #{i+1} 使用 {method} 方法而非POST")
        
        # 检查混合内容
        resources = []
        page.on("request", lambda req: resources.append(req.url))
        
        # 重新加载以捕获所有资源
        page.reload()
        page.wait_for_load_state("networkidle")
        
        if page.url.startswith("https:"):
            http_resources = [url for url in resources if url.startswith("http:")]
            if http_resources:
                print(f"安全风险: 在HTTPS页面中发现 {len(http_resources)} 个HTTP资源")
                for url in http_resources[:5]:  # 只显示前5个
                    print(f"  - {url}")
        
        browser.close()

2.12- 多浏览器并行测试

并行运行测试以提高效率:

import asyncio
from playwright.async_api import async_playwright

async def run_in_browser(browser_type, url):
    async with async_playwright() as p:
        browser_types = {
            "chromium": p.chromium,
            "firefox": p.firefox,
            "webkit": p.webkit
        }
        
        browser = await browser_types[browser_type].launch()
        context = await browser.new_context()
        page = await context.new_page()
        
        print(f"在 {browser_type} 中运行测试...")
        
        # 访问页面
        await page.goto(url)
        title = await page.title()
        
        # 执行测试操作
        await page.fill("#search-input", "测试关键词")
        await page.click("#search-button")
        await page.wait_for_selector(".results")
        
        # 获取结果
        result_count = await page.locator(".result-item").count()
        
        # 截图
        await page.screenshot(path=f"results_{browser_type}.png")
        
        # 关闭浏览器
        await browser.close()
        
        return {
            "browser": browser_type,
            "title": title,
            "results": result_count
        }

async def parallel_browser_testing():
    # 在三个主流浏览器上并行运行相同的测试
    tasks = [
        run_in_browser("chromium", "https://example.com"),
        run_in_browser("firefox", "https://example.com"),
        run_in_browser("webkit", "https://example.com")
    ]
    
    results = await asyncio.gather(*tasks)
    
    # 比较结果
    for result in results:
        print(f"{result['browser']} 测试结果:")
        print(f"  标题: {result['title']}")
        print(f"  搜索结果数: {result['results']}")
        print("-" * 30)

# 运行并行测试
asyncio.run(parallel_browser_testing())

2.13- 调试技巧

2.13.1- 智能调试工具

def advanced_debugging():
    with sync_playwright() as p:
        # 启动带有调试选项的浏览器
        browser = p.chromium.launch(
            headless=False,
            devtools=True,  # 自动打开开发者工具
            slow_mo=100     # 放慢操作速度便于观察
        )
        context = browser.new_context()
        
        # 捕获控制台日志
        context.on("console", lambda msg: 
            print(f"浏览器控制台 [{msg.type}]: {msg.text}")
        )
        
        # 捕获页面错误
        context.on("page", lambda page: 
            page.on("pageerror", lambda err: 
                print(f"页面错误: {err}")
            )
        )
        
        # 启用详细API调用日志
        context.set_default_timeout(10000)
        page = context.new_page()
        
        try:
            # 执行测试
            page.goto("https://example.com")
            
            # 模拟JS错误场景
            page.evaluate("() => { throw new Error('这是一个故意的错误'); }")
        except Exception as e:
            # 当错误发生时自动截图
            page.screenshot(path="error_state.png")
            
            # 捕获DOM快照
            html_snapshot = page.content()
            with open("error_dom.html", "w", encoding="utf-8") as f:
                f.write(html_snapshot)
            
            print(f"测试失败: {e}")
            print(f"已保存错误现场截图和DOM快照,用于分析")
        
        browser.close()

2.13.2- Playwright Inspector

def use_playwright_inspector():
    # 设置环境变量启用Inspector
    import os
    os.environ["PWDEBUG"] = "1"
    
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        context = browser.new_context()
        page = context.new_page()
        
        # Inspector会自动打开
        page.goto("https://example.com")
        
        # 可以通过Inspector逐步执行
        page.fill("#username", "test_user")
        page.fill("#password", "test_pass")
        page.click("button[type=submit]")
        
        # Inspector允许检查元素、变更选择器等
        page.wait_for_selector(".dashboard")
        
        browser.close()

2.14- 自动化报告生成

生成美观的 HTML 测试报告:

import datetime
from jinja2 import Template

def generate_test_report(test_results):
    # 报告模板
    template_str = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>Playwright自动化测试报告</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            .header { background: #f4f4f4; padding: 20px; border-radius: 5px; }
            .summary { display: flex; margin: 20px 0; }
            .summary-box { flex: 1; padding: 15px; border-radius: 5px; margin-right: 10px; color: white; }
            .passed { background: #4CAF50; }
            .failed { background: #F44336; }
            .total { background: #2196F3; }
            .test-case { border: 1px solid #ddd; padding: 15px; margin-bottom: 10px; border-radius: 5px; }
            .test-passed { border-left: 5px solid #4CAF50; }
            .test-failed { border-left: 5px solid #F44336; }
            img { max-width: 100%; height: auto; border: 1px solid #ddd; margin-top: 10px; }
        </style>
    </head>
    <body>
        <div class="header">
            <h1>Playwright自动化测试报告</h1>
            <p>生成时间: {{ timestamp }}</p>
        </div>
        
        <div class="summary">
            <div class="summary-box total">
                <h2>总测试数</h2>
                <p>{{ total_tests }}</p>
            </div>
            <div class="summary-box passed">
                <h2>通过</h2>
                <p>{{ passed_tests }}</p>
            </div>
            <div class="summary-box failed">
                <h2>失败</h2>
                <p>{{ failed_tests }}</p>
            </div>
        </div>
        
        <h2>测试详情</h2>
        {% for test in tests %}
        <div class="test-case {% if test.passed %}test-passed{% else %}test-failed{% endif %}">
            <h3>{{ test.name }}</h3>
            <p><strong>状态:</strong> {% if test.passed %}通过{% else %}失败{% endif %}</p>
            <p><strong>耗时:</strong> {{ test.duration }}ms</p>
            {% if test.description %}
            <p><strong>描述:</strong> {{ test.description }}</p>
            {% endif %}
            {% if not test.passed %}
            <p><strong>错误:</strong> {{ test.error }}</p>
            {% endif %}
            {% if test.screenshot %}
            <p><strong>截图:</strong></p>
            <img src="{{ test.screenshot }}" alt="{{ test.name }} 截图">
            {% endif %}
        </div>
        {% endfor %}
    </body>
    </html>
    """
    
    # 统计数据
    passed_tests = sum(1 for test in test_results if test.get("passed", False))
    failed_tests = len(test_results) - passed_tests
    
    # 渲染报告
    template = Template(template_str)
    html_report = template.render(
        timestamp=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        total_tests=len(test_results),
        passed_tests=passed_tests,
        failed_tests=failed_tests,
        tests=test_results
    )
    
    # 保存报告
    with open("test_report.html", "w", encoding="utf-8") as f:
        f.write(html_report)
    
    print(f"测试报告已生成: test_report.html")

2.15- Playwright 与人工智能结合

结合 OCR 和 AI 进行更智能的自动化测试:

from playwright.sync_api import sync_playwright
import pytesseract
from PIL import Image
import io
import re

def ai_assisted_testing():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        # 访问目标网站
        page.goto("https://example.com")
        
        # 使用OCR识别页面上不易定位的文本
        screenshot = page.screenshot()
        image = Image.open(io.BytesIO(screenshot))
        
        # 使用OCR识别图像中的文本
        detected_text = pytesseract.image_to_string(image, lang='chi_sim+eng')
        
        # 在文本中搜索特定信息
        if re.search(r"错误|失败|Error|Failed", detected_text, re.IGNORECASE):
            print("警告: 检测到页面可能包含错误信息")
            
        # 检查是否存在特定价格信息
        price_matches = re.findall(r"¥\s*(\d+(?:\.\d+)?)", detected_text)
        if price_matches:
            print(f"检测到价格信息: {price_matches}")
        
        # 使用感知哈希算法比较图像相似度
        from imagehash import phash
        
        current_hash = phash(image)
        
        # 与预期图像比较(假设已有基准图像哈希值)
        expected_hash = "f8f8f0f0f0f8f8f8"  # 示例哈希值
        
        # 计算哈希距离(越小越相似)
        hash_distance = sum(c1 != c2 for c1, c2 in zip(str(current_hash), expected_hash))
        similarity = 1 - (hash_distance / 64)  # 64位哈希
        
        print(f"页面视觉相似度: {similarity:.2%}")
        
        browser.close()

3- Playwright 的未来发展与社区生态

3.1- 最新特性与趋势

Playwright 正在快速发展,最新版本 (v1.40+) 引入了许多新特性:

  • 组件测试:直接测试 React、Vue 等前端组件
  • AI 辅助测试生成:使用人工智能自动生成测试用例
  • 增强的追踪功能:更详细的测试步骤记录和回放
  • 跨浏览器组件测试:确保组件在所有浏览器中表现一致

3.2- 与其他工具的比较

工具 优势 劣势
Playwright 跨浏览器、自动等待、强大 API 相对年轻的生态系统
Selenium 成熟稳定、广泛支持 速度慢、需要 WebDriver
Puppeteer 专注 Chrome、高性能 多浏览器支持有限
Cypress 开发体验好、调试简单 跨域限制、多标签页支持弱

3.3- 资源与学习路径

3.3.1- 进阶学习资料

3.3.2- 社区与支持

3.4- 实际应用场景与成功案例

3.4.1- 企业级应用

许多企业已经将 Playwright 应用到其自动化测试和业务流程中:

  • 微软 Teams: 使用 Playwright 测试跨浏览器兼容性
  • Netflix: 用于测试其复杂的用户界面
  • GitHub: 自动化测试与持续集成流程
  • Shopify: 电子商务页面测试和监控

3.4.2- 自动化成功案例

# 某金融企业实现的自动化报表系统
def automated_reporting_system():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context()
        
        # 登录内部系统
        page = context.new_page()
        page.goto("https://internal.example.com")
        
        # 自动认证
        page.fill("#username", os.environ.get("SYSTEM_USER"))
        page.fill("#password", os.environ.get("SYSTEM_PASS"))
        page.click(".login-button")
        
        # 导航到报表页面
        page.click("text=财务报表")
        page.select_option("#report-type", "monthly")
        page.fill("#date-range", "2023-01-01 to 2023-12-31")
        
        # 导出报表
        with page.expect_download() as download_info:
            page.click("#export-excel")
        download = download_info.value
        
        # 处理下载的报表
        download.save_as("monthly_report.xlsx")
        
        # 发送邮件通知
        print("报表已生成并保存")
        
        browser.close()

3.4.3- 电商监控系统

# 自动监控商品价格变化
def price_monitoring_system():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        context = browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.82 Safari/537.36"
        )
        
        # 要监控的商品列表
        products = [
            {"name": "商品A", "url": "https://example.com/product1"},
            {"name": "商品B", "url": "https://example.com/product2"},
            {"name": "商品C", "url": "https://example.com/product3"}
        ]
        
        # 上次的价格记录
        previous_prices = {}
        
        # 循环检查每个商品
        for product in products:
            page = context.new_page()
            page.goto(product["url"])
            
            # 等待价格元素加载
            page.wait_for_selector(".product-price")
            
            # 提取价格
            price_text = page.locator(".product-price").text_content()
            price = float(price_text.replace("¥", "").replace(",", "").strip())
            
            # 检查价格是否发生变化
            if product["name"] in previous_prices:
                old_price = previous_prices[product["name"]]
                if price != old_price:
                    price_change = ((price - old_price) / old_price) * 100
                    print(f"{product['name']} 价格变化: ¥{old_price} → ¥{price} ({price_change:.2f}%)")
                    
                    # 这里可以添加发送通知的代码
                    # send_notification(product, old_price, price)
            
            # 更新价格记录
            previous_prices[product["name"]] = price
            
            # 关闭页面
            page.close()
        
        browser.close()

4- 常见问题与解决方案 (FAQ)

4.1- 运行时问题

4.1.1- Q: 为什么我的选择器找不到元素?

A: 常见原因包括:

  1. 元素在 iframe 内
  2. 元素是动态加载的,需要等待
  3. 选择器语法错误

解决方案:

# 检查元素是否在iframe中
frame = page.frame_locator("iframe").first
frame.locator("your-selector").click()

# 增加等待时间
page.wait_for_selector("your-selector", timeout=30000)

4.1.2- Q: 如何处理隐形验证码 (如 reCAPTCHA)?

A: 可以采用以下策略:

  1. 在测试模式下禁用验证码
  2. 使用已认证的帐户
  3. 通过登录 API 而非 UI 绕过验证
# 绕过验证码的API登录示例
def login_via_api(email, password):
    with sync_playwright() as p:
        browser = p.chromium.launch()
        context = browser.new_context()
        
        # 创建页面获取必要的Cookie和CSRF令牌
        page = context.new_page()
        page.goto("https://example.com/login")
        
        # 提取CSRF令牌
        csrf_token = page.evaluate("""() => {
            return document.querySelector('meta[name="csrf-token"]').getAttribute('content')
        }""")
        
        # 发起API登录请求
        api_page = context.new_page()
        response = api_page.request.post(
            "https://example.com/api/login",
            data={
                "email": email,
                "password": password,
                "_csrf": csrf_token
            }
        )
        
        if response.ok:
            print("API登录成功")
            # 现在可以使用已登录的context进行后续操作
            page.goto("https://example.com/dashboard")
        else:
            print(f"登录失败: {response.status}")
        
        browser.close()

4.2- 性能问题

4.2.1- Q: 如何提高测试运行速度?

A: 以下是一些提升性能的方法:

  1. 使用无头模式 (headless=True)
  2. 禁用图片和字体加载
  3. 利用并行测试
  4. 复用认证状态而非重复登录
# 禁用图片和字体加载
browser = p.chromium.launch(headless=True)
context = browser.new_context(
    # 阻止图片、字体、样式加载以提高速度
    viewport={"width": 1280, "height": 720},
    java_script_enabled=True,
    bypass_csp=True,
    extra_http_headers={"Accept-Language": "zh-CN,zh;q=0.9"},
    # 禁用加载图片和字体
    route_from_har_not_found="abort",
    service_workers="block"
)

# 拦截和阻止不必要资源
page = context.new_page()
page.route("**/*.{png,jpg,jpeg,gif,svg,woff,woff2,ttf}", lambda route: route.abort())

5- 开源贡献与社区参与

Playwright 是一个活跃的开源项目,欢迎社区贡献:

  • 提交问题和功能请求: 通过 GitHub Issues 反馈使用中的问题
  • 贡献代码: 可以通过 Pull Requests 提交改进
  • 创建插件: 开发扩展 Playwright 功能的插件
  • 分享知识: 撰写博客、创建教程或在技术会议上分享经验

参与社区不仅能帮助改进工具,还能提升个人在自动化测试领域的专业知识与影响力。

6- Playwright 最佳实践总结

为了让你的 Playwright 自动化项目更可靠、更高效,请遵循以下最佳实践:

  1. 使用强壮的选择器

    • 优先使用 data-testid 等专用属性
    • 避免基于样式、位置的脆弱选择器
    • 使用可读性强的角色选择器(如 getByRole
  2. 合理的等待策略

    • 避免使用固定的 sleep 时间
    • 使用 waitForSelectorwaitForNavigation
    • 等待网络空闲或 DOM 事件
  3. 结构化的测试代码

    • 采用页面对象模式(POM)
    • 将选择器集中管理
    • 测试、页面操作和数据准备分离
  4. 高效处理认证

    • 使用 storage_state 保存和复用登录状态
    • 通过 API 而非 UI 进行登录
    • 使用环境变量管理敏感凭据
  5. 性能优化

    • 在 CI 中使用无头模式
    • 按需拦截和模拟网络请求
    • 并行运行独立测试

遵循这些实践可以让你的 Playwright 代码更易维护、更可靠,并且执行更快。

7- 结语

Playwright 作为新一代浏览器自动化工具,不仅简化了测试和自动化过程,还提供了更高级的功能来应对现代 Web 应用的复杂性。通过本文介绍的基础知识、高级技巧和最佳实践,你应该能够利用 Playwright 构建可靠、高效的自动化解决方案。

无论你是进行自动化测试、网页爬取还是工作流自动化,Playwright 都能显著提高你的工作效率。希望这篇全面指南能帮助你掌握这个强大工具,并在实际项目中充分发挥其潜力。

随着 Web 技术的不断发展,Playwright 也在持续进化,我们期待看到更多创新功能和应用场景。加入 Playwright 社区,与其他自动化爱好者一同探索这一令人兴奋的领域吧!

祝你自动化愉快!