Playwright动态网页爬虫实战:滚动加载与点击翻页的通用处理套路

先给结论:动态页面本质上就两种列表加载方式,一种是滚动到底触发加载,一种是点按钮触发下一页。这两种形态占掉日常所见动态列表的九成以上。你只要把这两套模板练熟,往后碰到任何需要交互才能翻页的网站,基本都是往这两个方向上套,区别只是选择器不同、等待条件不同而已。

这篇笔记我按自己的实战习惯来写,不是帮你把 Playwright 文档再抄一遍,而是把处理动态列表时真正用得上的套路捋出来:怎么判断页面加载完了、怎么避免重复提取、怎么判断已经翻到最后一页、什么时候该放弃 Playwright 去直接抓接口,这些才是动态爬虫真正花时间的地方。

1. 动态列表的根本认知:页面不是死的

1.1 为什么 requests 拿不到你要的数据

很多新手会先遇到一个诡异的现象:用 requests 请求一个列表页,返回的 HTML 里什么都有,唯独没有列表数据。这不是你写错了,而是页面本身就是空壳子——数据是页面加载完成后,由 JavaScript 再向后端发请求,拿到响应后渲染到 DOM 里的。服务端返回的 HTML 只有一个框架和一堆 script 标签。

在爬虫的语境里,这套机制叫动态渲染。网站这么做的原因很多,有的是为了前后端分离方便维护,有的是为了用户体验更好,也有的是为了增加自动化抓取的难度。但你要记住一个本质结论:只要是动态页面,你看到的列表数据一定不在最初的响应里,必须在浏览器里等一等、动一动、数据才出现。

这就解释了为什么要上 Playwright。requests 相当于你站在店门口往里喊,问老板菜单上有什么,老板没理你,因为菜单是店员(JavaScript)在店铺里用投影仪(DOM 渲染)放出来的。Playwright 不一样,它是直接把一个真人(无头浏览器)派进店里坐下,菜单放出来他就看得见,点哪桌的菜也清楚。

1.2 动态列表的两种典型形态

我接触过的动态列表,九成以上跑不出这两种形态:

  • 滚动加载:也叫无限滚动。页面先渲染第一批数据,你滚到接近底部时,页面检测到滚动事件,自动向接口请求下一批,插入到列表末尾,于是你继续滚、它继续加载,永远没有尽头,直到所有数据加载完或者后端起限制。
  • 点击翻页:页面渲染当前页数据,底部有按钮——可能是一排数字分页(1、2、3、4),也可能是一个“加载更多”“下一页”按钮。必须点击按钮,浏览器才会请求并渲染新的数据。注意,点击翻页里有个变种,就是按钮不换内容也不关掉,而是每次点击都在底部追加一批数据,这种看起来和滚动加载很像,但触发方式是点击。

这里的关键区别是触发方式不同。滚动加载靠监听 scroll 事件触发网络请求,点击翻页靠用户主动点击或键盘操作触发。你的爬虫代码必须模拟对应的动作,页面才肯继续喂数据。

理解了这一点,后面所有代码逻辑就顺理成章了:滚动加载的套路是“模拟滚动→等待渲染→提取数据→判断是否到底”,点击翻页的套路是“点击按钮→等待渲染→提取数据→判断是否到最后一页”。通用套路的核心,就是把这四个步骤用循环串起来。

2. 准备工作:Playwright 环境与最小可用模板

2.1 安装与启动

先说环境。Playwright 目前的稳定用法仍然是同步 API 配合 with 语句,代码结构清晰,调试也直观。安装就一句话:

pip install playwright

装完还需要装浏览器内核:

playwright install chromium

如果你的运行环境是服务器,可能还需要手动装一些系统依赖库,官方命令是playwright install-deps,建议在 Linux 服务器上部署前提前跑一遍。我第一次在纯净容器里跑就吃了这个亏,浏览器起不来,报错信息里全是缺 so 文件。

启动浏览器的结构非常固定,以后所有脚本基本都长这样:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://目标站点列表页") # ... 你的操作逻辑 browser.close()

这里headless=False是带界面运行,调试阶段强烈建议打开看真实效果。确认逻辑没问题后再改成headless=True,在服务器上无头跑。带界面跑还有一个好处:你能直观看到滚动加载时页面的变化节奏,有利于后面调整等待策略。

