news 2026/10/11 7:49:55

Selenium爬取天猫商品评论:异步加载、动态滚动与去重实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Selenium爬取天猫商品评论:异步加载、动态滚动与去重实战

做竞品分析想抓天猫商品评论,我第一反应是 requests 直接打接口。结果返回的 HTML 里根本没有评论数据,只有一个空壳。后来换成 Selenium 模拟浏览器,评论倒是出来了,可页面会自动加载,越滚越长,导出的数据里同一句好评重复出现。如果你也被 Python + Selenium 爬取天猫商品评论时的异步加载、动态滚动和去重折磨过,这篇内容就是按我这次完整踩坑后的思路写的。文章会覆盖异步加载的前端机制、Selenium 环境配置、动态滚动策略、评论字段提取、多层去重方案,以及让采集任务稳定跑完的细节。适合做电商竞品分析、选品调研和评论情感分析的读者参考。

提示:这里说的“异步加载”是指网页前端通过异步请求拉取并渲染数据,和 Python 异步编程里的 asyncio 没有关系。前者是我们观察到的页面行为,后者是代码执行模型,别混在一起。

1. 评论列表不在源码里:天猫异步加载的前端机制与Selenium选型

1.1 前端加载评论的三段式流程

先从最底层的问题说起:为什么直接 requests 拿不到评论?

现代电商商品页普遍是首屏框架先返回,HTML 里只包含页面骨架和基础商品信息。你打开商品详情页看到“累计评价”区域,其实是一个占位容器,里面没有数据。当浏览器执行 JavaScript 后,代码会拿着当前商品 ID 去请求一个评论接口,接口返回 JSON,前端再把 JSON 翻译成 DOM 节点插到评论区。这个“先请求、后渲染”的过程就是异步加载。

具体拆开是三段:页面加载完成后发起 XHR/Fetch 请求;服务端根据参数返回评论列表 JSON;前端回调函数遍历 JSON 并生成评论 DOM。整个过程不是一次性完成,尤其在天猫评论场景中,还会配合滚动触发翻页,每滚一段就发一次请求。所以你打开页面后快速拖动滚动条,能看到评论区不断往下“生长”。

我习惯把这个过程类比成自助餐厅:requests 相当于站在门口拍菜单,而 Selenium 是进去坐下的客人,能看着服务员一道一道把菜端上来,并把每一道菜记下来。这也是为什么很多人第一时间用 requests 抓不到任何评论数据的根本原因。

1.2 为什么早期方案都耗在了逆向接口上

既然评论是接口返回的,不少人会想:那我直接在浏览器开发者工具里找到 XHR 请求,拿到接口地址,再用 requests 模拟参数不就行了?

理论上可以,但现实很骨感。天猫评论接口不是简单的 GET 请求,它带有签名参数、时间戳、Cookie 校验,部分参数由 JavaScript 动态加密生成。你可以在某一瞬间把参数完整复制下来,构造出一次成功请求,但过一段时间参数算法一变,或者某个 token 过期,脚本立刻失效。维护这种逆向代码的成本,往往比采集任务本身高得多。

另外,requests 直接打接口还有一个问题:频率特征非常明显。同一个 IP 在几秒内连续请求几十次,接口返回状态会立刻异常,很容易触发风控。而 Selenium 驱动真实浏览器,有页面渲染、资源加载、事件触发的完整过程,虽然慢一些,但评论这种体量不算巨大的数据,综合成本反而最低。

1.3 为什么是Selenium,而不是Playwright或Pyppeteer

主流的浏览器自动化工具有 Selenium、Playwright、Pyppeteer。我最终选 Selenium 的原因很现实:生态最成熟,报错时搜索引擎里能直接搜到大量历史解决方案。而且本次任务的核心难点其实不在浏览器自动化本身,而在对异步加载节奏的判断和去重策略,用哪个框架都差不多。

三者的横向对比,可以参考我实测后的感受:

工具上手难度反自动化指纹社区资料量资源占用
Selenium低中等,需要处理 webdriver 标记极大高
Playwright中较好,默认更接近真实浏览器中高
Pyppeteer中一般少高

结论是:如果只是抓评论、跑短周期任务,Selenium 最合适;如果要做大规模、长期稳定的采集体系,可以再考虑 Playwright。下面所有代码都以 Selenium 为例。

