做电商数据分析的朋友应该都有体会,商品评论区就是个金矿。这个基于Python的京东评论数据分析可视化项目,目标很直接:把某件商品的几万条用户评论爬下来,清洗成干净的结构化数据,做分词、词频统计、情感分析,最后用pyecharts生成一眼能看懂的图表。整个过程涉及爬虫、数据处理、文本挖掘、可视化四条线,对于想独立完成一个完整数据项目的Python入门者来说是非常合适的练手题;对于电商运营或者产品经理来说,这套流程做完,能直接从评论里看出商品的真实口碑、用户痛点,以及好评和差评分别集中在哪些维度。
我最初做这个项目,是因为需要评估一款竞品的用户反馈,人工翻评论翻到崩溃。后来发现把评论采集和分析流程化以后,一个小时就能处理完过去一周都看不完的数据。这篇文章把完整方案拆开来讲,从接口分析、爬虫编写、数据清洗,到情感分析、可视化呈现,每一步都会给出可运行的代码和实际踩坑记录,希望能帮你少走一些弯路。
1. 项目整体拆解:先做对方案,再写代码
做这类项目最容易犯的错误是一上来就写爬虫,结果爬到一半发现字段对不上、接口改了、数据没法清洗。所以先把方案设计想清楚,后面会顺利很多。
1.1 这个项目到底在解决什么问题
商品评论数据本质上是一批非结构化的短文本。区别于销量、价格这类数值数据,评论文本里藏着用户的真实使用感受:散热好不好、续航行不行、尺码准不准、客服态度怎么样,这些信息在商品参数页里根本看不到。
把评论数据分析可视化以后,能解决几个实际场景的问题:
- 选品调研:分析某类目下多个商品的评论,能快速知道哪个竞品的负面槽点最少,哪个卖点被提及最多。
- 产品迭代:找出高频负面词,集中解决用户吐槽最多的几个问题,比闭门造车改产品有效得多。
- 口碑监控:按时间维度看评论情感变化趋势,能发现商品是不是在某次版本更新后口碑变差了。
- 内容创作:做商品评测、带货文案之前,用词云和情感占比快速了解用户关注点,写出来的内容更有针对性。
这些场景有一个共同需求:把大量非结构化文本变成结构化的量化指标。这就是这个项目的核心价值所在。
1.2 技术选型解析:为什么是requests + jieba + pyecharts
Python做这类项目的优势在于生态完备,几乎每个环节都有成熟的第三方库,不需要重复造轮子。我的选型如下:
| 环节 | 工具 | 选型理由 |
|---|---|---|
| 数据采集 | requests | 评论接口返回的是结构化JSON,不需要用浏览器渲染,requests足够稳 |
| 接口数据解析 | json + re | 接口带JSONP回调包裹,需要用正则剥离多余部分 |
| 数据清洗处理 | pandas | 处理表格型数据、去重、时间转换、分组统计非常高效 |
| 中文分词 | jieba | 中文分词的事实标准,支持自定义词典,简单易用 |
| 情感分析 | SnowNLP | 开箱即用,基于电商评论场景训练,对入门项目足够 |
| 可视化 | pyecharts | 交互式图表,输出HTML,方便分享和汇报 |
这几个库的组合非常经典,和用Java或Go做相比,Python的方案开发速度快,数据分析和可视化链路最短。特别是pandas和pyecharts配合,中间几乎不用写胶水代码。
1.3 整体流程设计:一条数据从采集到出图要走几步
整个项目的链路可以划分为五个环节:
- 采集层:请求评论接口,翻页拿到原始JSON数据
- 存储层:把评论ID、内容、时间、评分、点赞数等字段整理成CSV文件
- 清洗层:去重、去空、过滤无意义内容、处理时间格式
- 分析层:中文分词、词频统计、情感打分、按评分分组聚合
- 展示层:用pyecharts生成评分分布、时间趋势、词云、情感占比图
关键点在于:清洗和分析的质量直接决定可视化结果有没有价值。如果爬下来一万条数据,里面有三千条“此用户未填写评价内容”,不做处理直接词云,出来的图全是废话。
2. 评论数据采集:先找接口再写爬虫,效率翻倍
爬虫部分不是技术难点,难点在于找到正确的数据接口并处理细节。我第一次做的时候傻乎乎去解析整个商品页面的HTML,又慢又容易失效。后来打开开发者工具看了一下网络请求,发现评论区数据是单独通过异步接口加载的,直接请求JSON就好了。
2.1 找到评论数据的真实接口,别去解析HTML
打开京东商品页,按下F12进入开发者工具,切到Network面板,筛选XHR请求,往下翻评论区,能看到一个名为productPageComments.action的请求,这就是评论数据的来源。
接口地址大致长这样:
https://club.jd.com/comment/productPageComments.action?productId=100012043978&score=0&sortType=5&page=0&pageSize=10&isShadowSku=0&fold=1核心参数含义如下:
| 参数 | 说明 | 常见取值 |
|---|---|---|
| productId | 商品ID,打开商品详情页URL中能看到 | 如 100012043978 |
| score | 评分筛选 | 0全部、1差评、2中评、3好评、4追评 |
| sortType | 排序方式 | 5推荐排序、6时间排序 |
| page | 页码 | 从0开始 |
| pageSize | 每页条数 | 实测填10最稳,填太大可能拿不到完整数据 |
| isShadowSku | 是否获取SKU维度评论 | 固定0即可 |
这里有个容易踩坑的点:接口返回的不是纯JSON,而是带JSONP回调包裹的内容,形如fetchJSON_comment98({...})。用requests拿到的text是fetchJSON_comment98(...)这样一串,不能直接用response.json()解析,需要先把外层包裹剥离。
2.2 写一个稳定可断点续爬的采集脚本
下面是一个可直接运行的采集脚本框架,注释里写了关键细节:
import requests import json import re import csv import time import random def fetch_comments(product_id, page): url = "https://club.jd.com/comment/productPageComments.action" params = { "productId": product_id, "score": 0, # 全部评价 "sortType": 5, # 推荐排序 "page": page, # 页码从0开始 "pageSize": 10, "isShadowSku": 0, "fold": 1 } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": f"https://item.jd.com/{product_id}.html" } resp = requests.get(url, params=params, headers=headers, timeout=10) # JSONP格式: fetchJSON_comment98({...}) raw_text = resp.text json_text = re.search(r'\((.*)\)', raw_text, re.S).group(1) data = json.loads(json_text) return data.get("comments", []) def collect_all(product_id, max_pages=100): all_comments = [] for page in range(max_pages): try: comments = fetch_comments(product_id, page) if not comments: print(f"第{page}页无数据,停止采集") break for item in comments: all_comments.append({ "id": item.get("id"), "content": item.get("content", "").strip(), "score": item.get("score"), "time": item.get("creationTime"), "nickname": item.get("nickname", ""), "productSize": item.get("productSize", ""), "usefulVoteCount": item.get("usefulVoteCount", 0) }) print(f"第{page}页采集完成,累计{len(all_comments)}条") # 随机延时,模拟人工操作节奏 time.sleep(random.uniform(2, 5)) except Exception as e: print(f"第{page}页出错: {e}") time.sleep(10) continue return all_comments if __name__ == "__main__": product_id = "100012043978" # 替换为目标商品ID rows = collect_all(product_id, max_pages=50) with open("jd_comments.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows) print(f"共采集{len(rows)}条评论")几个细节说明:
- 存储CSV时用
utf-8-sig编码而不是utf-8,这样可以避免Excel打开时中文乱码。 Referer一定要设置,否则评论接口可能返回空数据。这个问题排查了很久才意识到。- 延时用
random.uniform(2, 5),避免请求间隔完全固定,太规律的请求容易被识别。虽然评论接口的反爬不算严格,但基本的谨慎还是要有。
2.3 反爬避坑:频率、请求头、时间窗口
京东评论接口整体上对爬虫不算苛刻,但有一些细节必须注意:
第一,pageSize不要填大。很多新手觉得每页填100一次性拉完效率高,实测填20经常只返回几条,填100甚至直接不返回数据。老老实实pageSize=10翻页,稳定比效率重要。
第二,请求频率一定要控制。我试过不延时连续请求,大概七八页之后,返回的JSON里就直接没有comments字段了,整个页面像是被临时限流。后来改成2到5秒随机延时,单线程跑几百页都没有出问题。
第三,注意评论只能爬到一定深度。京东不会把所有历史评论一次性开放到底,热门商品翻到几百页之后,接口会持续返回空列表。这不是你的代码有问题,是平台侧的访问策略限制,开头的一两千条数据已经足够做分析了,不用纠结全量。
第四,尽量选择非促销时段跑脚本。大促期间接口压力大,对这个评论接口的访问会更敏感,平时爬得好好的脚本,活动期间可能连续报错。
3. 数据清洗与分词:分析质量好不好,全看这一步
很多教程把大量篇幅放在爬虫上,对数据清洗一笔带过。但实际做完一遍以后你会发现,清洗才是决定分析结论有没有价值的关键环节。爬下来的原始数据里,垃圾文本和有效信息的比例经常能到四六开。
3.1 去重、时间转换、字段规整
第一步处理的是数据规整,主要做四件事:
import pandas as pd df = pd.read_csv("jd_comments.csv") # 1. 去重:按评论ID去重,防止翻页过程中数据重复 df = df.drop_duplicates(subset=["id"]) # 2. 去空:评论内容为空白的直接删除 df = df[df["content"].notna()] df = df[df["content"].str.strip() != ""] # 3. 时间字段转为标准格式 df["time"] = pd.to_datetime(df["time"]) # 4. 评分字段转为整数,便于后续分组 df["score"] = df["score"].astype(int) # 5. 过滤无意义文本 noise_keywords = ["此用户未填写评价内容", "默认好评", "好评", "收货后追加"] for kw in noise_keywords: df = df[~df["content"].str.contains(kw)]这一步做完,数据量可能少掉三成,不用心疼。这些被去掉的文本对分析没有贡献,留着反而会污染词频统计。
关于“好评”这个词要特别说明:直接按包含“好评”两个字符过滤会有风险,因为很多真实评论里会说“商品很好,值得好评”。稳妥做法是精确匹配“默认好评”或内容长度小于5的短评,而不是简单包含过滤。
3.2 文本清洗:把噪声字符一次处理干净
评论里经常混着HTML标签、表情符号、特殊字符和大量空白。不处理直接分词,词频统计里会出现一堆“rdquo”“nbsp”之类的无意义词条。
import re def clean_text(text): # 去除HTML标签 text = re.sub(r'<[^>]+>', '', str(text)) # 去除URL text = re.sub(r'https?://\S+', '', text) # 去除特殊符号和数字 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z\s]', '', text) # 合并多余空白 text = re.sub(r'\s+', ' ', text).strip() return text df["clean_content"] = df["content"].apply(clean_text)正则表达式里的[^\u4e00-\u9fa5a-zA-Z\s]是保留中文、英文字母和空白,其他的都替换掉。这个匹配范围对电商评价场景比较合适,因为中文短文本里,数字和特殊字符的信息量很低,提前去掉能明显提升词频统计的干净程度。
3.3 jieba分词与词频统计:让关键词自己浮出来
分词和词频统计是整个分析流程里最出效果的一步。用jieba做中文分词,结合自定义词表,能准确识别出“散热好”“续航强”“物流快”这类用户关心的关键词。
import jieba from collections import Counter # 加载自定义词典,提升专有名词识别准确率 # jieba.add_word("某型号手机") # jieba.add_word("散热表现") # 停用词表:这些词对分析没有价值,需要过滤 stopwords = set(["的", "了", "是", "在", "我", "有", "和", "就", "不", "都", "很", "也", "还", "这个", "那个", "东西", "商品", "感觉"]) def get_words(text): words = jieba.lcut(text) # 过滤停用词、单字、纯数字 filtered = [w for w in words if w not in stopwords and len(w.strip()) > 1] return filtered # 批量分词 df["words"] = df["clean_content"].apply(get_words) # 词频统计 word_counter = Counter() for words in df["words"]: word_counter.update(words) # 输出词频Top 50 for word, count in word_counter.most_common(50): print(f"{word}: {count}")实际做这个项目踩过两个坑:
第一个坑是品牌名和型号名会被错误拆开。比如“苹果手机”可能被分成“苹果”和“手机”,“某型号”可能被拆成两三个碎片。解决办法是给jieba添加自定义词表,在分词前把商品名、品牌名、型号名都加进去,能明显提高后续词频统计质量。
第二个坑是“东西”“商品”“京东”这类词频虚高。它们和商品真正的优缺点无关,但对词频排名影响很大。我最后是把这些词都加进了停用词表,或者用“词频>阈值且不在停用词表”双重条件来筛选。
4. 可视化呈现:用pyecharts把结论变成图表
可视化是整个项目最直观展示成果的环节。pyecharts生成的是交互式HTML图表,可以放大缩小、悬浮查看数据,比静态图片更适合放到汇报材料或者分享给团队其他人。
4.1 四个最值得做的可视化维度
做完大量评论数据分析以后,我总结出四个高频使用、信息密度高的可视化图表,建议优先做:
- 评分分布图:一眼看出商品整体口碑,是集中在4到5分,还是低分占比较大。
- 评论时间趋势折线图:能发现评论量在什么时间段激增,一般对应促销节点或负面事件爆发期。
- 高频词词云图:直观展示用户讨论最多的主题词,是“质量好”“物流快”还是“有异味”“售后差”。
- 情感占比图:把评论分成正向、中性、负向三类,量化整体口碑倾向。
这四个维度组合起来,基本回答了一个核心问题:这个商品被大家接受吗,如果不接受,主要问题出在哪。
4.2 pyecharts代码实操:柱状图、饼图、折线图、词云
先看评分分布柱状图,注意pyecharts 2.x版本的API写法:
from pyecharts import options as opts from pyecharts.charts import Bar, Pie, Line, WordCloud # 评分分布柱状图 score_counts = df["score"].value_counts().sort_index() bar = ( Bar() .add_xaxis([str(s) for s in score_counts.index]) .add_yaxis("评论数", score_counts.values.tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="商品评分分布"), xaxis_opts=opts.AxisOpts(name="评分"), yaxis_opts=opts.AxisOpts(name="评论数") ) ) bar.render("score_distribution.html")时间趋势折线图,需要先把数据按时间聚合:
# 按日期统计评论数量 df["date"] = df["time"].dt.date daily_counts = df.groupby("date").size().sort_index() line = ( Line() .add_xaxis([str(d) for d in daily_counts.index]) .add_yaxis("评论数", daily_counts.values.tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="评论数量时间趋势"), xaxis_opts=opts.AxisOpts(name="日期"), yaxis_opts=opts.AxisOpts(name="评论数") ) ) line.render("time_trend.html")情感占比饼图,这里需要先计算情感分类结果:
from snownlp import SnowNLP def sentiment_label(text): s = SnowNLP(str(text)) score = s.sentiments # 0到1之间,越接近1越正向 if score > 0.7: return "正向" elif score < 0.3: return "负向" else: return "中性" df["sentiment"] = df["clean_content"].apply(sentiment_label) sentiment_counts = df["sentiment"].value_counts() pie = ( Pie() .add("", [list(z) for z in zip(sentiment_counts.index, sentiment_counts.values)]) .set_global_opts(title_opts=opts.TitleOpts(title="评论情感倾向分布")) .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c} ({d}%)")) ) pie.render("sentiment_pie.html")词云图使用pyecharts自带的WordCloud组件:
# word_counter是前面词频统计得到的结果 word_data = [(word, count) for word, count in word_counter.most_common(100)] wc = ( WordCloud() .add("", word_data, word_size_range=[20, 100], shape="diamond") .set_global_opts(title_opts=opts.TitleOpts(title="评论高频词词云")) ) wc.render("wordcloud.html")pyecharts 2.x版本自带词云组件,不需要额外下载词云字体库,直接用就很方便。如果你的环境是1.x版本,部分API会有差异,建议直接安装最新版。
4.3 一张图表怎么读出有效结论
图做完以后,关键一步是解读。图表本身不会告诉你答案,但不同维度的组合能帮你建立完整的判断。
举个例子:某款手机评论分析中,评分分布图看起来是典型的好评型曲线,4到5分占比超过80%,词云里却出现了“发热”“耗电快”“卡顿”这几个高频负面词。这时候如果只看评分会误判商品口碑,但结合词云和情感分析,就能发现大部分好评集中在物流、外观、屏幕这些维度,而差评集中在性能体验上。
这就是组合分析的价值。单一图表很容易误导,多维度交叉验证,才能得出相对可靠的结论。
5. 常见问题排查:爬虫、编码、情感分析全踩过的坑
这个项目做完一遍,踩坑记录能写一长串。下面这几个是高频问题,整理成速查表供排查参考。
5.1 评论区接口请求异常
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 返回空数据,JSON里没有comments字段 | 缺少Referer请求头 | 在headers中加入商品详情页地址作为Referer |
| 爬几页后连续报错 | 请求频率过高触发限流 | 加大延时到5到8秒,暂停几分钟再继续 |
| 返回内容不是JSON格式 | 参数productId错误或商品无评价 | 检查商品ID,换个有评价的商品测试 |
| 翻页到某页后一直为空 | 接口只能访问最新若干页 | 设置max_pages上限,默认拿前几百页即可 |
最让人困惑的是第一类问题:请求能返回数据,但解析出来是空。我那时候反复检查参数和解析逻辑,最后发现就是缺少Referer。浏览器里正常访问是因为浏览器会自动带上来源页面的信息,用代码模拟请求时必须自己手动加上。
5.2 中文乱码与JSON解析失败
编码问题在中文爬虫项目里几乎避不开。京东评论接口本身返回UTF-8编码,正常情况下requests会自动处理,但有些环境下面还是会出现乱码。
处理思路是:第一步确认响应编码,用resp.encoding查看;第二步设置resp.encoding = "utf-8"强制指定。如果数据是从文本文件读取的,注意文件本身保存成UTF-8格式。CSV存储统一用utf-8-sig,这样Excel打开不乱码。
JSON解析失败的常见原因,是没处理JSONP外层包裹,直接用json.loads解析原始文本。处理办法是先用正则提取括号内的内容再解析:
import re import json raw_text = resp.text json_part = re.search(r'\((.*)\)', raw_text, re.S).group(1) data = json.loads(json_part)正则里的re.S参数容易漏掉,如果不加,遇到跨行的情况就匹配不到。
5.3 pyecharts图表渲染不出来
pyecharts常见的坑集中在版本差异上。1.x版本和2.x版本在导入路径和API写法上有明显区别。
拿图表初始化来说,1.x的写法是from pyecharts import Bar,2.x变成了from pyecharts.charts import Bar;配置项也从直接传参变成了add_global_opts()这种统一写法。如果按照旧教程写代码,很容易报错。
解决办法很简单:创建虚拟环境后,直接pip install pyecharts安装最新版,然后按2.x的API文档写。图表渲染出来的HTML文件,直接用浏览器打开即可查看,不需要起一台本地服务器。
另外一个容易忽略的点是:图表HTML文件里有大量的JS资源是从CDN加载的,如果处于网络受限环境,加载慢会导致图表显示空白。解决办法是下载HTML文件时确认网络通畅,或者在pycharts初始化时传入assets_host参数指定本地资源目录。
5.4 情感分析结果不可信
SnowNLP对电商评论的通用场景表现尚可,但经常出现“明明是好评,却判断成中性甚至负向”的情况。比如“外观惊艳,就是发货有点慢”这类带转折的句子,情感倾向其实是正向的,但模型可能算成中偏负。
提高准确率的思路有几个:
第一,结合评分做弱监督修正。简单来说,如果用户给了4分或5分,但SnowNLP算出很低的情感分,很大概率是误判,可以强制修正为正向。同理,1分到2分的高情感值评论也值得怀疑。
def correct_sentiment(row): score = row["score"] sentiment_value = row["sentiment_value"] if score >= 4 and sentiment_value < 0.4: return "正向" if score <= 2 and sentiment_value > 0.6: return "负向" if sentiment_value > 0.7: return "正向" if sentiment_value < 0.3: return "负向" return "中性"第二,针对细分领域做定向优化。SnowNLP的通用模型训练语料和数码3C、美妆、服装等品类的语言习惯有差异。如果项目面向某个固定品类,可以自己标注几百条数据,结合评分做二次训练或规则修正。
第三,优先级策略。对于争议样本,先看评分再结合关键词,比如评论里出现“垃圾”“受不了”“退款”,不管情感分多少,都直接归为负向。这种关键词规则和模型打分混合的方案,在工程上最实用。
最后分享一点实际体会
这个项目做完以后,最大的收获不是“我会爬虫了”,而是理解了数据处理流程里每一条链路之间的关系。爬虫只是把数据拿到手里,清洗决定了数据能不能用,分词决定了分析有没有颗粒度,可视化决定了结论能不能传达给别人,任何一个环节掉链子,整个项目都会卡住。
有一点我想特别提醒:做爬虫类项目一定要有边界意识。控制请求频率、不爬取非公开数据、只用于学习研究,这是基本功。评论数据虽然公开可访问,但大规模、高频率抓取对平台服务器会产生压力,练习时用少量数据验证流程就可以了。
如果后续想在这个项目上继续扩展,可以考虑几个方向:一是把分析对象从单个商品扩展到同品类的多个竞品,做对比分析;二是引入文本聚类,把评论文本按主题自动分组;三是把脚本封装成定时任务,每次促销活动后自动抓取一批新评论做口碑追踪。项目本身像一个数据管道的雏形,接上更多数据源和分析模块,就能变成一套实用的选品辅助工具。