2.2 三个必须提前掌握的核心操作

写动态列表爬虫之前,先把这三个操作刻进脑子里,后面全在用:

1. 等待元素出现

page.wait_for_selector("css选择器或text选择器", timeout=10000)

这个方法会阻塞直到元素出现,如果超时会抛异常。它的意义在于,动态加载是需要时间的,你提取数据前必须确保目标元素已经在 DOM 里了。10 秒只是常用默认值,实际跑的时候我会根据网络状况调整到 15 秒或 5 秒。

2. 执行 JS 滚动

page.evaluate("window.scrollTo(0, document.body.scrollHeight)")

这一行把页面滚到底部。对无限滚动页面来说,这个动作会触发加载逻辑。有些网站监听的是某个容器元素的 scroll 事件而不是 window,此时把document.body替换成对应容器的选择器即可:

page.evaluate("document.querySelector('.列表容器选择器').scrollTo(0, 100000)")

3. 提取数据

items = page.locator("列表项选择器").all() for item in items: title = item.locator("标题选择器").inner_text()

Playwright 的 locator 体系比 Selenium 更严密,支持 CSS、text、xpath 等。实际项目里 CSS 选择器最常用,text=选择器在处理“下一页”这种文字按钮时特别好用。

2.3 一个最小示例:先跑通再谈套路

我不建议直接上完整项目,先写个 20 行的最小示例验证环境没问题:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://example.com/list") # 找一个真实动态列表页 page.wait_for_selector(".list-item") count = page.locator(".list-item").count() print(f"当前页面加载了 {count} 条数据") browser.close()

这段代码的核心作用有两点:一是验证 Playwright 环境没问题,二是验证页面上列表项的选择器是否正确。选择器对了,真实项目里至少省掉一半的排查时间。

3. 滚动加载:无限列表的通用处理套路

3.1 套路的核心:循环滚、等到位、再提取、看终止

滚动加载页面的处理,本质上是一个四步循环:

  1. 滚动到底部触发加载
  2. 等待新数据渲染完成
  3. 提取这批新增数据或全量重提
  4. 判断是否已经滚到没有新数据的程度

如果你对循环还不太敏感,我拆开解释一遍。第一步的滚动动作必须真实发生,页面才会向接口发请求;第二步的等待最关键,很多人漏数据就是因为滚完立刻提取,这时候 DOM 还没来得及更新;第三步的提取有两种策略,后面细说;第四步是滚动加载的难点,页面没有明确的“页数”概念,你必须通过某种信号判断“加载完了”。

下面给一个通用模板,适配多数无限滚动列表:

from playwright.sync_api import sync_playwright import time def scroll_and_extract(url, item_selector, max_scrolls=50): """通用滚动加载提取 item_selector: 列表项选择器 max_scrolls: 最大滚动次数,防止死循环 """ results = [] with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() # 可选:拦截图片,加速加载 page.route("**/*.png", lambda route: route.abort()) page.route("**/*.jpg", lambda route: route.abort()) page.goto(url, timeout=60000) page.wait_for_selector(item_selector, timeout=10000) last_height = 0 scroll_count = 0 while scroll_count < max_scrolls: # 1. 滚动到底部 page.evaluate("window.scrollTo(0, document.body.scrollHeight)") scroll_count += 1 # 2. 等待页面高度变化或新元素出现 try: page.wait_for_function( "() => document.body.scrollHeight > arguments[0]", last_height, timeout=5000 ) except Exception: # 高度不再变化,认为到底了 break # 3. 提取当前所有项 items = page.locator(item_selector).all() titles = [item.inner_text() for item in items] results = titles # 全量覆盖,简单粗暴 # 4. 更新高度,进入下一轮 last_height = page.evaluate("document.body.scrollHeight") time.sleep(1) browser.close() return results

这段代码就是整套滚动加载的核心骨架。接下来我把几个细节展开讲。

3.2 滚动距离与速度不是随意选的

滚动加载不是滚得越深越好。很多页面每次滚动只加载一批数据,你一步滚到底部,看到的效果是页面疯狂“跳动”——因为滚动事件触发得太快,请求可能只发出去一部分。我在实际项目里踩过这个坑:一次性滚到底,结果只拿到前两三批数据,后面的数据页面根本没来得及请求。

所以实际写的时候,我会把滚动改成渐进式:

