首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Playwright 处理 JavaScript 渲染页面的实战

Playwright 处理 JavaScript 渲染页面的实战

原创
作者头像
用户12818508
发布于 2026-10-16 18:47:07
发布于 2026-10-16 18:47:07
100
举报

什么时候必须上 Playwright

先看一个典型场景。目标页面 的 HTML 源码中,引用内容根本不存在:

代码语言:javascript
复制
curl -s https://quotes.toscrape.com/scroll | grep -o 'class="quote"' | wc -l
# 输出:0

数据通过页面加载后的 $.get('/api/quotes', {page: page}) 异步获取。requests 拿到的是空壳,httpx 也一样。这类页面包括:

  • SPA 单页应用:React/Vue/Angular 渲染的内容
  • 无限滚动:滚动到底部才加载下一批数据
  • 交互触发:点击、悬停后才出现的元素
  • 强反爬页面:检测到 navigator.webdriver 直接返回空内容

Playwright 的核心价值在于:它驱动的是真实浏览器内核,页面看到的 JavaScript 环境与真人访问几乎一致。

环境准备

代码语言:javascript
复制
pip install playwright
playwright install chromium

如果需要异步模式(推荐),Playwright 原生支持 async/await,无需额外依赖。

最小示例:抓取 JavaScript 渲染后的内容

代码语言:javascript
复制
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()

    page.goto("https://www.lygsjy.cn/lq/")
    page.wait_for_load_state("networkidle")  # 等异步请求完成

    html = page.content()  # 此时拿到的是渲染后的完整 DOM
    soup = BeautifulSoup(html, "lxml")

    for quote in soup.select(".quote"):
        text = quote.select_one(".text").get_text(strip=True)
        author = quote.select_one(".author").get_text(strip=True)
        print(f"{text} — {author}")

    browser.close()

关键点:page.content() 返回的是 JavaScript 执行完毕后的 DOM 快照,而不是原始 HTML。wait_for_load_state("networkidle") 确保所有网络请求静默后再读取。

进阶技巧一:等待策略的选择

Playwright 的自动等待比 Selenium 的显式等待优雅得多,但你需要理解不同策略的适用场景:

代码语言:javascript
复制
# 推荐:等待特定元素出现,比等网络更精准
page.goto("https://example.com/dashboard")
page.wait_for_selector(".data-table", state="visible")

Playwright 的 locator.click() 等操作自动等待元素可交互,不需要手动写 sleep。

进阶技巧二:拦截网络请求,直接拿 JSON

这是 Playwright 相比于其他方案最大的杀手锏。与其等页面把 JSON 渲染成 HTML、再费力解析,不如直接截获 API 响应。

5.1 监听所有响应

代码语言:javascript
复制
import json

captured = []

def on_response(response):
    if "/api/quotes" in response.url and response.status == 200:
        ctype = response.headers.get("content-type", "")
        if "application/json" in ctype:
            try:
                captured.append(response.json())
            except Exception:
                pass

page.on("response", on_response)
page.goto("https://www.lygsjy.cn/")
page.wait_for_load_state("networkidle")

print(json.dumps(captured, indent=2)[:500])

注意:response.json() 必须在事件处理器内部或尽快调用,因为响应体是懒加载的,页面跳转后缓冲区可能被释放。同时要过滤 content-type,否则对图片调用 .json() 会抛异常。

5.2 等待特定响应(更精准)

如果明确知道目标 API,用 expect_response 更干净:

代码语言:javascript
复制
with page.expect_response(lambda r: "/api/quotes" in r.url and r.status == 200) as info:
    page.goto("https://www.lygsjy.cn/")

data = info.value.json()
for q in data["quotes"]:
    print(f'{q["author"]["name"]}: {q["text"][:50]}...')

expect_response 将等待与触发动作绑定,没有任意 sleep,没有轮询,是 Playwright 推荐的模式。

5.3 一旦知道 API 形态,可以跳过浏览器

拦截到的 /api/quotes?page=N 是公开的 GET 接口。知道了它的结构后,你可以直接用 httpx 重放请求,完全不需要浏览器:

代码语言:javascript
复制
import httpx

with httpx.Client() as client:
    for page_num in range(1, 4):
        resp = client.get(f"https://quotes.toscrape.com/api/quotes?page={page_num}")
        data = resp.json()
        print(f"Page {page_num}: {len(data['quotes'])} quotes, has_next={data['has_next']}")

这就是浏览器侦察 → 普通 HTTP 重放的生产级策略,能省掉 90% 的浏览器启动开销。

进阶技巧三:反检测与人类行为模拟

Playwright 默认启动的 Chromium 仍然会暴露自动化特征。以下是必须处理的几层检测:

