Playwright网络监听技术:高效抓取小红书无限滚动数据

📅 2026/7/2 22:26:26
Playwright网络监听技术:高效抓取小红书无限滚动数据
1. 项目概述为什么选择 Playwright 监听网络流来抓取小红书最近在做一个数据采集项目目标是小红书某个话题下的所有笔记。一开始我和很多人一样想着用传统的 Selenium 或者直接分析页面接口来搞定。但实际操作下来发现小红书这类现代单页应用SPA的无限滚动加载真是个“磨人的小妖精”。页面元素动态生成接口参数复杂且可能加密用常规的等待元素出现或者模拟滚动的方式不仅效率低下而且极其不稳定动不动就漏数据或者被反爬机制干扰。这时候Playwright的网络监听Network Monitoring能力就派上用场了。它的核心思路不是去费力地“猜”页面什么时候加载了新内容而是直接“监听”浏览器和服务器之间的所有网络通信。当页面滚动时必然会触发新的网络请求去获取更多笔记数据我们只要精准地捕获到这个特定的请求解析其响应就能稳定、高效地拿到原始数据。这种方法绕开了复杂的页面渲染逻辑直击数据源头堪称“优雅”。这个项目适合有一定 Python 基础了解 HTTP 协议并且被动态加载网站“折磨”过的爬虫开发者。如果你还在为抓取无限滚动列表而头疼那么这套“监听网络流”的方案或许能给你带来新的思路。2. 核心思路与方案选型监听 vs. 传统抓取在动手之前我们先厘清几种常见抓取无限滚动页面的方法并分析为什么监听网络流是更优解。2.1 传统方法的瓶颈模拟滚动与元素等待使用page.evaluate()执行 JavaScript 滚动页面然后使用page.wait_for_selector()或page.wait_for_function()等待新内容加载。这种方法的问题在于效率低需要不断滚动和等待耗时很长。不稳定网络延迟或页面渲染速度变化都可能导致等待超时从而中断爬虫。难以判断终点很难准确判断何时已加载完所有内容可能陷入无限滚动或提前结束。直接调用接口API通过浏览器开发者工具的 Network 面板找到加载数据的 XHR/Fetch 请求然后直接用requests库模拟这个请求。这种方法看似高效但面临巨大挑战参数复杂小红书等平台的接口通常带有加密参数如sign、token、x-sign这些参数由前端 JavaScript 动态生成逆向分析成本极高。易失效接口地址和参数可能频繁变动需要持续维护。风控严格直接调用数据接口缺少浏览器环境和用户行为模拟更容易触发反爬虫机制。2.2 Playwright 网络监听的优势Playwright 提供了强大的page.on(‘request’)和page.on(‘response’)事件监听器。我们的方案是监听所有响应在页面加载前就设置一个监听函数捕获所有网络请求的响应。过滤目标请求根据 URL 特征关键词如/api/sns/web/v1/feed、请求方法GET/POST等从海量请求中筛选出那个负责加载笔记列表的“数据请求”。解析响应数据一旦捕获到目标响应立即提取其 JSON 格式的响应体这里面包含了最原始的笔记数据。为什么说它优雅精准高效无需等待页面渲染完成直接获取数据速度极快。稳定可靠只要页面滚动触发数据请求我们就能捕获到不受页面渲染差异影响。绕过前端加密部分我们拿到的是服务器返回的原始响应。虽然请求发出时可能已携带加密参数但 Playwright 作为真实浏览器环境这些参数是由浏览器上下文自然生成的我们无需关心其生成逻辑。我们只关心响应结果。信息完整网络响应通常包含比页面渲染后更丰富、更结构化的原始数据。注意此方法并不能完全规避所有反爬措施。网站仍可通过请求频率、浏览器指纹、WebSocket 等方式进行风控。我们的方案是在模拟真人浏览的基础上选择了一条更稳健的数据获取路径。3. 环境准备与 Playwright 核心配置工欲善其事必先利其器。我们先搭建好爬虫环境。3.1 创建项目与安装依赖建议使用虚拟环境来管理依赖避免包冲突。# 创建项目目录并进入 mkdir xhs-crawler cd xhs-crawler # 创建虚拟环境 (Python 3.8) python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 安装核心库playwright pip install playwright # 安装Playwright所需的浏览器这里选择Chromium够用且轻量 playwright install chromium除了playwright我们可能还需要asyncio用于异步操作Playwright 天然支持异步效率更高以及json、csv等数据处理库这些都是 Python 标准库无需额外安装。如果需要存储数据到数据库再安装pymysql或sqlite3等。3.2 初始化 Playwright 与浏览器上下文Playwright 支持同步和异步两种 API。对于网络监听这种 I/O 密集型操作强烈推荐使用异步 API能更好地处理并发和等待事件。import asyncio from playwright.async_api import async_playwright import json async def main(): # 启动 Playwright管理浏览器进程 async with async_playwright() as p: # 启动一个 Chromium 浏览器实例headlessFalse 表示显示浏览器界面便于调试 browser await p.chromium.launch(headlessFalse, slow_mo100) # slow_mo 可减慢操作方便观察 # 创建一个浏览器上下文可以隔离 cookies、缓存等 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() # ... 后续的监听和页面操作将在这里进行 ... # 操作完成后关闭浏览器 await browser.close() # 运行主函数 asyncio.run(main())关键配置解析headlessFalse在开发调试阶段建议关闭无头模式这样你能看到浏览器实际运行情况确认页面是否正常加载、滚动是否生效。slow_mo100设置每个 Playwright 操作后延迟 100 毫秒让自动化操作慢下来方便人眼观察和调试。生产环境应移除或减小该值。new_context()创建独立的上下文非常重要。它相当于一个独立的浏览器会话可以设置独立的 User-Agent、代理、地理位置等避免不同任务间相互干扰。也为后续应对反爬如更换 User-Agent提供了灵活性。user_agent设置一个常见的桌面端 Chrome UA模拟普通用户。4. 核心实现监听、过滤与数据提取这是整个爬虫最核心的部分。我们将设置网络监听访问小红书页面模拟滚动并在滚动过程中捕获数据接口的响应。4.1 设置网络响应监听器我们需要在页面加载目标网址之前就设置好监听器以确保不会错过任何初始的或早期的请求。# 定义一个列表来存储我们捕获到的笔记数据 captured_notes [] async def handle_response(response): 处理网络响应的回调函数。 当页面收到任何响应时此函数都会被调用。 # 1. 根据URL特征过滤出目标接口 # 以小红书Web端首页信息流为例关键接口可能包含 ‘/api/sns/web/v1/feed’ # 你需要通过浏览器开发者工具F12 - Network - XHR/Fetch手动确认目标接口的URL模式 target_keyword /api/sns/web/v1/feed if target_keyword in response.url: # 2. 只处理状态码成功的响应 if response.status in [200, 201]: try: # 3. 尝试将响应体解析为JSON response_json await response.json() # 4. 从JSON中提取笔记数据 # 接口数据结构需要实际分析这里假设数据在 response_json[data][notes] 下 notes response_json.get(data, {}).get(notes, []) if notes: print(f捕获到 {len(notes)} 条笔记数据) for note in notes: # 提取关键字段例如笔记ID、标题、内容、点赞数等 note_info { note_id: note.get(id), title: note.get(title), desc: note.get(desc), # 正文摘要 likes: note.get(likes), user: note.get(user, {}).get(nickname), timestamp: note.get(timestamp) } captured_notes.append(note_info) except Exception as e: # 如果响应不是JSON或解析失败打印错误有时可能是空响应或其他格式 print(f解析响应失败: {response.url}, 错误: {e}) else: print(f请求失败: {response.url}, 状态码: {response.status}) # 在创建 page 对象后加载URL前绑定监听器 page.on(response, handle_response)实操要点target_keyword的确定这是成功的关键。你必须手动打开小红书网页进入开发者工具的 Network 面板筛选 XHR/Fetch 请求然后不断滚动页面。观察哪个请求的响应里包含了新的笔记列表数据并记录下其 URL 中的独特且稳定的部分。不同页面首页、搜索页、用户页的接口可能不同。数据结构解析response_json的结构需要你仔细展开分析。使用print(json.dumps(response_json, indent2, ensure_asciiFalse))将捕获到的第一个响应漂亮地打印出来然后找到笔记数组的具体路径。可能是[‘data’][‘items’]也可能是其他。异步处理await response.json()是异步调用所以handle_response必须是async函数。4.2 访问页面与模拟无限滚动设置好监听器后我们导航到目标页面并模拟用户滚动行为以触发数据加载。# 目标URL示例小红书“旅行”话题页 target_url https://www.xiaohongshu.com/explore?tag旅行 await page.goto(target_url, wait_untilnetworkidle) # wait_until 确保页面基本加载完毕 print(页面加载完成开始模拟滚动...) # 设置滚动参数 scroll_pause_time 2 # 每次滚动后等待的时间用于网络请求完成 max_scroll_attempts 20 # 最大滚动次数防止无限循环 scroll_attempt 0 # 用于判断是否还有新内容的简单标志 last_note_count 0 while scroll_attempt max_scroll_attempts: scroll_attempt 1 print(f第 {scroll_attempt} 次滚动...) # 记录滚动前的数据量 current_count len(captured_notes) # 执行滚动到底部的JavaScript await page.evaluate(window.scrollTo(0, document.body.scrollHeight);) # 等待一段时间让网络请求发生并完成 await page.wait_for_timeout(scroll_pause_time * 1000) # wait_for_timeout 单位是毫秒 # 检查是否有新数据被捕获 if len(captured_notes) current_count: # 如果数据量没有增加可能已加载到底或网络问题 print(f滚动后未发现新数据尝试第 {scroll_attempt} 次。) # 可以增加一个连续无新数据的计数器超过阈值则退出 else: print(f发现新数据当前总计: {len(captured_notes)} 条) last_note_count len(captured_notes) # 一个简单的终止条件如果连续3次滚动都没有新数据则停止 # 这里需要更健壮的判断例如检查页面底部是否有“没有更多了”的提示元素 # 我们可以尝试查找特定的结束标志 try: end_element await page.wait_for_selector(div.no-more:has-text(没有更多了), timeout2000) # 等待2秒看是否出现 if end_element: print(已到达页面底部停止滚动。) break except: # 没找到结束标志继续滚动 pass print(f滚动结束共捕获 {len(captured_notes)} 条笔记数据。)滚动策略详解wait_until‘networkidle’让page.goto()等待到网络基本空闲约500ms内无超过2个网络请求再继续确保页面主体框架已加载监听器已就位。滚动与等待的平衡scroll_pause_time是关键。太短数据请求还没返回就进行了下一次滚动导致漏数据太长则效率低下。2-3秒是一个比较安全的起点具体取决于网络速度和网站响应。可以动态调整例如在捕获到数据后等待时间可以稍短。终止条件单纯的固定次数滚动 (max_scroll_attempts) 不保险。最佳实践是结合多种判断数据层面连续 N 次滚动后捕获的数据量不再增长。页面层面使用page.wait_for_selector()或page.evaluate()检查页面底部是否出现了“没有更多内容”、“加载完毕”等特定元素或文本。接口层面监听的数据接口返回了has_more: false或cursor为空等标志。 这里我们演示了结合数据增长和页面元素检测的方法。你需要根据小红书实际的前端实现来调整选择器。4.3 数据存储与去重捕获到的数据通常需要保存下来。为了简单起见我们保存为 JSON 文件。在实际项目中你可能会存入数据库。import json from datetime import datetime def save_data(data, filename_prefixxhs_notes): 将数据保存为JSON文件 if not data: print(没有数据可保存。) return # 生成带时间戳的文件名 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename f{filename_prefix}_{timestamp}.json try: with open(filename, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f数据已成功保存到 {filename}) except IOError as e: print(f保存文件时出错: {e}) # 在主函数末尾滚动结束后调用保存 save_data(captured_notes)去重考虑在无限滚动中如果网络不稳定或滚动逻辑不完美可能会重复捕获同一条笔记。一个简单的去重方法是在handle_response函数中或在保存数据前根据笔记的唯一ID如note_id进行过滤。# 在 handle_response 内部或保存前进行去重 seen_ids set() unique_notes [] for note in captured_notes: note_id note.get(note_id) if note_id and note_id not in seen_ids: seen_ids.add(note_id) unique_notes.append(note) print(f去重前: {len(captured_notes)} 条, 去重后: {len(unique_notes)} 条) captured_notes unique_notes # 更新数据列表5. 进阶技巧与反爬策略应对一个能在生产环境稳定运行的爬虫必须考虑反爬机制。Playwright 提供了很多模拟真人行为的工具。5.1 模拟真人行为模式直接、规律地滚动到底部容易被识别为机器人。我们需要增加一些随机性和人类特征。import random async def human_like_scroll(page): 模拟人类滚动行为 # 1. 随机滚动距离而不是每次都到底部 viewport_height await page.evaluate(window.innerHeight) # 随机滚动当前视窗高度的 0.5 到 1.5 倍 scroll_amount random.randint(int(viewport_height * 0.5), int(viewport_height * 1.5)) await page.evaluate(fwindow.scrollBy(0, {scroll_amount})) # 2. 随机等待时间 pause_time random.uniform(1.5, 4.0) # 1.5到4秒之间的随机等待 await page.wait_for_timeout(int(pause_time * 1000)) # 3. 偶尔小幅回滚或横向滚动更拟人 if random.random() 0.1: # 10%的概率 await page.evaluate(window.scrollBy(0, -100)) await page.wait_for_timeout(500) # 4. 模拟鼠标移动可选消耗资源 # await page.mouse.move(random.randint(0, 500), random.randint(0, 500)) # 在主循环中用 human_like_scroll 替换简单的 scrollTo 和固定等待 # await human_like_scroll(page)5.2 管理 Cookies 与登录态很多数据需要登录后才能查看。Playwright 可以持久化登录状态。# 方法一手动登录后保存状态适合需要交互式登录如扫码 context await browser.new_context(...) page await context.new_page() await page.goto(https://www.xiaohongshu.com/) # **手动进行登录操作**比如扫码 input(请在浏览器中完成登录然后按回车继续...) # 将登录状态保存到文件 await context.storage_state(path./xhs_login_state.json) await browser.close() # 下次启动时直接加载这个状态 context await browser.new_context(storage_state./xhs_login_state.json) page await context.new_page() # 此时页面已是登录状态警告存储的登录状态Cookies有有效期。且网站更新登录策略后可能失效。此方法主要用于减少频繁扫码的麻烦。5.3 使用代理IP高频率请求同一网站容易被封IP。使用代理是常见解决方案。# 在启动浏览器时设置代理 browser await p.chromium.launch( headlessFalse, proxy{ server: http://your-proxy-server:port, # 例如 http://123.45.67.89:8080 # 如果需要用户名密码认证 username: your_username, password: your_password } )代理注意事项确保代理 IP 的匿名性和稳定性。免费代理大多不可靠建议使用付费的住宅代理或高质量数据中心代理。可以在代码中集成代理池实现自动切换。5.4 处理动态加载与懒加载图片有时笔记列表接口返回的数据中图片链接可能是缩略图或带有动态参数。如果需要高清图可能需要进一步处理。此外页面上的图片是懒加载的直接通过page.screenshot()可能截不到完整图。可以强制加载图片# 在滚动结束后可以执行JS让所有懒加载图片加载出来 await page.evaluate(() { window.scrollTo(0, 0); let images document.querySelectorAll(img[loading\lazy\]); images.forEach(img { img.loading eager; // 强制立即加载 // 或者滚动到图片位置触发加载 // img.scrollIntoView({behavior: smooth, block: center}); }); }) await page.wait_for_load_state(networkidle) # 等待图片加载6. 完整代码整合与优化将上述所有模块整合并加入更健壮的异常处理和日志记录。import asyncio import json import random from datetime import datetime from playwright.async_api import async_playwright class XHSSpider: def __init__(self, headlessFalse, use_proxyFalse, proxy_configNone): self.headless headless self.use_proxy use_proxy self.proxy_config proxy_config self.captured_data [] self.seen_ids set() async def init_browser(self): 初始化浏览器和上下文 self.p await async_playwright().start() launch_options { headless: self.headless, slow_mo: 50 # 生产环境可设为0或移除 } if self.use_proxy and self.proxy_config: launch_options[proxy] self.proxy_config self.browser await self.p.chromium.launch(**launch_options) context_args { viewport: {width: 1920, height: 1080}, 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, # 可以加载之前保存的登录状态 # storage_state: ./xhs_login_state.json } self.context await self.browser.new_context(**context_args) self.page await self.context.new_page() def on_response(self, response): 网络响应监听回调注意这里为了简化用了同步函数装饰异步实际需调整 # 实际应用中这里应定义为 async 函数并在主循环中通过 page.on(response, ...) 绑定 # 此处为逻辑示意 pass async def setup_listener(self): 设置网络监听器 async def _handle_response(response): target_keywords [/api/sns/web/v1/feed, /api/sns/web/v2/note/feed] # 可配置多个关键词 if any(kw in response.url for kw in target_keywords): if response.status 200: try: data await response.json() # 根据实际接口结构解析这里是一个示例 items data.get(data, {}).get(items, []) for item in items: note_data self.parse_note_item(item) if note_data and note_data[id] not in self.seen_ids: self.seen_ids.add(note_data[id]) self.captured_data.append(note_data) print(f捕获笔记: {note_data.get(title, 无标题)[:20]}...) except Exception as e: print(f解析接口 {response.url} 失败: {e}) self.page.on(response, _handle_response) def parse_note_item(self, item): 解析单条笔记数据根据实际接口结构调整 # 这是一个示例解析函数 return { id: item.get(id), title: item.get(title), desc: item.get(desc), likes: item.get(likes_count, 0), user_nickname: item.get(user, {}).get(nickname), timestamp: item.get(time), note_url: fhttps://www.xiaohongshu.com/explore/{item.get(id)} } async def human_scroll(self, max_attempts50): 模拟人类滚动行为 no_new_data_count 0 for attempt in range(1, max_attempts 1): print(f[滚动尝试 {attempt}/{max_attempts}]) prev_count len(self.captured_data) # 随机滚动 scroll_height random.randint(500, 1500) await self.page.evaluate(fwindow.scrollBy(0, {scroll_height})) # 随机等待 await self.page.wait_for_timeout(random.randint(1500, 3500)) # 检查是否触底 is_at_bottom await self.page.evaluate(() { return (window.innerHeight window.scrollY) document.body.scrollHeight - 10; }) # 检查是否有“没有更多”的提示 try: end_flag await self.page.wait_for_selector(div:has-text(没有更多了), div:has-text(END), timeout2000) if end_flag: print(检测到页面结束标志。) return True except: pass # 判断数据是否增长 if len(self.captured_data) prev_count: no_new_data_count 1 if no_new_data_count 5: # 连续5次无新数据 print(连续多次滚动未获取新数据可能已加载完毕。) return True else: no_new_data_count 0 print(f数据更新当前总数: {len(self.captured_data)}) if is_at_bottom and no_new_data_count 2: print(已滚动到底部且无新数据。) return True print(f达到最大滚动次数 {max_attempts}。) return False async def crawl(self, url): 主爬取流程 await self.init_browser() await self.setup_listener() print(f正在访问: {url}) await self.page.goto(url, wait_untilnetworkidle) await self.page.wait_for_timeout(3000) # 初始加载等待 # 开始滚动抓取 await self.human_scroll(max_attempts30) print(f抓取结束共获得 {len(self.captured_data)} 条去重笔记。) # 保存数据 if self.captured_data: self.save_to_json() await self.close() def save_to_json(self): 保存数据到JSON文件 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename fxhs_data_{timestamp}.json with open(filename, w, encodingutf-8) as f: json.dump(self.captured_data, f, ensure_asciiFalse, indent2) print(f数据已保存至: {filename}) async def close(self): 关闭资源 await self.context.close() await self.browser.close() await self.p.stop() async def main(): spider XHSSpider( headlessFalse, # 调试时设为False use_proxyFalse, # 根据需求开启 # proxy_config{server: http://your-proxy:port} ) target_url https://www.xiaohongshu.com/explore?tag旅行 await spider.crawl(target_url) if __name__ __main__: asyncio.run(main())7. 常见问题排查与调试技巧在实际运行中你肯定会遇到各种问题。这里记录一些典型问题的排查思路。7.1 监听不到目标请求症状程序运行了也在滚动但captured_data始终为空。排查步骤确认URL关键词打开浏览器开发者工具 (F12)切换到 Network 面板筛选 XHR/Fetch。手动滚动页面观察哪个请求的 Preview 或 Response 里包含笔记数据。确保你使用的target_keyword完全匹配这个请求URL的一部分。注意URL可能包含查询参数?后面的部分关键词要匹配路径部分。检查监听器绑定时机确保page.on(‘response’, ...)是在page.goto()之前设置的。如果之后设置可能会错过初始请求。关闭无头模式设置headlessFalse亲眼观察页面是否正常加载、滚动是否生效。打印所有请求URL在handle_response函数开头简单打印response.url看看监听器是否在工作以及目标请求是否真的被浏览器发出。有时网站可能使用了 WebSocket 或 GraphQL而不是简单的 REST API。检查登录状态某些接口需要登录才能访问。确保你的浏览器上下文 (context) 带有有效的登录 Cookies通过storage_state加载。7.2 数据解析失败或结构不对症状能捕获到请求但response.json()报错或解析出的notes为空。排查步骤检查响应状态码确保只处理200或201的响应。查看原始响应在handle_response里如果解析 JSON 失败可以尝试用response.text()先看看返回的是什么。可能是 HTML 错误页面或者是不同的数据格式。分析数据结构将成功捕获的第一个响应体用json.dumps(..., indent2)漂亮地打印到控制台或文件。仔细研究其嵌套结构找到笔记数组的真实路径。路径可能像[‘data’][‘items’]、[‘feeds’]、[‘result’][‘data’]等。注意分页参数无限滚动通常涉及分页。查看请求的 URL 或请求体是否有cursor、page、since_id等参数。我们的监听方法自动捕获了每次滚动触发的请求所以通常不需要手动管理分页。7.3 滚动无法触发新请求或提前停止症状滚动几下就停了或者一直滚动但数据不增加。排查步骤调整等待时间scroll_pause_time或随机等待区间可能太短网络请求还没返回就判定为无新数据。适当延长等待时间或改为等待某个特定元素出现如果新数据加载后有特定元素。优化终止条件我们的示例使用了简单的“连续 N 次无新数据”和“检测结束标志”。你需要根据小红书页面的实际 UI调整检测结束标志的选择器。可能是.loading-end、.no-more等 class也可能是特定的文本内容。模拟更真实的滚动使用human_like_scroll函数增加滚动的随机性。固定的scrollToBottom行为特征太明显。检查页面是否被限制打开浏览器看看滚动时页面是否弹出了验证码、登录模态框或者提示“操作过于频繁”。如果遇到说明你的爬虫行为已被识别。需要增加更复杂的反反爬策略如更长的随机延迟、使用住宅代理、甚至模拟鼠标移动和点击。7.4 程序运行速度慢或内存占用高症状爬取一段时间后程序变慢或崩溃。优化建议关闭不必要的资源加载可以在上下文中设置拦截并阻止图片、样式表、字体等非必要资源的加载大幅提升速度。context await browser.new_context( ..., # 拦截请求只放行文档和XHR/Fetch bypass_cspTrue, # 可能需要 ) await context.route(**/*.{png,jpg,jpeg,svg,gif,css,woff,woff2}, lambda route: route.abort())注意拦截资源可能导致页面渲染不正常但因为我们不依赖视觉元素只监听网络请求所以通常可行。先测试是否影响目标数据接口的触发。定期清理数据如果爬取量极大注意captured_data列表的内存占用。可以考虑分批写入文件或数据库。使用无头模式生产环境运行时设置headlessTrue可以节省大量GUI渲染资源。合理控制并发虽然 Playwright 支持多页面并发但对于单个网站过高的并发请求会导致 IP 被封。建议单进程单页面并设置合理的请求间隔。7.5 应对反爬虫机制WebDriver 检测Playwright 默认会暴露一些属性如navigator.webdriver可能被网站检测。可以通过启动参数尝试隐藏browser await p.chromium.launch( headlessFalse, args[--disable-blink-featuresAutomationControlled] ) # 或者在上下文中注入JS来覆盖属性 await context.add_init_script( Object.defineProperty(navigator, webdriver, { get: () undefined }); )验证码这是最棘手的。如果遇到验证码可以考虑降低请求频率这是最有效的方法。使用付费打码服务如第三方API。手动处理在headlessFalse模式下如果弹出验证码程序可以暂停 (input(“请手动完成验证码后按回车...”))但这不适合自动化。指纹识别网站会收集浏览器指纹Canvas, WebGL, Fonts 等。Playwright 的每个上下文相对独立但高级反爬仍可能识别。使用真实浏览器配置文件或更复杂的指纹混淆库成本较高。调试是一个迭代的过程。核心思路是先用最简化的代码如只监听、不滚动确认能捕获到数据然后逐步加入滚动逻辑最后再优化反爬和稳定性。多利用headlessFalse模式观察多用print语句输出关键变量这是定位问题最快的方法。