def scroll_gradually(page, step=800, delay=0.5): """每次滚动固定步长,给页面缓冲时间""" current = 0 max_height = page.evaluate("document.body.scrollHeight") while current < max_height: current += step page.evaluate(f"window.scrollTo(0, {current})") time.sleep(delay) max_height = page.evaluate("document.body.scrollHeight")

步长选 800 是因为在很多屏幕尺寸下,800 像素大约是视口高度的 80%,不会触发浏览器的“快滚到底”加速行为,也不会因为步子太小导致循环次数太多。delay 选 0.5 秒是给网络请求留时间。注意这俩参数不是定死的,页面加载快就用大步长、短延迟,加载慢就反过来。

另一种做法是滚动一步、等一批数据出现、再滚下一步,这种最稳,但速度慢。我通常用在要求高完整性的场景,比如爬全量商品信息。

3.3 终止条件的本质判断

滚动加载最大的坑是“不知道什么时候结束”。前面模板里我用的是比较页面高度——当高度不再变化认为到底。这个办法对大多数页面有效,但有几个例外:

  • 页面底部有推荐区块、页脚,滚动不会再加载新数据,但高度仍然存在。此时你会停不下来,一直循环直到 max_scrolls 超限。
  • 部分页面的滚动容器不是 document.body,而是某个内嵌 div,此时document.body.scrollHeight永远是同一个值,判断失效。

针对第一种情况,我会检查最后两次滚动之间,列表项的个数是否还在增加。如果连续滚动多次,数量不再变化,就认为到底:

prev_count = 0 stable_rounds = 0 while scroll_count < max_scrolls: page.evaluate("window.scrollTo(0, document.body.scrollHeight)") time.sleep(1) current_count = page.locator(item_selector).count() if current_count == prev_count: stable_rounds += 1 if stable_rounds >= 3: break else: stable_rounds = 0 prev_count = current_count

连续三次数量没变化,基本可以断定到底了。这个阈值我调过很多次,三次是比较可靠的平衡点——网络慢的时候偶尔一两次没加载出来,三次还没变化那大概率是真没了。

针对第二种内嵌容器的情况,改动也只是把滚动目标和高度判断都换成容器元素:

page.evaluate("document.querySelector('.feed-list').scrollTo(0, 1000000)")

3.4 数据提取的两种策略

滚动加载场景下,数据提取有两个流派,各有适用场景:

全量覆盖式:每次滚动后重新提取页面所有列表项,直接覆盖结果列表。优点是逻辑简单,不会重复,因为每次都是全量;缺点是如果单轮提取耗时较长,滚动后页面可能又插入了新数据,最后会有轻微遗漏。适合列表项数量不夸张(几百条内)的场景。

增量追加式:每次都重新提取,然后按某个唯一标识(比如标题、链接)去重后追加到结果。优点是速度快,适合上万条数据的场景;缺点是去重逻辑要写好,否则容易重复。

实际操作中我多数用增量追加,因为滚动加载的列表动辄几十甚至上百次请求,全量覆盖的时间成本太高。示例:

seen = set() all_data = [] while ...: # 滚动、等待 items = page.locator(item_selector).all() for item in items: title = item.locator(".title").inner_text() if title in seen: continue seen.add(title) all_data.append(title)

这里用标题作为唯一标识,前提是标题不会重复。如果列表有重复标题的可能,改用详情页链接或者数据 ID 更稳妥。

4. 点击翻页:分页与“加载更多”的通用套路

4.1 分页按钮的套路:点、等、提、判

点击翻页比滚动加载的终止条件更直观,因为页面给了你“最后一页”的信号。但操作步骤本身仍然要写对。通用的分页翻页模板长这样:

def paginate_click(url, item_selector, next_btn_selector, max_pages=10): """通用点击翻页提取 next_btn_selector: 下一页按钮的选择器 """ all_data = [] with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto(url, timeout=60000) page.wait_for_selector(item_selector, timeout=10000) for page_num in range(1, max_pages + 1): print(f"正在提取第 {page_num} 页") # 1. 提取当前页数据 items = page.locator(item_selector).all() for item in items: all_data.append(item.inner_text()) # 2. 判断下一页按钮是否存在且可点击 next_btn = page.locator(next_btn_selector) if next_btn.count() == 0 or not next_btn.is_enabled(): print("没有下一页按钮了,结束") break # 3. 点击下一页 next_btn.click() # 4. 等待页面刷新(等待旧元素消失或新元素出现) try: page.wait_for_selector(item_selector, timeout=10000) except Exception: print(f"第 {page_num} 页点击后没有加载出新元素") break # 可选:等待 URL 变化或页码变化 time.sleep(0.5) browser.close() return all_data

