先看一个典型场景。目标页面 的 HTML 源码中,引用内容根本不存在:
curl -s https://quotes.toscrape.com/scroll | grep -o 'class="quote"' | wc -l
# 输出:0数据通过页面加载后的 $.get('/api/quotes', {page: page}) 异步获取。requests 拿到的是空壳,httpx 也一样。这类页面包括:
navigator.webdriver 直接返回空内容Playwright 的核心价值在于:它驱动的是真实浏览器内核,页面看到的 JavaScript 环境与真人访问几乎一致。
pip install playwright
playwright install chromium如果需要异步模式(推荐),Playwright 原生支持 async/await,无需额外依赖。
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 的显式等待优雅得多,但你需要理解不同策略的适用场景:
# 推荐:等待特定元素出现,比等网络更精准
page.goto("https://example.com/dashboard")
page.wait_for_selector(".data-table", state="visible")Playwright 的 locator.click() 等操作自动等待元素可交互,不需要手动写 sleep。
这是 Playwright 相比于其他方案最大的杀手锏。与其等页面把 JSON 渲染成 HTML、再费力解析,不如直接截获 API 响应。
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() 会抛异常。
如果明确知道目标 API,用 expect_response 更干净:
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 推荐的模式。
拦截到的 /api/quotes?page=N 是公开的 GET 接口。知道了它的结构后,你可以直接用 httpx 重放请求,完全不需要浏览器:
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 仍然会暴露自动化特征。以下是必须处理的几层检测:
navigator.webdrivercontext = browser.new_context()
page = context.new_page()
# 在页面加载前注入补丁
page.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
""")注意:设为 false 本身也是一个 bot 特征——真实浏览器返回的是 undefined。设为 undefined 更自然。
完美的指纹补丁也挡不住“机器人般的操作节奏”。加一点随机性:
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)不要每次都从零开始。保存并复用浏览器状态:
# 第一次运行后保存
context.storage_state(path="session.json")
# 下次直接复用
context = browser.new_context(storage_state="session.json")指纹伪装解决的是“你是谁”,代理解决的是“你从哪里来”。即使是完美的浏览器指纹,来自数据中心 IP 的请求也会被标记。生产环境建议结合住宅代理使用:
context = browser.new_context(
proxy={"server": "http://residential-proxy:port"}
)以 quotes.toscrape.com/scroll 为例,这是一个需要滚动触发加载的页面:
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 调用,用响应监听器截获数据,用计数变化判断是否到底。
场景 | 推荐方案 |
|---|---|
纯静态 HTML | requests + BeautifulSoup |
API 可直接调用 | httpx 直接请求 |
数据在 JS 渲染后出现 | Playwright |
需要登录/交互后获取 | Playwright |
大规模高并发 | httpx + asyncio |
反爬极强的商业站点 | Playwright + 住宅代理 + 行为模拟 |
核心原则:能用 HTTP 客户端解决的,不要上浏览器。浏览器是最后的武器,不是第一选择。
Playwright 的能力越强,越需要克制:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。