(二十二)用 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 定位器
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 响应中的文本。