这段代码有几个地方需要特别注意,我逐个说一下。

第一,提取数据和点击操作的顺序不能反。必须先提取当前页数据再点击下一页,否则你会漏掉当前页。这个顺序听起来是废话,但我见过有人把点击写在提取前面的脚本,一跑就翻到第二页才开始提取,第一页数据直接丢了。

第二,点击后不能立刻提取,必须等待页面刷新。这里有一个判断刷新完成的技巧——等待当前页的某个标志性元素消失,或者等待页码数字变化。可以结合wait_for_selector的state参数:

# 等待旧元素进入 detached 状态(从 DOM 中移除) try: page.wait_for_selector(".pagination .current", state="detached", timeout=10000) except Exception: pass # 如果当前页码元素没有消失,可能页面是局部刷新

有的站点翻页是整页跳转,整个 DOM 都会变化,此时上面的代码用来等待旧 DOM 卸载,它消失说明新页面已经在渲染了。有的站点是局部刷新,只换列表区域,分页按钮不变,这段时间等待条件要换成“列表区域内容变化”才行。

第三,页码不是数字而是“加载更多”按钮时,点击后按钮不会消失,数据会继续追加。这种情况的终止条件不能靠按钮消失判断,要靠数据数量是否变化。

4.2 “加载更多”按钮:披着点击外衣的滚动加载

很多站点没有数字分页,只有一个“加载更多”按钮,点击一次加载一批新数据,按钮还在原地。这种形态我习惯把它当作滚动加载来处理,只不过触发动作从滚动变为点击。

模板如下:

def click_load_more(url, item_selector, load_more_selector, max_clicks=50): all_data = [] with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto(url, timeout=60000) page.wait_for_selector(item_selector, timeout=10000) prev_count = 0 stable_rounds = 0 for i in range(max_clicks): # 提取当前数据 items = page.locator(item_selector).all() current_count = len(items) # 判断是否还在变 if current_count == prev_count: stable_rounds += 1 if stable_rounds >= 2: print("加载更多已到头") break else: stable_rounds = 0 prev_count = current_count # 汇总数据 for item in items: all_data.append(item.inner_text()) # 点击加载更多 btn = page.locator(load_more_selector) if btn.count() == 0 or not btn.is_visible(): print("加载更多按钮不存在或不可见") break btn.click() # 等待新数据渲染 time.sleep(1) browser.close() return all_data

注意这里用“连续两轮数量不变”作为终止条件,因为按钮点击后数据是追加的,只能在数量上做文章。有些页面加载更多到最后一页后按钮会被移除,这时判断btn.count() == 0会直接跳出;如果按钮还在但点击无效,就是数量不变的情况。

4.3 点击翻页时最容易踩的坑

点击翻页里最容易翻车的场景有三个,我单独拎出来:

坑一:按钮明明定位到了,click 却没有任何反应。原因通常是按钮被另一个元素遮挡,或者是按钮绑定的事件在某个初始化完成后才生效。解决方法有两个:一是改用btn.click(force=True)强制点击,二是用 JavaScript 直接触发:

page.evaluate("document.querySelector('按钮选择器').click()")

前者适合遮挡不严重的情况,后者属于绕过式触发,能解决大部分“假点击”。

坑二:点击后页面会打开新标签页。有些页面的“下一页”是target="_blank"外链,直接点击会开新标签,Playwright 默认不会自动切换过去。处理方式是监听新页面事件:

with page.expect_popup() as popup_info: next_btn.click() new_page = popup_info.value new_page.wait_for_load_state() # 然后在 new_page 上操作

不过说实话,遇到这种页面我通常先看看 URL 规律——很多外链下一页其实就是个分页 URL,直接拼参数用 requests 反而更简单。

坑三:点击翻页时登录态丢了。某些站点的分页接口需要登录凭证,页面跳转时 cookie 或 token 校验不过去。排查方式是观察点击翻页后的响应状态,用 Playwright 的 network 事件:

page.on("response", lambda resp: print(resp.url, resp.status))

如果点击后看到某个接口返回 401 或 403,大概率就是登录态问题。解决方式是把登录流程前置,先登录成功后再进列表页,而不是直接访问列表页。

5. 机制与选型:什么时候该用 Playwright,什么时候该绕开

5.1 两种加载方式的对比速查

用表格给你整理一个对比,方便以后遇到页面时直接判断用哪套模板:

对比维度滚动加载点击翻页(分页按钮)点击翻页(加载更多)
触发方式滚动到特定位置点击数字/下一页按钮点击加载更多按钮
终止条件滚动高度不再变化/列表项数量连续不变下一页按钮消失/禁用按钮消失/数量连续不变
提取速度较慢,受滚动节奏限制较快,翻页间隔可控较慢,取决于加载速度
稳定性中,容易滚过头高,分页逻辑清晰中,按钮状态判断需要小心
常用等待策略wait_for_function 高度变化等待旧元素 detached等待列表项数量增加
数据完整性高(理论上可滚到底)高(页数明确)高(直到按钮消失)

我个人的经验是,遇到滚动加载先看接口,遇到点击翻页优先用 Playwright 硬跑。原因很简单:滚动加载往往背后是一个连续翻页的接口,接口参数通常就 page 或 offset 两个数字,构造起来很容易;而点击翻页虽然也有接口,但不少后端校验了请求头、Referer、加密参数,直接用 requests 反而不如 Playwright 模拟点击稳定。这不是绝对的,但作为快速选型的参考很有效。

5.2 什么时候应该放弃 Playwright 转抓接口

Playwright 不是银弹,在两种场景下我会主动放弃它:

第一个场景是接口结构极其简单时。打开开发者工具,滚动列表,如果发现每一次滚动都是同一个接口,URL 参数里就一个 page 数字或者一个 offset 偏移量,且响应是干净的 JSON,直接用 requests 循环请求接口,速度至少是 Playwright 的十倍,还不用处理浏览器资源。这时候硬用 Playwright 属于给自己加戏。

判断方式很简单:滚动页面看 Network 面板,如果请求 URL 规律清晰、没有特殊加密参数,先用 requests 试试。能通就直接抓接口,不折腾。

第二个场景是页面极其复杂,滚动加载还嵌套了多级 iframe、Shadow DOM。这种页面用 Playwright 写选择器会非常痛苦,一步步穿透层级不仅代码冗长,而且页面一改版就崩。相比之下,直接分析接口往往更省事。不过注意,如果接口响应是加密文本或者需要特殊签名,那就还是老老实实回归 Playwright,至少它不跟你玩加密那一套。

5.3 Playwright 与 Selenium 的选型差异

既然你已经学到 Playwright 了,顺便把和 Selenium 的差异说清楚。两者都能做动态页面爬取,但体验差距很明显:

  • 安装体验:Playwright 一条命令装完能跑,自带浏览器内核;Selenium 需要额外下载对应版本驱动,还得手动配路径,版本一不匹配就报错。
  • API 设计:Playwright 的 locator 体系明显更现代,支持自动等待;Selenium 的 find_element 系列写完经常要手动 sleep 等渲染。
  • 调试能力:Playwright 自带 codegen 录制功能和 trace 回放,定位选择器时可以直接录制操作生成代码,这在 Selenium 里没有对应级别的工具。
  • 稳定性:Playwright 的 auto-waiting 机制很大程度上避免了元素未出现的竞态问题。

不过 Selenium 在一点上仍有优势:老项目生态成熟,网上资料多,部分浏览器自动化场景(如既有系统兼容)只能用它。新项目我基本只用 Playwright。

6. 常见问题与排查技巧实录

最后集中整理我在实际项目中踩过的坑和排查方法,按出现频率排序,遇到问题直接来对号入座。

6.1 元素定位到了,点击却没有反应

现象:代码执行到btn.click()不报错,程序也不终止,但页面就是没有任何变化。

排查步骤:

  1. 在带界面模式下看按钮是否真的被点击了(有没有高亮或涟漪效果)。
  2. 检查按钮是否被遮罩层遮挡,用btn.is_visible()验证。
  3. 查看 Network 面板,确认点击后是否发出了请求。

