Playwright数据采集实战:从自动化测试到高效爬虫的进阶指南
1. 项目概述:从测试到采集的思维跃迁
如果你和我一样,在自动化测试领域摸爬滚打了几年,手里玩得最熟的家伙事儿可能就是 Selenium 或者 Puppeteer。但最近一两年,一个叫 Playwright 的家伙横空出世,以其“开箱即用”的跨浏览器支持和强大的网络拦截能力,迅速成了我们圈子里的新宠。最开始,我也只是把它当作一个更稳定、更快的 Selenium 替代品来写 UI 自动化测试脚本。直到有一次,我需要从几十个结构各异、交互复杂的网站上定时抓取一些价格和库存数据,用传统的爬虫框架(比如 Scrapy 配合 Selenium)搞得焦头烂额——浏览器实例管理麻烦、反爬策略层出不穷、动态内容加载难以捉摸。就在那时,我猛然意识到,我手里这个“测试工具” Playwright,其底层能力简直是为现代 Web 数据采集量身定做的。
这个项目,或者说这篇指南,就是记录了我如何将 Playwright 从一个纯粹的“自动化测试框架”,拓展为一把锋利高效的“数据采集瑞士军刀”的完整心路历程和实践方案。它适合已经对 Playwright 有基础了解,想探索其测试之外潜力的开发者,也适合那些被复杂动态网站数据采集折磨得痛不欲生的数据分析师或爬虫工程师。我们将彻底抛开“测试断言”的思维定式,聚焦于如何利用 Playwright 的浏览器自动化能力,稳定、高效、优雅地从 Web 上获取我们需要的任何数据。
2. 核心理念与架构设计:为何是Playwright?
在深入代码之前,我们必须先想清楚一个问题:市面上浏览器自动化工具那么多,为什么偏偏是 Playwright 在数据采集场景下表现如此突出?这绝不仅仅是“它比较新”那么简单,而是其架构设计理念与数据采集的核心需求高度契合。
2.1 与传统爬虫及Selenium的对比
我们不妨先做个简单的对比,这能帮你快速理解 Playwright 的定位。
| 特性维度 | 传统爬虫 (Requests/Scrapy) | Selenium | Playwright (我们的选择) |
|---|---|---|---|
| 核心原理 | 直接发送 HTTP 请求,解析 HTML 响应。 | 通过 WebDriver 协议控制真实浏览器。 | 通过 DevTools Protocol 等现代化协议直接与浏览器内核通信。 |
| 对动态内容的支持 | 差。无法执行 JavaScript,对于 SPA(单页应用)或 Ajax 加载的内容束手无策。 | 好。完全在真实浏览器环境中运行,能处理所有动态内容。 | 极佳。不仅支持,还提供了等待页面元素、网络请求完成的专用 API,更可靠。 |
| 执行速度 | 极快。无浏览器开销,纯网络 IO。 | 慢。需要启动完整的浏览器进程,资源消耗大。 | 较快。启动速度优于 Selenium,且支持无头模式,资源控制更精细。 |
| 稳定性与可靠性 | 一般。严重依赖目标网站结构,反爬策略(如 IP 封锁、验证码)应对复杂。 | 一般。不同浏览器驱动版本兼容性问题多,元素定位有时不稳定。 | 高。自动等待机制减少了“竞态条件”导致的失败,跨浏览器行为一致性强。 |
| 反爬对抗能力 | 弱。需要额外处理 headers、cookies、代理、验证码等。 | 较强。模拟真人浏览器,但指纹可能被检测。 | 强。可模拟多种设备、地理位置、语言,并能生成更“真实”的浏览器指纹,规避检测。 |
| 网络请求控制 | 完全可控。可以精细地构建和修改请求。 | 弱。主要通过浏览器发生,难以拦截和修改。 | 强大。可以监听、修改、拦截任何网络请求(page.route),这是数据采集的杀手锏。 |
| 开发与调试体验 | 尚可。依赖日志和响应内容分析。 | 一般。调试依赖浏览器开发者工具,但集成度不高。 | 优秀。自带强大的调试工具(Playwright Inspector),可录制脚本、时间旅行调试。 |
从表格中可以清晰看到,Playwright 在处理现代复杂 Web 应用、提升采集稳定性和提供深度控制能力这三个数据采集最关键的痛点上,提供了最佳的平衡点。它既具备了 Selenium 的“所见即所得”能力,又通过更现代的协议获得了更好的性能和更强的控制力。
2.2 数据采集专用架构思维
当我们把 Playwright 用于数据采集时,我们的代码架构思维需要发生根本转变:
- 从“断言”到“提取”:测试脚本的核心是“断言”(Assert),验证页面状态是否符合预期。而采集脚本的核心是“提取”(Extract),无论页面变成什么样,我们的目标是拿到数据。这意味着错误处理逻辑从“测试失败”变为“数据缺失或异常,尝试其他获取路径”。
- 从“线性执行”到“状态机与队列”:一个测试用例通常是线性的:打开页面 -> 操作 -> 断言 -> 结束。数据采集往往是非线性的:从一个入口 URL 开始,发现新的链接,加入队列,循环处理。我们需要引入任务队列(如
asyncio.Queue)和状态管理。 - 从“单次运行”到“持续与调度”:测试通常独立运行。数据采集则需要考虑定时任务、增量更新、断点续采、分布式扩展等。我们的脚本要设计成可被外部调度器(如 Celery, APScheduler)调用的模块化函数。
- 从“报告生成”到“数据持久化”:测试输出是 Allure/HTML 报告。采集输出是结构化的数据,需要存入数据库(MySQL, PostgreSQL, MongoDB)、数据仓库或文件(CSV, JSON, Parquet)。
基于这些思维,一个健壮的 Playwright 数据采集项目,其核心架构通常如下图所示(此处用文字描述):
- 调度层:负责触发采集任务,管理任务优先级和频率。
- 核心采集引擎:基于 Playwright,封装了浏览器管理、页面导航、元素定位、数据提取、异常重试等通用逻辑。
- 解析器模块:针对不同网站,编写特定的数据解析规则(XPath/CSS 选择器,或更复杂的解析函数)。
- 数据管道:清洗、验证提取到的数据,并传输到存储层。
- 存储层:各类数据库或文件系统。
- 监控与日志:记录任务执行状态、性能指标和错误信息,便于排查问题。
3. 环境搭建与核心API精讲
工欲善其事,必先利其器。让我们先搭建一个高效、可复用的 Playwright 数据采集开发环境。
3.1 高效环境配置与依赖管理
我强烈建议使用venv或conda创建独立的 Python 虚拟环境,并使用pip安装 Playwright。
# 1. 创建并激活虚拟环境(以 venv 为例) python -m venv playwright-data-env source playwright-data-env/bin/activate # Linux/macOS # playwright-data-env\Scripts\activate # Windows # 2. 安装 Playwright Python 包 pip install playwright # 3. 安装 Playwright 自带的浏览器(Chromium, Firefox, WebKit) # 这一步非常重要!它确保了浏览器版本的统一和环境的纯净。 playwright install chromium # 通常 Chromium 兼容性最好,也最快 # playwright install firefox # playwright install webkit注意:
playwright install命令可能会因为网络问题下载缓慢或失败。你可以通过设置环境变量来使用国内镜像加速,例如set PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright(Windows) 或export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright(Linux/macOS),然后再执行安装命令。
对于依赖管理,使用requirements.txt文件是标准做法。除了playwright,我们通常还会加入一些辅助库:
# requirements.txt playwright>=1.40.0 pandas>=2.0.0 # 用于数据处理和分析 sqlalchemy>=2.0.0 # 用于ORM方式操作数据库 psycopg2-binary # PostgreSQL驱动,按需选择 pymongo # MongoDB驱动,按需选择 redis>=4.0.0 # 用于分布式任务队列或缓存 beautifulsoup4>=4.12.0 # 作为备用解析库,有时比Playwright内置解析更灵活3.2 必须掌握的核心API与模式
Playwright 的 API 设计非常优雅。对于数据采集,以下这几个是必须吃透的“王牌技能”。
3.2.1 浏览器上下文(BrowserContext)与用户状态隔离
这是 Playwright 相比 Selenium 的一大优势。BrowserContext相当于一个独立的浏览器会话,拥有独立的 cookies、localStorage 和会话状态。在数据采集中,这非常有用:
- 多账号/会话管理:为每个采集账号创建一个独立的 Context,彼此数据完全隔离,避免串号。
- 指纹模拟:可以在每个 Context 上设置不同的用户代理(User-Agent)、视口大小、地理位置、语言等,模拟不同的设备,降低被识别为机器人的风险。
- 资源控制:可以为 Context 单独设置代理、忽略 HTTPS 错误、禁用图片加载以提升速度等。
import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: # 启动浏览器,推荐使用无头模式(headless=True)用于生产环境采集 browser = await p.chromium.launch(headless=True, slow_mo=50) # slow_mo 可放慢操作,便于调试 # 创建一个浏览器上下文,并设置自定义设备属性 context = await browser.new_context( user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', viewport={'width': 1920, 'height': 1080}, locale='zh-CN', timezone_id='Asia/Shanghai', # 忽略证书错误,某些内部或测试网站可能需要 ignore_https_errors=True, # 设置代理(重要!用于IP轮换) # proxy={'server': 'http://your-proxy-server:port'} ) # 在这个上下文中创建页面 page = await context.new_page() # ... 你的采集逻辑 ... await page.goto('https://example.com') # 采集结束后,关闭上下文和浏览器 await context.close() await browser.close() asyncio.run(main())3.2.2 智能等待(Auto-waiting)与显式等待
Playwright 的操作(如click,fill,get_by_role)内置了智能等待:它们会等待元素可操作(可见、启用、稳定)后再执行。这极大地减少了编写显式等待(time.sleep)的需要,让代码更健壮。
但在数据采集中,我们经常需要等待特定内容出现,这需要用到更灵活的等待策略:
# 等待某个元素出现在DOM中 await page.wait_for_selector('.product-list', state='attached') # 等待网络请求完成(非常实用!) # 等待直到没有超过500ms的网络请求 await page.wait_for_load_state('networkidle') # 等待特定URL的响应 async with page.expect_response('**/api/products*') as response_info: await page.click('#load-more') response = await response_info.value # 直接获取API返回的JSON数据,无需从HTML解析! data = await response.json() # 等待函数条件为真 await page.wait_for_function(""" () => { const items = document.querySelectorAll('.item'); return items.length >= 10; } """)3.2.3 网络请求拦截与修改(page.route)—— 数据采集的“核武器”
这是 Playwright 数据采集能力封神的关键。许多现代网站的数据是通过 XHR/Fetch API 动态加载的,这些数据通常以结构清晰的 JSON 格式返回。与其费力地解析渲染后的 HTML,不如直接拦截这些 API 请求,拿到原始数据。
# 拦截所有JSON类型的请求,并直接获取其响应体 async def handle_route(route): # 获取请求对象 request = route.request # 只处理我们关心的API请求 if '/api/data' in request.url: # 继续发出请求并获取响应 response = await route.fetch() # 获取响应体(JSON格式) body = await response.text() data = json.loads(body) print(f"拦截到数据: {data}") # 这里可以直接将 data 存入队列或进行下一步处理 # ... # 最后,需要完成路由(fulfill)或继续(continue) await route.fulfill(response=response) # 返回原始响应,不影响页面渲染 else: # 不关心的请求,直接放行 await route.continue_() # 在页面导航前,注册路由处理器 await page.route('**/*', handle_route) # 拦截所有请求,性能开销大,慎用 # 更推荐精确匹配 await page.route('**/api/*.json', handle_route) await page.goto('https://data-rich-site.com')你甚至可以修改请求或响应:
# 修改请求头,例如添加认证令牌 await route.continue_(headers={**route.request.headers, 'Authorization': 'Bearer token'}) # 伪造响应,用于屏蔽某些资源(如广告、图片)以加速加载 if request.resource_type == 'image': await route.fulfill(status=200, body='') # 返回空图片 else: await route.continue_()3.2.4 元素定位与数据提取
Playwright 提供了多种定位器(Locator),比传统的 XPath/CSS 选择器更可靠,因为它们也内置了自动等待。
# 1. 使用文本定位(非常直观) buy_button = page.get_by_text('立即购买', exact=True) # 2. 使用角色定位(ARIA,语义化,最推荐) search_box = page.get_by_role('textbox', name='搜索') submit_button = page.get_by_role('button', name='搜索') # 3. 使用占位文本定位 email_input = page.get_by_placeholder('请输入邮箱') # 4. 传统的CSS和XPath(作为补充) items = page.locator('css=.product-item').all() first_item = page.locator('xpath=//div[@class="product-list"]/div[1]') # 数据提取 # 获取单个元素的文本、属性 title = await page.locator('h1').text_content() link = await page.get_by_role('link').get_attribute('href') # 获取一组元素的所有文本或属性 all_titles = await page.locator('.title').all_text_contents() all_links = await page.locator('a.product-link').evaluate_all('nodes => nodes.map(n => n.href)')4. 实战:构建一个健壮的商品数据采集器
理论讲得再多,不如动手实践。让我们构建一个从电商网站采集商品列表数据的完整脚本。这个例子涵盖了从启动、导航、等待、提取到异常处理和保存的全流程。
4.1 场景定义与目标拆解
目标:从某个示例电商网站https://demo-webstore.com的“笔记本电脑”分类下,采集所有商品页的标题、价格、SKU 和详情链接。挑战:
- 页面是动态加载的,商品列表可能通过“加载更多”按钮或滚动分页。
- 需要处理网络延迟和元素加载时间。
- 需要优雅地处理可能出现的异常(如元素缺失、网络超时)。
- 需要将采集到的数据结构化保存。
4.2 分步实现与代码详解
我们将采用异步(async/await)模式,这是 Playwright 的推荐方式,能更好地处理 I/O 密集型操作。
import asyncio import json import logging from datetime import datetime from typing import List, Dict, Optional from playwright.async_api import async_playwright, Page, BrowserContext, TimeoutError # 配置日志,便于调试和监控 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class ProductScraper: def __init__(self, headless: bool = True): self.headless = headless self.data: List[Dict] = [] # 存储采集结果 async def setup_browser(self): """初始化浏览器和上下文""" self.playwright = await async_playwright().start() # 使用 Chromium,稳定性最好 self.browser = await self.playwright.chromium.launch( headless=self.headless, args=['--disable-blink-features=AutomationControlled'] # 隐藏自动化特征 ) # 创建上下文,模拟真实用户 self.context = await self.browser.new_context( viewport={'width': 1920, 'height': 1080}, user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', locale='zh-CN', # 可以在这里加载已保存的 cookies 文件,避免重复登录 # storage_state='auth_state.json' ) # 设置默认超时时间 self.context.set_default_timeout(30000) # 30秒 async def scrape_category(self, url: str, max_pages: int = 5): """采集指定分类页面的商品""" page = await self.context.new_page() try: logger.info(f"开始采集分类页: {url}") await page.goto(url, wait_until='networkidle') # 等待网络空闲 current_page = 1 while current_page <= max_pages: logger.info(f"正在处理第 {current_page} 页") # 1. 等待商品列表容器加载完成 await page.wait_for_selector('.product-grid', state='visible') # 2. 提取当前页所有商品卡片 product_cards = await page.locator('.product-card').all() if not product_cards: logger.warning("未找到商品卡片,可能页面结构已变化或已无数据。") break for card in product_cards: try: product_data = await self._extract_product_data(card) if product_data: self.data.append(product_data) logger.debug(f"已采集商品: {product_data['title']}") except Exception as e: logger.error(f"提取单个商品数据时出错: {e}") # 继续处理下一个商品,不因单个失败而终止 # 3. 尝试翻到下一页 next_button = page.locator('a:has-text("下一页")').first if await next_button.is_visible(): # 点击前等待按钮可点击 await next_button.wait_for(state='visible') await next_button.click() # 等待新页面内容加载,通过等待一个新增的元素或URL变化 await page.wait_for_load_state('networkidle') # 简单等待一下,确保内容稳定 await page.wait_for_timeout(1000) current_page += 1 else: logger.info("已到达最后一页。") break except TimeoutError as e: logger.error(f"页面操作超时: {e}") except Exception as e: logger.error(f"采集过程中发生未知错误: {e}") finally: await page.close() logger.info(f"页面采集结束,共采集到 {len(self.data)} 条商品数据。") async def _extract_product_data(self, card_locator) -> Optional[Dict]: """从单个商品卡片元素中提取数据""" try: # 使用内部选择器,相对于卡片定位,更精确 title_elem = card_locator.locator('.product-title') price_elem = card_locator.locator('.price') sku_elem = card_locator.locator('.sku') link_elem = card_locator.locator('a.product-link') # 使用 text_content() 并去除空白字符 title = await title_elem.text_content() price_text = await price_elem.text_content() sku = await sku_elem.text_content() link = await link_elem.get_attribute('href') if not all([title, price_text]): logger.warning(f"商品数据不完整,跳过。标题: {title}, 价格: {price_text}") return None # 简单的数据清洗 title = title.strip() if title else '' # 提取价格中的数字 import re price = float(re.search(r'[\d,.]+', price_text.replace(',', '')).group()) if price_text else 0.0 sku = sku.strip() if sku else '' # 补全链接(如果是相对路径) if link and link.startswith('/'): link = f'https://demo-webstore.com{link}' return { 'title': title, 'price': price, 'sku': sku, 'link': link, 'scraped_at': datetime.now().isoformat() } except Exception as e: logger.error(f"在提取商品数据时发生错误: {e}") return None async def save_data(self, format: str = 'json', filename: str = None): """保存采集到的数据""" if not filename: timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f'products_{timestamp}.{format}' if format == 'json': with open(filename, 'w', encoding='utf-8') as f: json.dump(self.data, f, ensure_ascii=False, indent=2) logger.info(f"数据已保存为 JSON 文件: {filename}") elif format == 'csv': import pandas as pd df = pd.DataFrame(self.data) df.to_csv(filename, index=False, encoding='utf-8-sig') logger.info(f"数据已保存为 CSV 文件: {filename}") else: logger.error(f"不支持的格式: {format}") async def run(self, start_url: str): """主运行流程""" await self.setup_browser() try: await self.scrape_category(start_url, max_pages=3) # 示例只采集3页 await self.save_data('json') # 也可以保存为CSV # await self.save_data('csv', 'products.csv') finally: # 确保资源被正确关闭 await self.context.close() await self.browser.close() await self.playwright.stop() # 运行脚本 if __name__ == '__main__': scraper = ProductScraper(headless=False) # 调试时可设为 False 看浏览器操作 asyncio.run(scraper.run('https://demo-webstore.com/category/laptops'))4.3 关键技巧与避坑指南
- 选择器策略:优先使用
get_by_role(),get_by_text(),get_by_label()等语义化定位器,它们最稳定。CSS 选择器作为备用,XPath 在复杂情况下使用。避免使用可能变化的类名(如.js-product-item-123)。 - 等待的艺术:
wait_for_selector和wait_for_load_state('networkidle')是黄金组合。避免使用固定的page.wait_for_timeout(),除非是等待动画等非网络操作。 - 错误处理:对每个可能失败的操作(如元素定位、文本提取)进行
try...except包裹。一个商品的解析失败不应导致整个任务崩溃。 - 资源管理:务必在
finally块中关闭page,context,browser和playwright对象,防止资源泄漏,尤其是在长时间运行的脚本中。 - 无头模式:生产环境务必使用
headless=True。调试时可设为False,并配合slow_mo参数放慢操作速度,观察发生了什么。
5. 高级技巧:应对复杂场景与性能优化
当基本采集跑通后,你会遇到更棘手的场景:需要登录、遇到反爬、数据量巨大。别担心,Playwright 都有应对之策。
5.1 处理登录与会话保持
很多数据在登录后才能访问。Playwright 可以轻松模拟登录,并保存登录状态供后续使用。
async def login_and_save_state(page: Page, login_url: str, username: str, password: str): """模拟登录并保存认证状态""" await page.goto(login_url) # 填写登录表单 - 使用角色定位器最佳 await page.get_by_label('用户名或邮箱').fill(username) await page.get_by_label('密码').type(password, delay=100) # 模拟人工输入延迟 await page.get_by_role('button', name='登录').click() # 等待登录成功,例如跳转到首页或出现用户菜单 await page.wait_for_url('**/dashboard**') # 或者等待某个登录后特有的元素 await page.wait_for_selector('.user-avatar', state='visible') logger.info("登录成功!") # 将当前上下文的存储状态(cookies, localStorage)保存到文件 await page.context.storage_state(path='auth_state.json') logger.info("认证状态已保存至 auth_state.json") # 下次启动时,可以直接加载这个状态文件,无需再次登录 context = await browser.new_context(storage_state='auth_state.json') page = await context.new_page() # 此时 page 已经处于登录状态 await page.goto('https://example.com/protected-data')5.2 绕过常见反爬机制
现代网站的反爬手段层出不穷,Playwright 提供了多种武器来应对。
- 模拟真人行为:添加随机延迟、移动鼠标轨迹、随机滚动页面。
import random async def human_like_click(page, selector): element = page.locator(selector) box = await element.bounding_box() # 随机点击点(在元素范围内) x = box['x'] + box['width'] * random.uniform(0.2, 0.8) y = box['y'] + box['height'] * random.uniform(0.2, 0.8) await page.mouse.move(x, y, steps=random.randint(10, 20)) # 移动鼠标 await page.wait_for_timeout(random.randint(200, 800)) # 随机等待 await page.mouse.click(x, y) - 修改WebDriver属性:一些网站通过检测
navigator.webdriver属性来识别自动化工具。Playwright 可以将其设置为undefined。await page.add_init_script(""" Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """) - 使用代理IP池:这是应对 IP 封锁最有效的方法。在创建
BrowserContext时设置代理。proxy_list = ['http://proxy1:port', 'http://proxy2:port'] proxy = random.choice(proxy_list) context = await browser.new_context(proxy={'server': proxy}) - 处理验证码:这是一个难题。对于简单图片验证码,可以尝试 OCR 库(如
pytesseract,但成功率有限)。对于复杂验证码(如点选、滑块),通常需要接入第三方打码平台的服务 API。Playwright 可以截图验证码区域,发送给 API,然后模拟输入结果。
5.3 性能优化与大规模采集
当需要采集成千上万个页面时,性能成为瓶颈。以下是几个优化方向:
- 并发控制:利用异步特性,同时控制多个页面(
Page)或浏览器上下文(Context)进行采集。但要注意目标网站的承受能力,避免被封。async def scrape_many_urls(urls: List[str], max_concurrent: int = 3): semaphore = asyncio.Semaphore(max_concurrent) async def scrape_one(url): async with semaphore: # 控制并发数 context = await browser.new_context() page = await context.new_page() try: await page.goto(url) # ... 采集逻辑 ... finally: await page.close() await context.close() tasks = [scrape_one(url) for url in urls] await asyncio.gather(*tasks) - 资源拦截:在不需要图片、样式、字体等资源时,可以拦截并阻止它们加载,大幅提升页面加载速度。
await page.route('**/*.{png,jpg,jpeg,svg,gif,webp,css,woff,woff2}', lambda route: route.abort()) - 复用浏览器实例:避免为每个任务都启动/关闭浏览器。可以创建一个浏览器实例池。
- 分布式采集:对于超大规模采集,需要将任务分发到多台机器。可以将 URL 队列放在 Redis 中,多个独立的 Playwright 采集节点从队列中消费任务。需要处理好状态同步和去重。
6. 常见问题排查与调试技巧
即使经验丰富,踩坑也是常态。这里记录了我遇到的一些典型问题及解决方法。
6.1 元素找不到或操作超时
这是最常见的问题。
- 检查选择器:首先用 Playwright Inspector (
playwright codegen) 录制一下操作,看看它生成的选择器是什么。确保你的选择器在页面完全加载后依然有效。 - 检查iframe:目标元素是否在
<iframe>里面?如果是,你需要先定位到 iframe,然后切换到它的content_frame中进行操作。frame = page.frame_locator('iframe[name="content"]').first element_inside_frame = frame.locator('button') - 检查阴影DOM(Shadow DOM):现代 Web 组件可能使用 Shadow DOM。Playwright 的定位器可以穿透 Shadow DOM,但有时需要特殊语法
:light或使用 JavaScript 直接访问。 - 增加等待时间或改变等待条件:也许
networkidle不够,需要等待某个特定元素出现。尝试page.wait_for_selector(selector, state='visible', timeout=60000)增加超时。
6.2 页面卡死或无响应
- 设置超时和超时处理:为
page.goto和关键操作设置合理的超时,并使用try...except TimeoutError进行捕获,然后重试或放弃。 - 禁用不必要的功能:启动浏览器时添加参数,如
--disable-gpu,--disable-dev-shm-usage(Docker 中常用),--no-sandbox。 - 检查内存泄漏:确保每个
page和context在使用后都被正确关闭(close())。长期运行的脚本,可以定期重启浏览器实例。
6.3 被网站识别为机器人
- 检查指纹:使用像
https://bot.sannysoft.com/这样的网站测试你的浏览器指纹。确保navigator.webdriver已被隐藏。 - 模拟更完整的环境:在
new_context中设置完整的user_agent,viewport,locale,timezone_id,甚至geolocation和permissions。 - 降低请求频率:在请求间添加随机延迟,模拟人类阅读时间。
- 轮换代理和User-Agent:使用不同的 IP 和浏览器标识。
6.4 高效调试:Playwright Inspector 与追踪
Playwright 自带的调试工具极其强大。
- 录制脚本:
playwright codegen https://example.com会打开一个浏览器和代码生成器,你的操作会被实时转换成代码。这是编写新脚本的绝佳起点。 - 时间旅行调试:在脚本中设置
headless=False并添加slow_mo参数,可以观察每一步操作。更高级的是使用追踪(Tracing)。
生成的# 开始记录追踪 await context.tracing.start(screenshots=True, snapshots=True) # ... 执行你的采集操作 ... # 停止并保存追踪文件 await context.tracing.stop(path='trace.zip')trace.zip可以用 Playwright 的命令行工具 (playwright show-trace trace.zip) 打开,这是一个图形化界面,可以回放整个操作过程,查看每个时间点的 DOM 快照、网络请求和 console 日志,是排查疑难杂症的终极利器。
从自动化测试到数据采集,Playwright 展现出的能力边界远超其设计初衷。它不仅仅是一个“无头浏览器”,更是一个强大的 Web 交互与数据获取平台。掌握它,意味着你拥有了以程序化方式与任何现代 Web 应用对话的能力。这套方法我已经在多个生产级别的数据采集项目中成功应用,从简单的信息聚合到复杂的业务流程自动化,Playwright 都证明了其价值。当然,每个网站都是独特的战场,最关键的还是灵活运用这些工具和思路,不断调试和适应。希望这份指南能帮你少走弯路,直接进入高效采集的快车道。如果在实践中遇到新的问题,不妨回头看看网络请求拦截(page.route)和浏览器上下文(BrowserContext)这两个核心武器,它们往往是破解难题的关键。
