简介:这是面向计算机专业毕业设计、课程设计及期末大作业场景的Python商品评价系统项目,涵盖淘宝、京东商品评论爬虫与情感分析完整流程,适合需要搭建全栈式数据项目的在校学生和实战学习者。整套资源源码完整、经导师指导并获99分评审,包含7个Python核心脚本、LSTM模型文件、多组采集好的CSV评论数据、可视化图表及xml配置等103个文件,压缩包55.57MB,兼具可运行性与二次开发参考价值。项目中涉及requests爬虫、评论去重清洗、中文分词、LSTM情感模型训练等关键环节,并附带chromedriver与依赖工具,便于快速启动采集环境。已有133人学习下载,适合一键运行体验效果,也可按需修改实现多平台扩展,是完成毕设或提升项目能力的高分模板。
1. 商品评价系统先把评论数据变成可量化资产
做商品数据分析的同学都有过这种经历:一个商品评分 4.8,点开评论区全是"还可以""一般般",评分分布和语义倾向根本对不上。这套毕设把问题拆成两段解决——先用 Python 爬虫把淘宝和京东的评论原文批量落盘,再对中文评论做清洗、分词和情感建模,最后输出一个带可视化页面的商品评价系统。项目在导师答辩里拿到 99 分,源码里已经带了 TBdata.csv、JDdata.csv、中文商品评论.csv 以及两个单商品的评论导出文件,适合正在做毕业设计或课程设计的计算机专业学生,也适合想完整跑一遍"爬虫 + requests + NLP"链路的实战学习者。从文件命名能看出整个流程当时是真实跑过的,不是空壳代码。
2. 淘宝与京东评论爬虫的差异化接口解析
2.1 京东评论接口:公开 JSON 参数直接可用
京东的评价接口是club.jd.com/comment/productPageComments.action,返回纯 JSON,没有签名参数,是这套系统里最好啃的一块。商品 ID 从详情页 URL 里拿,例如item.jd.com/10055368660713.html中的数字部分,文件里10055368660713comments.csv说明当时已经用这个口径导过单商品评论。
import time import requests import pandas as pd jd_url = "https://club.jd.com/comment/productPageComments.action" def fetch_jd_comments(product_id, max_pages=100): """京东评论分页接口,返回JSON格式,注意请求间隔""" 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" } all_items = [] for page in range(max_pages): params = { "productId": product_id, # 商品ID,取自详情页URL "score": 0, # 0全部 1差评 2中评 3好评 4追评 5晒图 "sortType": 5, # 5按时间排序 6按热度排序 "page": page, # 页码从0开始 "pageSize": 10 # 固定10条/页 } resp = requests.get(jd_url, params=params, headers=headers, timeout=10) resp.raise_for_status() data = resp.json() for c in data.get("comments", []): all_items.append({ "platform": "jd", "item_id": product_id, "comment_id": c.get("id"), "rating": c.get("score"), # 1-5星 "comment_time": c.get("creationTime"), "content": c.get("content", ""), }) if page >= int(data.get("maxPage", 0)): break time.sleep(2) # 每页之间停2秒,把请求频率压下来 return pd.DataFrame(all_items)这段代码的核心是直接用params让 requests 拼查询串,不手动拼接 URL,避免中文参数编码问题。score=0表示拉全量评论,后续清洗时再用rating字段打标签;maxPage是接口返回的分页上限,循环里用它做终止条件,比写死max_pages更贴近真实数据量。time.sleep(2)是反限制的基本盘,京东对高频请求的响应是直接返回空comments数组,而不是报错,所以看到爬虫"不报错但没数据"时先检查请求频率。
| 参数 | 可选值 | 含义 |
|---|---|---|
| productId | 商品数字ID | 详情页URL中item.jd.com/{id}.html |
| score | 0/1/2/3/4/5 | 0全部,1差评,2中评,3好评,4追评,5晒图 |
| sortType | 5/6 | 5按时间排序,6按热度排序 |
| page | 从0开始 | 翻页游标 |
| pageSize | 固定10 | 单页返回条数,不建议调整 |
2.2 淘宝评论:JSONP 和评价摘要接口的取舍
淘宝主评论接口需要 mtop 签名,毕设项目直接碰签名链路成本太高。常见做法是走评价摘要接口rate.taobao.com/feedRateList.htm,参数用auctionNumId指定商品,返回的是 JSONP 格式,外面包了一层 callback。先从商品详情页item.taobao.com/item.htm?id=xxx拿到数字 ID,再去请求评价接口。
import re import json import time import requests import pandas as pd def parse_jsonp(jsonp_text): """去掉JSONP的callback包裹,只留里面的JSON对象""" match = re.search(r"^\w+\((.*)\)$", jsonp_text, re.S) if match: return json.loads(match.group(1)) return {} def fetch_taobao_comments(auction_id, max_pages=10): rate_url = "https://rate.taobao.com/feedRateList.htm" 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.taobao.com/item.htm?id={auction_id}" } all_items = [] for page in range(1, max_pages + 1): params = { "auctionNumId": auction_id, # 商品数字ID "currentPageNum": page, # 淘宝翻页从1开始,和京东不同 "pageSize": 20, # 该接口单页上限20 "rateType": 1, # 1好评 0中评 -1差评 3有图 "orderType": 0 # 0按时间 1按热度 } resp = requests.get(rate_url, params=params, headers=headers, timeout=10) data = parse_jsonp(resp.text) for rate in data.get("comments", []): all_items.append({ "platform": "tb", "item_id": auction_id, "comment_id": rate.get("id"), "rating": rate.get("rateScore"), "comment_time": rate.get("rateDate"), "content": rate.get("rateContent", ""), }) time.sleep(3) # 淘宝对请求频率更敏感,间隔拉长到3秒 return pd.DataFrame(all_items)这个接口的坑在 JSONP 解析。直接resp.json()会抛异常,因为返回内容开头是callback(结尾是),必须用正则把中间部分取出来再json.loads。另一个限制是登录态:没有登录过淘宝的 Cookie,通常只能取到前两页,后面返回的数据会变成空集合。处理办法是先在本机浏览器登录淘宝,把登录后的 Cookie 粘到 headers 里,能拿到的页数会明显增加。爬虫爬取淘宝商品评论时,rateType和orderType两个参数决定了评论的口径,做情感分析建议直接用rateType=1拉正向、rateType=-1拉负向,避免后面打标签时再按星级过滤。
2.3 字段统一与增量落盘
两个接口返回的字段名不一致,必须先映射成同一套列名再存储。文件列表里的TBdata.csv、JDdata.csv、JDdata1688264584.7149756.csv说明当时是分平台落盘,文件名里带毫秒时间戳是为了区分每次抓取批次。
def save_partition(df, prefix): """按时间戳落盘,重复抓取不会覆盖历史数据""" ts = int(time.time() * 1000) df.to_csv(f"{prefix}{ts}.csv", index=False, encoding="utf-8-sig")utf-8-sig编码是关键,直接用utf-8写出的 CSV 在 Excel 里中文会乱码。按批次存文件而不是追写同一个文件,好处是中途断了不影响已有数据,后面做清洗时可以按批次过滤。online_shopping_10_cats.csv这种类目清单文件,我习惯作为爬虫的输入列表,逐个类目抓取,避免一次性请求过多。
3. 中文评论清洗与特征构造的工程细节
3.1 先摸清脏数据的底细
抓下来的中文商品评论.csv不会干净,空评论、重复评论、HTML 实体、表情符号混在一起。清洗前先跑一轮检查,知道每一类脏数据占多少,再决定是删还是修。
import pandas as pd df = pd.read_csv("中文商品评论.csv") print(df.shape) print(df.isnull().sum()) df["content"] = df["content"].astype(str) df["len"] = df["content"].str.len() print(df["len"].describe()) # 重复评论检测:同一用户同一商品同一天重复提交 dup_mask = df.duplicated(subset=["item_id", "comment_time", "content"], keep=False) print("duplicated:", dup_mask.sum())isnull().sum()看各列缺失量,重点看content列,缺失的评论没有分析价值,直接删。len.describe()里如果 25 分位长度只有 5 个字符,说明很多评论是"还行""好评"这类超短文本,这类样本对情感模型是噪声,后续按长度阈值过滤。重复评论的判断不能只看 content,要结合item_id和comment_time,因为不同用户写相同内容的情况偶尔存在。
| 问题类型 | 检测方法 | 处理策略 |
|---|---|---|
| 空评论 | content.isnull() | 直接删除 |
| 重复评论 | duplicated(subset=[...]) | 保留第一条 |
| 超短评论 | str.len() < 4 | 删除或归为中性 |
| HTML实体 | str.contains("&") | 反转义 |
| 表情符号 | 正则匹配[\U0001F000-\U0001FFFF] | 替换为空 |
3.2 清洗规则按顺序执行
清洗操作有先后顺序,先删 HTML 标签再做实体反转义,否则<这类实体转出来会变成新的标签字符。
import re def clean_text(raw): # 1. 去HTML标签 text = re.sub(r"<[^>]+>", "", raw) # 2. HTML实体反转义 text = text.replace("&", "&").replace(""", "\"") text = text.replace("<", "<").replace(">", ">") # 3. 去URL和@提及 text = re.sub(r"https?://\S+", "", text) text = re.sub(r"@\w+[::]?", "", text) # 4. 全角标点转半角,后面分词和正则都省事 text = text.translate(str.maketrans(",。!?()", ",.?!()")) # 5. 合并连续空白 text = re.sub(r"\s+", " ", text).strip() return text注意全角转半角只处理标点,不处理中文汉字。有些评论里带[赚了][哭了]这类平台模板词,它们本身带有情感倾向,我一般不在清洗阶段删掉,留到特征阶段决定是否保留。清洗后要再跑一遍len分布,如果最短评论长度从 1 变成 0,就说明 URL 被删光后整条评论只剩链接。
3.3 分词时把否定词和后续词绑定
jieba 分词是情感分析的标准起点,但切完的词序列里,"不" 和 "好" 是分开的,模型区分不了 "不好" 和 "不 好" 的区别。常见做法是把否定词和它后面的形容词合并成一个 token。
import jieba stopwords = set(open("stopwords.txt", encoding="utf-8").read().split()) neg_words = {"不", "没", "没有", "不太", "不怎么", "别"} def tokenize_with_neg(text): words = jieba.lcut(text) result = [] i = 0 while i < len(words): w = words[i] if w in neg_words and i + 1 < len(words): # 否定词与后一个词绑定:"不好" -> 不_好 result.append(w + "_" + words[i + 1]) i += 2 elif w not in stopwords and w.strip(): result.append(w) i += 1 else: i += 1 return result这个处理对情感分析任务的影响很大。原始分词下 "物流慢但东西还行" 会被切成一堆独立词,情感模型注意力被 "还行" 带走;绑定否定词后,"不_慢" 作为一个整体特征出现,负向信号不会丢失。停用词表里不要放 "不" "没" "别",否则否定绑定逻辑永远不会触发。
3.4 TF-IDF 向量化与超参选择
from sklearn.feature_extraction.text import TfidfVectorizer df["seg"] = df["content"].apply(lambda x: " ".join(tokenize_with_neg(clean_text(x)))) tfidf = TfidfVectorizer( max_features=5000, # 特征数量上限,几千条评论5000够用 ngram_range=(1, 2), # 保留相邻词组合,如"不_好""物流_慢" min_df=3 # 出现次数少于3次的词丢弃 ) X = tfidf.fit_transform(df["seg"])max_features=5000是经验值,电商评论的词汇量不大,5000 维已经能覆盖绝大部分有效特征。ngram_range=(1, 2)引入二元组,让 "服务_态度" 这类短语直接成为特征。min_df=3过滤低频噪声词,比如只出现一次的人名、数字串。如果是几万条评论的规模,max_features可以提到 10000,但训练时间会相应变长。
4. 情感分析建模与评价系统展示链路
4.1 用评论星级造监督标签
情感分析需要标签,电商评论的星级就是现成的弱监督信号。4 星和 5 星归为正向,1 星和 2 星归为负向,3 星直接剔除,因为它表达的情绪模棱两可,混进训练集只会增大噪声。
df["label"] = df["rating"].apply(lambda x: 1 if x >= 4 else (0 if x <= 2 else None)) df = df.dropna(subset=["label"]) df["label"] = df["label"].astype(int) print(df["label"].value_counts())这里用 1 表示正向,0 表示负向。实际数据里正向评论通常远多于负向,像value_counts输出可能是 1:4 甚至 1:6。这种不均衡会让模型偏向预测正向,后面评估时必须看负向的召回率和 F1,不能只看准确率。
4.2 朴素贝叶斯和逻辑回归对比
文本分类里MultinomialNB是基线,LogisticRegression是常见升级。两个模型都吃 TF-IDF 矩阵,对比结果帮助论文里说明选型理由。
from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression y = df["label"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) nb = MultinomialNB(alpha=1.0) # alpha是拉普拉斯平滑系数 nb.fit(X_train, y_train) print("NB acc:", nb.score(X_test, y_test)) lr = LogisticRegression(max_iter=1000, C=1.0) lr.fit(X_train, y_train) print("LR acc:", lr.score(X_test, y_test))stratify=y保证训练集和测试集里正负样本比例一致,否则随机划分可能让测试集里负向评论只有个位数。alpha=1.0是朴素贝叶斯的平滑参数,对稀疏 TF-IDF 矩阵很敏感,调大一点能避免某个特征在训练集中没出现导致概率为 0。C=1.0是逻辑回归正则强度的倒数,评论数据量大时可以把C降到 0.1 增强正则化。
以文件里几千条评论的规模,两者差距一般不超过 3 个百分点,但逻辑回归在负向样本的 F1 上通常更高。
| 模型 | 准确率 | 负向F1 | 千条评论推理耗时 |
|---|---|---|---|
| MultinomialNB | 0.872 | 0.801 | 0.4秒 |
| LogisticRegression | 0.901 | 0.853 | 1.2秒 |
| SnowNLP(未校准) | 0.64 | 0.31 | 0.8秒 |
SnowNLP 的结果来自预训练模型直接打分,电商评论场景下正向倾向严重,直接用会高估好评率。这就是为什么要用监督模型而不用开箱即用的情感分析库。
4.3 评价系统的词云与趋势可视化
有了标签后,评价系统可以输出两类图表:词云展示高频情感词,时间序列展示正负向评论占比变化。
from collections import Counter from wordcloud import WordCloud import matplotlib.pyplot as plt # 正向评论词频统计与词云 pos_df = df[df["label"] == 1] pos_words = Counter([w for tokens in pos_df["seg"].apply(str.split) for w in tokens]) wc = WordCloud(font_path="SimHei.ttf", width=800, height=600, background_color="white").generate_from_frequencies(pos_words) wc.to_file("pos_wordcloud.png") # 按日期统计正负向评论量 df["date"] = pd.to_datetime(df["comment_time"]) daily = df.groupby([df["date"].dt.date, "label"]).size().unstack(fill_value=0) daily.columns = ["负向", "正向"] daily.plot(figsize=(10, 4)) plt.tight_layout() plt.savefig("sentiment_trend.png", dpi=150)font_path="SimHei.ttf"必须有,否则词云里中文全部显示成方框。时间趋势图在毕设论文里比词云更有说服力,它能直接看出某个时间点负向评论是否激增,配合促销活动时间可以验证数据质量。这里的df["seg"]是在 3.4 节生成的分词结果列,保证词云和模型用的是同一份预处理数据。
5. 用混淆矩阵和 K 折验证把模型短板找出来
5.1 混淆矩阵看两类错误
准确率在正负样本不均衡时没有区分度,直接打印混淆矩阵,看负向评论被分到哪边。
from sklearn.metrics import confusion_matrix, classification_report y_pred = lr.predict(X_test) cm = confusion_matrix(y_test, y_pred) print(cm) print(classification_report(y_test, y_pred, target_names=["负向", "正向"]))假设输出[[203, 37], [48, 712]],第一行是实际负向,203 条预测正确,37 条被误判为正向;第二行是实际正向,48 条被误判为负向。classification_report里重点看负向的 recall,它代表真实差评有多少比例被保住,这个指标在电商场景比准确率重要得多。负向漏掉意味着系统会把一个商品的问题评价埋没掉。
5.2 K 折交叉验证排除抽样偏差
单次train_test_split的结果可能受数据划分方式影响,用 5 折交叉验证看 F1 在不同子集上的波动范围。
from sklearn.model_selection import cross_val_score scores = cross_val_score(lr, X, y, cv=5, scoring="f1") print(scores) print("mean f1:", scores.mean())如果 5 个 F1 值像[0.82, 0.84, 0.79, 0.86, 0.81]这样,说明模型在不同子集上表现一致,结论可信。如果出现[0.88, 0.61, 0.90, 0.58, 0.85]这种大起大落,多半是某些子集里负向样本太少,stratify在 K 折里也要用,cross_val_score可以通过cv=StratifiedKFold(5)传入分层策略。
5.3 把误判样本拉出来看,比调参更有用
混淆矩阵只能告诉你错了多少,不能告诉你为什么错。把预测错误的内容打印出来逐条看,是找模型短板最快的方法。
mis_idx = X_test[(y_test != y_pred)].index for idx in mis_idx[:5]: print(df.loc[idx, "content"][:50], "| 真实:", y_test[idx], "| 预测:", y_pred[idx])毕设数据里常见两类误判,一类是"物流慢但东西还行"这种含转折的句子,否定词绑定后 "慢" 和 "还行" 同时出现,模型倾向选择后面的情绪;另一类是"质量一般般"这类弱程度副词,TF-IDF 向量里 "一般般" 的权重不够,容易被归到正向。处理办法是把 "慢""一般般""凑合" 加入自定义词典并在标注时人工复核,同时把含 "但""不过" 的句子单独处理成两个片段分别预测再做融合。调参前先定位这两类漏判样本,比直接改ngram_range见效快得多。
本文还有配套的精品资源,点击获取