解决方案:先试force=True,再试 JS 点击。如果 JS 点击也没有反应,有两种可能——按钮绑定的监听事件还没完成初始化,等待时间加长;或者按钮不是真的翻页按钮,而是某个弹出层的触发点。常见于页面里其实有两个“下一页”按钮,一个隐藏在弹窗里。定位时选择器写得要具体,把父容器也带上。

6.2 wait_for_selector 超时,页面一直卡在加载状态

现象:wait_for_selector抛 TimeoutError,但页面肉眼可见已经渲染了部分数据。

原因:选择器写得和实际 DOM 结构不匹配,或者目标元素渲染完成后又被页面 JavaScript 篡改了(比如先渲染数据再套一个壳子)。

解决方案:换选择器,优先用稳定的 class 名或 data 属性。不要用那些带数字后缀的动态 class(很多框架生成的类名每次刷新都会变)。另外可以先用page.content()打印当前 HTML,肉眼确认目标元素真实存在后,再据此改选择器。

6.3 提取到的数据重复

现象:循环跑完后,结果里有大量一模一样的条目。

原因:增量追加时去重逻辑失效,或者滚动/点击发生的速度太快,页面没及时加载新数据,导致同一批数据被提取了多次。

解决方案:先用标题加链接一起作为唯一键,如果是纯文本列表没有链接,用“序号 + 标题”组合去重。另外检查一下循环里的等待时间,太短的话加长到 1 秒以上。

6.4 无头模式下页面加载速度异常慢

现象:headless=True 时,原本带界面跑得好好的页面,无头模式下一片空白,或者等了很久才渲染。

原因:部分网站检测到无头浏览器的指纹特征会返回降级页面。这不是 Playwright 的问题,是目标站点识别了 navigator.webdriver 之类的特征。

解决方案:加启动参数规避,常见做法是去掉部分 webdriver 特征:

browser = p.chromium.launch( headless=True, args=[ "--disable-blink-features=AutomationControlled", "--disable-dev-shm-usage", ] )

加--disable-dev-shm-usage能解决一部分容器环境下渲染内存不足的问题。如果仍然不行,退一步用 headless=False 在后台跑。

6.5 页面弹窗、广告盖住了按钮

现象:页面正常加载,但弹了一个新用户引导的遮罩层,click()报错说元素不可点击,因为它被遮挡了。

解决方案:先关弹窗再点按钮,弹窗关闭按钮通常可以强杀:

try: page.locator(".close-btn").click(timeout=3000) except Exception: pass

如果弹窗没有显式关闭按钮,可以直接按 ESC 键,部分弹窗支持键盘关闭。再不行就用page.evaluate把遮罩层的 display 设为 none。

page.evaluate("document.querySelector('.mask').style.display = 'none'")

6.6 翻页后页码没变,实际跳到了别的页

现象:点击 “下一页”按钮后,URL 变了但页码数字没变,或者提取到的数据是上一页的。

原因:页面的翻页是按表单提交实现的,点击按钮后其实发了一次 POST 请求,页面 URL 不会变,DOM 局部刷新。此时不能用“等待旧元素消失”来判断刷新完毕,因为分页区域可能根本没变化。

解决方案:改用“等待列表区域第一个条目变化”的方式。实现可以用wait_for_function:

page.wait_for_function( """(old_title) => { const first = document.querySelector('.list-item .title'); return first && first.innerText !== old_title; }""", first_title_at_current_page, timeout=10000 )

实践中这个判断往往比等待“某个元素消失”更可靠,因为局部刷新的页面里,很多元素是复用的,不会消失。

6.7 网络请求被目标站点的频率限制拦截

现象:滚动加载跑到中途,某一次滚动后页面不再加载新数据,重启浏览器发现前几十次都很正常,后面无论如何都不出数据。

原因:触发频率限制,目标站点对单位时间内的列表接口请求数做了限制,短时间内高频滚动就会被封。

解决方案:控制滚动速度,每两次滚动之间增加随机间隔。时间用 0.8 到 1.5 秒之间的随机浮点数,避免请求节奏规律化,也让行为更接近真人浏览。另外,可以在请求中加入 Cookie 保持会话统一,而不是每次重新开启浏览器,否则身份特征变化更容易触发风控。

实际项目中我的取舍习惯