2. 环境准备里最容易被忽略的三件事:Driver匹配、iframe切换与显式等待

2.1 依赖安装、Driver匹配与启动参数

先从安装说起。Selenium 现在的安装很直接:

pip install selenium

但很多人接着就卡在 chromedriver 版本不匹配上。Chrome 浏览器会自动更新,而 chromedriver 必须和浏览器内核版本严格对应,否则直接报 SessionNotCreatedException。我不建议手动下载对应版本,更推荐用 Webdriver Manager 自动管理:

from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

我之前为了图省事,装了一个固定版本的 chromedriver,结果 Chrome 一自动更新,整套脚本就废了。后来换成 Webdriver Manager,至少不用每次手动找版本。

启动时我会加这几类参数:

from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--headless=new") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--window-size=1920,1080") options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(options=options)

简单解释几个关键参数:--window-size必须显式设置,否则无头模式下默认视口可能被识别为窄屏,天猫会返回移动端布局,评论 DOM 结构完全不一样。--disable-blink-features=AutomationControlled和excludeSwitches用来降低“自动化测试”指纹,但别指望这几行能百分百绕过检测,真实项目中还会有滑块验证,后面会说应对方式。

2.2 切换iframe:很多评论就是在这里丢的

天猫商品评论页有一个很容易踩的坑:评论区可能被包在 iframe 子框架里。你在主页面里怎么都定位不到评论节点,不是选择器写错了,而是你还没进入那个 frame。

判断方法很简单,在浏览器开发者工具里查看评论区域的 DOM 树,看外层是不是<iframe>。如果是,先切进去再操作:

frame = driver.find_element(By.CSS_SELECTOR, "iframe[id*='comment'], iframe[class*='iframe']") driver.switch_to.frame(frame)

评论处理完再切回主文档:

driver.switch_to.default_content()

现在很多页面为了隔离样式和数据层,会把列表类模块都塞进 iframe。我写完这套流程后也复用到其他平台,发现 90% 的“定位不到元素”问题都出在 iframe 这一层。排查时第一件事要先检查 iframe。

2.3 显式等待:别再用time.sleep碰运气

页面加载完成不等于评论渲染完成。异步请求的响应时间受网络影响波动很大,固定 sleep 5 秒可能有时还没渲染完,有时又白白浪费 3 秒。

用显式等待才是稳定做法:

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait = WebDriverWait(driver, 15) comment_area = wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, ".tm-list-container")) )

我总结一个经验:整个采集循环里只有两类等待是必要的,一类是等待某个元素出现,用presence_of_element_located;另一类是等待数量条数变化,用循环加条件判断。其余地方尽量少用固定 sleep,这样程序更稳,也不容易被误判成机器行为。

3. 动态滚动不是一把梭:滚动容器、加载节奏与结束条件的设计

3.1 先判断页面是滚动加载还是点击加载

天猫评论模块在不少商品页里并不是自动滚动加载,而是底部有一个“查看更多评价”或“点击加载更多”按钮。如果没判断清楚就执行 scrollTo,拉到页面最底部也不会触发任何新数据。

我的处理顺序是:先看评论区域底部有没有按钮。有,就优先处理按钮点击;没有,再走滚动逻辑。判断按钮可以这样写:

buttons = driver.find_elements( By.XPATH, "//button[contains(text(),'查看更多')] | //a[contains(text(),'查看更多')]" ) if buttons: buttons[0].click() else: scroll_and_wait()

这个判断很重要。我最初直接按通用滚动方案写,结果跑了半天只抓到第一屏,还以为是天猫改了接口,最后发现就是没有触发加载入口。后来在代码里先把“点击加载”的情况排除掉,才真正稳定下来。

3.2 滚动容器、步长与停止条件

如果确认是滚动加载,接下来还有一个隐蔽的坑:滚动并不一定是 window 滚动。有些评论列表外层有一个 overflow:auto 的容器,你滚动 window 毫无反应,数据永远不加载。

所以我在滚动函数里做了一个开关,支持滚动指定容器:

def scroll_container(driver, container=None): if container: driver.execute_script( "arguments[0].scrollTo(0, arguments[0].scrollHeight);", container ) else: driver.execute_script( "window.scrollTo(0, document.documentElement.scrollHeight);" )

容器不是自己猜的,而是先找到评论列表的父节点,看它有没有滚动条。判断方式是在浏览器控制台执行:

arguments[0].scrollHeight > arguments[0].clientHeight

如果为 true,说明这个元素自身可滚动,就把它作为滚动目标传进去。

滚动触发后,数据加载需要时间。所以每次滚动后我不会立即继续,而是先等待,再统计当前评论数。停止条件是“连续 N 轮数量无变化且页面滚动条已到底”。N 我一般取 5,太少容易在接口响应慢的时候误判,太多又浪费时间。

3.3 加载失败的兜底逻辑与频率控制

动态加载最常见的失败是触发平台风控提示,比如页面突然弹出滑块验证,或者评论区变成“暂时加载失败,请刷新重试”。这些情况如果不去处理,脚本会一直傻乎乎地滚下去。

我加了两个兜底。第一,每次滚动前快速检查页面有没有出现验证码或异常提示元素,出现了就停止滚动,等一段时间后刷新重试。第二,滚动间隔不能固定。这里不是为了绕什么规则,而是真实用户的滚动速度本来就有随机性,固定间隔反而像程序。我会用random.uniform(1.5, 3.0)作为间隔。

顺便回应一下你在热搜里看到的“selenium 网页左右滑动”:横向滚动的逻辑和纵向一样,只是把 scrollTo 的坐标从 scrollHeight 换成 scrollLeft 和 scrollWidth。天猫评论这个场景不涉及,但如果是抓横向图片列表,可以沿用同一个函数思路,改个参数就行。另外,这套滚动判断逻辑不仅适用于天猫,京东的评论页、动态加载的社区页也基本都是同一个套路。

4. 从评论节点到结构化字典:选择器定位、字段拆分与文本清洗

4.1 如何找到稳定的评论根节点定位方式

天猫评论的 class 名改版比较频繁,每次抓的时候我都要重新审查元素,不建议把过时的 class 写死。更稳妥的做法是先观察所有评论节点的公共父结构,然后在多个候选选择器里做 fallback。

我第一次写的时候直接用了一个具体 class,结果隔了一周再跑就全失效了。后来改成多选择器 fallback 的写法:

items = driver.find_elements( By.CSS_SELECTOR, "[class*='review-item'], [class*='comment-item'], [class*='tm-list-item']" ) if not items: items = driver.find_elements( By.XPATH, "//div[contains(@class, 'item') and .//span[contains(text(), '星评')]]" )

不管选择器怎么变,核心思路是:先定位到每一条评论的根节点,后续所有字段都在这个根节点下找,避免从全页面暴力检索导致串行数据。

4.2 字段拆分:内容、评分、时间、SKU与追评

拿到评论根节点后,字段提取就清晰了。每一个字段都做 try/except,因为部分评论没有追评,部分用户不显示 SKU,部分评论被折叠。

我的提取逻辑大致如下:

results = [] for item in items: try: nickname = item.find_element(By.CSS_SELECTOR, ".user-nick").text.strip() except Exception: nickname = "" try: content = item.find_element(By.CSS_SELECTOR, ".review-content").text.strip() except Exception: content = "" try: sku = item.find_element(By.CSS_SELECTOR, ".sku-info").text.strip() except Exception: sku = "" try: rate = item.find_element(By.CSS_SELECTOR, ".rate-star").get_attribute("class") except Exception: rate = "" results.append({ "nickname": nickname, "content": content, "sku": sku, "rate": rate, })

评分字段在天猫不一定以文字形式出现,很多是用星级 class 控制,比如“star-5”“star-4”。所以get_attribute("class")比拿文字更稳。如果你能直接拿到文字数字,优先提取数字,方便后面做评分分布统计。

4.3 文本清洗、数据归一化与编码问题

提取到的原始文本通常带换行、空格和 HTML 实体,比如 “ ”。这些不整洁数据如果直接拿去去重,会出现“同一句话因为空格位置不同,被判成两条”,所以清洗必须在去重之前完成。

import re def clean_text(s): if not s: return "" s = s.replace("\n", " ").replace("\r", " ").replace("\u3000", " ") s = s.replace("&nbsp;", " ").replace("&amp;", "&") s = re.sub(r"\s+", " ", s).strip() return s