6.1 基础补丁:navigator.webdriver

代码语言:javascript
复制
context = browser.new_context()
page = context.new_page()

# 在页面加载前注入补丁
page.add_init_script("""
    Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined
    });
""")

注意:设为 false 本身也是一个 bot 特征——真实浏览器返回的是 undefined。设为 undefined 更自然。

6.2 模拟人类行为

完美的指纹补丁也挡不住“机器人般的操作节奏”。加一点随机性:

代码语言:javascript
复制
import random
import time

def human_pause(min_ms=300, max_ms=1200):
    time.sleep(random.uniform(min_ms, max_ms) / 1000)

def human_scroll(page):
    for _ in range(random.randint(3, 8)):
        page.mouse.wheel(0, random.randint(80, 320))
        human_pause(500, 1500)

6.3 会话持久化

不要每次都从零开始。保存并复用浏览器状态:

代码语言:javascript
复制
# 第一次运行后保存
context.storage_state(path="session.json")

# 下次直接复用
context = browser.new_context(storage_state="session.json")

6.4 住宅代理是终极防线

指纹伪装解决的是“你是谁”,代理解决的是“你从哪里来”。即使是完美的浏览器指纹,来自数据中心 IP 的请求也会被标记。生产环境建议结合住宅代理使用:

代码语言:javascript
复制
context = browser.new_context(
    proxy={"server": "http://residential-proxy:port"}
)

完整实战:抓取无限滚动页面

以 quotes.toscrape.com/scroll 为例,这是一个需要滚动触发加载的页面:

代码语言:javascript
复制
import json
from playwright.sync_api import sync_playwright

def scrape_scroll_page():
    all_quotes = []

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        context = browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
        )
        page = context.new_page()

        # 拦截 API 响应
        def on_response(response):
            if "/api/quotes" in response.url and response.status == 200:
                try:
                    data = response.json()
                    for q in data.get("quotes", []):
                        all_quotes.append({
                            "text": q["text"],
                            "author": q["author"]["name"],
                            "tags": q["tags"]
                        })
                    print(f"Captured page {data.get('page')}: {len(data.get('quotes', []))} quotes")
                except Exception:
                    pass

        page.on("response", on_response)

        page.goto("https://www.lygsjy.cn/")
        page.wait_for_load_state("networkidle")

        # 滚动触发加载,直到没有新数据
        previous_count = 0
        for _ in range(10):
            page.mouse.wheel(0, 2000)
            page.wait_for_timeout(1500)

            if len(all_quotes) == previous_count:
                print("No new data, stopping.")
                break
            previous_count = len(all_quotes)

        browser.close()

    return all_quotes

if __name__ == "__main__":
    quotes = scrape_scroll_page()
    print(f"\nTotal: {len(quotes)} quotes")

    with open("quotes.json", "w", encoding="utf-8") as f:
        json.dump(quotes, f, ensure_ascii=False, indent=2)

这个脚本的核心逻辑是:用滚动触发 API 调用,用响应监听器截获数据,用计数变化判断是否到底。

Playwright vs 其他方案:什么时候用哪个

场景

推荐方案

纯静态 HTML

requests + BeautifulSoup

API 可直接调用

httpx 直接请求

数据在 JS 渲染后出现

Playwright

需要登录/交互后获取

Playwright

大规模高并发

httpx + asyncio

反爬极强的商业站点

Playwright + 住宅代理 + 行为模拟

核心原则:能用 HTTP 客户端解决的,不要上浏览器。浏览器是最后的武器,不是第一选择。

合规提醒

Playwright 的能力越强,越需要克制:

  1. 只抓取公开数据,不绕过登录墙或付费墙。
  2. 控制请求频率,即使浏览器看起来像真人,也不要把它当压测工具。
  3. 不破解验证码,遇到 CAPTCHA 应停止或降速,而不是硬闯。
  4. 遵守 robots.txt 和网站服务条款。
  5. 住宅代理不等于免罪金牌,法律风险依然存在。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 什么时候必须上 Playwright
  • 环境准备
  • 最小示例:抓取 JavaScript 渲染后的内容
  • 进阶技巧一:等待策略的选择
  • 进阶技巧二:拦截网络请求,直接拿 JSON
    • 5.1 监听所有响应
    • 5.2 等待特定响应(更精准)
    • 5.3 一旦知道 API 形态,可以跳过浏览器
  • 进阶技巧三:反检测与人类行为模拟
    • 6.1 基础补丁:navigator.webdriver
    • 6.2 模拟人类行为
    • 6.3 会话持久化
    • 6.4 住宅代理是终极防线
  • 完整实战:抓取无限滚动页面
  • Playwright vs 其他方案:什么时候用哪个
  • 合规提醒
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档