用户评论是网络上最有价值的公开数据之一。评分、文字反馈和已验证购买标记能让你了解人们对产品的真实看法,且持续更新。问题在于,大多数评论页面采用客户端渲染,并分布在数十乃至数百屏中,普通 HTTP 请求只会返回一个空壳。本教程展示如何用一个小型、可运行的 Python 流水线抓取用户评论:渲染 JavaScript 密集型页面、解析结构化字段、翻页遍历完整集合、存储结果,以及可选地进行情感分析。
为保持诚实且可辩护,整个教程仅涵盖公开评论:任何人无需登录即可看到的评分、标题、正文、日期和已验证徽章。它不涉及用户账号、登录后才能看到的内容,或平台已公开显示之外的任何个人数据。末尾的道德与服务条款章节不是套话,请在将本代码用于实际流量之前先阅读。
为什么要抓取用户评论
单条评论只能代表一个人的看法。数千条经过结构化处理、可以查询的评论,才能告诉你一款产品在哪里领先、在哪里悄悄流失客户。这正是其价值所在:将已渲染的评论页面转化为干净、可比较的数据,你可以将其绘制成图、随时间追踪,或输入模型。团队将其用于竞争基准测试、产品差距分析、品牌监控,以及跟踪发布或修复后情感的变化。
这与任何电商网络爬虫作业的问题形态相同。评论的不同之处在于体量和分页:数据分散在多个页面上,延迟加载,且随着请求量增大,反机器人防护也会升级。因此,该方法必须从第一个请求起就处理好渲染、分页和封锁问题。
为什么普通请求在评论页面上失败
用裸 HTTP 客户端请求现代评论 URL,你通常会得到状态 200,但正文中几乎没有评论内容。有两个不利因素。第一,大多数平台通过 JavaScript 在浏览器中渲染评论,因此初始 HTML 只是一个骨架,只有在页面脚本运行后才会填充内容。第二,评论网站能快速识别自动化流量:不像真实浏览器的数据中心 IP 和请求模式在看到渲染内容之前就会被挑战或封锁。
因此,一个可用的评论爬虫需要在一次请求中同时具备两样东西:一个能真正渲染页面的浏览器,以及一个被平台识别为真实访客的 IP。你可以自己搭建无头浏览器加轮换住宅代理池,但把它们组合起来并保持健康运行才是大部分工作。Crawlbase Crawling API 将两者折叠进一次调用:你把 URL 连同 JavaScript token 一起发过去,它在可信 IP 后面渲染页面,并返回已完成的 HTML 供你解析。如果你倾向于跳过对常见目标的选择器编写,Crawling API 会以 JSON 格式返回已解析的字段,而如果需要原始代理访问,可以使用 Smart AI Proxy。
Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 先在真实浏览器中渲染页面。评论页面是客户端渲染的,因此这里需要 JS token。使用普通 token 返回的是与普通请求相同的空壳,评论卡片缺失。
从评论中提取的字段
即使标记不同,值得捕获的字段在大多数平台上是一致的。针对每条评论卡片,目标是以下五个字段:
- 评分星级分数,标准化为数字。大多数网站使用 1 到 5 分;有些使用 1 到 10 分,需要在后续转换。
- 标题评论者给出的简短标题(如果平台有此字段)。
- 正文评论文字本身,这是承载实际信号的定性部分。
-
日期评论发布时间,最好来自机器可读的
datetime属性,而非显示文本。 - 已验证平台是否将其标记为已验证购买,让你之后可以过滤掉可信度较低的评论。
目标是建立一个统一的稳定模式,让来自不同来源的评论无需逐来源清理即可对齐。单一统一的结构效果很好:
{ "rating": 4.5, "title": "Exactly what I needed", "body": "Arrived early and works as described.", "date": "2026-01-10", "verified": true, "url": "https://www.example.com/product/123/reviews?page=2" }
一旦每条评论都符合这个结构,跨产品和跨平台的分析就只是过滤和分组,而非重新格式化。
搭建项目
你需要 Python 3 和一个带有 JS token 的 Crawlbase 账号(注册后在控制台获取)。创建项目文件夹并安装所需库。
python --version mkdir review-scraper && cd review-scraper python -m venv venv && source venv/bin/activate pip install crawlbase beautifulsoup4
两个依赖各司其职:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML。标准库的 re、csv 和 json 模块负责标准化和存储,无需额外安装其他内容。请将你的 token 保留在源代码之外:将其导出为环境变量,并在运行时读取。
获取已渲染的 HTML
首先获取完整页面。Python 客户端将 API 封装在一次 get 调用中。你传递两个对评论网站很重要的选项:ajax_wait 告知 API 等待异步内容加载,page_wait 在加载后再等待固定毫秒数,让延迟渲染的评论卡片有时间出现。5秒是一个合理的起点;如果结果较少,可以适当增加。
import os from crawlbase import CrawlingAPI # JS token renders the page in a real browser before returning HTML api = CrawlingAPI({"token": os.environ["CRAWLBASE_JS_TOKEN"]}) options = { "ajax_wait": "true", "page_wait": 5000, } def fetch_html(url): response = api.get(url, options) if response["status_code"] != 200: raise RuntimeError(f"fetch failed: {response['status_code']}") return response["body"].decode("utf-8") if __name__ == "__main__": url = "https://www.example.com/product/123/reviews" print(fetch_html(url)[:2000])
运行后你应该看到带有评论卡片的真实标记,而不是普通请求返回的空壳。这能在你编写任何选择器之前确认渲染正常工作。客户端返回的 response["body"] 是字节类型,因此解码一次后将字符串传递给解析器即可。
评论页面需要在可信 IP 后面渲染页面,而这只需一次调用。Crawling API 接受 JS token,在真实浏览器中运行页面,在服务器端轮换住宅 IP,并将渲染完成的 HTML 返回给你,省去了自己运行无头浏览器集群和代理池的麻烦。先在免费层级向公开评论页面发起请求。
用 BeautifulSoup 解析评论
拿到 HTML 后,将其加载到 BeautifulSoup 并遍历评论卡片。每张卡片包含你所需的字段,但不同平台的类名不同,因此解析器使用一组候选选择器,取第一个匹配的。在浏览器开发者工具中检查实时页面,找到目标平台的当前选择器,然后将每个字段映射到对应选择器。
import re from bs4 import BeautifulSoup def first_text(card, selectors): for sel in selectors: el = card.select_one(sel) if el: return el.get_text(separator=" ", strip=True) return "" def parse_reviews(html, source_url=""): soup = BeautifulSoup(html, "html.parser") cards = soup.select("[data-review-id], article[class*='review'], .review-card") reviews = [] for card in cards: rating_raw = first_text(card, ["[data-rating]", ".star-rating", "[class*='star']"]) match = re.search(r"(\d+(?:\.\d+)?)", rating_raw) date_el = card.select_one("time[datetime], [data-review-date]") reviews.append({ "rating": float(match.group(1)) if match else None, "title": first_text(card, [".review-title", "h3", "[class*='title']"]), "body": first_text(card, [".review-body", "[data-review-text]", "p"]), "date": (date_el.get("datetime") if date_el else ""), "verified": card.select_one("[class*='verified']") is not None, "url": source_url, }) return [r for r in reviews if r["body"]]
评论平台会在没有通知的情况下更改类名和数据属性。请将上述选择器视为起始模板,而非固定合约。当提取返回空字段时,重新检查实时页面并更新候选列表。这对任何生产爬虫来说都是正常维护,不代表有什么问题。
first_text 辅助函数使解析器具有可移植性:为每个字段提供一组可能的选择器,返回第一个命中的结果,因此适配新平台主要是编辑这些列表,而非重写逻辑。过滤掉 body 为空的评论,可以排除共享评论容器类的布局卡片和广告位。
翻页遍历所有评论
获取一页数据几乎不够用。大多数平台将评论分散在数十或数百页上,通常使用 ?page=2 风格的查询参数。如果只请求第一页,你会错过大部分数据。模式是在循环中构建页面 URL,获取并解析每一页,当某页没有返回评论或达到你设定的限制时停止。
import time def scrape_all_reviews(base_url, max_pages=25): all_reviews = [] for page in range(1, max_pages + 1): sep = "&" if "?" in base_url else "?" page_url = f"{base_url}{sep}page={page}" html = fetch_html(page_url) reviews = parse_reviews(html, page_url) if not reviews: break # empty page means we ran past the last one all_reviews.extend(reviews) print(f"page {page}: {len(reviews)} reviews") time.sleep(1) # pace requests so you stay under rate limits return all_reviews
几点实际注意事项。设置一个合理的 max_pages,以防布局变化导致无限循环。一旦某页没有评论就立即停止。如果你的目标使用无限滚动而非编号页面,将 Crawling API options 字典中的 scroll 选项加入,而非构建页面 URL;循环的其余部分不变。
跨平台标准化
对于单平台上的单个产品,解析器输出通常干净到可以直接存储。一旦你从多个来源抓取评论,细微的不一致就会出现:一个网站评分满分10分,另一个使用相对日期如"3天前",字段名称也各有差异。一个轻量的标准化处理可以保持所有数据可比较。
def normalize(review, scale=5): rating = review.get("rating") if rating is not None and scale != 5: # map any scale onto a common 0-5 range review["rating"] = round(rating / scale * 5, 2) review["body"] = " ".join(review["body"].split()) return review
将评分转换为统一量表,折叠正文中的空白字符,并在某个来源字段命名不同时对齐字段名称。对于单个平台,可以跳过此步骤;对于多平台分析,这正是防止数据漂移的关键。
存储结果
迭代期间输出到控制台没问题,但你需要将数据保存到磁盘。CSV 是最简单的目标格式,任何电子表格都可以打开;标准库的 csv 模块将每个字典键映射为一列。
import csv def save_csv(reviews, path="reviews.csv"): fields = ["rating", "title", "body", "date", "verified", "url"] with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() writer.writerows(reviews) print(f"saved {len(reviews)} reviews to {path}")
如果你更喜欢用 SQL 查询数据,可以用标准库的 sqlite3 模块将相同的行写入 SQLite 表;解析和分页逻辑保持不变。JSON Lines 是另一个好选择,适合将记录流式传输到下游流水线。将各部分串联起来,整个流程只需几次调用:
if __name__ == "__main__": base = "https://www.example.com/product/123/reviews" reviews = scrape_all_reviews(base, max_pages=25) reviews = [normalize(r) for r in reviews] save_csv(reviews)
可选:对正文文本进行情感分析
一旦评论结构化完毕,情感分析只是一个短小的附加步骤。像 VADER 这样轻量的基于规则的模型可以在无需训练的情况下为每条评论提供情感极性分数,足以标记最愤怒和最满意的评论,并随时间追踪平均情感。
# pip install vaderSentiment from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer analyzer = SentimentIntensityAnalyzer() def add_sentiment(reviews): for r in reviews: score = analyzer.polarity_scores(r["body"])["compound"] r["sentiment"] = round(score, 3) return reviews
compound 分数范围为 -1(非常负面)到 +1(非常正面)。如需更深入的分析,可以将相同的正文文本传入基于 transformer 的分类器或托管 NLP 服务;到此为止的流水线不需要改变。
扩展到多个产品
对单个产品使用循环加休眠就够了。当你需要跨数百个 URL 抓取评论时,你开始在自己的代码中与并发、重试和调度作斗争。这时 Crawler 就发挥作用了:你不需要逐一拉取页面,而是将 URL 列表推送给 Crawlbase,它在云端处理这些页面,渲染每一页并通过 webhook 将完成的 HTML 送达你的端点。失败的请求会自动重试,因此你不必守着队列。对于少量页面,Crawling API 就足够了;超过这个量,Crawler 可以消除大部分运维开销。
保持不被封锁
即使渲染问题已解决,评论网站仍会监视爬虫形态的流量。以下几个习惯可以让运行保持健康,适用于任何高难度商业目标。
-
控制请求节奏。在紧循环中猛敲同一产品的评论是最快被限速的方式。分页循环中的
time.sleep是有意为之的,请保留它。 - 善用轮换。住宅代理池将请求分散到许多真实用户 IP,避免单个地址触发速率限制。Crawling API 会为你处理这一切;如果你自建技术栈,这是最关键的环节。
- 关注状态码。运行中开始返回挑战或错误,说明当前的速率或 IP 层级已经不够用了。将响应状态视为信号而非噪音,当它变化时退让。
更完整的方法请参阅如何在不被封锁的情况下抓取网站。如果想将这种托管方式与手动搭建的无头浏览器技术栈进行比较,用 Python 和 Selenium 进行网络爬虫详细介绍了那种构建过程。
诚实的部分:服务条款与个人数据
抓取大型商业网站处于法律灰色地带,是否被允许取决于平台的服务条款、你所在的司法管辖区以及你对数据的用途。许多评论网站在其条款中限制自动访问,因此无论你的工具多么谨慎,抓取行为都可能违反这些条款。这里的任何代码都不能改变这一事实,它只是让技术部分得以实现。
以下几条值得坚守。只采集公开评论:任何人无需账号即可看到的评分、标题、正文、日期和已验证标记。遵守网站的 robots.txt 及其声明的速率预期,并将请求量控制在不给任何人服务器造成压力的范围内。不要采集平台已公开显示之外的个人数据,也不要试图对评论者进行去匿名化或将其评论与其他来源的数据关联。如果你计划商业再利用这些数据,请获得许可或正式数据协议,而非假设沉默即代表同意。
本教程刻意将范围限定在公开评论内容,因为这条线让工作有充分的合理性。它不涉及任何登录后才能看到的内容、账号或个人资料数据,也不涉及以登录用户身份执行的操作。如果你的项目需要公开评论之外的内容,正确的做法是向平台申请官方 API 或数据协议,而非搭建更聪明的爬虫。有关托管访问与原始爬虫的区别,电商网络爬虫是一篇有益的参考文章。
核心要点
- 评论页面是客户端渲染的。普通请求返回空壳,因此必须在解析前渲染页面。
-
渲染和可信 IP 一起到位。使用 JS token 的 Crawling API 在一次调用中同时完成两者;
ajax_wait和page_wait控制等待内容的时间。 - 解析到统一模式。用候选选择器捕获评分、标题、正文、日期和已验证字段,并预期这些选择器会随时间漂移。
- 分页就是数据本身。循环遍历页面 URL,遇到空页就停止,并控制请求节奏以保持在速率限制以下。
- 先存储,再分析。写入 CSV、SQLite 或 JSON Lines,在数据结构化后再附加情感分析。
- 坚守公开评论。遵守服务条款和 robots.txt;不涉及账号,不采集超出公开显示范围的个人数据。
常见问题
如何从 JavaScript 密集型网站抓取用户评论?
大多数评论平台在客户端渲染卡片,因此原始 HTTP 请求返回状态 200 但评论缺失。你需要基于浏览器的获取方式。将 URL 连同 JS token 一起发送给 Crawling API,它会在真实浏览器中渲染页面后返回 HTML,这样 BeautifulSoup 解析时每条评论都已存在。ajax_wait 和 page_wait 选项控制等待延迟加载内容的时间。
抓取用户评论需要普通 token 还是 JS token?
JS token。普通 token 获取静态 HTML,在评论网站上与普通请求返回的空壳相同。JS token 先在真实浏览器中渲染页面,因此当你的解析器运行时,评论卡片已经存在于 HTML 中。
如何获取所有评论而不只是第一页?
大多数平台使用 ?page=2 风格的参数进行分页。在循环中构建页面 URL,获取并解析每一页,当某页没有返回评论或达到你设定的限制时停止。对于使用无限滚动而非编号页面的网站,将 Crawling API 的 scroll 选项传入,而非构建页面 URL;循环的其余部分保持不变。
我的选择器返回空字段。发生了什么变化?
几乎可以确定是平台的标记发生了变化。评论网站会在没有通知的情况下更改类名和数据属性,因此上个月有效的选择器可能已经失效。在浏览器开发者工具中重新检查实时评论页面,并更新解析器中的候选选择器列表。定期维护选择器对任何生产爬虫来说都是正常的。
我可以对抓取的评论进行情感分析吗?
可以。一旦评论被标准化为一致的模式,正文文本就可以直接输入 NLP 步骤。像 VADER 这样的基于规则的模型无需训练即可为每条评论提供情感极性分数;如需更细致的分析,可以将相同文本传入基于 transformer 的分类器或托管 NLP 服务。爬取流水线不需要改变。
抓取用户评论合法吗?
这取决于平台的服务条款、你所在的司法管辖区以及你的目的,而且许多网站限制自动访问。严格坚守公开评论内容,遵守 robots.txt 和速率预期,不要采集超出公开显示范围的个人数据,也不要试图识别具体的评论者。商业再利用时,请获得许可或正式数据协议,而非依赖爬虫。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

