先说一个场景:我想在几款价位相近的“某类数码产品”里挑一个入手,光看详情页的官方宣传完全没用,评论才是真实口碑。但我翻了三十多页评论、手动复制了几十条关键吐槽之后,整个人是崩溃的——一千条评论里有八百条“京东物流快”,剩下两成才是有效信息。于是我把这个需求做成了一个小工具,用 Python 把京东评论抓下来,跑完分词、词频、情感分析之后,再用词云和图表把它们可视化。整个过程不复杂,但踩坑不少,这篇文章把完整方案和经验一起整理出来。
这个项目适合两类人:一是想用 Python 把爬虫、数据分析、可视化三条链路打通一遍的初学者;二是真的需要做商品口碑调研、竞品分析的朋友。全文不搞花架子,直接讲清楚每一步是怎么想的、为什么这么做,以及哪些地方是你自己动手时最容易摔跟头的。
1. 先想清楚:这套系统到底要回答什么问题
做这种小项目,最忌讳一上来就写爬虫代码。我先问自己一个问题:拿到一万条评论之后,我真正想知道什么?
1.1 需求拆解:从模糊的“看口碑”到具体指标
如果只是“看看口碑好不好”,那手动翻几页就够了,根本不用写代码。需要写代码,一定是因为问题可以被量化。我当时把需求拆成了五条,每条都要对应一个可视化图表:
一是评分结构:五星和一星是不是两极分化?很多商品评分看似 4.5,实际上大量五星好评和大量一星差评互相抵消,这跟“大多数人觉得还不错”完全是两回事。
二是时间趋势:评论是不是集中在某个促销节点?如果某段时间评论数量突然暴增,说明活动引流效果明显,这段时间的评论质量大概率需要打折看。
三是高频词和关键词:大家反复提到的是“续航”“发热”“物流”还是“颜值”?同一类商品,口碑侧重点能直接告诉你它真实的产品定位。
四是情感倾向:正面评论和负面评论的比例,以及在差评里被反复提到的问题点是什么。
五是集中吐槽点:一到两星的评论里,除了情绪发泄,真正反复出现的产品缺陷是什么。
这些需求最后对应到图表上就是:评分分布条形图、评论数时间折线图、词云、情感占比饼图、低频差评关键词清单。有了这张图,整个项目的边界就清晰了:不是做推荐系统,不是做刷单检测,就是围绕上述几个指标搞一套“采集-清洗-分析-展示”的管道。
1.2 整体流程与技术选型
流程分四段:
评论采集 -> 清洗与中文NLP预处理 -> 统计分析 -> 可视化展示选型上,我用的是这样一套组合:
| 环节 | 工具 | 选择理由 |
|---|---|---|
| 网络请求 | requests | 轻量,京东评论接口是标准 JSON,不需要浏览器渲染 |
| 数据存储 | pandas + CSV/parquet | 中间结果落盘,方便断点续跑和多轮实验 |
| 中文分词 | jieba | 社区成熟、词典可控,对电商评论这种短文本足够用 |
| 情感分析 | SnowNLP | 开箱即用,不需要自己训练模型,适合中文短句的粗粒度判断 |
| 词云 | wordcloud | 生成速度快,支持自定义中文字体 |
| 图表 | pyecharts | 交互式图表,适合出 HTML 报告,也能直接嵌到 Jupyter |
这套方案的特点是不追求大而全,每一环都选“够用且好调”的库。比如情感分析,如果一开始就上 BERT 模型,环境配置就会劝退一拨人;SnowNLP 虽然只输出一个 0~1 的得分,但对做电商评论的正负倾向粗判来说,性价比极高。
1.3 环境准备:版本问题比想象中多
要提前装好的依赖如下,我直接给一份可用的 requirements.txt:
requests==2.31.0 pandas==2.0.3 jieba==0.42.1 snownlp==0.12.3 wordcloud==1.9.2 pyecharts==2.0.5 matplotlib==3.7.2其中有两个版本上的坑,提前说清楚:
第一个是 pyecharts 的大版本。0.5.x 和 1.x 之后的 API 完全不一样,网上大量老教程用的还是 0.5 时代的写法,比如Pie里传attr和v。你如果照着复制,2.x 版本会直接报TypeError。我建议直接用 2.x,然后统一用add()传列表的方式写。
第二个是 wordcloud 的中文字体。默认字体不支持中文,生成出来全是“豆腐块”。必须显式指定一个系统中文字体路径,Windows 一般是C:/Windows/Fonts/simhei.ttf,macOS 一般是/System/Library/Fonts/PingFang.ttc。这一步后面会单独讲。
2. 评论采集:找到 JSON 接口、拼参数、拿数据
很多人以为爬京东评论要先模拟登录、破解验证码,其实不是。商品详情页里的评论数据走的是一个独立的 JSON 接口,完全不需要处理页面 HTML,只要拿到商品 ID 就能直接请求。
2.1 请求分析与参数组装
打开某个商品的详情页,地址栏里的一串数字就是商品 ID,比如https://item.jd.com/100012345678.html里的100012345678,这个 ID 就是后续所有请求的核心。
评论接口的路径大致是这样的:
https://api.m.jd.com/?appid=item-v3&functionId=pc_club_productPageComments&client=pchome&body={"productId":"100012345678","page":0,"pageSize":10,"sortType":5}这个接口返回的是标准 JSON,关键的几个字段是:
comments:当前页评论列表comments[].content:评论正文comments[].creationTime:评论时间comments[].score:评分,一般是 1~5comments[].nickname:脱敏后的用户昵称comments[].productColor/productSize:购买的颜色和规格maxPage/maxCommodityPage:可翻页数上限afterCount / beforeCount:追评和好评总数
这里有个很容易搞混的点:接口里的page是从 0 开始的,而且存在多个翻页上限。有的商品评论总量很大,但接口里能翻的页数有限,不是你想要多少页就有多少页。后面踩坑部分我会详细说这个。
写请求代码时,一个必不可少的细节是请求头。京东对没有User-Agent的请求会直接拒绝。我用的请求头模板是这样:
import requests import json import time def fetch_comments(product_id, page, page_size=10): url = "https://api.m.jd.com/?appid=item-v3&functionId=pc_club_productPageComments" params = { "client": "pchome", "body": json.dumps({ "productId": str(product_id), "page": page, "pageSize": page_size, "sortType": 5 }, ensure_ascii=False) } 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" ), "Referer": f"https://item.jd.com/{product_id}.html", "Accept": "application/json, text/plain, */*" } resp = requests.get(url, params=params, headers=headers, timeout=10) resp.raise_for_status() return resp.json()注意拼接body时用json.dumps,不要手写字符串。手写很容易把引号和空格搞错,接口一收到非法 JSON 就会返回空数据。
2.2 翻页逻辑与数据落盘
拿到首页数据之后,先读maxPage和maxCommodityPage,然后用一个循环把多页数据攒下来。我的习惯是每取到一页就往硬盘上保存一份原始 JSON,而不是先全部攒在内存里。原因很简单:一旦中途网络抖动、被限流,或者程序异常退出,至少前面抓到的数据都还在,重跑时直接跳过已有页码就行。
def crawl_comments(product_id, max_pages=100, page_size=50): all_comments = [] for page in range(max_pages): try: data = fetch_comments(product_id, page, page_size) except Exception as e: print(f"[第{page}页] 请求失败: {e}") time.sleep(5) continue comments = data.get("comments", []) if not comments: break all_comments.extend(comments) # 原始数据落盘,便于后期回溯 with open(f"raw_pages/{product_id}_page_{page}.json", "w", encoding="utf-8") as f: json.dump(comments, f, ensure_ascii=False, indent=2) # 控制频率,避免因请求过快触发风控 time.sleep(shift_sleep(page)) if page + 1 >= data.get("maxPage", 0): break return all_comments def shift_sleep(page): # 前 20 页用 1~2 秒,之后逐步拉长到 3~4 秒 base = 1.0 + (page % 5) * 0.4 if page > 20: base += (page - 20) * 0.05 return min(base, 4)这里用了一个分段限速的策略,简单说就是“每翻几页就多停一点”。电商数据接口普遍有频率限制,短时间内狂暴请求很容易触发验证码或者直接返回空列表。老实说,做个人学习项目没必要把采集速度拉满,把效率降一半,把稳定性升三倍,很划算。
2.3 合规与自我保护:采集环节的底线
很多教程不会提这一节,但我建议每个做爬虫项目的人都把它当作默认配置:
一是控制频率。上面代码里的time.sleep不是摆设,是保护双方的系统。你这边请求被限流,浪费的是时间;那边服务端压力大,影响的是其他正常用户。
二是只取公开数据,不做越权操作。评论区是公开可见的,采集公开数据用于个人学习和分析,问题不大。但不要把账号登录态、Cookie 等敏感信息带入脚本,也不要尝试绕过任何验证机制。
三是数据用途。做出来的图表、词云,用于自己决策参考或写成技术博客没问题;如果要商用或大范围传播,务必注意数据来源和合规要求。这属于基本常识,不展开说,但要记住。
3. 评论清洗与中文 NLP 预处理:从原始 JSON 碎片到干净词表
采集到的原始数据不能直接用。JS 转义、HTML 标签、全半角符号、空白字符混在一起,分词效果会非常差。这一阶段的目标是:把一条评论变成一个干净的中文句子,再切词、过滤、统计,最后得到一份可分析的词频表。
3.1 字段提取与基础清洗
先把所有页的 JSON 合并成一张表:
import pandas as pd def load_all_comments(crawl_dir): frames = [] for path in glob.glob(f"{crawl_dir}/*.json"): with open(path, "r", encoding="utf-8") as f: comments = json.load(f) rows = [] for c in comments: rows.append({ "id": c.get("id"), "content": c.get("content", ""), "creation_time": c.get("creationTime", ""), "score": c.get("score"), "nickname": c.get("nickname", ""), "product_color": c.get("productColor", ""), "product_size": c.get("productSize", ""), "after": c.get("afterUserComment", {}) }) frames.append(pd.DataFrame(rows)) df = pd.concat(frames, ignore_index=True) return df.drop_duplicates(subset=["id"])drop_duplicates(subset=["id"])很关键。评论翻页一多,接口偶尔会返回重复数据,去重在这里做掉,后面就不会污染统计结果。
正文清洗这一步,目标是去掉所有非文字内容。实测下来,一条评论里常见的干扰包括:HTML 实体("、&)、转义斜杠、京东用户之类的前缀、乱码字符、纯粹的表情符号。写一个clean_text()函数:
import re def clean_text(text): if not isinstance(text, str): return "" text = re.sub(r"&[a-zA-Z]+;", "", text) # HTML 实体 text = re.sub(r"\\u[0-9a-fA-F]{4}", "", text) # 转义 unicode text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:()¥%]", "", text) text = re.sub(r"\s+", "", text) return text.strip()这里保留中英文和数字,去掉大部分特殊符号。有一个取舍是:要不要保留“¥”和数字?如果要分析“价格敏感度”,保留数字有意义;如果想纯粹看产品词,那数字信息量较低。我的做法是默认去掉,毕竟后面统一用词频看产品特征,价格信号可以从评分分布侧面反映。
3.2 中文分词:停顿感比准确率更重要
jieba 分词在电商短文本上的表现,不算完美但足够用。真正影响效果的不是分词器本身,而是停用词表。
如果不做任何过滤,“京东、物流、快递、速度、东西、收到、不错”这类词会霸占词频榜首,完全淹没你真正关心的产品特征词。停用词表要自己攒,而且要根据商品品类迭代。我第一版跑词云时,满屏都是“京东”“物流”“很快”,这就是停用词没做好的典型症状。
停用词表的构建思路是三层叠加:
第一层是通用词:的、了、是、在、我、很、都、也、就、还。 第二层是电商评论专属词:京东、物流、快递、卖家、发货、收到、客服、包装、速度、正品。 第三层是品类词语,比如做数码产品时,可以保留“屏幕、续航、手感”这类体现产品属性的词,但去掉“东西、产品、这个”这类无信息量词。
import jieba stopwords = set() with open("stopwords.txt", "r", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) def segment(text): return [w for w in jieba.lcut(text) if w not in stopwords and w.strip() and len(w) > 1]len(w) > 1这一步是过滤单字词。单字词在词云里大多是噪声:“好”“很”“差”“没”单独出现,既看不出产品属性,又容易让词云视觉上变碎。做中文文本分析时,这个经验可以直接套用。
3.3 情感分析:粗粒度够用,别过度精确
情感分析用了 SnowNLP,原因是它对中文短句的默认模型已经比较稳,不需要额外训练数据。
from snownlp import SnowNLP def sentiment_score(text): if not text: return 0.5 try: return SnowNLP(text).sentiments except Exception: return 0.5sentiments返回 0~1 之间的分值,越接近 1 表示越正向。我做了一个简单的三分类映射:大于 0.6 算正向,小于 0.4 算负向,中间是中性。
实测下来,SnowNLP 对“物流快”“质量好”“值得买”这类句子判断比较准,但对反讽类评论(比如“真不错,一个月就坏了”)会判断偏正。这是因为它本质上是基于词袋的贝叶斯模型,不理解上下文转折。解决办法是把情感分析和差评关键词提取结合起来用:先筛出低分评论,再在低分评论里做词频统计,找真正的吐槽点。单独看情感得分有误差,但“低分评论+高频词”这个组合非常可靠。
4. 可视化呈现:用图表回答初期拆解的五个问题
数据处理完之后,就到了最有成就感的环节。这个阶段的原则是:一张图只能回答一个问题,别把图表塞成一锅炖菜。
4.1 评分分布条形图:一眼看穿两极分化
评分分布用的是 pyecharts 的 Bar。先把score映射成星数,再分组计数:
from pyecharts.charts import Bar from pyecharts import options as opts score_counts = df["score"].value_counts().sort_index() bar = ( Bar() .add_xaxis([f"{s}星" for s in score_counts.index]) .add_yaxis("评论数", score_counts.values.tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="商品评分分布"), yaxis_opts=opts.AxisOpts(name="条数"), ) ) bar.render("score_distribution.html")这张图能给到的洞察很直接:如果 5 星和 1 星都很多、中间很少,说明产品有鲜明的“两极口碑”,通常是优点突出但缺点也突出;如果 4 星和 5 星占绝对多数,说明主流用户是满意的;如果 1 星占比超过 10%,就要认真看差评内容了。
我在实际测试里遇到过“五星多但词云充满问题词”的情况。原因是有大量“好评返现”或刷单评论,内容空洞。遇到这种,把评分分布和高频词结合起来看,比任何单一指标都有效。
4.2 评论文量与时间折线:识别营销节点的水分
评论里带了creationTime字段,把时间精确到天聚合一下:
df["creation_time"] = pd.to_datetime(df["creation_time"]) daily_counts = df.groupby(df["creation_time"].dt.date).size() line = ( Line() .add_xaxis([str(d) for d in daily_counts.index]) .add_yaxis("评论数", daily_counts.values.tolist(), is_smooth=True) .set_global_opts( title_opts=opts.TitleOpts(title="评论数随时间分布"), xaxis_opts=opts.AxisOpts(type_="category", name="日期"), datazoom_opts=[opts.DataZoomOpts(range_start=0, range_end=100)], ) ) line.render("time_trend.html")注意,评论接口能返回的时间范围有限,通常只能抓到销量高峰之后的部分评论,所以这条线不代表全量历史,只代表“这个时间段内用户的评论节奏”。但它的价值在于识别异常峰值:如果某一天评论数量是平日的几十倍,大概率是当天的活动流量导入。这一天的评论里,“便宜”“性价比”“活动价”这类词会出现得很密集,分析产品口碑时要对该时段适当降权。
4.3 高频词词云:产品卖点与槽点一眼看清
词云是整个项目里视觉冲击力最强、也最容易出问题的部分。核心代码如下:
from wordcloud import WordCloud import matplotlib.pyplot as plt def build_wordcloud(word_freq, font_path="C:/Windows/Fonts/simhei.ttf"): wc = WordCloud( font_path=font_path, width=1000, height=700, background_color="white", max_words=150, collocations=False, ) wc.generate_from_frequencies(word_freq) return wccollocations=False一定要设置。默认情况下 wordcloud 会基于 n-gram 把“充电”“很快”拼成“充电很快”作为一个词组,这看起来合理,但会掩盖单个词的真实频次,尤其是对“快速”“续航”这种关键词的统计会失真。
另外,做词云的输入最好用“分词后的词频字典”,而不是直接传整段文本。直接传文本,wordcloud 内部会自己分词,英文效果还行,中文切得乱七八糟,几乎不可用。我都是在分词阶段就生成word_freq,这一步顺手且可控:
from collections import Counter word_freq = Counter() for content in df["content"]: word_freq.update(segment(clean_text(content))) wc = build_wordcloud(word_freq) plt.figure(figsize=(12, 8)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.savefig("wordcloud.png", dpi=200)生成词云后,我习惯同时保存一份“Top 50 词频表”CSV。原因很现实:词云适合视觉呈现,但具体到“什么词排第几”,还是表格更精确。
4.4 情感占比与差评关键词:从“感觉”到“结论”
情感分析结果可以用一个环形图展示:
from pyecharts.charts import Pie sentiment_label = ["正向", "中性", "负向"] sentiment_value = [ int((score > 0.6).sum()), int(((score >= 0.4) & (score <= 0.6)).sum()), int((score < 0.4).sum()), ] pie = ( Pie() .add("", [list(z) for z in zip(sentiment_label, sentiment_value)]) .set_global_opts(title_opts=opts.TitleOpts(title="评论情感倾向分布")) .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c} ({d}%)")) ) pie.render("sentiment_pie.html")比饼图更重要的是差评关键词。把sentiment_score < 0.4的评论单独拎出来,再跑一次词频统计,往往能得到非常扎心但又有价值的结论:比如一款手机,差评里“发热”“掉电”“卡顿”排前三;另一款耳机,差评里高频出现“降噪”“电流声”。这些词对购买决策的帮助,比任何官方宣传都大。
5. 实际跑项目时踩过的坑:完整排查链路
这部分是全文含金量最高的地方。所有问题都不是我凭空想象的,而是实际跑数据时一步步定位出来的,每个问题我都把排查思路写完整。
5.1 评论条数与翻页上限对不上
第一次跑某个商品时,接口返回的maxPage是 100,但循环跑到第 40 页左右就开始频繁返回空列表。第一次排查看的是page_size和maxPage的关系:评论总数远大于 1000,但maxPage上限就是 100,说明接口本身做了翻页深度限制,只允许访问最近的前若干条评论。
这个问题的应对思路不是硬翻,而是换排序方式。接口的sortType参数可以切换排序规则,常用值是 5(默认综合)、6(时间优先)、3(好评优先)、4(差评优先)。把sortType分别设为 6 和 4 各抓一轮,能拿到两个不同的样本集,按时间优先的适合做趋势,按差评优先的适合做吐槽点提取。同一个商品多跑几个排序字段,最后合并时按评论 ID 去重,数据量明显更完整。
5.2 “京东”刷屏词云,问题出在停用词而非分词
第一版词云生成后,图里几乎全是“京东”“物流”“很快”“好评”,真正的产品特征词少得可怜。一开始我以为是 jieba 分词有问题,把用户词典调了半天,结果没有任何改善。
后来把 Top 50 词频表拉出来逐一看了才明白:不是分词错,是这些噪声词在真实数据里的出现频次确实远高于产品特征词。京东平台用户天然会提到“京东物流快”,买文具的提到“京东发货”,这些词对平台很重要,但对商品口碑分析来说就是噪声。
解决办法就是前面提到的三层停用词表。把“京东”“物流”“快递”“发货”“好评”“正品”“速度”全部扔进停用词表,词云的可用性立刻提升了一个档次。这件事给我的经验是:做中文词频项目,花在停用词表上的时间永远值得。
5.3 wordcloud 中文乱码与“词团化”
乱码问题很好排查,就是字体缺失。指定font_path后立刻恢复。更隐蔽的是collocations默认值问题。我一开始没设置collocations=False,词云里大量出现“充电很快”“值得推荐”这类组合词,看起来像是很智能地提取了短语,实际上把单个关键词的排名挤下去了。关闭之后,“充电”“推荐”“很快”各归各位,整体统计口径才算统一。
5.4 接口偶发返回空数据,直接写死循环里
跑数据时偶尔会遇到一整页请求成功但comments为空,可能是瞬时数据为空,也可能是触发风控的早期信号。如果脚本这时候直接break,就会提前结束采集,导致数据量不够。
我改造后的逻辑是:连续出现 3 次空页才终止循环;单个空页先 sleep 3 秒重试一次。核心逻辑如下:
empty_streak = 0 for page in range(max_pages): data = fetch_comments(product_id, page, page_size) if not data.get("comments"): empty_streak += 1 if empty_streak >= 3: break time.sleep(3) continue empty_streak = 0 # 正常处理这个“连续 N 次才算结束”的策略,在低质量网络环境里能有效避免因为网络抖动导致的采集中断,很值得在类似项目里推广。
5.5 重复数据的隐蔽来源:追评与多规格商品
去重如果只按content去重是不可靠的。同一段话用户可以复制粘贴,不同用户也可以写出完全相同的内容。真正可靠的做主键是评论 IDid。但如果采集了多个规格(颜色、版本)的评论,同一个评论会带着不同的productColor出现在多个数据里,ID 不同、内容相同。这种情况我选择保留第一条,并在最终统计时按 ID 去重后再按内容二次去重。
6. 让这套代码不止跑一次:简易评论监控的扩展方向
文章写到这里,项目本身已经完整跑通。但实际上,把这套代码改造成一个“可复用、可扩展、可长期运行”的监控工具,才是真正让它产生持续价值的地方。
6.1 定时任务与增量采集
评论数据每天都会有新增。如果只跑一次,那一天的快照,看个热闹没问题,但做不了趋势分析。我后续的做法是加一个定时任务,每天抓一次当天的增量评论,只保留creation_time在昨天到今天之间的记录,追加到同一个 CSV 里。Linux 上用 cron,Windows 上用任务计划程序,都只需要一行配置。
这里要提醒的是,定时任务跑久了,数据文件会越来越大,建议每天跑完自动做一个归档,按月份拆文件,不要一个 CSV 堆到几十万行才想起来整理。
6.2 多商品横向对比:同一套代码换个 ID 就能跑
我把所有抓取逻辑封装成了ProductReviewAnalyzer类,输入是商品 ID,输出是分析报告目录。这样对比多款商品时,只需要循环遍历一串 ID,就能分别生成各自的词云、图表和 CSV 报告。这个设计对做竞品分析特别有用——几个同类竞品的差评关键词一对比,哪个品控更稳,一目了然。
6.3 输出带图表的 HTML 报告
pyecharts 渲染出来的是独立 HTML,多张图非常难管理。我的做法是先用 pyecharts 生成每张图的 HTML 或保存为图片,再用一个简单的 HTML 模板把它们拼到一个页面里。标题、商品名、采集时间、图表、结论性文字都放进去。最终交付给别人的,就是一个自带导航的报告文件,不用打开一堆窗口。
最后,两件实操中反复验证过的小事
一个是停用词表别指望一次建完。每次跑完新品类商品,把词云里新出现的低信息量词加进表里,这个表会随着你的项目越用越值钱。我现在的停用词表已经从最初 80 多词涨到了 400 多词,覆盖了数码、日用、食品、图书多个品类。整个过程不着急,边跑边加就很自然。
另一个是原始 JSON 数据别急着删。因为分析思路会迭代,你今天觉得没用的字段,明天可能就有用了。比如第一次写文章时我没存productColor,后来想按颜色粒度分析差评时,还得重新抓一遍。吃一堑长一智,现在原始数据我总是留一份,分析和源数据从物理上就分开。
这套基于 Python 电商评论分析可视化的流程,从爬虫到词云的全部代码量其实不超过四百行。真正花时间的不是写代码,是理解数据里的噪声和信号。看完这篇文章,建议找一个你真正感兴趣的商品试一次——只有数据带上你关心的真实问题,整个链路的知识才会真正沉淀成你自己的能力。