Playwright实战:破解无限滚动网页爬虫难题

📅 2026/7/8 17:56:54 👁️ 阅读次数 📝 编程学习
Playwright实战:破解无限滚动网页爬虫难题

1. 项目概述:为什么滚动加载是爬虫的“硬骨头”?

最近在做一个数据分析项目,需要从几个资讯聚合平台和社交媒体上抓取历史内容。一开始我像往常一样用requestsBeautifulSoup,代码跑起来挺快,但一看数据量,傻眼了——怎么只有最近二三十条?页面明明显示有成千上万条内容。问题就出在“滚动加载”上。现代网页,尤其是内容流形式的,为了性能和用户体验,普遍采用了无限滚动(Infinite Scroll)或分页滚动加载的技术。你第一次访问页面,服务器只返回一个“壳”和第一屏的数据。当你滚动到页面底部时,浏览器会悄悄向后台发送一个请求,拿到下一批数据,然后通过JavaScript动态地插入到DOM中。这个过程对用户是无感的,但对于传统爬虫来说,就像面对一堵会自己长高的墙:你不动,墙就停在那里;你不动,就永远看不到墙后面的东西。

这就是为什么Playwright这类现代浏览器自动化工具变得如此重要。它不是一个简单的HTTP请求库,而是一个能驱动真实浏览器(如Chromium、Firefox、WebKit)的“遥控器”。你可以用它模拟人类的所有操作:点击、输入、滚动,甚至等待特定的网络请求或元素出现。对于滚动加载的页面,核心思路就是“骗过”网页:让浏览器以为真的有用户在不停地滚动鼠标滚轮,从而触发加载逻辑,直到所有内容都呈现在DOM里,我们再一次性抓取。

这个实战项目,我将带你用Playwright完整地攻克一个需要滚动加载的动态网页。我会从环境搭建、核心原理讲起,然后手把手带你写一个健壮的爬虫脚本,其中会包含如何智能判断加载完成、如何处理各种异常状况,以及如何将数据规整地保存下来。无论你是想爬取商品列表、新闻资讯、社交媒体动态,还是评论区内容,这套方法都通用。

2. 核心原理与工具选型:为什么是Playwright?

在动手之前,我们得先搞清楚“敌人”是谁,以及我们选的“武器”到底强在哪里。

2.1 动态网页与滚动加载的机制

传统的静态网页,HTML文档在服务器端就完全生成好了,一次性发送给浏览器。爬虫只需要解析这个HTML就能拿到所有数据。动态网页则不同,它更像一个应用程序(SPA,单页应用)。服务器首次返回的是一个非常简单的HTML框架和一堆JavaScript代码。浏览器执行这些JS代码后,才会去请求实际的数据(通常是通过AJAX调用RESTful API获取JSON格式的数据),然后再由JS动态地创建和插入DOM元素来渲染页面。

滚动加载是动态网页的一种常见数据加载策略。其技术实现通常基于“交叉观察器”(Intersection Observer API)或监听滚动事件。当页面滚动时,JS会检测一个特定的“哨兵”元素(比如一个加载动画div,或者列表的最后一项)是否进入了可视区域。一旦进入,就触发一个函数,去请求下一页的数据,拿到数据后,再通过DOM操作将新内容追加到现有列表的尾部。

所以,爬取这类页面的关键,从“获取静态HTML”变成了“触发JS执行并等待其完成数据加载和渲染”。

2.2 Playwright vs. Selenium vs. Pyppeteer

市面上能驱动浏览器的工具不止一个,为什么我强烈推荐Playwright?我们来做个快速对比:

特性PlaywrightSeleniumPyppeteer (Puppeteer的Python版)
诞生背景微软出品,较新(2020年),专为现代Web测试和自动化设计。历史最悠久,生态庞大,是行业标准之一。谷歌Puppeteer的Python端口,但维护和更新有时滞后。
浏览器支持统一API支持Chromium、Firefox、WebKit三大内核。支持所有主流浏览器,但不同浏览器需要不同的驱动(WebDriver)。主要针对Chromium/Chrome。
执行速度非常快。通信协议高效,启动和操作速度快。相对较慢,WebDriver协议开销较大。快,但略逊于Playwright。
自动等待内置智能等待。大部分操作(如click,fill)会自动等待元素可操作。需要手动设置等待(隐式/显式等待),否则极易报错。有自动等待,但不如Playwright全面和稳定。
功能特性功能强大且现代。原生支持拦截和修改网络请求录制脚本移动端模拟多页面上下文等。功能全面,但一些高级特性需要复杂配置或第三方库。功能强大,接近Playwright,但API设计略有不同。
安装部署一条命令自动下载浏览器,非常省心。需要单独下载浏览器驱动,并确保版本匹配,繁琐易出错。类似Playwright,但有时会遇到依赖问题。
社区与文档文档优秀,社区活跃增长快。社区最大,资料最多,但质量参差不齐。社区较小,文档主要跟随Puppeteer。

实操心得:我早期项目大量使用Selenium,被其繁琐的驱动管理和脆弱的等待机制折磨得不轻。后来切换到Playwright,最大的感受就是“稳”和“快”。它的自动等待几乎让我忘记了time.sleep的存在,网络拦截功能对于分析数据接口和优化爬取效率是神器。对于爬虫场景,特别是需要应对复杂交互和反爬的现代网站,Playwright是目前综合体验最好的选择,没有之一。

2.3 项目环境准备

我们使用Python版本的Playwright。确保你的电脑上已经安装了Python(建议3.8及以上版本)。

首先,通过pip安装Playwright库:

pip install playwright

安装完成后,我们需要安装它所需的浏览器内核。Playwright很贴心地提供了安装命令,它会下载Chromium、Firefox和WebKit到本地,但不会干扰你系统已安装的浏览器。

playwright install

这个步骤可能会花费一些时间,因为它要下载几百兆的浏览器文件。建议在网络通畅的环境下进行。

注意事项:在某些企业内网或受限环境下,playwright install可能会因为网络问题失败。你可以尝试使用镜像源,或者手动下载浏览器二进制文件。更简单的办法是,在代码中指定使用系统已安装的Chrome或Edge(非Chromium),但这需要额外配置,且可能失去一些版本一致性带来的稳定性。对于爬虫项目,我强烈建议使用Playwright自带的Chromium,保证环境纯净。

3. 爬虫核心设计与思路拆解

面对一个需要滚动加载的页面,我们不能蛮干。一个健壮的爬虫脚本,其设计思路应该像下棋一样,走一步看三步。下面是我总结的核心四步走策略:

第一步:人工侦察,分析页面行为。在写任何代码之前,先用浏览器手动访问目标网站。打开开发者工具(F12),重点关注“网络”(Network)标签页。然后开始滚动页面,观察有哪些新的网络请求被触发。通常,你会看到一些XHR或Fetch请求,其响应是JSON格式的数据,里面就包含了我们想要的内容列表。记下这个请求的URL、方法(GET/POST)、请求头(特别是可能有的认证信息)和请求参数。如果能直接抓到数据接口,那将是最高效的方式,可以绕过浏览器渲染直接请求数据。但很多网站会对接口进行加密、签名或验证,这时我们就不得不采用更通用的“浏览器模拟”方案。

第二步:模拟滚动,触发加载。如果接口分析困难,我们就采用浏览器模拟。核心是让Playwright控制页面进行滚动。这里不是简单的一次性滚动到底,而是需要“循环滚动-等待-检查”的策略。我们需要让页面滚动到某个位置(通常是底部),然后等待可能的加载动画出现并消失,或者等待新内容被添加到DOM中。

第三步:判断何时停止。无限滚动不能真的无限爬下去。我们需要一个明确的停止条件。常见的条件有:

  1. 内容条数达到目标:比如我只想爬500条。
  2. 滚动后页面高度不再增加:连续滚动几次,页面总高度不变,说明没有新内容了。
  3. 出现“没有更多内容”的提示元素:很多网站在加载完所有数据后,会显示一个“已加载全部”或“END”的提示。
  4. 超时:设置一个最大滚动次数或最长运行时间,防止脚本卡死。

