🔥本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!!

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [📌 上期回顾](#📌 上期回顾)
- [🎯 本期目标](#🎯 本期目标)
- [💡 为什么需要Playwright?](#💡 为什么需要Playwright?)
- [🔧 技术方案拆解](#🔧 技术方案拆解)
- [📝 完整实现](#📝 完整实现)
- [🔍 代码关键点解析](#🔍 代码关键点解析)
-
- [1. 等待策略的选择](#1. 等待策略的选择)
- [2. 截图的正确时机](#2. 截图的正确时机)
- [3. 资源拦截提速](#3. 资源拦截提速)
- [4. 上下文复用](#4. 上下文复用)
- [📊 实战验收](#📊 实战验收)
- 🎨进阶优化方向
-
- [1. 滚动加载处理](#1. 滚动加载处理)
- [2. Cookie管理](#2. Cookie管理)
- [3. 弹窗处理](#3. 弹窗处理)
- [4. 视频截图](#4. 视频截图)
- [5. 并发控制](#5. 并发控制)
- [6. 移动端模拟](#6. 移动端模拟)
- [⚠️ 常见坑点](#⚠️ 常见坑点)
-
- [1. 无头模式调试困难](#1. 无头模式调试困难)
- [2. 选择器不稳定](#2. 选择器不稳定)
- [3. 内存泄漏](#3. 内存泄漏)
- [4. 超时设置不合理](#4. 超时设置不合理)
- [5. 忽略iframe](#5. 忽略iframe)
- [💼 实际应用场景](#💼 实际应用场景)
- [🎯 本期总结](#🎯 本期总结)
- [📖 下期预告](#📖 下期预告)
- [🌟 文末](#🌟 文末)
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》
订阅后更新会优先推送,按目录学习更高效~
📌 上期回顾
上一期《Python爬虫零基础入门【第九章:实战项目教学·第10节】下载型资源采集:PDF/附件下载 + 去重校验!》我们搞定了文件下载的工程化------流式下载、SHA256去重、断点续传、文件验证。从此PDF、Excel等附件资源也能批量采集,不重复、不遗漏。
但现实给了我们当头一棒:打开一个新闻站点,页面空空如也,右键查看源代码也是空的,只有几行<script>标签应用大量使用JavaScript动态渲染。数据不在HTML里,而是前端通过AJAX请求拿到后用React/Vue渲染出来。传统的requests只能拿到空壳HTML,根本抓不到真实内容。
这时候你需要一个真实的浏览器。 今天咱们就用Playwright来搞定动态页面。
🎯 本期目标
这一期你会得到:
- Playwright基础操作:打开页面、等待加载、获取渲染后HTML
- 等待策略精讲:元素可见、网络空闲、超时控制
- 截图调试技巧:每一步都截图,问题无处藏身📸
- 常见坑点规避:无头模式、弹窗处理、Cookie管理
- 性能对比:Playwright vs Requests,什么场景该用谁
验收标准很直接:能稳定采集50个动态页面,每个都有截图验证,失败率<5%
💡 为什么需要Playwright?
动态页面的三种形式
形式1:AJAX异步加载
html
<!-- 初始HTML -->
<div id="content">加载中...</div>
<!-- JavaScript执行后 -->
<div id="content">
<h1>新闻标题</h1>
<p>新闻正文...</p>
</div>
requests拿到的是"加载中...",Playwright能等到真实内容出现。
形式2:无限滚动
javascript
window.addEventListener('scroll', () => {
if (reachedBottom) {
loadMoreData(); // 滚动到底部才加载
}
});
requests根本触发不了滚动事件,Playwright可以模拟真实用户滚动。
形式3:点击翻页
html
<button onclick="loadNextPage()">下一页</button>
没有真实的URL跳转,点击按钮才触发AJAX请求。Playwright能模拟点击。
Requests的局限
python
response = requests.get(url)
print(response.text)
# 输出:<div id="root"></div> 空的!
**为什么?**因为requests只能拿到服务器返回的原始HTML,不执行JavaScript。
Playwright的优势
python
page.goto(url)
page.wait_for_selector('#content') # 等待元素出现
html = page.content() # 拿到渲染后的完整HTML
本质:Playwright控制一个真实的浏览器(Chromium/Firefox/WebKit),和你手动打开浏览器看到的页面完全一致。
🔧 技术方案拆解
方案一:等待策略
动态页面的核心难点是:不知道什么时候加载完。
Playwright提供多种等待策略:
-
等待元素出现
pythonpage.wait_for_selector('.article-title', timeout=10000) -
等待网络空闲
pythonpage.goto(url, wait_until='networkidle') -
等待指定时间
pythonpage.wait_for_timeout(3000) # 等3秒(不推荐) -
等待函数返回True
pythonpage.wait_for_function('() => document.querySelectorAll(".item").length >= 20')
选择原则:优先用元素等待,其次网络空闲,最后才固定延迟。
方案二:截图调试
python
page.screenshot(path='step1_opened.png')
page.click('.load-more')
page.screenshot(path='step2_clicked.png')
为什么截图这么重要?
- 远程服务器跑爬虫,你看不到页面长啥样
- 失败时留下现场,方便复盘
- 对比截图能快速发现变化
方案三:选择器策略
Playwright支持多种选择器:
python
# CSS选择器(最常用)
page.locator('.article-title')
# XPath
page.locator('xpath=//div[@class="content"]')
# 文本匹配
page.locator('text=下一页')
# 组合
page.locator('button:has-text("提交")')
技巧:用Chrome DevTools先验证选择器,再写到代码里。
方案四:性能优化
python
# 禁用图片/CSS(提速50%+)
context.route('**/*.{png,jpg,jpeg,gif,svg,css}', lambda route: route.abort())
# 无头模式(节省资源)
browser = playwright.chromium.launch(headless=True)
# 复用浏览器上下文(减少启动时间)
context = browser.new_context()
📝 完整实现
整体架构说明
核心组件:
- PlaywrightClient:浏览器客户端封装
- WaitStrategy:等待策略集合
- ScreenshotHelper:截图助手
- PlaywrightSpider:动态爬虫基类
数据流向:
json
启动浏览器 → 打开页面 → 等待加载 → 截图验证 → 提取数据 → 保存HTML → 关闭浏览器
代码实现
python
"""
Playwright动态爬虫工具包
功能:页面渲染、等待策略、截图调试
"""
import json
import time
import hashlib
from pathlib import Path
from typing import Optional, Dict, Any, List, Callable
from dataclasses import dataclass, asdict
from datetime import datetime
import logging
from playwright.sync_api import (
sync_playwright,
Browser,
BrowserContext,
Page,
TimeoutError as PlaywrightTimeout
)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
# =============================================================================
# 1. 配置与数据类
# =============================================================================
@dataclass
class PlaywrightConfig:
"""Playwright配置"""
headless: bool = True # 无头模式
timeout: int = 30000 # 默认超时(毫秒)
wait_until: str = "domcontentloaded" # 页面加载等待策略
# 性能优化
disable_images: bool = False # 禁用图片
disable_css: bool = False # 禁用CSS
# 浏览器参数
viewport: Dict[str, int] = None # 视口大小
user_agent: Optional[str] = None # User-Agent
# 截图
screenshot_on_error: bool = True # 失败时自动截图
screenshot_dir: Path = Path("screenshots")
def __post_init__(self):
if self.viewport is None:
self.viewport = {"width": 1920, "height": 1080}
self.screenshot_dir.mkdir(parents=True, exist_ok=True)
@dataclass
class PageResult:
"""页面采集结果"""
url: str
html: str
screenshot_path: Optional[str]
load_time: float
success: bool
error: Optional[str] = None
metadata: Dict[str, Any] = None
def __post_init__(self):
if self.metadata is None:
self.metadata = {}
# =============================================================================
# 2. 等待策略
# =============================================================================
class WaitStrategy:
"""等待策略集合"""
@staticmethod
def wait_for_element(
page: Page,
selector: str,
timeout: int = 10000,
state: str = "visible"
):
"""
等待元素出现
Args:
page: 页面对象
selector: CSS选择器
timeout: 超时时间(毫秒)
state: 元素状态(visible/attached/hidden)
"""
try:
page.wait_for_selector(
selector,
timeout=timeout,
state=state
)
logger.debug(f"元素已出现: {selector}")
return True
except PlaywrightTimeout:
logger.warning(f"元素等待超时: {selector}")
return False
@staticmethod
def wait_for_network_idle(
page: Page,
timeout: int = 30000,
idle_time: int = 500
):
"""
等待网络空闲
Args:
page: 页面对象
timeout: 总超时时间
idle_time: 空闲时长(毫秒)
"""
try:
page.wait_for_load_state("networkidle", timeout=timeout)
logger.debug("网络已空闲")
return True
except PlaywrightTimeout:
logger.warning("网络空闲等待超时")
return False
@staticmethod
def wait_for_function(
page: Page,
script: str,
timeout: int = 10000
):
"""
等待JavaScript表达式返回True
Args:
page: 页面对象
script: JavaScript代码
timeout: 超时时间
"""
try:
page.wait_for_function(script, timeout=timeout)
logger.debug(f"函数条件已满足: {script[:50]}...")
return True
except PlaywrightTimeout:
logger.warning(f"函数等待超时: {script[:50]}...")
return False
@staticmethod
def smart_wait(
page: Page,
selector: Optional[str] = None,
network_idle: bool = False,
custom_script: Optional[str] = None,
timeout: int = 10000
):
"""
智能等待(组合策略)
优先级:元素 > 自定义脚本 > 网络空闲
"""
if selector:
return WaitStrategy.wait_for_element(page, selector, timeout)
if custom_script:
return WaitStrategy.wait_for_function(page, custom_script, timeout)
if network_idle:
return WaitStrategy.wait_for_network_idle(page, timeout)
# 兜底:固定延迟
page.wait_for_timeout(timeout)
return True
# =============================================================================
# 3. 截图助手
# =============================================================================
class ScreenshotHelper:
"""截图助手"""
def __init__(self, screenshot_dir: Path = Path("screenshots")):
self.screenshot_dir = screenshot_dir
self.screenshot_dir.mkdir(parents=True, exist_ok=True)
self.counter = 0
def generate_filename(self, prefix: str = "page", url: str = "") -> str:
"""生成截图文件名"""
self.counter += 1
# 从URL提取域名
from urllib.parse import urlparse
domain = urlparse(url).netloc.replace(':', '_') if url else "unknown"
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{prefix}_{domain}_{timestamp}_{self.counter:04d}.png"
return str(self.screenshot_dir / filename)
def take_screenshot(
self,
page: Page,
path: Optional[str] = None,
prefix: str = "page",
full_page: bool = False
) -> str:
"""
截图
Args:
page: 页面对象
path: 指定路径(可选)
prefix: 文件名前缀
full_page: 是否全页截图
Returns:
截图路径
"""
if not path:
path = self.generate_filename(prefix, page.url)
try:
page.screenshot(path=path, full_page=full_page)
logger.info(f"截图已保存: {path}")
return path
except Exception as e:
logger.error(f"截图失败: {e}")
return ""
# =============================================================================
# 4. Playwright客户端
# =============================================================================
class PlaywrightClient:
"""
Playwright浏览器客户端
功能:
- 浏览器生命周期管理
- 页面操作封装
- 错误处理与重试
"""
def __init__(self, config: Optional[PlaywrightConfig] = None):
self.config = config or PlaywrightConfig()
self.playwright = None
self.browser: Optional[Browser] = None
self.context: Optional[BrowserContext] = None
self.screenshot_helper = ScreenshotHelper(self.config.screenshot_dir)
# 统计
self.stats = {
'total_pages': 0,
'success': 0,
'failed': 0,
'screenshots': 0
}
def start(self):
"""启动浏览器"""
if self.browser:
logger.warning("浏览器已启动")
return
logger.info("启动Playwright浏览器...")
self.playwright = sync_playwright().start()
# 启动Chromium
self.browser = self.playwright.chromium.launch(
headless=self.config.headless
)
# 创建上下文
context_options = {
'viewport': self.config.viewport,
'user_agent': self.config.user_agent,
}
self.context = self.browser.new_context(**context_options)
# 性能优化:拦截资源
if self.config.disable_images or self.config.disable_css:
self._setup_resource_blocking()
logger.info("浏览器已启动")
def _setup_resource_blocking(self):
"""设置资源拦截"""
block_patterns = []
if self.config.disable_images:
block_patterns.extend([
'**/*.png', '**/*.jpg', '**/*.jpeg',
'**/*.gif', '**/*.svg', '**/*.webp'
])
if self.config.disable_css:
block_patterns.append('**/*.css')
def handle_route(route):
if any(route.request.url.endswith(ext) for ext in block_patterns):
route.abort()
else:
route.continue_()
self.context.route('**/*', handle_route)
logger.info(f"已拦截资源类型: {block_patterns}")
def stop(self):
"""关闭浏览器"""
if self.context:
self.context.close()
if self.browser:
self.browser.close()
if self.playwright:
self.playwright.stop()
logger.info("浏览器已关闭")
def fetch_page(
self,
url: str,
wait_selector: Optional[str] = None,
wait_network_idle: bool = False,
screenshot: bool = True,
save_html: bool = True
) -> PageResult:
"""
采集页面
Args:
url: 目标URL
wait_selector: 等待的元素选择器
wait_network_idle: 是否等待网络空闲
screenshot: 是否截图
save_html: 是否保存HTML
Returns:
PageResult
"""
if not self.browser:
self.start()
self.stats['total_pages'] += 1
start_time = time.time()
page = self.context.new_page()
screenshot_path = None
html_content = ""
try:
# 1. 打开页面
logger.info(f"正在访问: {url}")
page.goto(
url,
wait_until=self.config.wait_until,
timeout=self.config.timeout
)
# 2. 等待加载
if wait_selector:
WaitStrategy.wait_for_element(
page,
wait_selector,
timeout=self.config.timeout
)
if wait_network_idle:
WaitStrategy.wait_for_network_idle(
page,
timeout=self.config.timeout
)
# 3. 截图
if screenshot:
screenshot_path = self.screenshot_helper.take_screenshot(
page,
prefix="success",
full_page=False
)
self.stats['screenshots'] += 1
# 4. 获取HTML
if save_html:
html_content = page.content()
load_time = time.time() - start_time
self.stats['success'] += 1
logger.info(
f"页面加载成功 | 耗时:{load_time:.2f}s | "
f"HTML长度:{len(html_content)}"
)
return PageResult(
url=url,
html=html_content,
screenshot_path=screenshot_path,
load_time=load_time,
success=True,
metadata={'title': page.title()}
)
except Exception as e:
load_time = time.time() - start_time
self.stats['failed'] += 1
logger.error(f"页面加载失败: {url} | {type(e).__name__}: {e}")
# 失败时截图
if self.config.screenshot_on_error:
try:
screenshot_path = self.screenshot_helper.take_screenshot(
page,
prefix="error"
)
self.stats['screenshots'] += 1
except:
pass
return PageResult(
url=url,
html="",
screenshot_path=screenshot_path,
load_time=load_time,
success=False,
error=str(e)
)
finally:
page.close()
def print_stats(self):
"""打印统计信息"""
print("\n" + "="*60)
print("📊 Playwright采集统计")
print("="*60)
print(f"总页面数: {self.stats['total_pages']}")
print(f"成功: {self.stats['success']}")
print(f"失败: {self.stats['failed']}")
print(f"成功率: {self.stats['success']/self.stats['total_pages']*100:.1f}%")
print(f"截图数: {self.stats['screenshots']}")
print("="*60 + "\n")
# =============================================================================
# 5. 动态爬虫基类
# =============================================================================
class PlaywrightSpider:
"""
动态爬虫基类
子类只需实现parse方法
"""
def __init__(
self,
name: str = "playwright_spider",
config: Optional[PlaywrightConfig] = None
):
self.name = name
self.client = PlaywrightClient(config)
# 结果保存
self.output_dir = Path(f"output/{name}")
self.output_dir.mkdir(parents=True, exist_ok=True)
self.results: List[Dict[str, Any]] = []
def parse(self, html: str, url: str) -> Dict[str, Any]:
"""
解析页面(子类实现)
Args:
html: 渲染后的HTML
url: 页面URL
Returns:
解析结果
"""
raise NotImplementedError("子类需实现parse方法")
def run(self, urls: List[str]):
"""批量采集"""
logger.info(f"开始批量采集 | 总数:{len(urls)}")
self.client.start()
try:
for i, url in enumerate(urls, 1):
logger.info(f"[{i}/{len(urls)}] {url}")
# 采集页面
result = self.client.fetch_page(url)
if result.success:
# 解析
try:
data = self.parse(result.html, url)
data['_meta'] = {
'url': url,
'screenshot': result.screenshot_path,
'load_time': result.load_time
}
self.results.append(data)
logger.info(f"解析成功 | 字段数:{len(data)}")
except Exception as e:
logger.error(f"解析失败: {e}")
else:
logger.error(f"采集失败: {result.error}")
self._save_results()
finally:
self.client.stop()
self.client.print_stats()
def _save_results(self):
"""保存结果"""
if not self.results:
logger.warning("无结果可保存")
return
output_file = self.output_dir / f"results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jsonl"
with open(output_file, 'w', encoding='utf-8') as f:
for item in self.results:
f.write(json.dumps(item, ensure_ascii=False) + '\n')
logger.info(f"结果已保存: {output_file} | 共{len(self.results)}条")
# =============================================================================
# 6. 使用示例
# =============================================================================
class DemoSpider(PlaywrightSpider):
"""示例爬虫"""
def parse(self, html: str, url: str) -> Dict[str, Any]:
"""解析页面"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
title = soup.find('title')
h1_elements = soup.find_all('h1')
return {
'title': title.get_text(strip=True) if title else "",
'h1_count': len(h1_elements),
'html_length': len(html)
}
def demo_basic_usage():
"""示例1: 基础使用"""
print("\n🔹 示例1: 基础页面采集")
config = PlaywrightConfig(
headless=True,
timeout=10000
)
client = PlaywrightClient(config)
client.start()
try:
# 采集单个页面
result = client.fetch_page(
"https://httpbin.org/html",
screenshot=True
)
if result.success:
print(f"✅ 采集成功")
print(f" URL: {result.url}")
print(f" 加载时间: {result.load_time:.2f}s")
print(f" HTML长度: {len(result.html)}")
print(f" 截图: {result.screenshot_path}")
else:
print(f"❌ 采集失败: {result.error}")
finally:
client.stop()
def demo_wait_strategy():
"""示例2: 等待策略"""
print("\n🔹 示例2: 等待策略演示")
client = PlaywrightClient()
client.start()
try:
# 等待特定元素
result = client.fetch_page(
"https://httpbin.org/delay/2", # 延迟2秒
wait_network_idle=True,
screenshot=True
)
print(f"加载完成 | 耗时:{result.load_time:.2f}s")
finally:
client.stop()
def demo_batch_crawl():
"""示例3: 批量采集"""
print("\n🔹 示例3: 批量采集")
spider = DemoSpider(name="demo_spider")
urls = [
"https://httpbin.org/html",
"https://httpbin.org/html",
"https://httpbin.org/status/404", # 会失败
]
spider.run(urls)
def demo_screenshot_debug():
"""示例4: 截图调试"""
print("\n🔹 示例4: 截图调试")
config = PlaywrightConfig(screenshot_on_error=True)
client = PlaywrightClient(config)
client.start()
try:
# 故意访问不存在的选择器
result = client.fetch_page(
"https://httpbin.org/html",
wait_selector=".this-does-not-exist", # 不存在
screenshot=True
)
if not result.success:
print(f"❌ 失败,但已保存截图: {result.screenshot_path}")
finally:
client.stop()
if __name__ == "__main__":
# 运行示例
demo_basic_usage()
# demo_wait_strategy()
# demo_batch_crawl()
# demo_screenshot_debug()
🔍 代码关键点解析
1. 等待策略的选择
python
# ❌ 不推荐:固定延迟
page.wait_for_timeout(5000) # 可能太长或太短
# ✅ 推荐:等待元素
page.wait_for_selector('.content', state='visible')
# ✅ 推荐:网络空闲
page.goto(url, wait_until='networkidle')
原则:明确等待条件优于盲目等待。
2. 截图的正确时机
python
# 打开页面后立即截图
page.goto(url)
page.screenshot(path='step1_initial.png')
# 等待后再截图
page.wait_for_selector('.content')
page.screenshot(path='step2_loaded.png')
# 失败时也要截图
except Exception:
page.screenshot(path='error.png')
技巧:关键步骤都截图,出问题能快速定位。
3. 资源拦截提速
python
# 拦截图片/CSS
context.route('**/*.{png,jpg,css}', lambda route: route.abort())
效果:加载速度提升50%+,节省带宽。
注意:如果需要提取图片URL,别拦截图片。
4. 上下文复用
python
# ❌ 每次都启动浏览器(慢)
for url in urls:
browser = playwright.chromium.launch()
page = browser.new_page()
# ...
browser.close()
# ✅ 复用浏览器上下文(快)
browser = playwright.chromium.launch()
context = browser.new_context()
for url in urls:
page = context.new_page()
# ...
page.close()
browser.close()
差距:启动浏览器每次耗时1-2秒,复用后几乎无开销。
📊 实战验收
运行测试
bash
python playwright_demo.py
预期输出
json
🔹 示例1: 基础页面采集
2026-01-24 16:45:30 [INFO] __main__: 启动Playwright浏览器...
2026-01-24 16:45:32 [INFO] __main__: 浏览器已启动
2026-01-24 16:45:32 [INFO] __main__: 正在访问: https://httpbin.org/html
2026-01-24 16:45:34 [INFO] __main__: 截图已保存: screenshots/success_httpbin.org_20260124_164534_0001.png
2026-01-24 16:45:34 [INFO] __main__: 页面加载成功 | 耗时:2.15s | HTML长度:3741
✅ 采集成功
URL: https://httpbin.org/html
加载时间: 2.15s
HTML长度: 3741
截图: screenshots/success_httpbin.org_20260124_164534_0001.png
查看截图
bash
open screenshots/ # macOS
explorer screenshots\ # Windows
验收标准
- 能正确加载动态页面
- 等待策略生效(元素出现后才继续)
- 截图清晰可见
- HTML包含渲染后的完整内容
- 无头模式正常运行
- 失败时自动截图
🎨进阶优化方向
1. 滚动加载处理
python
def scroll_to_load_more(page: Page, max_scrolls: int = 10):
"""滚动加载更多内容"""
previous_height = page.evaluate('document.body.scrollHeight')
for i in range(max_scrolls):
# 滚动到底部
page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
# 等待新内容加载
page.wait_for_timeout(2000)
# 检查高度是否变化
new_height = page.evaluate('document.body.scrollHeight')
if new_height == previous_height:
logger.info(f"滚动{i+1}次后无新内容")
break
previous_height = new_height
logger.info(f"滚动{i+1}次,高度:{new_height}")
2. Cookie管理
python
# 保存Cookie
cookies = context.cookies()
with open('cookies.json', 'w') as f:
json.dump(cookies, f)
# 加载Cookie
with open('cookies.json') as f:
cookies = json.load(f)
context.add_cookies(cookies)
用途:保持登录状态,避免重复登录。
3. 弹窗处理
python
# 监听弹窗
page.on('dialog', lambda dialog: dialog.accept())
# 或手动处理
try:
page.click('button')
page.wait_for_selector('.confirm-dialog')
page.click('.confirm-button')
except:
pass
4. 视频截图
python
# 录制视频
context = browser.new_context(
record_video_dir="videos/",
record_video_size={"width": 1280, "height": 720}
)
page = context.new_page()
# ... 操作页面
# 关闭时自动保存视频
page.close()
5. 并发控制
python
from concurrent.futures import ThreadPoolExecutor
def fetch_with_own_browser(url):
"""每个线程独立浏览器"""
pw = sync_playwright().start()
browser = pw.chromium.launch()
page = browser.new_page()
page.goto(url)
html = page.content()
browser.close()
pw.stop()
return html
# 并发采集
with ThreadPoolExecutor(max_workers=3) as executor:
results = executor.map(fetch_with_own_browser, urls)
注意:Playwright单个浏览器实例不是线程安全的,需要每线程独立浏览器。
6. 移动端模拟
python
# 使用预设设备
context = browser.new_context(
**playwright.devices['iPhone 13']
)
# 或自定义
context = browser.new_context(
viewport={'width': 375, 'height': 667},
user_agent='Mozilla/5.0 (iPhone...)',
is_mobile=True,
has_touch=True
)
⚠️ 常见坑点
1. 无头模式调试困难
问题:headless=True时看不到页面,不知道哪里出错。
解决:
python
# 开发时用有头模式
config = PlaywrightConfig(headless=False)
# 加慢速度方便观察
page.goto(url)
page.wait_for_timeout(3000) # 暂停3秒
2. 选择器不稳定
问题:今天能用的选择器,明天就失效。
解决:
python
# ❌ 脆弱
page.locator('.css-1a2b3c > div:nth-child(3)')
# ✅ 稳定
page.locator('[data-testid="article-title"]') # 优先用data属性
page.locator('h1:has-text("新闻标题")') # 或文本匹配
3. 内存泄漏
问题:长时间运行后内存占用越来越高。
解决:
python
# 定期重启浏览器
for i, url in enumerate(urls):
if i % 100 == 0: # 每100个URL重启
client.stop()
client.start()
client.fetch_page(url)
4. 超时设置不合理
python
# ❌ 太短,复杂页面加载不完
page.goto(url, timeout=5000)
# ❌ 太长,失败页面浪费时间
page.goto(url, timeout=120000)
# ✅ 合理
page.goto(url, timeout=30000)
page.set_default_timeout(10000) # 单个操作10秒
5. 忽略iframe
问题:目标内容在iframe里,主页面选不到。
解决:
python
# 切换到iframe
frame = page.frame_locator('iframe[name="content"]')
frame.locator('.article').text_content()
💼 实际应用场景
场景1:无限滚动列表
python
def crawl_infinite_scroll(url: str, target_count: int = 100):
"""采集无限滚动列表"""
client = PlaywrightClient()
client.start()
page = client.context.new_page()
page.goto(url)
items = set()
scroll_count = 0
while len(items) < target_count and scroll_count < 50:
# 获取当前页面的项目
current_items = page.locator('.item').all_text_contents()
items.update(current_items)
logger.info(f"已采集{len(items)}条 | 目标{target_count}条")
# 滚动
page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
page.wait_for_timeout(2000)
scroll_count += 1
page.close()
client.stop()
return list(items)
场景2:点击翻页
python
def crawl_pagination(url: str, max_pages: int = 10):
"""点击翻页采集"""
client = PlaywrightClient()
client.start()
page = client.context.new_page()
page.goto(url)
all_data = []
for page_num in range(1, max_pages + 1):
# 等待内容加载
page.wait_for_selector('.item-list')
# 截图
page.screenshot(path=f'screenshots/page_{page_num}.png')
# 提取数据
items = page.locator('.item').all()
all_data.extend([item.text_content() for item in items])
# 点击下一页
try:
next_button = page.locator('button:has-text("下一页")')
if not next_button.is_visible():
logger有下一页了")
break
next_button.click()
page.wait_for_load_state('networkidle')
except:
break
page.close()
client.stop()
return all_data
场景3:登录后采集
python
def crawl_with_login(login_url: str, username: str, password: str):
"""登录后采集"""
client = PlaywrightClient()
client.start()
page = client.context.new_page()
# 1. 登录
page.goto(login_url)
page.fill('input[name="username"]', username)
page.fill('input[name="password"]', password)
page.click('button[type="submit"]')
# 等待登录成功
page.wait_for_selector('.user-avatar', timeout=10000)
logger.info("登录成功")
# 2. 保存Cookie(下次可复用)
cookies = client.context.cookies()
with open('cookies.json', 'w') as f:
json.dump(cookies, f)
# 3. 采集需要登录的页面
page.goto('https://example.com/protected-page')
html = page.content()
page.close()
client.stop()
return html
场景4:优先API降级Playwright
python
def smart_fetch(url: str):
"""优先用API,失败才用Playwright"""
import requests
try:
# 先尝试requests
response = requests.get(url, timeout=10)
html = response.text
# 检查是否为空壳
if len(html) > 500 and '<div id="root"></div>' not in html:
logger.info("使用requests成功")
return html
except:
pass
# requests失败,用Playwright
logger.info("降级到Playwright")
client = PlaywrightClient()
client.start()
result = client.fetch_page(url)
client.stop()
return result.html
原则:能用requests就不用Playwright(快10倍+)。
🎯 本期总结
今天我们掌握了Playwright动态爬虫:
✅ 浏览器控制 :启动、配置、资源拦截
✅ 等待策略 :元素、网络空闲、自定义函数
✅ 截图调试 :每步留痕,问题无处藏📸
✅ 性能优化 :无头模式、上下文复用、资源拦截
✅ 实战技巧:滚动加载、点击翻页、登录态维护
核心思想就一句话:用真实浏览器渲染页面,但要控制好性能和稳定性🌐
Playwright很强大,但也有代价------比requests慢10倍,资源占用高。所以原则是:先尝试找API,实在找不到再用Playwright。
📖 下期预告
动态列表:滚动加载采集300条(带终止条件)
今天我们学会了Playwright的基础操作,下一期我们解决一个超常见的难题:无限滚动列表如何稳定采集大量数据?
- 如何判断已经滚动到底(无新内容)
- 去重机制(避免重复采集)
- 终止条件设计(数量/时间/高度)
- 中断恢复(采集一半断网怎么办)
- 内存控制(滚动300次不爆内存)
实战目标:稳定采集300条数据,去重率100%,失败可恢复🎯
作业(可选):
- 安装Playwright:
pip install playwright && playwright install - 运行demo,观察截图生成
- 找一个动态网站,用Playwright采集试试
- 对比同一页面用requests和Playwright的HTML差异
有问题随时在评论区讨论,咱们下期见!💪
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。