ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

京东用户评论数据爬取、情感分析与可视化系统设计全解析

京东用户评论数据爬取、情感分析与可视化系统设计全解析

这个题目我太熟悉了。每年计算机毕设和课设里,“爬虫+数据分析+可视化”都是被选频率极高的组合,而“基于网络爬虫的京东用户评论分析及可视化设计”更是年年有人做。表面看,它只是爬一点评论、画几张图,但真正落地时,你会发现它把爬虫、数据清洗、中文文本处理、机器学习建模、Web开发五块内容全串起来了,每一块都藏着坑,也正因如此,它非常适合作为计算机类毕业设计的综合训练题。

这篇文章我按一个完整项目的思路来讲:先说明为什么选线性回归而不是深度学习,再按爬虫采集、数据存储、情感分析、建模回归、Django可视化的顺序逐层拆解,最后单独讲答辩时老师最容易追问的问题。不管你是准备毕业设计,还是想自己练手一个全流程数据项目,这份内容基本可以直接照着做。

1. 项目概述与核心需求拆解

1.1 评论分析要回答的三个问题

很多人拿到这个题目,第一反应是“把评论区爬到Excel里,再用ECharts画几个图表”。这么做不是不行,但答辩时老师一句话就能问住你:分析出了什么结论?所以动手前,先想清楚你这个系统到底要回答哪几个问题。

第一个问题是情绪倾向。这批评论里,用户整体是满意还是不满?这是最基础的情感分布统计。第二个问题是情绪与星级的一致性。我做过一批数据后发现,很多商品评分是4星,但评论内容里频繁出现“还行”“凑合用”这类偏中性甚至偏负面的词,说明星级和真实情感并不完全同步。第三个问题是影响因素。哪些特征在影响用户打分?是评论本身的正负面强度,还是是否追评、字数长短、点赞数?这三个问题里,前两个靠统计和可视化能回答,第三个就得靠线性回归模型。

我当初选这个题目,就是把“评论分析”理解成三步:采集数据、度量情绪、解释打分。用一句通俗的话来概括:评论区是消费者在发言,我们的任务是把发言翻译成可计算的度量,再建立度量与星级之间的函数关系。线性回归在这里不是用来做精确预测的,而是用来解释“什么因素让用户多打了一颗星”。

所以说,这个项目虽然标题带了“大数据”和“深度学习”两个热词,但真正的核心是“小规模全流程数据分析”。数据量做到几千条级别,能跑通、能解释、能演示,就已经达到毕设要求了。

1.2 技术栈选型背后的理由

技术选型直接决定你后面一个月是轻松还是痛苦。我按采集层、存储层、分析层、展示层四层来说。

层级技术方案选择理由
采集层Python requests + parsel简单直接,能拿到JSON数据,解析成本低
存储层Django ORM + SQLite/MySQLORM省去手写SQL,后台管理现成可用
分析层pandas + jieba + 情感词典轻量可解释,不用训练大模型
建模层scikit-learn LinearRegression可解释性最强,答辩最好讲
展示层Django模板 + ECharts + Ajax图表丰富,中文文档齐全

为什么用Django而不是Flask?说实话,Flask写个简单的可视化接口也够用,但Django自带Admin后台、ORM、模板继承和CSRF保护,工程结构看起来更完整。对毕设答辩来说,“工程完整性”是很重要的印象分。另外,Django的ORM在做统计聚合时特别顺手,后面所有图表接口都是几条聚合语句的事。

为什么坚持用线性回归而不是深度学习?这是很多同学容易走偏的地方。标题里出现“深度学习”就非要上个LSTM或者BERT,但实际评论数据往往只有两三千条,深度学习模型很容易过拟合,而且训练环境配置就能耗掉好几天。线性回归则不同,它能直接告诉你“情感得分每提高1分,星级提高0.6星”这种可解释的结论,老师追问起来你完全能接住。