第四步:提取与保存数据。当所有内容都加载到DOM中后,我们就可以像处理静态页面一样,使用Playwright提供的选择器(Selector)来定位和提取我们需要的数据(如标题、链接、发布时间等)。然后将数据结构化地保存下来,比如存为JSON文件或CSV文件,方便后续分析。

基于这个思路,我们的脚本框架就清晰了:

  1. 启动浏览器,打开目标页面。
  2. 进入滚动加载循环,直到满足停止条件。
  3. 在循环中:执行滚动 -> 等待加载 -> 检查状态。
  4. 循环结束后,提取页面上的所有目标数据。
  5. 清理数据并保存。
  6. 关闭浏览器。

4. 实战代码:一步步构建健壮的滚动爬虫

理论讲完了,我们直接上代码。假设我们要爬取一个模拟的无限滚动新闻列表页(你可以用任何类似的网站进行测试,比如一些图片分享网站或社交媒体的时间线)。

4.1 基础脚本搭建

首先,我们导入必要的库,并编写一个最简单的滚动函数。

import asyncio from playwright.async_api import async_playwright import json import time async def scroll_page(page, scroll_step=500, scroll_delay=1.5): """ 模拟人类滚动页面。 :param page: Playwright的页面对象 :param scroll_step: 每次滚动的像素距离,模拟一次鼠标滚轮滚动 :param scroll_delay: 每次滚动后的等待时间,模拟人类阅读间隔 """ # 获取页面当前高度 previous_height = await page.evaluate('document.body.scrollHeight') while True: # 执行JavaScript,将页面滚动到当前滚动位置 + scroll_step 的地方 # 这里使用`window.scrollBy`比直接跳到某个位置更自然 await page.evaluate(f'window.scrollBy(0, {scroll_step})') # 等待一段时间,让可能触发的动态内容有时间加载 await page.wait_for_timeout(int(scroll_delay * 1000)) # wait_for_timeout 单位是毫秒 # 再次获取滚动后的页面高度 new_height = await page.evaluate('document.body.scrollHeight') # 如果页面高度没有变化,说明可能滚动到底部了(或者没有新内容) if new_height == previous_height: # 再尝试滚动一次,并多等一会儿,防止网络延迟 await page.evaluate(f'window.scrollBy(0, {scroll_step})') await page.wait_for_timeout(3000) new_height = await page.evaluate('document.body.scrollHeight') if new_height == previous_height: print("页面高度未再增加,可能已滚动到底部。") break # 更新上一次的高度记录 previous_height = new_height print(f"已滚动,页面当前总高度:{new_height}px")

这个scroll_page函数是核心之一。它通过page.evaluate()方法在浏览器环境中执行JavaScript代码来操控页面。scroll_step不宜设置过大,否则会“跳”过触发加载的观察点。scroll_delay也很关键,给足网络请求和JS渲染的时间。

4.2 整合智能停止条件的完整爬虫

上面的基础滚动函数有个问题:它只在页面高度不变时停止。但对于一些“加载到底显示提示”的网站,我们可以在高度不变前就提前知道结束了。下面是一个更健壮的版本,整合了多种停止条件。

