bigywater.tech
爬虫工程 · 59

无限滚动:IntersectionObserver 触发的请求

无限滚动的页面,HTML 里只有第一屏的数据,其余要滚动才会加载。爬虫怎么滚、什么时候停、遇到视口很高时为什么会卡住?这篇用一个本机的无限滚动页面和 CDP 逐个试。

10 / 40 / 100
不滚动 / 匀速滚动并用“3 次没增长”判到底 / 每次滚到底部 时抓到的条目数(总共 100)
10 对 100
高视口(3000 px)下朴素写法与重新 observe 写法最终的条目数
956 对 2017 ms
用结束标记判断到底 与 用 1.0 秒静默期判断到底 的耗时

不滚动

实测 · 本机执行输出
  打开后条目数 10,1.5 秒后条目数 10,服务器收到 /api/items 请求 1 次(页面一打开,空列表里的哨兵就在视口内,IntersectionObserver 第一次回调就加载了第 1 页;加载完后哨兵被推到视口外,不滚动就不会再触发);页面总高度 1020 px

页面打开时只有第一页数据,哨兵被推到视口外之后不再触发。HTML 解析或 requests 只能拿到这一页(见第 54 篇)。

匀速滚动容易提前停

实测 · 本机执行输出
  滚了 11 次(含最后确认不再增加的 3 次),条目数 40,/api/items 请求 4 次,用时 1397 ms,页面显示结束标记:False

匀速滚动加“连续几次没增长就停”很容易提前停下:我的例子里每页 1000 px,滚 3 次 300 px 还没碰到哨兵,就在 40 条停了(100 条里只抓到 40 条)。

每次滚到底部

实测 · 本机执行输出
  跳了 11 次,条目数 100,/api/items 请求 10 次,用时 1741 ms,结束标记:True

每次滚到页面底部更可靠,哨兵一定会进入视口:11 次滚动拿到全部 100 条,10 次请求,最后出现结束标记。

高视口的坑

实测 · 本机执行输出
  朴素:只响应"进入视口":不滚动等 1.5 秒后条目数 10;再滚动 2 次后 10;/api/items 请求共 1 次
  修正:加载完成后重新 observe:不滚动等 1.5 秒后条目数 40;再滚动 8 次后 100;/api/items 请求共 10 次

IntersectionObserver 在元素的可见性发生变化时才回调。如果加载完一页后哨兵仍在视口内(视口很高),就不会再有回调,页面卡住——这是页面自己的 bug,但也会让爬虫拿不到数据。页面如果每次加载后重新 observe 就不会卡。

到底了

实测 · 本机执行输出
  用结束标记判断:956 ms 到底;用"条目数 1.0 秒内不再增加"判断:2017 ms(比结束标记多出约 1060 ms;这是两次独立加载的差,其中包含那 1.0 秒静默期);两者都得到 100 条

能读到页面的结束标记(例子里哨兵变成 END)就用它。只能靠“条目数在静默期内不再增长”判断时,静默期要比一次加载的最长耗时长,否则会和第二节一样提前停下。

分步演示

无限滚动
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。页面是本机服务器上的一个小页面:列表加哨兵,IntersectionObserver 观察哨兵,进入视口就 fetch 下一页(服务器每次延迟 0.08 秒),共 10 页每页 10 条每条 100 px;Chrome 154 通过 pcdp.py 控制,用 Emulation.setDeviceMetricsOverride 设置视口(600 px 和 3000 px),用 window.scrollTo 滚动,数条目数;各情形各跑一次
    • 没有验证:真实站点的无限滚动(有的用滚动事件而不是 IntersectionObserver,有的按偏移量请求);用 CDP 的 Input.dispatchMouseEvent(mouseWheel) 模拟真实滚轮;“加载更多”按钮式的翻页;虚拟列表(只渲染可见的行,滚过的行会被移除)里抓取全部数据的办法;滚动触发的请求被限速时的处理
    京ICP备2026067502号-1京公网安备11010502063487号