ECharts这部分不用犹豫,它是目前做中文可视化图表支持最好的前端库之一。饼图、柱状图、折线图、词云都有现成示例,换个数据就能用,比手动拼图表省一半时间。

2. 爬虫采集:把网页评论区变成结构化数据

2.1 京东评论异步接口的请求规律

爬虫是这个项目里最容易“出师未捷身先死”的环节。很多新手第一次写爬虫时,直接用requests去请求商品详情页的HTML,结果发现页面源码里干干净净,根本没有评论内容。原因很简单:评论是异步加载的,真正的数据来自一个独立的JSON接口。

京东商城的评论接口核心参数就几个:productId是商品ID,score是评分筛选条件,sortType是排序方式,page是页码,pageSize是每页条数。请求时headers里必须带User-Agent,同时带上Referer指向对应商品页,否则很容易被拦。下面这个请求函数是我实际用过的简化版本:

import requests def fetch_comments(product_id, page=0, page_size=10): url = "https://sclub.jd.com/comment/productPageComments.action" params = { "productId": product_id, "score": 0, "sortType": 5, "page": page, "pageSize": page_size, "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) data = resp.json() return data.get("comments", [])

这里我用sortType=5表示按时间排序,用score=0表示获取全部评分。score还可以传其他值来筛选好评、中评、差评,具体取值在不同时期可能有调整,你开发时先打开商品页筛选一下,用浏览器开发者工具观察实际请求参数,再固化到代码里,这样最可靠。

反爬这块务必克制。个人毕设项目只需要控制请求节奏,比如每请求一页暂停1到3秒,随机延时而不是固定延时。遇到频繁请求导致验证码出现,就停下来等一段时间再继续。注意这类采集仅用于个人学习研究,遵守平台访问规则,不要做大规模高并发抓取,也不要去采集非公开信息,这是底线要求。

2.2 评论解析与文本清洗细节

接口返回的JSON里,最关键的是comments数组。每一条评论常见的字段包括content评论内容、score评分(1到5星)、creationTime评价时间、nickname用户昵称、userLevelName用户等级、productColor和productSize是购买的商品规格,还有afterUserComment追评信息。取数据时不要贪多,先把这些核心字段存下来。

评论内容清洗是个很容易被低估的工作。原始评论里经常混着HTML标签、 这类实体、表情符号、乱码空白字符,还有繁体字。直接拿去分词和建模会带进大量噪音。我的清洗函数一般长这样:

import re import html from bs4 import BeautifulSoup def clean_comment(raw): if not raw: return "" text = html.unescape(raw) text = BeautifulSoup(text, "html.parser").get_text() text = re.sub(r"[\s\r\n\t]+", " ", text) text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、:;\"'()《》【】\- ]", "", text) return text.strip()

注意最后一行正则的作用:保留中文、英文、数字和常用标点,把emoji和特殊符号直接删掉。为什么要保留中文标点而不是全删?因为“很好!”和“很好”的语义有细微差别,标点符号对句子的完整性判断有帮助。清洗完的文本既要用作展示,也要作为情感分析的输入。

这里有个经验,字段creationTime在接口里是类似2024-05-12 10:30:00的字符串,要提前转成Python的datetime对象,或者存成Django的DateTimeField,后面做时间序列趋势图时你就知道这个步骤有多重要了。

2.3 数据表设计与去重策略

数据表结构直接决定后面写统计接口的效率。我建议在Django里建一个Comment模型,字段设计如下:

from django.db import models class Comment(models.Model): product_id = models.CharField(max_length=32, db_index=True) comment_id = models.CharField(max_length=64, unique=True) content = models.TextField() score = models.IntegerField() sentiment_score = models.FloatField(default=0) comment_time = models.DateTimeField(db_index=True) nickname = models.CharField(max_length=64, blank=True) like_count = models.IntegerField(default=0) is_after = models.BooleanField(default=False) review_days = models.IntegerField(default=0)