async def scrape_infinite_scroll_page(url, max_scroll_attempts=50, target_item_count=None): """ 爬取无限滚动页面。 :param url: 目标网页地址 :param max_scroll_attempts: 最大滚动尝试次数,防止无限循环 :param target_item_count: 目标抓取的项目数量,达到后即停止 :return: 提取到的数据列表 """ async with async_playwright() as p: # 启动浏览器,headless=False表示显示浏览器界面,调试时非常有用 browser = await p.chromium.launch(headless=False, slow_mo=100) # slow_mo让操作变慢,方便观察 context = await browser.new_context() page = await context.new_page() print(f"正在访问页面: {url}") await page.goto(url, wait_until='networkidle') # wait_until='networkidle' 等待页面网络空闲 all_items = [] scroll_attempts = 0 # 定义我们想要提取的数据项的选择器(根据实际网页修改) # 例如,假设每个新闻条目在一个 class='news-item' 的div里 item_selector = '.news-item' while scroll_attempts < max_scroll_attempts: scroll_attempts += 1 print(f"\n--- 第 {scroll_attempts} 次滚动尝试 ---") # 1. 在滚动前,先获取当前已加载的项目数量 current_items = await page.query_selector_all(item_selector) current_count = len(current_items) print(f"滚动前,已检测到 {current_count} 个项目。") # 停止条件1:如果设置了目标数量且已达到 if target_item_count and current_count >= target_item_count: print(f"已达到目标项目数 {target_item_count},停止滚动。") break # 2. 执行滚动 previous_height = await page.evaluate('document.body.scrollHeight') await page.evaluate('window.scrollTo(0, document.body.scrollHeight)') # 直接滚动到底部 # 或者使用更平滑的滚动: await page.mouse.wheel(0, 1500) # 3. 等待新内容加载 # 方法A:等待可能出现的“加载中”元素消失(如果网站有的话) # try: # await page.wait_for_selector('.loading-spinner', state='hidden', timeout=5000) # except: # pass # 如果没有这个元素,忽略 # 方法B:等待特定时间,让网络请求完成 await page.wait_for_timeout(3000) # 等待3秒 # 方法C(推荐):等待新的项目元素出现 try: # 等待至少比当前多一个项目出现,最多等5秒 await page.wait_for_function( f'document.querySelectorAll("{item_selector}").length > {current_count}', timeout=5000 ) print("检测到新内容已加载。") except Exception as e: print(f"等待新内容超时或未加载: {e}") # 可能已经加载完了,检查是否有“无更多内容”的提示 no_more_selector = '.no-more, .end-of-list, .finished-loading' # 常见提示元素的类名 no_more_element = await page.query_selector(no_more_selector) if no_more_element and await no_more_element.is_visible(): print("检测到‘已无更多内容’提示,停止滚动。") break # 如果既没新内容,也没提示,再检查一次高度 new_height = await page.evaluate('document.body.scrollHeight') if new_height == previous_height: print("页面高度未增加,且无新内容,停止滚动。") break # 否则,继续尝试滚动 continue # 4. 再次检查项目数量,确认加载成功 new_items = await page.query_selector_all(item_selector) new_count = len(new_items) if new_count > current_count: print(f"成功加载了 {new_count - current_count} 个新项目。") else: print("警告:滚动后未检测到新项目,可能触发了反爬或页面结构有变。") # 这里可以加入截图功能以便调试 # await page.screenshot(path=f'debug_scroll_{scroll_attempts}.png') print(f"\n滚动结束,共尝试 {scroll_attempts} 次。开始提取数据...") # 滚动结束后,提取所有项目的数据 all_item_elements = await page.query_selector_all(item_selector) for index, item in enumerate(all_item_elements): # 这里根据实际网页结构提取数据,以下是示例 data = {} try: # 假设标题在 .title 的链接里 title_elem = await item.query_selector('.title a') data['title'] = await title_elem.inner_text() if title_elem else 'N/A' data['link'] = await title_elem.get_attribute('href') if title_elem else 'N/A' # 假设发布时间在 .publish-time 里 time_elem = await item.query_selector('.publish-time') data['publish_time'] = await time_elem.inner_text() if time_elem else 'N/A' # 假设摘要/简介在 .summary 里 summary_elem = await item.query_selector('.summary') data['summary'] = await summary_elem.inner_text() if summary_elem else 'N/A' except Exception as e: print(f"提取第 {index} 个项目时出错: {e}") data['error'] = str(e) all_items.append(data) # 可以每提取100条打印一次进度,避免输出刷屏 if (index + 1) % 100 == 0: print(f"已提取 {index + 1} 条数据...") print(f"数据提取完成,共提取到 {len(all_items)} 条数据。") # 关闭浏览器 await browser.close() return all_items # 运行爬虫 async def main(): # 替换成你的目标URL target_url = "https://example-news-site.com/infinite-scroll-list" data = await scrape_infinite_scroll_page(url=target_url, max_scroll_attempts=30, target_item_count=200) # 保存数据到JSON文件 with open('scraped_data.json', 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) print("数据已保存到 scraped_data.json") # 运行异步主函数 if __name__ == '__main__': asyncio.run(main())

