公司动态

Python Playwright爬虫实战:高效采集动态网页数据

📅 2026/7/18 8:17:08
Python Playwright爬虫实战:高效采集动态网页数据
1. 项目概述为什么是Playwright最近在社区里看到不少朋友在讨论数据采集尤其是用Python做爬虫。传统的requestsBeautifulSoup或者Scrapy框架依然是主力但越来越多的人开始抱怨现在的网页越来越“花哨”了大量数据通过JavaScript动态加载甚至藏在需要交互比如点击按钮、滚动页面之后才会出现的元素里。对付这些“现代”网页老办法要么力不从心要么写出来的代码又臭又长维护起来头疼。这正是我这次想分享的切入点。标题里的“Playwright爬虫数据采集代码示例”核心价值就在于它提供了一个应对现代Web应用的、更优雅高效的解决方案。Playwright不是一个单纯的爬虫库它是一个由微软开源的浏览器自动化测试框架。但正因为它能“驱动”真实的浏览器如Chromium, Firefox, WebKit所以它天然具备了处理动态内容、执行复杂交互的能力这让它在数据采集领域大放异彩。简单来说如果你要采集的网站数据是异步加载的AJAX。需要登录后才能访问。页面结构复杂依赖大量JavaScript渲染。有反爬机制对简单的HTTP请求拦截严格。那么Playwright很可能就是你的“瑞士军刀”。它模拟的是真实用户行为从浏览器指纹到网络请求都高度仿真极大地降低了被反爬策略识别和封锁的风险。接下来的内容我会用一个完整的、可运行的代码示例带你从零开始深入理解如何用Playwright构建一个健壮的数据采集脚本并分享我踩过的一些坑和实战技巧。2. 核心思路与方案选型在动手写代码之前我们先理清思路。一个数据采集项目无外乎几个核心环节发起请求、解析页面、提取数据、处理异常、存储结果。Playwright在这些环节中扮演了什么角色我们又该如何搭配其他工具2.1 Playwright vs. 传统方案的优劣对比为什么选择Playwright而不是Selenium或纯HTTP请求我们可以从几个维度来看特性/方案requestsBeautifulSoup/lxmlSeleniumPlaywright核心原理发送HTTP请求解析静态HTML。通过WebDriver协议驱动浏览器。通过DevTools协议直接与浏览器内核通信。处理动态内容无法直接处理需手动分析API或使用Selenium辅助。优秀可等待元素、执行JS。极佳内置智能等待执行JS更高效。执行速度极快无浏览器开销。较慢浏览器启动和驱动通信有开销。较快协议更高效支持无头模式且轻量。资源消耗极低。高每个实例一个完整浏览器进程。中等但优于Selenium上下文隔离好。安装与配置简单pip install即可。复杂需下载对应浏览器驱动并匹配版本。简单playwright install一键安装浏览器。跨浏览器支持无。支持但需各自驱动。原生支持Chromium, Firefox, WebKitAPI统一。网络拦截与模拟需借助mitmproxy等较复杂。有限支持。强大可轻松拦截修改请求/响应模拟地理、语言等。移动端模拟无。有限支持。支持良好可模拟特定设备如iPhone。选择Playwright的关键理由对现代Web的完美支持单页应用SPA是常态Playwright的等待策略wait_for_selector,wait_for_load_state能优雅地处理这些异步加载。开发体验友好自动等待机制减少了大量time.sleep的猜测代码。Playwright Codegen可以录制操作生成代码对快速原型开发帮助巨大。强大的调试能力playwright debug模式、追踪录制trace viewer功能让定位页面问题变得直观。统一的API一套代码稍作调整即可运行在三大浏览器引擎上方便测试兼容性或应对不同站点的限制。2.2 项目整体架构设计我们的示例项目将采集一个模拟的图书网站信息。这个网站有分页、有动态加载、可能需要处理登录状态为简化示例中跳过登录但会给出思路。整体架构如下初始化与配置启动Playwright创建浏览器上下文Context设置代理、用户代理UA、视口大小等模拟真实环境。导航与等待访问目标URL使用Playwright的智能等待确保目标内容加载完成。数据定位与提取使用CSS选择器或XPath定位到包含数据的HTML元素。数据处理与清洗从提取的原始文本中通过正则表达式或字符串方法清洗出结构化数据如书名、价格、评分。分页与循环定位“下一页”按钮或构造分页URL实现自动翻页采集。异常处理与持久化网络超时、元素缺失等异常情况的捕获与处理并将最终数据保存为JSON或CSV文件。资源清理关闭浏览器上下文和浏览器实例释放资源。我们将使用asyncio异步API因为Playwright的异步API性能更高更适合I/O密集型的爬虫任务。同时我们会搭配pandas进行简单的数据整理和导出。3. 环境准备与核心依赖安装工欲善其事必先利其器。让我们先把环境搭建起来。3.1 创建虚拟环境与安装包强烈建议使用虚拟环境来管理项目依赖避免包冲突。# 1. 创建项目目录并进入 mkdir playwright-spider-demo cd playwright-spider-demo # 2. 创建虚拟环境这里使用venv你也可以用conda python -m venv venv # 3. 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 4. 安装Playwright pip install playwright # 5. 安装Playwright所需的浏览器Chromium, Firefox, WebKit # 这一步会下载浏览器时间稍长请保持网络通畅 playwright install chromium # 如果你需要测试其他浏览器也可以安装 firefox 或 webkit # 6. 安装数据处理和存储需要的库 pip install pandas注意playwright install命令会下载浏览器到用户目录下的缓存中。如果遇到网络问题导致下载失败可以尝试设置镜像源或者使用playwright install --help查看离线安装选项。3.2 验证安装与快速测试安装完成后写一个最简单的脚本验证一切是否正常。# test_install.py import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: # 启动Chromium浏览器headlessTrue表示无头模式不显示GUI browser await p.chromium.launch(headlessTrue) # 创建一个新的浏览器上下文类似于一个独立的隐身会话 context await browser.new_context() # 在新标签页中打开页面 page await context.new_page() # 导航到百度 await page.goto(https://www.baidu.com) # 获取页面标题并打印 title await page.title() print(f页面标题: {title}) # 截图保存可选 await page.screenshot(pathbaidu.png) # 关闭资源 await context.close() await browser.close() asyncio.run(main())运行这个脚本python test_install.py如果输出页面标题: 百度一下你就知道并且当前目录下生成了baidu.png图片那么恭喜你Playwright环境已经成功配置。4. 实战构建一个完整的图书信息采集器现在我们进入核心部分。假设我们要采集一个模拟网站http://books.toscrape.com这是一个著名的爬虫练习网站上的图书信息。虽然它是静态网站但我们将用Playwright的方式来处理并模拟一些动态场景。4.1 目标分析与页面结构探查首先手动打开目标网站用浏览器的开发者工具F12查看页面结构。列表页http://books.toscrape.com/catalogue/page-1.html每本书在一个article classproduct_pod元素内。我们需要的信息书名h3 a的title属性、价格p classprice_color、库存状态p classinstock availability、评分p classstar-rating的class如Three代表3星。分页页面底部有next按钮点击可进入下一页。实操心得在写爬虫前花10分钟仔细分析页面结构能节省后面几小时的调试时间。重点关注数据的容器元素和其唯一标识如class、id。善用开发者工具的Copy selector或Copy XPath功能但不要完全依赖自动生成的路径可能很脆弱。4.2 核心代码实现与逐行解析下面是我们完整的采集脚本我将结合代码详细解释每个部分。# book_scraper_async.py import asyncio import pandas as pd from playwright.async_api import async_playwright import logging import json from typing import List, Dict, Any # 配置日志方便调试 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) class BookScraper: def __init__(self, headless: bool True): 初始化采集器 :param headless: 是否使用无头模式。调试时可设为False看浏览器操作。 self.headless headless self.all_books: List[Dict[str, Any]] [] # 存储所有图书数据 async def scrape_page(self, page, page_num: int) - bool: 采集单个列表页的数据 :param page: Playwright页面对象 :param page_num: 当前页码用于日志 :return: 是否成功采集到数据 logger.info(f正在采集第 {page_num} 页...) try: # 核心等待图书列表的容器元素加载完成 # 这里使用 wait_for_selector 确保动态内容已渲染 await page.wait_for_selector(article.product_pod, timeout10000) # 使用 query_selector_all 获取所有图书元素 book_elements await page.query_selector_all(article.product_pod) logger.info(f第 {page_num} 页找到 {len(book_elements)} 本书。) for book_element in book_elements: book_info {} # 1. 提取书名 (从h3 a标签的title属性) title_elem await book_element.query_selector(h3 a) if title_elem: book_info[title] await title_elem.get_attribute(title) else: book_info[title] None # 2. 提取价格 price_elem await book_element.query_selector(p.price_color) if price_elem: price_text await price_elem.text_content() # 清洗价格字符串例如 £51.77 - 51.77 book_info[price] float(price_text.replace(£, )) if price_text else None else: book_info[price] None # 3. 提取库存状态 stock_elem await book_element.query_selector(p.instock.availability) if stock_elem: stock_text await stock_elem.text_content() book_info[in_stock] In stock in stock_text.strip() if stock_text else False else: book_info[in_stock] False # 4. 提取评分 (从star-rating类的第二个单词如Three) rating_elem await book_element.query_selector(p.star-rating) if rating_elem: class_name await rating_elem.get_attribute(class) # class可能是 star-rating Three rating_map {One: 1, Two: 2, Three: 3, Four: 4, Five: 5} rating_word class_name.split()[-1] if class_name else None book_info[rating] rating_map.get(rating_word, None) else: book_info[rating] None # 5. 提取详情页链接 (可选用于后续深度采集) link_elem await book_element.query_selector(h3 a) if link_elem: book_info[detail_url] await link_elem.get_attribute(href) else: book_info[detail_url] None self.all_books.append(book_info) return True except Exception as e: logger.error(f采集第 {page_num} 页时发生错误: {e}) # 可以在这里保存错误页面截图辅助调试 # await page.screenshot(pathferror_page_{page_num}.png) return False async def run(self, start_url: str, max_pages: int 5): 主运行函数 :param start_url: 起始列表页URL :param max_pages: 最大采集页数防止无限循环 async with async_playwright() as p: # 启动浏览器推荐chromium兼容性最好 browser await p.chromium.launch(headlessself.headless) # 创建上下文可以在这里统一设置用户代理、视口、语言等 context await browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) page await context.new_page() current_url start_url current_page_num 1 while current_url and current_page_num max_pages: # 导航到当前页 try: await page.goto(current_url, wait_untilnetworkidle, timeout30000) # wait_untilnetworkidle 等待页面网络活动基本停止对SPA友好 except Exception as e: logger.error(f导航到 {current_url} 失败: {e}) break # 采集当前页数据 success await self.scrape_page(page, current_page_num) if not success: logger.warning(f第 {current_page_num} 页采集失败停止翻页。) break # 寻找“下一页”按钮 next_button await page.query_selector(li.next a) if next_button: # 获取下一页的链接 next_url await next_button.get_attribute(href) # 处理相对路径 from urllib.parse import urljoin current_url urljoin(current_url, next_url) current_page_num 1 logger.info(f找到下一页跳转到: {current_url}) else: logger.info(已到达最后一页。) break # 关闭浏览器 await context.close() await browser.close() logger.info(f采集结束共采集到 {len(self.all_books)} 条图书数据。) def save_to_file(self, format: str json): 将采集到的数据保存到文件 if not self.all_books: logger.warning(没有数据可保存。) return filename fbooks_data.{format} if format json: with open(filename, w, encodingutf-8) as f: json.dump(self.all_books, f, ensure_asciiFalse, indent2) elif format csv: df pd.DataFrame(self.all_books) df.to_csv(filename, indexFalse, encodingutf-8-sig) else: logger.error(f不支持的格式: {format}) return logger.info(f数据已保存至: {filename}) async def main(): scraper BookScraper(headlessTrue) # 生产环境用True调试可改为False start_url http://books.toscrape.com/catalogue/page-1.html await scraper.run(start_url, max_pages3) # 先采集3页作为演示 scraper.save_to_file(json) # 保存为JSON scraper.save_to_file(csv) # 同时保存一份CSV if __name__ __main__: asyncio.run(main())4.3 代码关键点深度解析异步上下文管理器 (async with):async_playwright()和browser.launch()都支持异步上下文管理确保资源如浏览器进程在使用后被正确关闭即使发生异常也是如此。这是编写健壮代码的好习惯。智能等待 (wait_for_selector,wait_until): 这是Playwright的精华。page.goto(url, wait_untilnetworkidle)networkidle会等待页面在至少500毫秒内没有超过2个网络连接时才算加载完成这对加载了大量异步资源的现代网页非常有效。其他选项还有load(DOMContentLoaded事件),domcontentloaded,commit。page.wait_for_selector(article.product_pod)显式等待我们关心的数据容器出现。这比固定sleep或等待整个页面加载完更精准、更高效。元素查询与数据提取:page.query_selector_all(css selector)返回一个元素列表ElementHandle。element.query_selector(css selector)在某个元素内继续查找子元素。text_content()vsinner_text()text_content获取所有文本包括script和style里的通常不用inner_text更接近用户看到的会考虑CSS样式如display: none的元素不会被包含。爬虫中通常用text_content()。get_attribute(attr_name)获取元素属性值如href,title,src。错误处理与日志脚本中使用了try...except包裹核心采集逻辑并记录了详细的日志。在实际项目中你还需要考虑更细粒度的重试机制例如某个元素找不到时重试几次。分页逻辑我们通过查找具有特定CSS选择器li.next a的“下一页”按钮来实现自动翻页。这是一种通用模式。有些网站分页是通过URL参数如?page2控制的那时就需要解析当前URL并构造下一页URL。5. 高级技巧与性能优化基础的采集器已经能工作但要用于生产环境还需要考虑更多。5.1 请求拦截与资源过滤很多网页加载了图片、字体、CSS、广告脚本等资源对于爬虫来说这些是不必要的只会拖慢速度并消耗流量。Playwright可以轻松拦截和过滤请求。async def set_up_context(context): 设置上下文并拦截不必要的请求 # 路由拦截 await context.route(**/*, lambda route: handle_route(route)) async def handle_route(route): 处理单个路由请求 # 获取请求资源类型 resource_type route.request.resource_type # 只允许文档、XHRAjax、脚本、fetch请求通过 if resource_type in [document, xhr, script, fetch]: await route.continue_() else: # 中止图片、样式表、字体等请求 await route.abort() # 在主函数中创建context后调用 context await browser.new_context() await set_up_context(context) page await context.new_page()注意过度拦截可能导致页面渲染异常因为有些脚本是页面功能所必需的。建议先观察页面网络请求确定哪些是数据请求通常是XHR或Fetch再针对性拦截。对于目标网站可以先不禁用任何请求稳定后再逐步优化。5.2 并发控制与速率限制同时打开太多页面会消耗大量内存和CPU也容易被目标网站封禁。我们需要控制并发度并添加请求间隔。import asyncio from asyncio import Semaphore async def scrape_with_concurrency(start_urls, max_concurrent3, delay1.0): 并发采集多个起始URL :param start_urls: 起始URL列表 :param max_concurrent: 最大并发页面数 :param delay: 每次请求之间的延迟秒 semaphore Semaphore(max_concurrent) # 控制并发信号量 tasks [] for i, url in enumerate(start_urls): # 为每个URL创建一个任务并传递信号量 task asyncio.create_task(scrape_single_url(url, semaphore, delay * i)) tasks.append(task) await asyncio.gather(*tasks) async def scrape_single_url(url, semaphore, initial_delay): 采集单个URL受信号量控制 async with semaphore: await asyncio.sleep(initial_delay) # 初始延迟错开启动 # ... 实际的页面打开和数据采集逻辑 ... logger.info(f开始采集: {url}) # 模拟采集时间 await asyncio.sleep(2) logger.info(f完成采集: {url})在实际项目中更精细的控制可以结合asyncio.Queue和生产者-消费者模型。5.3 处理复杂交互登录、下拉、弹窗Playwright的强大之处在于能模拟几乎所有用户交互。模拟登录以表单为例await page.goto(https://example.com/login) # 等待输入框出现 await page.wait_for_selector(#username) # 填充表单 await page.fill(#username, your_username) await page.fill(#password, your_password) # 点击登录按钮 await page.click(button[typesubmit]) # 等待登录成功后的跳转或某个元素出现 await page.wait_for_selector(#user-profile, timeout10000) # 登录后的状态cookies, localStorage会被自动保存在context中处理下拉加载滚动# 方法1模拟滚动到底部 await page.evaluate(window.scrollTo(0, document.body.scrollHeight)) # 等待新内容加载需要观察页面行为 await page.wait_for_timeout(2000) # 简单等待不稳定 # 更好的方法是等待某个新出现的元素 # await page.wait_for_selector(.new-item-class, timeout5000) # 方法2自动滚动直到没有新内容 prev_height None while True: curr_height await page.evaluate(document.body.scrollHeight) if prev_height curr_height: break await page.evaluate(fwindow.scrollTo(0, {curr_height})) await page.wait_for_timeout(1000) # 等待加载 prev_height curr_height处理弹窗/对话框# 监听对话框alert, confirm, prompt并接受 page.on(dialog, lambda dialog: dialog.accept()) # 或者更精确地处理 def handle_dialog(dialog): print(f对话框消息: {dialog.message}) if dialog.type alert: dialog.accept() elif dialog.type confirm: dialog.dismiss() # 或 dialog.accept() page.on(dialog, handle_dialog)6. 反爬策略应对与道德规范使用Playwright这类浏览器自动化工具虽然绕过了很多基于请求头或简单JS的反爬但并非万能。高价值的网站可能有更复杂的检测机制。6.1 常见反爬手段与Playwright应对浏览器指纹检测网站通过检测navigator,screen,WebGL等属性来判断是否为真实浏览器。应对Playwright创建的浏览器上下文本身具有完整的指纹。但你可以进一步随机化或自定义这些属性不过要小心过度修改导致指纹异常。context await browser.new_context( viewport{width: 1366, height: 768}, user_agent随机UA字符串, localezh-CN, timezone_idAsia/Shanghai, # 可以覆盖更多属性但需谨慎 # permissions[geolocation] # 模拟地理位置授权 )行为模式检测过于规律的请求间隔、鼠标移动轨迹过于机械等。应对引入随机延迟、使用page.mouse.move()模拟人类鼠标移动轨迹。import random await asyncio.sleep(random.uniform(1.0, 3.0)) # 随机延迟1-3秒 # 模拟鼠标移动到某个元素上 element await page.query_selector(button) box await element.bounding_box() await page.mouse.move(box[x] box[width]/2, box[y] box[height]/2)验证码这是最棘手的。应对规避尝试降低请求频率避免触发验证码。识别服务对接第三方打码平台如超级鹰、图鉴等但会增加成本和复杂度。手动处理在调试模式headlessFalse下弹出验证码时手动输入。Cookie复用登录一次后保存context的cookies下次直接加载使用避免重复登录触发验证码。# 保存cookies cookies await context.cookies() with open(cookies.json, w) as f: json.dump(cookies, f) # 加载cookies with open(cookies.json, r) as f: cookies json.load(f) await context.add_cookies(cookies)6.2 遵守Robots协议与道德规范这是每个爬虫开发者必须牢记的底线。尊重robots.txt在爬取前访问目标网站/robots.txt查看是否允许爬取你目标目录以及规定的爬取延迟Crawl-delay。虽然Playwright不直接解析robots.txt但你应该手动遵守。控制爬取速度设置合理的请求间隔如2-5秒避免对目标网站服务器造成压力。我们的示例中使用了delay参数。识别并遵守User-Agent声明有些网站在robots.txt里声明了友好的爬虫User-Agent你可以使用它。只爬取公开数据不要尝试爬取需要付费登录或明确声明版权保护的数据。明确数据用途爬取的数据应用于个人学习、研究或公益目的切勿用于商业牟利或侵犯他人权益。重要提示网络上的公开言论如“写爬虫要限制下压力太大把正规爬虫挤得都没带宽了”反映了部分网站管理员的无奈。作为开发者我们有责任维护良好的网络生态。一个负责任的爬虫应该是“礼貌”的、可识别的通过合理的User-Agent、且对服务器影响最小的。7. 调试、问题排查与性能监控即使代码写得再好运行时也难免遇到各种问题。Playwright提供了一系列强大的调试工具。7.1 常用调试技巧慢动作与无头模式调试时关闭无头模式并启用慢动作可以看清每一步操作。browser await p.chromium.launch(headlessFalse, slow_mo500) # 每个操作延迟500毫秒录制操作使用playwright codegen命令可以打开一个浏览器和代码生成器你的操作会被实时转换成Playwright代码。这是学习API和快速生成脚本原型的利器。playwright codegen http://books.toscrape.com截图与录屏在关键步骤或出错时截图。await page.screenshot(pathdebug.png, full_pageTrue) # 或者录制整个会话的追踪信息更强大 await context.tracing.start(screenshotsTrue, snapshotsTrue) # ... 执行操作 ... await context.tracing.stop(pathtrace.zip) # 用 playwright show-trace trace.zip 查看控制台输出执行页面中的JavaScript并打印结果。console_log await page.evaluate(() { return document.title; }) print(console_log) # 或者监听页面的console事件 page.on(console, lambda msg: print(f页面日志: {msg.text}))7.2 常见问题排查表问题现象可能原因排查步骤与解决方案**元素找不到 (TimeoutError) **1. 选择器错误或过时。2. 页面未完全加载/元素是动态生成的。3. 元素在iframe内。4. 页面有弹窗遮挡。1. 用开发者工具重新检查元素使用更稳定的选择器如>页面卡住/加载慢1. 网络问题或目标服务器慢。2. 页面有无限循环的JS或未完成的资源请求。3. 拦截了关键资源。1. 增加page.goto的timeout。2. 使用wait_untildomcontentloaded而非networkidle或设置超时后强制继续。3. 检查路由拦截规则是否误杀了必要资源如API请求。被网站屏蔽1. IP被识别为爬虫并封禁。2. 浏览器指纹被检测。3. 行为模式异常。1. 使用代理IP池轮换。2. 确保使用完整的浏览器上下文避免使用--disable-blink-features等容易被检测的标志。3. 增加随机延迟、模拟鼠标移动、使用更“人类”的浏览模式。内存泄漏/浏览器崩溃1. 未正确关闭页面或上下文。2. 打开的页面过多。3. 页面内有内存泄漏的JS。1. 始终使用async with或确保在finally块中调用close()。2. 控制并发页面数及时关闭不再需要的页面await page.close()。3. 定期重启浏览器实例。7.3 性能监控与优化建议对于长时间运行的大规模采集任务监控是必要的。日志分级使用logging模块设置DEBUG,INFO,WARNING,ERROR等级别生产环境只记录ERROR和关键INFO。统计指标记录已采集页面数、成功率、平均耗时、失败URL等。资源监控监控脚本进程的内存和CPU使用情况。如果持续增长可能存在内存泄漏。优化建议重用浏览器实例避免为每个任务都启动/关闭浏览器开销巨大。创建多个上下文Context每个上下文有独立的cookie、缓存但共享浏览器进程比启动多个浏览器更轻量。谨慎使用page.evaluate频繁在页面和Python环境之间传递数据有性能成本尽量批量操作。禁用不必要的功能如对于纯数据采集可以禁用图片加载、Service Worker等。browser await p.chromium.launch(args[--disable-images, --disable-service-worker])8. 项目扩展与进阶方向这个示例项目是一个起点。根据实际需求你可以从以下几个方向进行扩展分布式爬虫使用CeleryRedis或ScrapyScrapy-Redis架构将URL分发到多台机器或多个进程用Playwright作为下载器中间件来处理动态页面。集成Scrapy虽然Scrapy本身是异步框架但直接集成Playwright的异步API需要一些技巧。可以使用scrapy-playwright这样的第三方中间件它很好地桥接了两者。数据管道采集后的数据可以实时推送到消息队列如Kafka、RabbitMQ然后由下游的数据清洗、入库服务消费。容器化部署将爬虫脚本、Playwright浏览器打包进Docker镜像便于在云服务器或K8s集群上调度运行。注意在Docker中运行需要安装必要的系统依赖。更复杂的解析对于极其复杂的页面可以结合BeautifulSoup或parsel进行HTML解析或者直接使用Playwright获取渲染后的HTML字符串再用这些库解析有时比纯Playwright选择器更灵活。API逆向工程如果网站的数据是通过API接口XHR/Fetch获取的直接用Playwright拦截这些请求直接获取结构化数据通常是JSON效率远高于解析HTML。使用page.on(request)和page.on(response)事件监听器可以轻松做到。我个人在长期使用Playwright进行数据采集后最大的体会是它模糊了“爬虫”和“自动化测试”的边界。你用来保证网页功能正确的工具同样可以高效地获取数据。这种思路的转变往往能帮你打开一扇新的大门。最后一个小技巧善用playwright inspector通过设置PWDEBUG1环境变量启动它能让你以可交互的方式单步调试你的脚本对于理解页面加载顺序和定位元素找不到的问题有奇效。