拓冰建站拓冰建站
首页 / 资讯中心 / 正文

无限滚动页面爬虫实战:从接口逆向到Playwright自动化抓取

凡是动手写过 Python 爬虫的十有八九都遇过这种页面第一屏能抓到往下翻也正常但翻到某一个位置之后浏览器地址栏压根不变内容却一批接一批自动加载出来这就是典型的“无限滚动”页面。这类页面没有传统意义上的第 2 页、第 3 页你想靠改 URL 参数批量翻页完全行不通很多人就是卡死在这一步。今天这篇把处理“无限滚动 自动点击 内容抓取”的完整思路拆开讲从怎么观察页面请求到用浏览器自动化工具模拟滚动和点击再到数据去重与存储最后聊几个我实际踩过的坑。适合刚学会 requests 但被动态页面劝退的新手也适合想把采集流程做成稳定工具的工程师参考——看完你能独立写出自己的瀑布流采集脚本。1. 技术拆解无限滚动页面为什么难抓1.1 无限滚动页面的几种实现机制传统分页页面很好识别URL 里带 page 或 pn 之类的参数上一页下一页都有明确链接。这种页面的正文内容基本都在服务端渲染完成你用 requests 请求一次拿到的 HTML 里就有全套数据解析起来非常直接。无限滚动是另一种形态。服务端只把第一批内容发过来客户端 JavaScript 监听滚动事件当用户滚动到接近页面底部的一个阈值时前端代码立即发一个异步请求XHR 或 fetch 都常见去服务端要下一批数据拿到 JSON 之后再用 JS 往 DOM 里动态插入。也就是说真正的“下一页”逻辑根本不在服务端而是藏在浏览器里的一段 JS 里。这个机制差异决定了普通爬虫为什么会失效。requests 模拟的是一次普通 HTTP 请求拿到的只是首次加载的静态 HTML后续数据是浏览器本地执行 JS 之后发起的二次请求requests 本身没有机会去触发它。所以传统写法在无限滚动页面上抓来抓去始终只有开头那一小段内容而且性能表现也容易让人误判为“网站数据太少”。无限滚动还有一种常见变体——“加载更多”按钮。内容不会自己出来必须有人点一下按钮前端才会发起下一次异步请求。更复杂一点的页面是滚动和按钮混着来前面几轮滚动就自动加载滚到某一个层级之后必须点按钮才能继续。另外还有很多详情页里的“展开全文”“查看全部评论”原理一模一样都是前端事件驱动二次请求。搞清楚机制之后解决思路自然而然就出来了要拿全量数据要么模拟浏览器行为滚动、点击要么直接定位那个二次请求把接口参数补全后自己循环请求。这两条路线对应的就是后文要谈的不同技术选型。1.2 三大技术方案的选型分析方案一纯 requests 逆向接口。这是最理想的做法直接在开发者工具里观察滚动时发出的异步请求找到返回 JSON 的 XHR 接口分析参数后用 requests 循环翻页。没有浏览器开销速度最快维护成本也最低。但前提是接口好找、参数好补。很多内容平台的接口都带了签名参数、时间戳、加密 token 之类的东西简单的还能算出来复杂的加密短时间根本逆向不动这时候就需要换方案。方案二Selenium ChromeDriver。Selenium 是老牌方案生态成熟网上资料多但配置确实烦琐需要下载跟浏览器版本严格匹配的 driverChrome 一升级driver 就容易失效运行速度也偏慢。处理现代页面时它的等待机制还不够智能“页面元素还没加载好就去点击”这类问题经常出现得靠开发者自己写显式等待去补。方案三Playwright。这是我这几年处理动态页面的主力工具。它解决了 Selenium 的不少痛点自带浏览器内核下载不用单独去翻 driver内置了页面操作的自动等待机制点击前会等元素稳定选择器引擎把相对定位、文本定位、角色定位都做了封装。更关键的是它能方便地监听网络请求抓无限滚动页面时可以一边滚动一边把服务端真正返回的 JSON 保存下来这个能力在日常调试中真的能省很多事。选型逻辑归纳起来就一句话能走接口优先走接口接口被加密拦住就退到浏览器渲染方案上渲染方案优先考虑 Playwright不用被 Selenium 的存量惯性绑住。方案配置成本渲染能力请求观察速度稳定性requests低无需手动分析最快依赖接口可用性Selenium中完整需配合抓包慢一般经常超时Playwright低完整内置监听中高自动等待可靠2. 环境准备不再为浏览器驱动头疼2.1 五分钟搭好可复用的开发环境先说基础环境。在命令行输入python --version确认本机 Python 版本无限滚动抓取建议 Python 3.8 以上3.10 或 3.11 都很合适新版本对异步支持更好后面做并发也方便。然后创建虚拟环境这是我一直坚持的习惯别图省事直接装在全局环境里。不同项目依赖版本经常冲突虚拟环境隔离之后换项目直接删掉重建就行python -m venv venv # Windows: venv\Scripts\activate # macOS / Linux: source venv/bin/activate激活环境后安装依赖pip install playwright beautifulsoup4 lxml requests再下载 Playwright 对应的浏览器内核playwright install chromium如果官方源下载慢可以把 playwright 的下载源切换到国内镜像具体配置在它的官方文档里有属于常规操作。装完后用playwright --version验证一下环境。IDE 这块PyCharm 就把项目解释器指到 venv 路径VS Code 需要装 Python 插件然后在左下角选择解释器这个步骤对调试特别关键——无限滚动这种逻辑绕的脚本断点调试能替你省下一大把猜测时间。2.2 Playwright 的几个关键能力为什么我不用 Selenium 而选 Playwright展开讲四个对无限滚动场景特别有价值的能力。第一自动等待。page.click()、page.locator().click()这些操作默认会等待元素可见、可用再执行不用像 Selenium 老写法那样堆一堆time.sleep去猜网络耗时。滚动页面的加载时延本身就不稳定自动等待不会因为网络慢就误判失败。第二内置多浏览器内核。chromium、firefox、webkit 都能跑跨内核覆盖能发现一些只在特定内核下出现的问题比如某些页面在 firefox 下点击事件绑定方式不同。第三命令playwright codegen。它会打开浏览器录制你的鼠标操作并自动生成代码这对定位选择器、熟悉页面结构帮助巨大。遇到难搞的按钮我一般先录制一遍再把录到的选择器拿过来手动调整。第四网络监听。page.on(response)可以实时捕获所有接口响应这样你不用频繁切到 DevTools 去人工找接口直接在代码里把滚动过程中的 JSON 存下来效率差很多。第五通过page.evaluate()执行自定义 JavaScript。一些特殊场景比如页面监听的是scroll事件而不是按钮点击直接注入原生 JS 去控制滚动更精准。3. 核心实现自动滚动、点击与抓取的完整流程3.1 第一步先在开发者工具里找接口开工之前先定策略不要一上来就写代码先打开浏览器开发者工具F12切到 Network 面板把过滤条件勾到 Fetch/XHR然后在页面上手动往下慢慢滚动几次。观察 Network 面板里新增了哪些请求找到返回内容和列表数据对应的接口点开一个请求看 Response 预览确认返回格式是 JSON 还是 HTML 片段。找到接口后看它的 Query String 部分。常见的参数有page、cursor、offset、lastId、timestamp、sign。前几种是明文的直接在代码里改就行sign这种一般是 JS 现场算出来的签名如果没法轻易复现那就别在逆向接口上死磕直接转到渲染方案。如果接口参数是纯数字翻页那么用 requests 就能解决上一段示例代码帮忙贴出来import requests import time headers { 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 } for page in range(1, 11): url https://example.com/api/feed params {page: page, limit: 20} resp requests.get(url, headersheaders, paramsparams, timeout10) if resp.status_code ! 200: break data resp.json() items data.get(list, []) if not items: break for item in items: print(item.get(id), item.get(title)) time.sleep(0.5) # 控制节奏别把服务端打崩注意如果接口参数里带签名或者需要先登录拿到 token那 requests 方案的成本会明显上升。这时候我建议改成 Playwright 渲染方案虽然是笨办法但往往是最省心、最不容易被参数加密卡住的路子。3.2 第二步Playwright 模拟滚动加载和按钮点击Playwright 方案的核心逻辑是一段 while 循环模拟真实用户行为打开页面滚动到底部等待内容加载完成如果出现“加载更多”按钮就点击然后继续判断是否有新内容出现如果没有新内容或者达到最大轮数立刻停止。下面这份代码是我常用的模板你换成自己的目标站点后重点改三处页面 URL、内容卡片的选择器、按钮的选择器from playwright.sync_api import sync_playwright MAX_ROUNDS 30 STABLE_THRESHOLD 3 # 连续几轮没有任何新内容就退出 def get_card_count(page): # 根据目标页面的 DOM 结构调整这里以常见的卡片列表为例 return page.locator(div.card-item).count() def main(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # 调试时建议先 False page browser.new_page( 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.goto(https://example.com/feed, timeout60000) # 先触发第一轮滚动确保页面底部异步请求发出 page.mouse.wheel(0, 3000) page.wait_for_timeout(2000) stable_round 0 last_count get_card_count(page) for round_no in range(MAX_ROUNDS): # 滚动到页面底部 page.mouse.wheel(0, 12000) page.wait_for_timeout(1500) # 如果出现“加载更多”按钮优先点击 load_more page.locator(button:has-text(加载更多)) if load_more.count() 0 and load_more.is_visible(): try: load_more.click(timeout3000) page.wait_for_timeout(1500) except Exception as e: print(f点击加载更多失败: {e}) # 统计当前卡片数量判断是否还有新数据 current_count get_card_count(page) print(f第 {round_no 1} 轮当前卡片数: {current_count}) if current_count last_count: stable_round 1 if stable_round STABLE_THRESHOLD: print(连续多轮没有新内容停止滚动) break else: stable_round 0 last_count current_count # 页面最终内容 html page.content() print(f最终卡片数量: {get_card_count(page)}) browser.close() return html if __name__ __main__: html main()几个关键细节需要说明一下。滚动这里page.mouse.wheel(0, 8000)是模拟鼠标滚轮向下滚动 8000 像素量给大一点能确保触发页面的滚动监听。也可以用page.evaluate(window.scrollTo(0, document.body.scrollHeight))直接拉到页面底部两种方式效果类似但前者更接近真人行为对反爬策略更友好。加载完成判断我刻意没有用固定 sleep而是用“卡片数量连续几轮不变”作为停止条件。这个思路本质上是把“外部状态观察”做成了轮询器相比死等 N 秒靠谱得多——你永远不知道服务端这次响应要 500ms 还是 3s给自己设一个稳定阈值既不会漏数据也不会死循环。按钮点击的容错也很重要。很多“加载更多”按钮到了最后一页会变成灰色 disabled 状态点击前先判断is_visible()和is_enabled()能省掉很多无谓的异常。另外代码里我用page.locator(button:has-text(加载更多))这种文本定位方式比依赖一串动态 class 名稳得多因为很多前端框架的 class 名是构建时随机生成的。3.3 第三步内容解析、去重与存储渲染完成之后页面 HTML 已经包含所有动态加载的内容用 BeautifulSoup 解析即可from bs4 import BeautifulSoup import json def parse_items(html): soup BeautifulSoup(html, lxml) items [] for card in soup.select(div.card-item): title_tag card.select_one(h3.title) link_tag card.select_one(a.link) time_tag card.select_one(span.time) if title_tag is None: continue items.append({ title: title_tag.get_text(stripTrue), url: link_tag.get(href) if link_tag else , publish_time: time_tag.get_text(stripTrue) if time_tag else , }) return items如果目标页面的内容结构复杂或者解析规则经常变我建议配合 Playwright 的网络监听直接从接口响应里拿 JSON。这个方法稳得多因为 JSON 里的字段比 DOM 里的标签稳定。代码也很简单collected [] def on_response(response): if api/feed in response.url and response.status 200: try: data response.json() collected.extend(data.get(list, [])) except Exception: pass page.on(response, on_response)把这段监听挂上之后不管页面是滚动加载还是点击加载只要接口返回一次collected里就会自动追加一批原始数据。这种方式对前端渲染异常、标签调整都不敏感是我个人最喜欢的方式。去重一定要做尤其是滚动加点击混合的页面经常会重复渲染同一批数据。去重的唯一标识最好用内容自身的链接或者服务端分配的 ID。如果是新闻资讯没有稳定 ID就把“标题 发布时间”拼起来取 MD5import hashlib def make_sign(item): raw f{item[title]}-{item[publish_time]}.encode(utf-8) return hashlib.md5(raw).hexdigest() seen set() unique_items [] for item in items: sign make_sign(item) if sign not in seen: seen.add(sign) unique_items.append(item)存储层面数据量不超过几千条的话直接落 CSV 或 JSON 文件就行要做增量更新、按条件查询的话建议用 SQLite建表时加个唯一索引彻底根除重复写入问题。SQLite 是 Python 标准库自带的功能零配置、单文件、够用到百万量级。4. 常见问题与排查技巧实录4.1 元素定位不到selector 明明有却找不到这个问题在无限滚动页面上极其常见遇到时先别怀疑选择器写错大概率是下面几个原因。第一页面内容包在 iframe 里。桌面端页面为了隔离三方内容经常把评论、推荐位放在 iframe 里Playwright 的操作默认不会穿进 iframe。解法是先用page.frame_locator(iframe)拿到 iframe 内的定位器再去做滚动和点击。第二Shadow DOM。很多现代组件库为了样式隔离用了 Shadow DOM常规选择器穿透不进去。Playwright 对 Shadow DOM 支持还可以但嵌套 Shadow root 时依然有坑真要抓这种页面优先考虑从接口 JSON 拿数据别硬在 DOM 里翻。第三懒加载导致元素不存在。滚动之前页面上根本没这个元素得先滚到它附近才会被创建。所以点击某些按钮前要确保元素先scroll_into_view_if_needed()再用 Playwright 的wait_for_selector等它出现最后才执行点击。有个小技巧如果元素路径太深直接用文本定位或者page.get_by_role(button, name加载更多)比一层层找嵌套标签省心得多。4.2 滚动停不下来或者数据一直重复滚动停不下来的常见原因有两个页面本身是真正无限流理论上永远有新数据比如某些纯时间流信息或者你的停止条件写得太宽松。建议加两个硬性保护最大滚动轮数MAX_ROUNDS和一个连续空转阈值STABLE_THRESHOLD。代码里我把最大轮数设成 30连续 3 轮没新数据就退出这两个参数在极端情况下能防止脚本跑到天荒地老。数据重复则是另一类问题。很多页面在滚动时不会清掉旧 DOM而是把新内容追加到尾部你用“卡片总数”判断是否加载成功时没问题但如果你是从接口响应里收集 JSON接口每滚动一次可能返回同一批数据这时候必须做去重。去重不能只靠 set 存对象因为 HTML 和 JSON 表示同一个内容的方式不同务必要用统一字段加工出签名再做去重。4.3 点击“加载更多”按钮无效按钮明明可见点击后页面没有任何反应这我遇到过无数回。排查顺序有讲究。先检查按钮是不是 disabled 状态很多框架的按钮到边界之后还显示但点不动。代码里判断is_enabled()可以提前止损。然后看一下按钮是否被遮挡。页面上出现了悬浮区、广告层、Cookie 提示条都可能导致 Playwright 点击时提示“intercepts pointer events”。解决方法是先关闭弹窗/悬浮层或者用click(forceTrue)强制点击。但注意 force 只是跳过可见性检查并不绕过 JS 里的事件绑定逻辑某些极端情况还得手动派发点击事件。再有一种情况页面的点击事件不是绑定在 button 上而是绑定在它的父级 div 上或者绑定的是整块卡片的 click handler这时候你点击按钮本身可能什么都不会触发。解法是改用 JS 直接派发事件page.locator(button:has-text(加载更多)).evaluate( (el) el.click() )这种方法能直接触发原生 click 事件绕开一部分前端框架自带的拦截逻辑。4.4 反爬风控以及必须说的合规运行无限滚动页面的数据加载接口往往都有风控。短时间请求次数过多轻则返回验证码页面重则封 IP。规避风控的第一原则是控制频率。requests 方案里给请求加随机延时Playwright 方案里不要连续滚动每轮之间留出 1 到 2 秒的随机等待。有条件的话可以准备一组不同浏览器的 User-Agent 轮流用但注意不要频繁切换否则反而容易触发设备指纹风控。同样重要的还有合规性。做爬虫不是不能做但必须在合理边界内先看目标网站根目录下的robots.txt明确哪些路径允许爬取不采集个人敏感信息不贩卖数据不对目标站点发起超出正常用户行为的高频请求。尤其是抓取个人账号数据、联系方式这一类既有法律风险也有道德风险我自己的原则是不碰。做技术练手用公开的开源数据或者自己的测试站点最好。5. 性能优化与工程化扩展5.1 从一次性脚本到稳定可复用的采集团队单个脚本能跑通算完成了 30% 的工作。真正要日常稳定使用接下来该做工程化改造。第一把代码拆模块。页面加载逻辑、网页解析逻辑、存储逻辑分成不同文件后续某一个页面的 DOM 结构变了只需要改解析模块不需要动滚动逻辑。第二完善日志。别用 print 打天下用 Python 的logging模块输出到文件。跑了多久、滚到第几轮、抓了多少条、哪一轮出现异常这些信息在排查问题时都是救命的信息。第三增加断点续跑。脚本中途挂了重启不该从头再来应该把已抓取的签名保存到本地文件里重新跑的时候直接加载已有签名集合做去重。这个改动不大但对上千条数据的采集任务收益非常明显。第四设置失败重试机制。单轮请求失败不代表全部失败捕获异常后等几秒重试一次重试超过三次再放弃并记录失败上下文。5.2 异步并发与分布式爬虫的扩展路径单浏览器实例滚动抓取速度有限数据量上来之后可以用 Playwright 的异步 API 开多个 page 同时抓不同的列表页。注意这里不是无限提高并发通常同时开 3 到 5 个浏览器实例就差不多是上限了再多容易触发风控本机资源也吃紧。如果走纯接口方案用concurrent.futures.ThreadPoolExecutor控制并发请求数这里线程池的收益比单线程循环高很多但要让每个线程的请求间隔错开避免同一时刻集中打爆目标服务器。再往上走就是分布式采集架构。经典做法是 Scrapy 负责页面调度与解析SQLite 或 Redis 做任务队列与去重集合多个 worker 机器同时消费 URL 任务。scrapy-redis 这个成熟方案可以做到 URL 去重下沉到 Redis多机共享同一个待抓取队列数据产出进同一个存储。但说句实在话项目规模没有到几百万条数据真的不必上分布式架构复杂度会把你拖垮。还需要提一个行业趋势。现在各大平台的前端加密参数越来越重接口签名逻辑越来越复杂很多团队开始尝试用大模型辅助分析 JavaScript 加密逻辑自动生成逆向代码这类“大模型逆向爬虫”在社区里讨论度很高。但对多数实际任务来说我更推荐先用浏览器渲染方案绕开参数逆向毕竟 Playwright 模拟的就是真实浏览器绝大多数加密参数根本不需要解。只有渲染方案也拿不到数据的时候再去考虑逆向的事而且要评估好合规成本与技术难度。最后分享几个我的使用习惯这阵子频繁写无限滚动抓取脚本沉淀了几个小习惯分享给大家。第一所有参数以常量形式放在文件开头统一管理。MAX_ROUNDS、STABLE_THRESHOLD、超时时间、选择器写成一个 config 段。改需求的时候不用在函数体里到处找魔法数字。第二页面结构不稳定时优先从接口 JSON 拿数据。DOM 解析只是兜底方案接口字段和 DOM 标签哪一个更稳定大多数时候是前者。第三抓取时建立一种“观察者思维”。不要假设页面会按你想的状态加载每一轮都做好“没有新内容就退出”的准备。一个无限滚动页面它的服务端状态随时可能变化你的脚本要有足够的容错空间。我实际使用的过程中最大的教训是不设上限的滚动脚本。曾经脚本开着没管第二天早上发现它还在页面上疯狂滚动数据重复了几万条。那次之后我所有的采集脚本都强制加最大轮数和连续空转阈值宁可少抓几条也不失控。再提醒一句玩爬虫工具选型只是第一步真正的技术含量在于如何稳定、高效地解决目标问题同时在合规区间内运行。这些边界别去碰技术路就走得长远。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门