Python 爬虫(四):动态页面——先找数据,再考虑上浏览器

Posted on Fri 16 October 2026 in 技术 • Sucre Liu

"JS 渲染"不等于"只能上浏览器"

当 requests 拿到的 HTML 里没有你想要的内容,第一反应往往是"上 Selenium/Playwright"。但上浏览器是最贵的方案:内存、CPU、速度、稳定性,代价都不小。

更重要的是,很多所谓"动态页面",数据其实就在你眼皮底下。决策顺序应该是:

  1. 数据是否内嵌在 HTML 里(<script> 中的 JSON)?→ 直接抽,不用浏览器。
  2. 是否有独立的接口(Network 里的 XHR/fetch)?→ 直接打接口。
  3. 以上都不行,才轮到 Playwright。

按这个顺序,你会发现大部分需求在第 1、2 步就解决了。本文基于 Playwright 1.61。

第 1 层:数据内嵌在页面里

看一个"JS 渲染"的真实例子——quotes.toscrape.com/js/。直接请求它,HTML 里没有任何 .quote 元素:

import requests
html = requests.get("https://quotes.toscrape.com/js/", timeout=10).text
print(html.count('class="quote"'))     # 0 —— 你以为要上浏览器

但往下翻,它在页面里塞了一个 <script>:

<script>
    var data = [ {"tags": [...], "author": {...}, "text": "..."}, ... ];
    for (var i in data) { document.write("<div class='quote'>...") }
</script>

数据是完整的、结构化的 JSON,就写在 HTML 里。 直接抽它,比上浏览器快几十倍,也稳得多:

import json, re, requests

html = requests.get("https://quotes.toscrape.com/js/", timeout=10).text
m = re.search(r"var data = (\[.*?\]);", html, re.S)
quotes = json.loads(m.group(1)) if m else []
print(len(quotes), quotes[0]["author"]["name"])

同样的套路适用于现代前端框架:Next.js 的 window.__NEXT_DATA__、Nuxt 的 window.__NUXT__、Vue/React 服务端注入的 window.__INITIAL_STATE__。页面看着是 JS 渲染的,数据却常常已经在初始 HTML 里了。

注意:内嵌脚本不一定是合法 JSON(可能用单引号、有 undefined、经过转义)。先尝试 json.loads,不行再退到更宽松的解析或正则。

第 2 层:找到真正的接口

如果数据不在 HTML 里,它多半来自某个接口。打开浏览器 DevTools → Network → Fetch/XHR,刷新页面,看有没有返回 JSON 的请求。

找到之后,直接打这个接口:

r = requests.get(
    "https://example.com/api/items",
    params={"page": 1, "size": 20},
    headers={"User-Agent": UA, "Referer": "https://example.com/list"},
    timeout=10,
)
data = r.json()

要点:

  • 接口往往需要 Referer、Authorization 或分页参数,照着 Network 里的原样抄。
  • 接口返回的是纯数据,没有 HTML 解析的脆弱性——这是最理想的情况。
  • 有些接口是 GraphQL(都是 POST,body 是 query),照样能打。

先找接口,再考虑渲染,是省时间的第一原则。

第 3 层:确实需要浏览器时

只有当数据确实是运行时才生成(依赖复杂 JS、需要点击/滚动/拖动),或者你需要交互(登录后操作、无限滚动、评测验证),才上 Playwright。

安装:

pip install playwright
playwright install chromium        # Linux 服务器可加 --with-deps

同步 API 的一个完整例子:

from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup

URL = "https://quotes.toscrape.com/js/"
UA = "MySpider/1.0 (+https://example.com/bot)"

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    ctx = browser.new_context(user_agent=UA, viewport={"width": 1280, "height": 800})

    # 提速:直接丢掉图片/字体/媒体等无关资源
    ctx.route(
        "**/*",
        lambda route: route.abort()
        if route.request.resource_type in {"image", "font", "media"}
        else route.continue_(),
    )

    page = ctx.new_page()
    page.goto(URL, wait_until="domcontentloaded", timeout=30_000)

    # 关键:等真正的目标元素出现,而不是靠 sleep
    page.wait_for_selector("div.quote", timeout=15_000)

    html = page.content()          # 此时 DOM 已渲染
    browser.close()

soup = BeautifulSoup(html, "lxml")
print(len(soup.select("div.quote")))

几个必须掌握的点:

  • wait_for_selector 而不是 time.sleep。固定睡眠既慢又不可靠;等元素出现才是正确的同步方式。
  • 谨慎用 wait_until="networkidle"。长轮询、WebSocket、埋点心跳会让页面永远"不空闲",这个等待常常超时或永不返回。用 domcontentloaded + 显式元素等待更稳。
  • context 隔离 Cookie 与存储。并发时每个任务一个 context,别共用一个 page。
  • locator 自带自动等待:page.locator("div.quote").count() 比 query_selector_all 更符合 Playwright 的设计意图。
  • 异步场景用 playwright.async_api,可配合 asyncio 并发多个 page——但并发数要克制。

一个绕不开的成本对比

直接请求(httpx/requests) Playwright
内存 低 高(每个浏览器进程数百 MB 起)
速度 每秒可数十请求 每秒常只有个位数
稳定性 高 受浏览器/驱动/渲染影响
检测风险 低 高(指纹、navigator.webdriver 等)
能力 无 JS 完整 JS 与交互

结论很直接:能用 HTTP 解决的,绝不上浏览器。

关于"反爬"与 Playwright 的关系

很多站会检测无头浏览器并拦截。这里要摆正态度:

  • 不要以"对抗检测"为目标。伪装指纹、注入脚本绕过检测,既不稳定,也可能违反服务条款。
  • 如果站点明确在拦截自动化,先问它有没有官方 API。没有的话,评估是否值得做,以及是否合规。
  • Playwright 的正当用途是自动化你自己的操作和抓取允许抓取的动态站点,不是"破防"。

实操建议

  1. 复用浏览器实例:一个 browser 开多个 context,比每次启动浏览器快得多。
  2. 屏蔽无关资源(图片/字体/媒体/统计),能显著提速。
  3. 设置合理超时,并在 finally 里确保关闭浏览器,避免僵尸进程。
  4. 调试时用 headless=False + page.screenshot() 看清到底渲染成了什么。
  5. 把"找数据层"固化进流程:每次遇到动态页,先花 5 分钟看 Network 和页面脚本,再决定要不要浏览器。

小结

  • "JS 渲染"≠"必须上浏览器"。先找内嵌 JSON,再找XHR 接口,最后才是 Playwright。
  • quotes.toscrape.com/js/ 就是反例:数据明明写在 <script> 里。
  • 用 Playwright 时,等元素而不是 sleep,慎用 networkidle,隔离 context,屏蔽无关资源。
  • 浏览器的成本与检测风险都高,能 HTTP 就 HTTP。
  • 不把"绕过反爬"当目标;优先官方 API,合规优先。

下一篇讲工程化与反爬:重试与退避怎么写、并发怎么控、代理怎么用、增量抓取与去重怎么做——把脚本变成能长期跑的系统。