这个脚本已经具备了很强的实用性。它通过wait_for_function来智能等待新内容,并设置了多种停止条件(最大尝试次数、目标数量、无更多内容提示、页面高度不变)。slow_mo参数在调试时非常有用,你可以看到浏览器每一步的操作。

4.3 高级技巧:网络请求监听与优化

有时候,滚动触发的数据加载是通过AJAX请求完成的。如果我们能直接拦截到这个请求,获取其返回的JSON数据,效率会远高于等待DOM渲染完成再解析HTML。Playwright的page.on('request')page.on('response')事件监听器可以帮我们做到这一点。

async def scrape_with_request_interception(url): """通过监听网络请求来高效爬取数据""" async with async_playwright() as p: browser = await p.chromium.launch(headless=True) # 生产环境用headless=True context = await browser.new_context() page = await context.new_page() # 创建一个列表来存储捕获到的数据 captured_data = [] # 定义响应监听函数 def handle_response(response): # 检查响应URL是否包含我们关心的关键词(比如API路径) if '/api/news/list' in response.url and response.status == 200: try: # 异步获取响应体(JSON格式) # 注意:这里是在同步函数中调用异步,需要特殊处理。 # 更稳妥的做法是使用 asyncio.create_task print(f"捕获到API请求: {response.url}") # 实际代码中,这里应该用异步方式处理 # 例如: asyncio.create_task(process_response(response)) except Exception as e: print(f"处理响应时出错: {e}") # 监听响应事件 page.on('response', handle_response) # 由于上面的监听器是同步的,处理JSON比较麻烦,我们换一种更清晰的异步方式 async def async_handle_response(response): if '/api/news/list' in response.url and response.status == 200: try: json_data = await response.json() # 假设返回的JSON结构是 {“data”: {“list”: [...]}} items = json_data.get('data', {}).get('list', []) if items: print(f"从API解析到 {len(items)} 条新数据。") captured_data.extend(items) except Exception as e: print(f"解析JSON响应失败: {e}") # 移除之前的监听器,添加异步监听器 page.remove_listener('response', handle_response) page.on('response', async_handle_response) await page.goto(url, wait_until='networkidle') # 即使监听API,我们可能仍需要滚动来触发这些请求 scroll_attempts = 0 while scroll_attempts < 20: scroll_attempts += 1 old_data_count = len(captured_data) await page.evaluate('window.scrollTo(0, document.body.scrollHeight)') # 等待可能触发的API请求 await page.wait_for_timeout(4000) if len(captured_data) == old_data_count: # 连续两次滚动没有新数据,可能结束了 print("连续滚动未捕获新数据,可能已加载完毕。") break print(f"通过API监听共捕获 {len(captured_data)} 条结构化数据。") await browser.close() return captured_data

实操心得:网络请求监听是Playwright爬虫的“王牌功能”。一旦你成功定位到数据接口,爬取效率会呈指数级提升,而且拿到的直接是结构化的JSON,省去了解析HTML的麻烦。但要注意,很多网站的API请求带有加密参数(如token、signature),直接模拟调用会比较困难。此时,监听并重用浏览器自动发起的请求是最佳策略。你可以在监听器中把关键的请求头和URL参数都记录下来。

5. 常见问题、反爬策略与排查技巧实录

即使有了完美的脚本,在实际爬取过程中你依然会碰到各种“坑”。下面是我踩过坑后总结出来的经验。

5.1 常见问题速查表