另外,写 CSV 到 Windows 环境时,记得用encoding="utf-8-sig",否则 Excel 打开会乱码。我见过不少人抓下来的评论在记事本里正常,在 Excel 里全是乱码,最后重跑一遍,浪费不少时间。评论时间字段如果是相对时间,比如“三天前”,建议在清洗阶段统一换算成具体日期,不然排序和去重都会出问题。

5. 同一句评论出现三次:去重键设计、哈希生成与SQL兜底去重

5.1 为什么滚动采集很难避开重复评论

动态滚动加载过程中,重复几乎是必然的。原因有几个:

一是同一个接口可能被前端的加载逻辑多次触发,返回的数据区间有重叠。二是滚动带动 DOM 更新时,旧节点没有及时清空,导致同一个评论被渲染了两份。三是我们在判断“加载完成”时可能卡了两轮,同一批数据被解析了两遍。

知道了原因,去重策略就不应该只在最后“手工删除重复项”,而是要在采集过程中实时去重,同时把“新增评论数”作为滚动停止条件的依据之一。如果某一轮新增为 0,再判断是否到底,这样既做了去重,又避免在底部空转。

5.2 去重键怎么设计:文本、哈希还是评论ID

这是整个去重设计里最关键的部分。直接拿评论内容做 key 是不行的,电商里“默认好评”这种复制粘贴内容太常见,不同用户可以发一模一样的话。只拿昵称做 key 更不行,同一用户可以买多个 SKU、发多条评价。

最理想的是每条评论有一个独立 ID。如果页面节点上有>comment_id = item.get_attribute("data-id") or item.get_attribute("data-comment-id")

如果没有 ID,就用组合键“昵称 + 评论时间 + SKU + 内容哈希”:

import hashlib def make_key(nickname, comment_time, sku, content): raw = "|".join([ clean_text(nickname), clean_text(comment_time), clean_text(sku), clean_text(content) ]) return hashlib.md5(raw.encode("utf-8")).hexdigest()

注意一点:内容在做哈希前必须已经经过统一清洗归一化,否则空格、换行差异会让同一个评论生成不同哈希,去重就是白做了。

5.3 三层去重的实现与对比

这套流程里我用了三层去重,每一层管一段,互相补充。

第一层是内存实时去重。维护一个seen集合,每分析完一个节点就把 key 放进去,下一次先判断 key 是否已存在。Python 的 set 本质就是数组去重的标准思路,在几万条评论的场景下速度完全够用。

第二层是文件落盘去重。实时把带 key 的记录追加写入 JSONL 或 CSV,下次重跑时先读取历史 key 集合。很多爬虫最后数据出错,都是因为只有内存 set,程序一崩全部重来,第二层就是解决这个问题的。

第三层是 SQL 兜底去重。即使前两层有漏网的,最后入库时再用 SQL 清理一次。比如把数据先写进 SQLite,再用这类语句删除重复值:

DELETE FROM comments WHERE id NOT IN ( SELECT MIN(id) FROM comments GROUP BY comment_key );

如果你用的是 MySQL,还可以直接给 comment_key 字段加 UNIQUE 索引,写入时用 INSERT IGNORE,最省事。三种方式的使用场景对比:

去重层级使用场景优点缺点
内存 Set采集过程实时判断速度快程序退出即丢失
文件/历史 key断点续抓可恢复文件需同步维护
SQL 唯一索引最终入库保证最终干净发现重复时可能已晚

6. 稳定跑完整个采集任务:断点续抓、限速与多实例并行

6.1 异常捕获、验证码处理与断点续抓

Selenium 脚本跑起来容易,但要稳定跑完一大片商品,需要处理很多异常。我遇到最多的有三个:元素瞬时报错、滚动超时、验证码弹窗。

元素瞬时报错的解法是每个字段用 try/except 包住,定位不到就填空值。滚动超时的原因是等待条件设得太短,可以适当拉长,并在超时后刷新页面重新尝试。验证码最麻烦,我目前的策略是:检测到验证码元素后,立刻停止当前商品抓取,把商品 ID 写入待重试文件,然后继续下一个。不要在同一页面上反复重试,很容易变成死循环。

断点续抓的核心是“每次成功解析一条评论,就立刻把记录写盘”。我之前习惯最后一次性写文件,结果一次崩溃让整批数据全部丢失,重新跑又是一两个小时。后来改成边解析边写,每条记录写入后立即 flush 一次,脚本崩了,下次运行时加载历史记录继续。