为什么comment_id要设置成唯一索引?因为翻页爬虫会有重复抓取的风险,尤其断点续爬时容易重复入库。入库前先get_or_create,或者先查一遍comment_id是否存在,能省掉很多去重的麻烦。

sentiment_score字段在采集阶段先给默认值,后续分析脚本统一回填。不要把情感打分和采集混在同一步骤里,采集的任务只管拿原始数据,分析的任务专注计算,解耦之后出了问题更容易排查。

review_days字段表示从购买到评价的间隔天数,这在模型里可以作为一个特征,但在采集阶段可能拿不到,可以先填0,后面根据评论时间和购买时间近似计算,或者直接从晒单信息里提取。字段宁可先留空,也不要为了凑数据编造内容。

3. 评论分析与线性回归模型

3.1 基于情感词典的轻量级情感打分

评论分析的关键一步是给每一条评论算一个情感分。我在这个项目里没有上BERT这类预训练模型,而是用“情感词典+程度副词+否定词”的启发式方法。原因有三:数据量小,几千条评论用大模型属于杀鸡用牛刀;词典法速度快,本地跑几万条评论也就几十秒;最关键的是可解释,答辩时你完全能讲清楚打分逻辑。

情感打分的基本原理是用jieba把评论切词,然后逐个匹配正负面情感词典,正面词加分,负面词减分,遇到“很”“非常”“太”这类程度副词就加权,遇到“不”“没”这类否定词就反转。整体思路就像给评论里每个词投票:好词投赞成票,坏词投反对票,程度副词决定票数权重。

import jieba sentiment_dict = { "好": 1.0, "满意": 1.5, "喜欢": 1.5, "推荐": 1.0, "垃圾": -2.0, "差": -1.5, "失望": -1.5, "退货": -2.0, } degree_words = {"很": 1.5, "非常": 1.8, "太": 1.5, "特别": 1.8, "有点": 0.7} negation_words = {"不", "没", "没有", "不太", "别"} def calc_sentiment(text): words = jieba.lcut(text) score = 0.0 degree = 1.0 negated = False for w in words: if w in negation_words: negated = True continue if w in degree_words: degree = degree_words[w] continue if w in sentiment_dict: s = sentiment_dict[w] if negated: s = -s * 0.5 negated = False score += degree * s degree = 1.0 return round(score, 4)

这个实现是简化版,实际使用中你还要处理“不是不好”这种双重否定,以及“性价比高”“发货快”这种组合词。我的建议是不要追求完美,因为情感词典法本身就是一个快速估计的工具。为了证明结果靠谱,你可以随机抽100条评论,人工判断正负面,再和算法打分对比,人工准确率一般能做到80%左右,这个数据在答辩时很有说服力。

情感词典从哪里来?网上有很多开源中文情感词典,挑一个体量在几千词左右的就行。但要注意,通用词典对电商领域的词覆盖不足,比如“物流快”“客服态度好”里的“快”“好”虽然能命中,但“性价比”这种词需要你手动补充。我建议在跑完一批数据之后,把高频词表导出来人工看一下,把跟商品评价强相关的词补充进词典,这一步能明显提升打分效果。

3.2 特征工程与线性回归训练

情感分算完之后,就可以构造线性回归的输入特征了。我最终选定的特征有五项:

特征名含义为什么有用
sentiment_score情感得分直接反映评论正负倾向
content_length清洗后评论字数字数多的评论通常信息量更大
like_count点赞数代表其他用户的认同程度
is_after是否追评(1/0)追评反映使用一段时间后的体验
review_days购买到评价间隔天数使用时长影响满意度

目标变量就是score,也就是1到5星的评分。特征准备好之后,直接用scikit-learn训练线性回归模型:

import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score df = pd.read_csv("jd_comments_processed.csv") X = df[["sentiment_score", "content_length", "like_count", "is_after", "review_days"]] y = df["score"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = LinearRegression() model.fit(X_train, y_train) print("回归系数:", model.coef_) print("截距:", model.intercept_) pred = model.predict(X_test) print("RMSE:", mean_squared_error(y_test, pred, squared=False)) print("R2:", r2_score(y_test, pred))

有几个细节要特别注意。一是random_state=42必须设定,这样每次运行结果一致,答辩时你提前截图的数据还能复现。二是特征之间的量纲差异很大,like_count可能是几百,sentiment_score可能只有几,如果不做标准化,回归系数的大小不能直接比较。我建议用StandardScaler先标准化,再训练模型,这样得到的系数才能反映特征的相对重要性。

三是评分数据非常偏斜。5星评论往往占一半以上,1星和2星很少。这种分布会让模型倾向于预测高分。如果发现R²低得离谱,不要慌,这不是程序出问题,而是数据特性决定的。用户打分本身主观性很强,3星和4星的界线很模糊,模型能解释一半的方差已经很不错了。

我跑的一批数据最终结果大概是这样的:R²约0.53,RMSE约0.92,平均绝对误差约0.73。单看预测精度确实不高,但回归系数给出了很有意思的结论:情感得分每提高1分,星级约高0.62;追评评论比未追评平均低0.18星;评论字数每增加100字,星级提高约0.05星。这些结论比“模型准确率”更像是一个分析项目该有的产出。

3.3 模型效果不好时的后备方案

如果R²连0.3都不到,或者答辩时被质疑“你的模型根本不准”,你得有Plan B。我建议按以下顺序调整:

第一,把目标变量从连续评分改成二分类:好评(4星及以上)和差评(3星及以下),用逻辑回归替代线性回归。二分类模型的准确率、精确率、召回率更容易讲得清楚,评委也更容易接受。第二,用PolynomialFeatures添加二次项和交互项,比如情感得分和点赞数的交互项,有时能明显提升拟合度。第三,改用岭回归处理特征共线性。这几个方案都是几行代码的事情,跑完对比一下R²或准确率,挑一个最高的写进论文就行。

千万不要直接上随机森林或XGBoost。不是说不能用,而是你一旦用了树模型,老师大概率会追问“那你的线性回归还在系统里有什么作用”“为什么不用深度模型”,解释成本立刻变高。毕设项目的目标不是刷榜,而是一个逻辑自洽、能讲完整故事的作品。

4. Django可视化系统的实现

4.1 统计接口与View实现

可视化系统本质上是把数据库里的分析结果通过接口交给前端展示。我建议把所有统计逻辑写在Django的view里,通过JSON接口返回,前端只负责渲染。这样后端和前端分离,后续改图表样式完全不影响数据逻辑。

主页总览接口是一个很好的起点,它返回总评论数、平均评分、好评率和平均情感分。实现起来用ORM聚合非常快:

from django.http import JsonResponse from django.db.models import Count, Avg from .models import Comment def overview_api(request): total = Comment.objects.count() if total == 0: return JsonResponse({"total": 0}) avg_score = Comment.objects.aggregate(avg=Avg("score"))["avg"] good_count = Comment.objects.filter(score__gte=4).count() good_rate = good_count / total * 100 avg_sent = Comment.objects.aggregate(avg=Avg("sentiment_score"))["avg"] return JsonResponse({ "total": total, "avg_score": round(avg_score, 2), "good_rate": round(good_rate, 2), "avg_sentiment": round(avg_sent, 2), })

接着是评分分布接口。用一条values加annotate查询就能拿到1到5星各自的数量:

def score_distribution_api(request): rows = ( Comment.objects .values("score") .annotate(total=Count("id")) .order_by("score") ) return JsonResponse({ "labels": [r["score"] for r in rows], "values": [r["total"] for r in rows], })

时间趋势图需要按天聚合平均情感分,可以用TruncDate函数,把时间截断到天,再计算每天的情感分平均值。注意返回给前端时,日期要转成字符串,否则JSON序列化会报错。

这里有个容易被忽略的点:如果评论总量上万条,每次刷新页面都实时全表聚合,数据库压力会比较大,页面也会变慢。解决办法有两种:一是给常用查询字段加索引,比如comment_time和score;二是把统计结果缓存到内存或Redis,缓存十分钟过期再重新计算。毕设场景下加索引就够了。

4.2 前端模板与ECharts图表

前端我用了Django模板加原生JavaScript,通过fetch从接口拿数据,再交给ECharts渲染。这样做的好处是模板简单,不需要另起一个前端工程。首页结构大致是一个总览卡片区,下面放评分分布图和情感趋势图。

{% extends "base.html" %} {% load static %} {% block content %} <div class="row"> <div class="col-md-6"> <div id="scoreChart" style="width:100%;height:360px;"></div> </div> <div class="col-md-6"> <div id="trendChart" style="width:100%;height:360px;"></div> </div> </div> {% endblock %} {% block script %} <script src="{% static 'js/echarts.min.js' %}"></script> <script> fetch("/api/score_distribution/") .then(res => res.json()) .then(data => { var chart = echarts.init(document.getElementById("scoreChart")); chart.setOption({ title: { text: "评分分布" }, xAxis: { type: "category", data: data.labels }, yAxis: { type: "value" }, series: [{ type: "bar", data: data.values }] }); }); </script> {% endblock %}

写ECharts时有个常见的坑:容器div必须指定高度,否则图表初始化后只有一个默认的几十像素高度,看着就像没渲染出来。另外,如果页面里有多个标签页切换,被隐藏的标签页里的图表可能宽度计算错误,需要在切换后再调用一次chart.resize()。

词云图是可视化里最抓眼球的部分。ECharts 5本身不内置词云功能,需要额外引入echarts-wordcloud.js扩展。如果你不想引入太多JS,可以自己做一个简化版“词云”:后端返回高频词和频次,前端把词渲染成大小不一的彩色span标签,随机排列。效果虽然朴素,但胜在不依赖额外插件。

4.3 项目运行、性能与部署注意

本地运行时,python manage.py runserver就够了。但有几处配置文件要处理:ALLOWED_HOSTS里要加上运行机器的IP或域名;TIME_ZONE务必设置成"Asia/Shanghai",否则时间序列图会差8个小时;DEBUG在演示时建议设为False,并提前collectstatic收集静态文件,避免答辩现场静态资源加载失败。

如果数据库用MySQL,建库时字符集一定要用utf8mb4,否则评论里的emoji会写入报错。用SQLite则基本不用操心编码问题,适合数据量不大的毕设项目。

页面卡顿是另一个常见问题。如果评论数据达到几万条,评分分布这种聚合查询还是很轻松的,但时间趋势图如果你的趋势线按天画,几万条会生成几百个点,看起来密集且不美观。我的做法是先按周聚合,每周算一个平均情感分,这样趋势线平滑,前端渲染也更快。这个细节在答辩时提出来,反而显得你考虑了真实场景下的性能优化。

5. 毕设答辩:演示节奏与高频问题

5.1 演示流程怎么安排

答辩演示通常只有8到10分钟,信息密度要控制好。很多同学喜欢从头开始讲爬虫代码,结果讲到一半时间就到了,最关键的可视化和模型结论反而没展开。我的建议是按下述节奏走:

第一步,用两句话讲清楚选题背景:电商评论数量大、人工看不完,需要自动分析和可视化。第二步,展示系统整体架构,不需要精细架构图,一张简单的分层示意就够。第三步,实机演示,顺序是先看主页总览,再看评分分布,再看情感趋势和词云,最后切到模型分析页面展示回归系数和R²。第四步,用一句总结性的话收尾:系统实现了从数据采集、情感分析到回归建模、可视化的完整链路。

演示前一定生成本地截图,把每个关键页面都截一遍。我有一次答辩时现场网络波动,接口数据加载不出来,全靠提前准备的截图救了场。这个习惯我建议你直接养成,它不是技巧,是保险。

投影环境下有一个视觉问题:字号。ECharts默认的图例和坐标轴文字在投影上非常小,后排基本看不清。设置textStyle时,fontSize至少12到14,标题至少16。答辩前最好在投影仪上实际看一眼,别只看笔记本屏幕。

5.2 老师最爱追问的问题与应对

毕设答辩的高频问题就那么几个,提前准备好参考回答,现场就不会慌。我把常见问题整理成了一张表:

问题参考回答思路
你的数据是怎么来的?是否合规?来自电商平台公开的评论接口,仅用于个人学习研究,请求频率很低,不做商业用途,不涉及非公开信息
为什么用线性回归,不用深度学习?数据规模不大,线性回归可解释性强,能直接得到特征影响系数,满足“分析”目标;深度学习更适合大规模数据和强非线性场景
情感分析的准确率如何?基于情感词典的启发式方法,人工抽样100条对比准确率约80%,具有局限性但完全能满足情感趋势分析需求
模型R²只有0.5,说明什么?用户打分主观性强,评分边界模糊,但回归系数的业务解释有意义,系统重点是趋势分析而非精确预测
你的系统有什么创新点?完整串起爬虫、清洗、分析、建模、可视化全流程,并把情感分与星级回归相结合,不只是单纯的图表展示

回答问题时有个原则:诚实说明方法局限,但马上给出补救或替代方案。说“我这个方法有缺陷”不可怕,可怕的是说“我没想到过这个问题”。比如被问情感词典不准时,你可以坦然说,“当前是快速启发式实现,后续可以替换为预训练模型,代码中预留了接口”。这样的对话既真实又专业。

5.3 做这个题目最容易踩的坑

最后一个部分,我凭自己的经验把做这个题目最容易踩的坑列出来。这些坑几乎每个人都遇过,提前知道能省出好几天时间。

第一个坑是数据量太少。只爬几十条评论,图表丑,模型也没法训练。建议至少选三到五个不同价格档位的商品,每个商品爬十页以上,总数据量做到两千条以上。数据量上来了,评分分布、情感趋势、回归模型才都有分析价值。

第二个坑是数据清洗不彻底。词云里全是“京东”“商品”“快递”“东西”这类通用词,高频词表完全没意义。处理办法有两步:在情感分析之前过滤停用词,同时把商品品牌词、平台词加进自定义停用词表。词云展示时,可以对词频做归一化,去掉过于高频的垃圾词。

第三个坑是采集过程中断后没有断点续传。爬虫跑到一半被限流,或者电脑息屏断网,之前爬的数据都在,但不知道爬到了哪一页。解决办法很简单:在数据库里记录当前商品ID和页码,每次重启爬虫时先查一下进度。或者干脆用Django脚本管理命令,把抓取逻辑封装成manage.py crawl_comments,配合数据库去重,中断后能自动绕过已经入库的评论ID。

第四个坑是答辩前数据丢失。数据库文件误删,或者临时表被清空,整个演示就黄了。我的习惯是每次抓完一批数据就导出一次CSV备份,不仅为了安全,也方便用pandas做离线分析。

第五个坑是评论时间字段的时区问题。Django默认使用UTC时区,如果你直接存字符串时间再格式化,最后趋势图的时间轴会偏移。正确做法是在settings.py里设置USE_TZ = False,或者在存储前统一把时间转换到本地时区。这个细节不留意,查起来会非常痛苦。

最后说点个人体会。这个题目的难度不在于某一个环节,而在于把五个环节串起来。我见过很多同学把时间全耗在爬虫上,爬了上万条评论却没时间做模型和页面,最后答辩时只能干讲。我的习惯是“倒着做”:先把Django框架和数据库表建好,再写一个能从CSV导入数据的后台脚本,然后去爬数据,边爬边向数据库插入,这样即使爬虫中途被限制,已经入库的部分也能支撑后续分析。答辩前一定要把模型系数、图表截图、关键代码位置都提前准备好,宁可少讲功能,也要把“为什么这么做”讲透,这个思路比任何技术细节都重要。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进