这篇写到这里已经很长了,最后再说一点工作流层面的个人习惯。我接到一个动态列表需求时,不是上来就写 Playwright 脚本,而是先花五分钟干三件事:打开开发者工具看 Network 里有没有直出的 JSON 接口;观察页面是滚到底加载还是点按钮才加载;确认有没有明显的分页参数规律。这三件事决定了整个方案是走接口直取还是走浏览器自动化。

如果接口直取可行,我绝不会为了“显得高级”而硬套 Playwright;如果接口加密严重,Playwright 就是最省心的兜底手段。这两种思维要并存,而不是一味迷信某个工具。

另外,Playwright 脚本写完后,我建议你花点时间把循环里的 sleep 优化成 wait_for 系列方法,让等待时间自适应页面加载速度。这样做的好处有两个——脚本更稳,不至于因为某次网络慢而误判终止;速度也更优,页面快的时候不等多余时间,页面慢的时候不提前抢跑。

动态页面难的不是技术本身,而是对“页面什么时候算加载完”的判断。这个感觉只能靠多看多跑积累,没有捷径。你把滚动加载和点击翻页这两套模板多跑几个真实网站,自然就摸到规律了。

相关新闻

图表设计实战指南:从架构图到流程图的高效信息表达

图表设计实战指南:从架构图到流程图的高效信息表达

先说个真实的场景。某个周末&#xff0c;一位后端朋友打电话给我&#xff0c;说新接手的系统文档写了两万字&#xff0c;他读了三遍&#xff0c;还是说不清数据从哪个服务进、哪个服务出。我让他把核心模块画出来&#xff0c;他花十分钟画了一张手绘图发我。我一眼就看懂了整个…

2026/10/22 14:36:13 阅读更多 →
服务停了端口还没放出来:TIME-WAIT 与端口复用

服务停了端口还没放出来:TIME-WAIT 与端口复用

授权与合规声明 本文全部操作对象均为自建隔离靶场&#xff08;本机容器或隔离虚拟机&#xff09;&#xff0c;涉及安全测试的环节必须以取得合法授权为前提。未经授权的渗透测试违反《中华人民共和国网络安全法》与《刑法》相关条款&#xff0c;须承担相应法律责任。本文只讲环…

2026/10/21 9:08:32 阅读更多 →
artcraft创意流水线:从想法到成品的工程化实践

artcraft创意流水线:从想法到成品的工程化实践

1. 从“artcraft”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“artcraft”这个标题&#xff0c;我脑子里蹦出来的第一反应是“艺术”加“手艺”的组合词。直觉告诉我&#xff0c;这大概率是一个跟创意工具、内容生成或者手工设计相关的项目。后来跟几个做独立开发…

2026/10/21 9:08:32 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年&#xff0c;我大部分时间都在跟浏览器标签页打交道&#xff0c;直到第一次把桌面应用跑起来&#xff0c;才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词&#xff0c;最开始是在某段时间频繁出现在同事的讨论里&#xff0c;一个用HTML…

2026/10/22 10:18:38 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档&#xff0c;主要面向智能锁安装人员、终端用户及售后维护者&#xff0c;帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件&#xff0c;大小4.37MB&#xff0c;内容结构清晰&#xff…

2026/10/22 11:01:53 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统&#xff0c;整车要求空载跑到8m/s&#xff0c;电机转速已经拉到额定点&#xff0c;驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令&#xff0c;转速纹丝不动&#xff0c;三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/22 9:44:47 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年&#xff0c;用它写过嵌入式驱动、做过游戏服务端&#xff0c;也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词&#xff0c;我其实挺感慨的——这门语言的难点从来不是语法多&#xff0c;而是知识点之间的耦合太深…

2026/10/22 10:42:00 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书以PDF格式发布&#xff0c;适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开&#xff0c;先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能&#xff0c;再给出完整安装步骤&…

2026/10/22 10:40:08 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历&#xff1a;翻开源码注释、论文摘要或者一份专业报告&#xff0c;每个字都认识&#xff0c;连在一起却像天书。想找人聊聊&#xff0c;身边要么没人懂&#xff0c;要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型&#xff0c;几秒钟之后&a…

2026/10/22 10:56:37 阅读更多 →

日新闻

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/22 1:47:45 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/21 21:32:28 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/22 5:12:39 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 21:50:56 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 21:51:55 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 16:24:48 阅读更多 →