6.2 多实例并行、限速与个人经验

再说一个很多人会踩的坑:Selenium 的 WebDriver 不是线程安全的,你很难在一个浏览器实例里开十几个线程同时滚动。我在第一版里想过用 ThreadPoolExecutor 并发操作同一个 driver,结果是各种乱序、重复等待、超时,完全不可控。

后来换成了多进程的方式,每个进程独立启动一个浏览器实例,负责不同商品,再把中间结果通过文件或队列汇总。这种方式最稳定。如果你不想写进程池,也可以直接多开几个脚本,每个脚本分配不同商品 ID 区间,最后统一合并。这也是“异步加载”在采集端的正确打开思路:网页异步加载是一回事,但我们的采集任务多个商品之间完全可以并行。

无论哪种方式,限速都别省。我的建议是每个商品之间的间隔至少 5-10 秒,两次滚动间隔随机分布在 1.5-3 秒,抓完一批后暂停更久。限速的意义不只是降低风险,更是给页面异步加载留出真实响应时间,数据反而抓得更全。IP 代理那些我建议先不碰,正常频率下基本用不到,真要用也要在请求侧做,而不是在 Selenium 侧盲目叠加。

做完以上这些,这套采集流程已经能比较稳定地跑通天猫商品评论。最后再说一个我一直沿用的小技巧:抓完所有数据后,先不要急着开始做分析,先用 SQL 去重语句把最终数据扫一遍,同时在本地保留一份原始未去重的 JSONL。我起初去重后就把原始文件删了,后来分析中发现某个字段全部为空,才发现去重键里包含了脏数据,根本无法回查。保留原始文件,你的去重操作才是可逆的。这套流程跑通之后,后续不管是换商品还是换平台,核心逻辑都可以复用,成本基本只剩修改选择器和等待节奏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 7:49:27

Java程序员必看:掌握AI应用开发,薪资翻倍不是梦!

文章调研显示&#xff0c;成都地区60%的Java开发者转向AI应用开发&#xff0c;薪资普遍增长。通过学习AI技术&#xff0c;如Prompt Engineering、RAG等&#xff0c;可显著提升职业竞争力。文章分析了AI应用开发的核心技能要求及市场薪资水平&#xff0c;强调AI不是替代程序员&a…

作者头像 李华
网站建设 2026/10/11 7:49:03

iFlow CLI实测:在终端里对话式编程,多模型切换与免费边界

最近一个多月&#xff0c;我的日常写代码方式被一个叫 iFlow CLI 的命令行工具改变了。以前写个脚本&#xff0c;要么在浏览器里开好几个 AI 聊天页面来回复制粘贴&#xff0c;要么在编辑器里装一堆插件&#xff0c;每天光在不同工具之间搬运代码就花掉不少时间。现在我把大量编…

作者头像 李华
网站建设 2026/10/11 7:47:44

ROS 2 如何与 IgH EtherCAT Master 协同?从 Topic 到 PDO 的实时数据链路设计

在机器人控制系统中&#xff0c;ROS 2 和 EtherCAT 经常同时出现。ROS 2 负责机器人软件模块之间的通信、任务编排和轨迹数据传递&#xff0c;EtherCAT 则负责控制器与伺服驱动器、远程 I/O 等工业设备之间的周期性数据交换。当两者组合起来时&#xff0c;一个看似简单的问题就…

作者头像 李华
网站建设 2026/10/11 7:41:05

图即代码:用diagram-design构建可维护的工程化图表系统

1. 从一张草图到一套系统&#xff1a;diagram-design 到底在解决什么问题第一次听到 diagram-design 这个词&#xff0c;很多人会下意识觉得它就是个“画图工具”或者“图表模板库”。但真正在项目里被图表折磨过的人会明白&#xff0c;它要解决的根本不是“怎么画”&#xff0…

作者头像 李华
网站建设 2026/10/11 7:40:24

车载激光雷达:2030年270亿市场空间的产业逻辑

各家车厂发布会开完&#xff0c;只要底盘上还顶着一颗“小雷达”&#xff0c;弹幕里就会飘过一句话&#xff1a;这车智驾硬件堆得真足。“车载激光雷达”这个配件&#xff0c;最近两年已经从实验室名词变成了发布会的固定卖点&#xff0c;甚至十五万级家用车也开始标配。与此同…

作者头像 李华