问题现象可能原因排查与解决方案
页面根本不滚动,或滚动后无新内容1. 滚动位置不对,未触发观察点。
2. 网站使用了非标准的滚动容器(不是body)。
3. 反爬机制阻止了自动化操作。
1. 使用page.mouse.wheel或滚动到特定元素(scrollIntoView)。
2. 用开发者工具检查真正的滚动容器(如div.scroll-container),然后针对它滚动:await page.evaluate('document.querySelector(\".scroll-container\").scrollTop = ...')
3. 启用slow_mo,增加随机延迟,使用更真实的用户代理(User-Agent)。
脚本卡在wait_for_functionwait_for_selector,一直超时1. 选择器写错了,元素不存在。
2. 元素加载出来了,但状态不对(如hiddenvsvisible)。
3. 页面框架(iframe)导致元素不在主页面。
1. 在浏览器控制台手动测试你的选择器:document.querySelectorAll('你的选择器')
2. 检查元素是否真的可见。有时元素存在但display: none。尝试state='attached'而非'visible'
3. 切换到正确的frame后再操作。
爬取的数据重复或缺失1. 滚动过快,DOM还未更新就进行了下一次计数。
2. 数据提取的选择器不够精确,抓到了无关元素。
3. 网站有内容去重机制,滚动时部分内容被替换。
1. 增加滚动后的等待时间(wait_for_timeout),或使用更可靠的等待条件(如等待特定数量的新元素出现)。
2. 精炼你的选择器,最好能唯一标识一个数据项容器。
3. 在每次提取数据时,根据唯一ID进行去重。
浏览器被网站检测到是自动化工具Playwright驱动的浏览器有特征会被检测。1. 使用browser.new_context()时注入user-agentviewport,模拟真实设备。
2. 尝试使用p.firefox.launch()p.webkit.launch(),不同浏览器内核被检测的概率不同。
3. 使用playwright-stealth等插件(如果有对应版本)进一步隐藏特征。
4.终极方案:如果网站反爬极强,考虑使用更底层的CDP协议或手动操作浏览器。
内存占用越来越高,最终崩溃页面加载的内容越来越多,DOM树过大。1. 定期清理不必要的页面。如果只爬一个页面,此问题不严重。
2. 如果爬取大量不同页面,务必在每个页面任务完成后关闭pagecontext
3. 考虑是否真的需要将所有数据都加载到同一个页面再提取?或许可以爬取一部分就提取保存一部分,然后刷新页面或打开新页面。

5.2 应对反爬策略的实战技巧

现代网站的反爬手段层出不穷,针对浏览器自动化的检测也越来越精密。

技巧一:伪装成普通浏览器。这是最基本也是最重要的一步。在创建浏览器上下文时,设置一个常见的用户代理和窗口大小。

context = await 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', viewport={'width': 1920, 'height': 1080}, # 还可以设置语言、时区等 locale='zh-CN', timezone_id='Asia/Shanghai', )

技巧二:模拟人类操作的不确定性。人类的滚动不是匀速的,点击也不是精准的。给你的自动化操作增加一些随机性和延迟。

import random async def human_like_scroll(page): """模拟人类不均匀的滚动""" current_scroll = 0 total_height = await page.evaluate('document.body.scrollHeight') while current_scroll < total_height: # 随机滚动一段距离(300-800像素) scroll_amount = random.randint(300, 800) current_scroll += scroll_amount # 滚动到目标位置 await page.evaluate(f'window.scrollTo(0, {current_scroll})') # 随机等待一段时间(0.5秒到3秒) wait_time = random.uniform(0.5, 3.0) await page.wait_for_timeout(int(wait_time * 1000)) # 偶尔(10%概率)小幅回滚一下,更像人类 if random.random() < 0.1: back_scroll = random.randint(-100, -50) await page.evaluate(f'window.scrollBy(0, {back_scroll})') await page.wait_for_timeout(500)

