Playwright实战:破解滚动加载与点击翻页的动态网页爬虫技术

📅 2026/7/3 15:34:07
Playwright实战:破解滚动加载与点击翻页的动态网页爬虫技术
1. 项目概述从“静态”到“动态”的爬虫思维跃迁很多刚接触爬虫的朋友在学会了用requests和BeautifulSoup抓取静态页面后信心满满地冲向一个看似简单的商品列表页结果代码一跑只抓回来寥寥几条数据浏览器里明明有几十上百条。这就是典型的“动态加载”陷阱。本章我们要解决的正是这个让新手头疼的“滚动加载”和“点击翻页”问题。这不仅仅是学一个Playwright的API调用更是爬虫思维的一次关键升级——从“请求-解析”的简单模式切换到“模拟用户-等待渲染-捕获状态”的浏览器自动化模式。滚动加载和点击翻页是Web 2.0时代以来最主流的两种动态数据加载方式。前者多见于社交媒体、电商商品流、新闻资讯通过滚动到页面底部触发加载更多后者则常见于论坛、搜索结果、表格数据通过点击“下一页”按钮或页码跳转。它们的共同点是数据并非一次性加载到初始HTML中而是通过JavaScript异步请求AJAX获取再动态插入到DOM里。你用requests拿到的只是那个空空如也的“壳”。Playwright作为现代浏览器自动化工具其核心价值在于它能完整地模拟一个真实用户的操作和浏览器环境。这意味着我们不仅能“看到”最终渲染出的数据还能“触发”那个加载数据的动作。本节我将带你拆解这两种场景下的通用解决套路让你掌握一套以不变应万变的实战方法而不是死记硬背几个案例。2. 核心思路拆解触发、等待与捕获在动手写代码之前我们必须把思路理清。处理动态加载本质上是一个“状态机”的管理过程。你的爬虫脚本需要精准地控制三个环节触发加载条件、等待数据就绪、捕获稳定状态。任何一环没处理好都会导致数据缺失或脚本卡死。2.1 滚动加载的通用逻辑滚动加载的核心是“触底检测”。浏览器通常通过监听滚动容器的scroll事件判断用户是否滚动到了底部或接近底部然后发起网络请求获取下一页数据。我们的自动化脚本要模拟这个过程。关键点在于如何定义“底部”对于整个页面窗口的滚动底部就是document.documentElement.scrollHeight文档总高度减去window.innerHeight视口高度。但对于页面内某个具有独立滚动条的容器比如一个固定高度的div底部则是该容器的scrollHeight减去它的clientHeight。一个健壮的滚动加载脚本不能只滚动一次。因为一次滚动触发的请求可能只加载了部分数据比如一次加载20条。你必须循环滚动直到确认没有新数据加载出来为止。这就是“判断scrollHeight是否变化”循环的由来。如果连续两次检测容器的总滚动高度没有变化就认为所有数据已加载完毕。2.2 点击翻页的通用逻辑点击翻页的逻辑相对更“显式”。你需要找到那个能触发翻页的按钮或链接如“下一页”、“加载更多”、页码“2”点击它然后等待页面更新。这里的陷阱比想象中多元素定位翻页按钮的类名或ID可能很通用如.btn也可能随着翻页动态变化。最稳妥的方式是寻找包含特定文本如“下一页”的元素。等待策略点击之后页面会发生什么可能是整个页面刷新传统网站也可能是局部DOM更新单页应用SPA。前者需要等待页面导航完成page.wait_for_load_state(‘networkidle’)后者则需要等待特定的新元素出现page.wait_for_selector(‘.new-item’)。终止条件翻页何时结束通常是翻页按钮变为禁用状态disabled属性、消失不见或者点击后页面内容不再变化比如总条目数不变。注意无论是滚动还是点击网络等待(wait_for_load_state) 和DOM等待(wait_for_selector) 的结合使用至关重要。只等网络空闲可能DOM还没渲染完只等某个元素可能网络请求还没发出去。通常的实践是先等一个主要的网络请求完成再等一个标志性的新内容元素稳定出现。3. 实战演练一页面全局滚动加载我们先从最常见的整个页面滚动加载开始。假设我们要爬取一个无限滚动的社交媒体时间线。3.1 基础滚动函数实现下面这个函数是一个经过实战检验的通用页面滚动到底部的模板。它模拟了人类滚动行为并包含了触底判断。import time from playwright.sync_api import sync_playwright, TimeoutError def scroll_page_to_bottom(page, max_scrolls50, scroll_pause_time2.0, scroll_step500): 将整个网页窗口滚动到底部以触发动态加载。 参数: page: Playwright的Page对象。 max_scrolls: 最大滚动次数防止无限循环。 scroll_pause_time: 每次滚动后等待的时间秒用于等待内容加载。 scroll_step: 每次滚动的像素距离。模拟人类滚动不宜过大。 last_height page.evaluate(() document.documentElement.scrollHeight) scroll_attempts 0 while scroll_attempts max_scrolls: # 方法A模拟按PageDown键更接近用户行为但可能不触发某些JS监听器 # page.keyboard.press(PageDown) # 方法B执行JS滚动一段距离更可靠直接控制滚动行为 page.evaluate(f() window.scrollBy(0, {scroll_step})) # 关键等待页面可能发生的网络请求和DOM更新 # 先等待可能的网络活动变得空闲 page.wait_for_load_state(networkidle, timeout5000) # 增加超时 # 再额外等待一小段时间确保JS渲染完成 time.sleep(0.5) # 计算滚动后的新高度 new_height page.evaluate(() document.documentElement.scrollHeight) # 判断是否已滚动到底部 if new_height last_height: # 高度未变可能已到底但再尝试滚动一次并等待更久以防延迟加载 print(f滚动后高度未变 ({new_height})尝试最终检查...) page.evaluate(f() window.scrollBy(0, {scroll_step})) time.sleep(scroll_pause_time * 1.5) # 给最后一次加载更多时间 final_height page.evaluate(() document.documentElement.scrollHeight) if final_height new_height: print(f确认已滚动至页面底部。总滚动次数{scroll_attempts 1}) break else: last_height final_height continue last_height new_height scroll_attempts 1 print(f滚动尝试 {scroll_attempts}当前文档高度{new_height}) time.sleep(scroll_pause_time) # 常规滚动间隔等待 if scroll_attempts max_scrolls: print(f警告已达到最大滚动次数 ({max_scrolls})可能未加载完全部内容。)代码解读与避坑指南scroll_step的选择不要一次性滚动到底scrollTo(0, document.body.scrollHeight)。许多网站的滚动监听器会检查滚动速度或位置一次性跳到底部可能被识别为机器人行为或者错过中间某些懒加载的图片/内容。用scrollBy分步滚动更安全。双重等待机制wait_for_load_state(‘networkidle’)是核心它等待页面网络活动基本停止。但有些网站用setTimeout延迟插入DOM所以后面补一个time.sleep(0.5)是经验之谈能解决很多偶发的元素找不到的问题。“高度未变”的最终检查这是防止在“临界状态”误判的关键。当一次滚动后高度没变不一定是真的结束了可能是网络稍有延迟。所以我们再滚一次并等待更长时间scroll_pause_time * 1.5做最终确认。这个技巧让我抓取知乎时间线的成功率提升了30%以上。max_scrolls安全阀必须设置这是防止脚本因网站逻辑错误或你的判断逻辑有瑕疵而陷入死循环的最后屏障。3.2 在完整爬虫流程中集成滚动光会滚动不够我们需要在滚动过程中或滚动完成后提取数据。通常有两种策略策略A滚动-采集分离先滚完再统一采适用于数据条目独立、不会随滚动改变DOM结构的情况。优点是逻辑清晰代码简单。def crawl_infinite_scroll_page(url): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # 调试时可设为False page browser.new_page() # 1. 导航到目标页 page.goto(url, wait_untilnetworkidle) time.sleep(3) # 等待初始JS执行和渲染 # 2. 执行滚动加载所有内容 scroll_page_to_bottom(page, max_scrolls30, scroll_pause_time2) # 3. 此时所有数据应已加载到DOM中一次性提取 # 假设每条数据在一个 classitem 的元素里 items page.locator(.item).all() data_list [] for item in items: title item.locator(.title).inner_text() if item.locator(.title).count() 0 else # ... 提取其他字段 data_list.append({title: title}) print(f共采集到 {len(data_list)} 条数据。) browser.close() return data_list策略B滚动-采集混合边滚边采适用于数据量极大、防止DOM节点过多导致浏览器内存溢出的情况或者需要实时处理的情况。def crawl_and_collect_during_scroll(page, url, item_selector.item): page.goto(url, wait_untilnetworkidle) time.sleep(2) collected_items set() # 用集合去重根据唯一ID或内容 last_count 0 stable_count 0 # 连续次数未采集到新内容的计数器 while stable_count 3: # 连续3次滚动没新内容就停止 # 滚动一次 page.evaluate(() window.scrollBy(0, 800)) page.wait_for_load_state(networkidle, timeout3000) time.sleep(1) # 采集当前视口及上方新出现的数据 current_items page.locator(item_selector).all() for item in current_items: # 假设每个item有一个data-id属性作为唯一标识 item_id item.get_attribute(data-id) if item_id and item_id not in collected_items: # 采集数据... collected_items.add(item_id) # 判断是否有新内容 if len(collected_items) last_count: stable_count 1 else: stable_count 0 last_count len(collected_items) print(f已采集 {last_count} 个唯一项。) return list(collected_items)实操心得对于绝大多数情况策略A先滚后采更简单可靠。策略B的难点在于“去重”和“判断何时停止”。如果网站没有为条目提供唯一ID你需要根据内容如标题、链接哈希去重这增加了复杂度。除非遇到页面滚动到后面明显变卡DOM节点过多否则我建议先用策略A。4. 实战演练二容器内滚动加载现在来看一个更棘手但也很常见的情况数据在一个固定高度的div容器内滚动加载而不是整个页面滚动。比如后台管理系统中的表格或者弹窗里的列表。4.1 定位滚动容器这是第一步也是最容易出错的一步。你需要用浏览器的开发者工具F12仔细检查DOM结构找到那个设置了overflow: auto或overflow-y: scroll样式的元素。定位技巧在元素审查器中将鼠标悬停在元素上看哪个div的样式显示有overflow属性。滚动那个小滚动条观察哪个元素的scrollHeight和clientHeight在变化。使用Playwright Inspector在代码中插入page.pause()运行脚本它会打开一个可交互的浏览器窗口你可以直接点击元素查看其选择器。假设我们找到了这个容器它的CSS选择器是div.data-container。4.2 针对容器的滚动函数下面的函数专门用于处理这类容器内的滚动加载。其逻辑与页面滚动类似但操作对象从window变成了具体的DOM元素。def scroll_container_to_bottom(page, container_selector, max_scrolls50, scroll_pause1.5): 滚动指定容器到底部。 参数: page: Playwright Page对象。 container_selector: 滚动容器的CSS选择器。 max_scrolls: 最大滚动尝试次数。 scroll_pause: 每次滚动后等待时间。 # 定位滚动容器 scroll_container page.locator(container_selector) if scroll_container.count() 0: raise ValueError(f未找到选择器为 {container_selector} 的滚动容器。) # 确保容器在视口中并聚焦模拟点击有时能解决焦点问题 scroll_container.scroll_into_view_if_needed() # 轻微点击容器边缘避免点到内部可交互元素 scroll_container.click(position{x: 5, y: 5}) time.sleep(0.5) previous_height scroll_container.evaluate(el el.scrollHeight) scroll_attempts 0 while scroll_attempts max_scrolls: # 核心对容器元素执行滚动到底部 scroll_container.evaluate(el el.scrollTo(0, el.scrollHeight)) # 等待可能的加载 time.sleep(scroll_pause) # 对于容器内加载有时也需要等待网络 try: page.wait_for_load_state(networkidle, timeout2000) except TimeoutError: pass # 忽略网络等待超时可能没有新请求 current_height scroll_container.evaluate(el el.scrollHeight) if current_height previous_height: # 高度未变可能到底了。再试一次并延长等待。 print(高度未增加进行最终确认...) scroll_container.evaluate(el el.scrollTo(0, el.scrollHeight)) time.sleep(scroll_pause * 2) final_height scroll_container.evaluate(el el.scrollHeight) if final_height current_height: print(f容器 {container_selector} 已滚动到底部。尝试次数{scroll_attempts1}) break else: previous_height final_height continue print(f容器滚动尝试 {scroll_attempts1}高度从 {previous_height} 增加到 {current_height}) previous_height current_height scroll_attempts 1 if scroll_attempts max_scrolls: print(f警告容器滚动达到最大次数 ({max_scrolls})可能未完全加载。)关键细节解析scroll_into_view_if_needed()和click()这两步非常重要。有些复杂的页面如果焦点不在容器上直接对其执行scrollTo可能无效。先滚动到视口再轻轻点击一下可以确保容器获得焦点模拟了真实用户的操作顺序。evaluate方法的使用scroll_container.evaluate(‘el el.scrollHeight’)是在浏览器环境中对定位到的这个具体元素el执行JavaScript代码。这是与元素交互的核心方法。网络等待的差异容器内滚动加载数据请求可能由容器本身的滚动事件触发也可能由全局的AJAX管理器处理。因此这里的网络等待 (wait_for_load_state) 我放在了try...except块中并设置了较短超时。如果容器滚动不触发明显的网络请求这个等待会被跳过不影响主流程。4.3 处理复杂容器与嵌套滚动有时你会遇到容器嵌套或者容器本身是动态生成的。这时需要更精细的定位和等待。# 案例容器在某个弹窗(modal)内需要先打开弹窗 page.locator(button.show-more-data).click() # 点击按钮打开弹窗 # 等待弹窗及其内部的滚动容器出现 modal page.locator(.modal-content) modal.wait_for(statevisible) # 弹窗内的滚动容器可能有一个动态生成的类名的一部分是固定的 # 使用CSS选择器匹配部分属性 scroll_container_in_modal modal.locator(div[class*scrollable-area]) # 或者使用XPath进行更灵活的定位 # scroll_container_in_modal modal.locator(xpath.//div[contains(style, overflow)]) if scroll_container_in_modal.count() 0: scroll_container_to_bottom(page, scroll_container_in_modal) # 注意这里传的是Locator对象不是选择器字符串 # 然后从容器内提取数据 items scroll_container_in_modal.locator(.list-item).all_text_contents()注意事项当页面结构非常复杂时Playwright的locator方法支持链式调用和相对定位如locator(‘…’).locator(‘…’)这比写一个很长的复杂选择器更易读、更健壮。优先使用text、has等语义化定位器它们对前端微小的样式改动不敏感。5. 实战演练三点击“加载更多”或翻页按钮点击翻页的逻辑与滚动不同它更依赖于对特定交互元素的识别和状态判断。5.1 基础点击翻页模式最常见的模式是有一个“加载更多”按钮点击后在当前列表末尾追加新内容。def click_load_more_until_end(page, button_selectorbutton:has-text(加载更多), max_clicks20): 循环点击“加载更多”按钮直到按钮消失或禁用。 参数: page: Playwright Page对象。 button_selector: 按钮的选择器。使用 :has-text() 非常实用。 max_clicks: 最大点击次数安全阀。 click_count 0 while click_count max_clicks: # 定位按钮 load_more_button page.locator(button_selector) # 判断按钮状态是否存在、是否可见、是否可用 if load_more_button.count() 0: print(‘加载更多’按钮已不存在可能已加载全部内容。) break is_visible load_more_button.is_visible() is_disabled load_more_button.get_attribute(disabled) is not None if not is_visible or is_disabled: print(‘加载更多’按钮不可见或已被禁用停止点击。) break # 点击按钮前可以记录当前的数据项数量用于后续判断 # item_count_before page.locator(.data-item).count() print(f第 {click_count 1} 次点击‘加载更多’...) load_more_button.click() # 点击后的等待策略组合等待 # 1. 等待可能的新网络请求 page.wait_for_load_state(networkidle, timeout10000) # 2. 等待新内容出现在DOM中例如等待新出现的数据项 # 这里假设新加载的条目会有一个动画类名或者我们等待一个已知的最后一项出现 # page.wait_for_selector(.data-item:last-child, stateattached, timeout5000) # 更通用的做法等待一小段时间让JS执行完毕 time.sleep(1.5) # 可选判断内容是否真的增加了防止无效点击 # item_count_after page.locator(.data-item).count() # if item_count_after item_count_before: # print(点击后内容未增加可能已到末尾。) # break click_count 1 time.sleep(0.5) # 点击间隔避免过快被识别为机器人 print(f共点击‘加载更多’按钮 {click_count} 次。) if click_count max_clicks: print(f达到最大点击次数 ({max_clicks})请检查是否已加载完全部数据。)5.2 处理传统分页带页码对于“上一页/下一页”或直接是页码链接的传统分页逻辑类似但终止条件通常是下一页链接失效。def crawl_by_pagination(page, base_url, max_pages100): 通过点击‘下一页’或页码链接进行翻页爬取。 假设第一页已经通过 page.goto(base_url) 加载。 current_page 1 all_data [] while current_page max_pages: print(f正在采集第 {current_page} 页...) # 1. 采集当前页数据 page_data extract_data_from_current_page(page) # 你的数据提取函数 all_data.extend(page_data) # 2. 寻找并点击下一页链接 # 方式A通过文本定位‘下一页’ next_link page.locator(a:has-text(下一页)) # 方式B通过包含特定文本的链接定位更精确 # next_link page.locator(a, has_text下一页) # 方式C通过页码定位如寻找包含当前页码1的链接 # next_page_num current_page 1 # next_link page.locator(fa.page-link:has-text({next_page_num})) if next_link.count() 0 or not next_link.is_visible(): print(f第 {current_page} 页后未找到‘下一页’链接采集结束。) break # 检查链接是否可点击没有‘disabled’类等 if disabled in (next_link.get_attribute(class) or ): print(‘下一页’链接处于禁用状态采集结束。) break # 3. 点击下一页 next_link.click() # 4. 等待新页面加载完成对于整页刷新的网站 # page.wait_for_load_state(load) # 等待load事件 # 对于SPA单页应用可能只是局部更新需要等待内容区域更新 page.wait_for_load_state(networkidle) # 等待一个标志性的新页面元素出现比如当前页码更新 # page.wait_for_selector(f[data-page{current_page 1}], stateattached) time.sleep(1) # 保守等待 current_page 1 print(f翻页采集完成共处理 {current_page - 1} 页获取 {len(all_data)} 条数据。) return all_data翻页爬虫的核心陷阱与对策URL模式变化有些网站点击“下一页”后URL会变化如从?page1变成?page2。你可以直接拼接URL并page.goto()这比模拟点击更稳定、更快。但要注意检查是否有防爬参数如token在URL里。SPA单页应用的页面状态在SPA中点击翻页浏览器地址栏的URL可能通过history API改变但页面没有完全重载。你的等待策略必须从wait_for_load_state(‘load’)调整为wait_for_load_state(‘networkidle’)加上对特定DOM更新的等待。反爬虫检测过于规律的点击间隔如固定1秒容易被识别。引入随机延迟time.sleep(random.uniform(1.0, 2.5))会好很多。对于重要网站甚至需要模拟更复杂的人类行为模式如在点击前随机移动鼠标。6. 高级技巧与异常处理实录掌握了基本套路我们来看看实战中那些“坑”和提升效率的技巧。6.1 处理懒加载Lazy Load图片与内容滚动加载常常伴随着图片的懒加载。这本身不影响文本数据的获取但如果你需要截图或确保所有资源加载完成就需要处理。# 在滚动到底部后可以强制触发所有懒加载图片的加载 page.evaluate( () { // 找到所有懒加载的图片通常data-src存放真实URLsrc是占位图 const lazyImages document.querySelectorAll(img[data-src]); lazyImages.forEach(img { if (img.dataset.src) { img.src img.dataset.src; } }); // 或者直接滚动所有图片到视口触发浏览器的原生懒加载 document.querySelectorAll(img[loadinglazy]).forEach(img { img.scrollIntoView({block: nearest}); }); } ) # 等待图片加载 page.wait_for_load_state(networkidle)6.2 应对动态变化的元素选择器一些现代前端框架如React、Vue会生成随机的类名如class”jsx-123abc”。你不能依赖这些类名来定位。解决办法是使用文本内容定位page.locator(‘button:has-text(“加载更多”)’)。这是最稳健的方式之一。使用属性选择器寻找稳定的属性如>from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10)) def safe_click_with_retry(page, selector, timeout10000): 带重试的点击操作 try: element page.locator(selector) element.wait_for(statevisible, timeouttimeout) element.click() # 点击后等待一个预期变化 page.wait_for_load_state(networkidle, timeouttimeout) return True except Exception as e: print(f点击元素 {selector} 失败: {e}) raise # 触发重试 # 在翻页循环中使用 try: safe_click_with_retry(page, a:has-text(下一页), timeout15000) except Exception as e: print(f“重试多次后翻页失败终止爬取。错误{e}”) break这里我用了tenacity库来实现优雅的重试。它会在失败后等待一段时间指数退避再重试最多3次。这对于应对临时的网络抖动或前端渲染延迟非常有效。6.4 性能优化减少不必要的等待如果你的目标是快速抓取数据那么scroll_pause_time和networkidle的等待时间就是性能瓶颈。可以进行优化动态调整等待时间如果连续几次滚动scrollHeight都没有变化可以逐步增加等待时间反之如果每次滚动都有新内容可以适当缩短。使用更精确的等待条件代替通用的networkidle可以监听特定的网络请求。用page.on(‘response’)事件监听器捕获加载数据的API请求完成事件这比等待所有网络活动停止快得多。并行化如果网站有多个独立的列表页可以使用Playwright的多个browser context或甚至多个进程来并行抓取。7. 一个完整的综合案例爬取模拟动态商品列表让我们用一个模拟案例把滚动加载和点击翻页结合起来。假设一个商品列表页初始加载20条滚动到底部会再加载20条最多100条同时也有一个“显示更多”按钮可以快速加载后续批次。import time from playwright.sync_api import sync_playwright import json def crawl_hybrid_product_list(url): 爬取一个同时支持滚动加载和按钮加载的混合模式商品列表 products [] with sync_playwright() as p: # 启动浏览器可配置代理、用户代理等 browser p.chromium.launch(headlessTrue) # 生产环境用True context browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... ) page context.new_page() # 1. 导航到页面 print(f“正在访问{url}”) page.goto(url, wait_untilnetworkidle, timeout60000) time.sleep(3) # 等待初始渲染 # 2. 首先尝试用滚动加载一部分 print(“开始滚动加载...”) scroll_attempts 0 last_product_count page.locator(.product-item).count() while scroll_attempts 10: # 限制滚动次数 page.evaluate(window.scrollTo(0, document.body.scrollHeight)) # 等待可能由滚动触发的新请求 try: page.wait_for_load_state(networkidle, timeout5000) except: pass time.sleep(2) current_count page.locator(.product-item).count() if current_count last_product_count: # 滚动未加载新商品尝试点击按钮 break print(f“滚动后商品数量从 {last_product_count} 增加到 {current_count}”) last_product_count current_count scroll_attempts 1 # 3. 查找并点击“显示更多”按钮 load_more_btn page.locator(button:has-text(显示更多), button:has-text(加载更多)) click_count 0 while load_more_btn.count() 0 and load_more_btn.is_visible() and click_count 5: # 检查按钮是否禁用 if load_more_btn.get_attribute(disabled): print(“‘加载更多’按钮已禁用。”) break print(f“点击‘加载更多’按钮 ({click_count 1})...”) load_more_btn.click() # 等待加载 - 可以监听特定API响应 # 这里我们等待商品列表容器内出现新的加载动画然后等待其消失 try: page.wait_for_selector(.loading-spinner, statevisible, timeout3000) page.wait_for_selector(.loading-spinner, statehidden, timeout10000) except: # 如果没有加载动画使用通用等待 page.wait_for_load_state(networkidle, timeout10000) time.sleep(2) # 更新按钮状态 load_more_btn page.locator(button:has-text(显示更多), button:has-text(加载更多)) current_count page.locator(.product-item).count() print(f“点击后商品数量{current_count}”) click_count 1 # 4. 最终再次尝试滚动确保所有懒加载内容都出现 print(“进行最终滚动以确保内容完整...”) for _ in range(3): page.evaluate(window.scrollTo(0, document.body.scrollHeight)) time.sleep(1.5) # 5. 提取所有商品信息 product_items page.locator(.product-item).all() print(f“开始提取 {len(product_items)} 条商品信息...”) for item in product_items: # 使用 locator 链式调用在元素范围内查找子元素 product { name: item.locator(.product-name).inner_text().strip() if item.locator(.product-name).count() 0 else , price: item.locator(.price).inner_text().strip() if item.locator(.price).count() 0 else , link: item.locator(a.product-link).get_attribute(href) if item.locator(a.product-link).count() 0 else , # 获取>问题现象可能原因排查步骤与解决方案滚动后scrollHeight不变但实际还有数据1. 滚动触发了加载动画但数据请求延迟或失败。2. 滚动事件监听器绑定在特定容器上而非window。3. 需要与页面进行交互如鼠标移动才能触发加载。1. 增加scroll_pause_time并使用page.wait_for_response()监听具体API。2. 使用Playwright Inspector(page.pause()) 检查滚动时哪个元素触发了事件切换为对容器的滚动。3. 在滚动前模拟鼠标移动page.mouse.move(x, y)。click()操作无效元素没反应1. 元素被遮挡如弹窗、遮罩层。2. 元素是div模拟的按钮需要触发不同事件。3. 页面状态未就绪元素尚未可交互。1. 使用element.click(forceTrue)强制点击慎用。2. 尝试element.dblclick()、element.hover()后再点击或用element.dispatch_event(‘click’)。3. 点击前增加等待element.wait_for(state‘visibleenabled’)。抓取到的数据是旧的不是滚动后的新数据数据提取时机不对在DOM更新前就执行了。确保在每次触发加载动作滚动/点击并等待完成后再执行数据提取操作。将数据提取代码放在等待语句wait_for_load_state,wait_for_selector之后。脚本运行一段时间后浏览器卡死或崩溃1. 内存泄漏DOM节点过多特别是边滚边采不清理。2. 无限循环导致资源耗尽。1. 对于超长列表考虑使用策略B边滚边采并移除已采元素或定期导航到新页面重新开始。2.务必设置max_scrolls/max_clicks等安全阀。使用try...finally确保浏览器最终被关闭。被网站识别为机器人1. Playwright指纹被检测。2. 行为模式过于规律。1. 使用browser.new_context()时注入stealth插件或自定义userAgent、viewport等。2. 引入随机延迟、随机滚动距离、模拟鼠标移动轨迹。考虑使用代理IP池。wait_for_selector超时1. 选择器写错了或元素根本不存在。2. 元素是动态生成的选择器需要更通用。3. 等待时间不够。1. 用Playwright Inspector实时验证选择器。2. 使用更宽松的选择器如text或has或改用wait_for_function等待某个JS条件成立。3. 增加timeout参数或改用page.wait_for_timeout()作为保底不推荐为首选。调试利器Playwright Inspector 和 Trace ViewerInspector (page.pause()): 在代码中插入page.pause()运行脚本时会自动打开一个带调试工具的浏览器窗口。你可以查看DOM、测试选择器、记录操作是定位元素和调试交互的首选工具。Trace Viewer: 在browser.new_context()时启用record_video或record_har或者在测试失败时自动保存追踪文件playwright.config.ts中配置。它可以像录像一样回放脚本执行的全过程查看每个时间点的网络请求、DOM快照和Console日志是分析复杂异步问题的终极武器。最后记住爬虫的本质是“模拟人”。多观察目标网站在真实浏览器中的行为滚动多快点击后有什么视觉反馈网络请求是什么样的用这些观察来指导你的自动化脚本你会写出更稳健、更高效的代码。动态页面爬取没有银弹但掌握了“触发-等待-捕获”这个核心循环以及本节提供的这些通用套路和调试方法绝大多数网站都将不在话下。