Python 现代开发学习笔记

(二十二)用 Playwright 执行页面交互

1. 安装浏览器依赖并打开练习页

Playwright 通过浏览器页面执行导航、查找元素和交互。先选择项目的 browser 依赖,再安装它使用的 Chromium:

# 将浏览器工具加入当前项目环境,再安装浏览器程序。
uv sync --locked --extra browser
uv run --extra browser python -m playwright install chromium
# 自动打开本机练习页、点击两次并提取运行后的卡片。
uv run --extra browser python ../demos/browser_collect.py

pip 路线使用 python -m pip install -e ".[browser]" 后,再执行 python -m playwright install chromium。Windows 中也可以用已安装的 Edge,运行示例时加 --channel msedge,代码就通过该浏览器通道启动。

2. 先登记等待,再触发动作

# 项目文件:demos/browser_collect.py
# 先登记响应等待再点击;接口完成后继续等页面卡片出现,最后读取当前 DOM。
import argparse
from urllib.parse import urlsplit

from playwright.sync_api import expect, sync_playwright
from crawlsite import running_site


def collect(base_url, channel=None):
    with sync_playwright() as playwright:
        browser = playwright.chromium.launch(headless=True, channel=channel)
        try:
            page = browser.new_page()
            response = page.goto(base_url + "/dynamic/", wait_until="domcontentloaded")
            if response is None or not response.ok:
                raise RuntimeError("目录页面没有正常打开")
            cards = page.locator("#results article")
            for number, label in [(1, "加载目录"), (2, "下一页")]:
                # 监听先于 click 建立,避免快速响应已经到达后才开始等待。
                with page.expect_response(
                    lambda r: (
                        urlsplit(r.url).path == "/api/catalog"
                        and urlsplit(r.url).query == f"page={number}"
                    )
                ) as pending:
                    page.get_by_role("button", name=label, exact=True).click()
                if not pending.value.ok:
                    raise RuntimeError("目录接口读取失败")
                expect(cards).to_have_count(number * 3)
            ids = [card.get_attribute("data-id") for card in cards.all()]
            return len(ids), sorted(set(ids))
        finally:
            browser.close()


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--channel", help="例如使用已安装的 msedge;省略则用 Playwright Chromium"
    )
    args = parser.parse_args()
    with running_site() as site:
        print(collect(site.base_url, args.channel))

两次点击后共有六张卡片,编号去重为 ['b101', 'b102', 'b103', 'b104'],输出的第一个数是 6。它对应目录前两页,尚未点击第三页。

sync_playwright 管理驱动运行范围,launch 启动浏览器,new_page 建立页面。goto(..., wait_until="domcontentloaded") 等到初始文档解析完成;按钮还没点击,所以此时没有接口卡片。

get_by_role("button", name=..., exact=True) 按角色和显示名称定位按钮;点击动作会等待元素处于可以交互的状态。locator("#results article") 表示当前页面中的一组卡片,后续动作发生后再次按定位器观察页面。Playwright 定位器

登记接口响应等待后点击按钮,响应到达后再等卡片数量更新,最后读取当前DOM

3. 网络响应到达与页面更新是两步

expect_response 在 click 之前建立等待,响应太快时也已经有人接收。谓词同时检查路径与 page 查询,避免把另一页响应当作本次完成。pending.value 取得对应响应,expect(cards).to_have_count(...) 则继续等页面真正出现要求数量的卡片。Playwright 网络观察

这里没有用 sleep(3) 猜测加载时间:接口到达后,脚本还要读取 JSON 和修改 DOM;页面元素出现后才读取 data-id。循环第二轮同样等待第二页,最后 browser.close 在 finally 里执行,某次交互出错也会关闭本次浏览器。

如果要保存运行后的 HTML,可以在满足页面检查后调用 page.content(),再交给 Beautiful Soup 处理。它取得当前 DOM 的 HTML 表示,已不同于最初 HTTP 响应中的文本。

笔记正文与全部题解 · Ctrl / ⌘ K