技巧三:处理验证码和登录墙。有些网站滚动几次后就会弹出验证码或要求登录。

  • 验证码:对于简单图形验证码,可以尝试用OCR库(如pytesseract)识别,但成功率有限。对于复杂验证码(如点选、滑块),通常需要引入第三方打码平台服务,这涉及到额外的成本和接口调用。
  • 登录墙:如果数据必须登录后才能看,你需要先用Playwright模拟登录流程。务必妥善保管登录凭证,不要硬编码在脚本里,可以考虑使用环境变量或配置文件。登录后,使用context.storage_state(path='state.json')保存cookies和本地存储状态,下次启动时用browser.new_context(storage_state='state.json')恢复登录状态,避免重复登录。

技巧四:使用代理IP。如果你的请求过于频繁,可能会被目标网站封禁IP。这时就需要使用代理池。

browser = await p.chromium.launch( headless=True, proxy={ 'server': 'http://your-proxy-server:port', # 如果需要认证 'username': 'your-username', 'password': 'your-password' } )

重要提示:代理IP的质量至关重要。免费的代理通常不稳定、速度慢且容易被目标网站识别。对于严肃的爬虫项目,建议使用可靠的付费代理服务。

5.3 调试与日志记录

当脚本不按预期工作时,系统的调试方法能帮你快速定位问题。

  1. 截图大法:在关键步骤或出错时截图,直观看到当时页面的状态。

    await page.screenshot(path='debug_after_scroll.png', full_page=True)
  2. 录制视频:Playwright可以录制整个操作过程,像看回放一样找问题。

    context = await browser.new_context(record_video_dir='videos/') # ... 你的爬取操作 ... # 关闭context时会自动保存视频
  3. 控制台输出:将页面的console日志和网络请求打印出来。

    # 监听console日志 def on_console(msg): print(f'浏览器Console [{msg.type}]: {msg.text}') page.on('console', on_console) # 监听失败请求 def on_request_failed(request): print(f'请求失败: {request.url} - {request.failure.error_text}') page.on('requestfailed', on_request_failed)
  4. 手动暂停:在脚本中插入await page.pause(),运行时会打开Playwright Inspector,你可以单步执行、查看选择器,就像使用IDE调试一样。

6. 性能优化与数据存储建议

当你要爬取大量数据时,效率和稳定性就成了首要考虑。

1. 异步并发爬取:上面的例子是单页面顺序爬取。Playwright支持异步并发,可以同时打开多个页面(Page)甚至多个浏览器上下文(Context)来并行工作,极大提升效率。但要注意目标网站的并发承受能力,别把人家爬挂了。

async def worker(url_queue, data_queue): async with async_playwright() as p: browser = await p.chromium.launch(headless=True) while not url_queue.empty(): url = await url_queue.get() data = await scrape_single_page(browser, url) # 你的单页爬取函数 await data_queue.put(data) await browser.close() # 创建任务队列并运行多个worker

2. 数据去重与增量爬取:在保存数据前,根据唯一标识(如文章ID、URL)进行去重。对于定期爬取的任务,可以记录上次爬取的最后一条数据的ID或时间戳,下次只爬取比这个时间新的数据。

3. 存储选择:

  • JSON/CSV文件:适合数据量不大(几万条以内)、结构简单的场景。简单易用。
  • SQLite数据库:适合中等数据量、需要简单查询的场景。无需安装数据库服务器。
  • MySQL/PostgreSQL:适合大数据量、高并发写入、需要复杂查询和持久化的生产环境。
  • MongoDB:适合数据结构灵活、变化频繁的场景。

4. 错误恢复与断点续爬:将爬取任务分解为多个批次或页码。每成功爬取并保存一个批次,就记录下进度(比如当前页码或最后一条数据的ID)。如果脚本中途因网络或错误中断,重启后可以从上次的进度点继续,而不是从头开始。

最后,也是最重要的道德与法律提醒:爬虫的世界并非法外之地。务必遵守网站的robots.txt协议,尊重版权和数据所有权。控制爬取频率,避免对目标网站服务器造成过大压力。对于明确禁止爬取或包含个人敏感信息的网站,请勿尝试。技术是工具,善用之为益,滥用之则害。