ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python+Django+Vue旅游评论情感分析系统实战:从数据采集到可视化看板

Python+Django+Vue旅游评论情感分析系统实战:从数据采集到可视化看板 简介这是一套面向高校学生与Python开发初学者的旅游景点评论情感分析完整项目源码采用PythonDjango后端与Vue前端技术栈可直接用于毕业设计、期末大作业或课程设计场景。项目含详细代码注释新手也能读懂部署简单下载后即可运行使用。压缩包共102个文件约47.72MB其中32个py文件承载Django后端业务逻辑与情感分析核心算法10个vue文件与6个js文件构成前端交互界面另有json配置、html模板、png与jpg图片资源及md说明文档目录结构清晰便于按模块查阅与二次开发。目前已有190人学习下载。项目经过严格调试功能完善、界面美观、操作便捷配套文档说明可帮助读者快速理解情感分析流程、前后端数据交互方式与整体架构设计是兼顾实用性与参考价值的高分项目范例。1. 从零搭一套旅游景点评论情感分析系统PythonDjangoVue 到底怎么落地打开任何一个在线旅游平台点进一个 5A 景区评论区动辄几万条。运营想知道「最近一个月差评集中在哪」靠人工翻页翻到崩溃。这就是旅游景点评论情感分析要解决的问题把非结构化的中文评论自动切成正面、负面、中性三类再按景点、时间、关键词聚合最后用 Web 界面呈现出来。整套系统用 Python 做算法、Django 做后端接口、Vue 做前端交互是目前高校课程设计和中小团队落地最常见的技术组合。它适合三类人想找一个完整项目练手的 Python 新手、需要交付课程设计的学生、以及要给景区做舆情看板的后端工程师。读完你能自己跑通数据采集、模型训练、接口开发、前端展示的完整链路也能看清每一步的参数边界和翻车点。2. 技术选型与数据链路为什么是 Django 而不是 Flask2.1 情感分析任务的技术栈拆解旅游评论情感分析本质是一个中文短文本三分类任务。输入是「风景不错就是门票太贵了」这样的句子输出是负面。整条链路分四层数据层负责采集和清洗评论算法层负责分词、向量化和分类服务层负责把模型包装成 HTTP 接口展示层负责图表和列表渲染。选 Django 而不是 Flask核心理由是这类项目天然需要 ORM、Admin 后台、用户认证和迁移工具。评论数据要存库、要按景点和日期筛选、要能人工复核标注结果Django 自带的这些能力直接省掉大量胶水代码。Flask 更轻但你要自己拼 SQLAlchemy、自己写后台项目一大就散。Vue 这边选 Vue 3 Element Plus是因为评论列表、分页、图表这类管理界面用组件库能快速搭出来比手写 DOM 省事得多。算法层我一般用 jieba 分词 SnowNLP 做基线再用 scikit-learn 的 TF-IDF 朴素贝叶斯或 SVM 做可训练版本。如果追求更高准确率可以上 HuggingFace 的中文预训练模型做微调但那是另一个量级的算力投入。课程设计级别TF-IDF 线性模型在旅游评论这种领域词汇集中的场景准确率做到 85% 左右是现实的。提示不要一上来就上 BERT。旅游评论的负面表达高度模板化「坑」「宰客」「不值」传统特征工程加线性模型性价比最高训练一次几十秒改起来也快。2.2 从评论采集到入库的完整步骤第一步是数据来源。常见做法是用 requests BeautifulSoup 抓公开评论页或者直接用平台开放接口。抓取时注意控制频率加 time.sleep否则容易被封 IP。抓下来的原始数据先落成 CSV字段至少包含景点名称、评论正文、评分、评论时间。import requests import csv import time from bs4 import BeautifulSoup def fetch_comments(scenic_id, pages10): 抓取指定景点的评论返回列表 all_comments [] headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } for page in range(1, pages 1): url fhttps://example.com/api/comments?scenic{scenic_id}page{page} resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: break data resp.json() for item in data.get(list, []): all_comments.append({ scenic: item[scenicName], content: item[content].strip(), score: item.get(score, 0), date: item.get(createTime, ) }) time.sleep(1.5) # 控制频率避免触发风控 return all_comments def save_to_csv(comments, pathraw_comments.csv): with open(path, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[scenic, content, score, date]) writer.writeheader() writer.writerows(comments) if __name__ __main__: data fetch_comments(A001, pages5) save_to_csv(data) print(f共抓取 {len(data)} 条评论)这段代码的关键参数有三个pages控制抓取页数time.sleep(1.5)控制请求间隔encodingutf-8-sig保证 Excel 打开 CSV 不乱码。timeout10防止某个请求卡死拖垮整个循环。失败时先看resp.status_code403 说明被拦需要换请求头或降频返回 JSON 结构变了就打印data看实际字段名。第二步是清洗。评论里混着表情符号、URL、重复刷屏内容。清洗逻辑去掉 HTML 标签和 URL过滤长度小于 5 的评论用 SimHash 或简单的去重集合去掉完全重复项。清洗完的数据存进 Django 的模型表字段设计建议加一个sentiment字段默认空留给后续标注或预测结果回填。2.3 Django 模型设计与数据迁移在 Django 里建 app然后定义评论模型。字段类型要贴合实际content用 TextFieldscore用 SmallIntegerFieldsentiment用 CharField 加 choices。# comments/models.py from django.db import models class Scenic(models.Model): name models.CharField(max_length100, uniqueTrue) city models.CharField(max_length50, blankTrue) def __str__(self): return self.name class Comment(models.Model): SENTIMENT_CHOICES [ (pos, 正面), (neg, 负面), (neu, 中性), ] scenic models.ForeignKey(Scenic, on_deletemodels.CASCADE, related_namecomments) content models.TextField() score models.SmallIntegerField(default0) sentiment models.CharField(max_length3, choicesSENTIMENT_CHOICES, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: indexes [models.Index(fields[scenic, sentiment])]on_deletemodels.CASCADE表示景点删除时评论一起删related_namecomments让你能用scenic.comments.all()反查。Meta.indexes给景点和情感建联合索引因为看板最常查的就是「某景点的负面评论」没索引数据量上万就明显卡。迁移命令是python manage.py makemigrations加python manage.py migrate改字段后重复执行即可。注意auto_now_addTrue只在创建时写入后续更新不会变需要更新时间就再加auto_nowTrue的字段。3. 情感分析模型训练从 jieba 分词到可复用的预测接口3.1 中文分词与停用词处理的参数细节中文和英文不同词之间没有空格必须先分词。jieba 是默认选择jieba.lcut()返回列表jieba.cut()返回生成器。旅游评论里有很多专有名词比如「玻璃栈道」「索道」「摆渡车」默认词典可能切错可以用jieba.add_word()补充。停用词表要针对旅游场景定制。通用停用词表的、了、是要加但「不」「没」「太」这类否定和程度词绝对不能删删了情感极性直接反转。我一般维护两份一份通用停用词一份旅游领域保留词后者优先级更高。import jieba import re STOPWORDS set([的, 了, 是, 在, 和, 就, 都, 而, 及, 与]) KEEP_WORDS set([不, 没, 太, 很, 非常, 特别, 有点]) def clean_text(text): text re.sub(r[^], , text) # 去 HTML 标签 text re.sub(rhttp\S, , text) # 去 URL text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) # 只留中英文数字 return text.strip() def tokenize(text): text clean_text(text) words jieba.lcut(text) return [w for w in words if w not in STOPWORDS or w in KEEP_WORDS] # 补充领域词 for w in [玻璃栈道, 摆渡车, 索道, 网红打卡]: jieba.add_word(w) print(tokenize(风景不错就是门票太贵了摆渡车还要另外收费)) # 输出大致为 [风景, 不错, 就是, 门票, 太, 贵, 摆渡车, 还要, 另外, 收费]clean_text里的正则[^\u4e00-\u9fa5a-zA-Z0-9]把标点和表情统一替换成空格避免它们被当成词。tokenize的过滤条件是「不在停用词表或者在保留词表」这样「太」「不」能留下来。参数上jieba.lcut的cut_all默认 False 走精确模式别改成 True全模式会产生大量无意义组合词反而拉低分类效果。3.2 TF-IDF 向量化与分类器训练分词完要转成数值向量。TF-IDF 的核心是一个词在当前评论里出现越多、在整个语料里出现越少权重越高。TfidfVectorizer的max_features控制保留多少维旅游评论语料几万条的话设 5000 到 10000 比较合适太大容易过拟合太小丢信息。ngram_range(1,2)让模型同时看单词和双词组合能捕捉「不 值」这种搭配。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from sklearn.svm import LinearSVC from sklearn.metrics import classification_report import joblib # X_raw 是清洗后的评论文本列表y 是对应标签列表 X_tokens [ .join(tokenize(t)) for t in X_raw] vectorizer TfidfVectorizer( max_features8000, ngram_range(1, 2), min_df2, max_df0.9 ) X_vec vectorizer.fit_transform(X_tokens) X_train, X_test, y_train, y_test train_test_split( X_vec, y, test_size0.2, random_state42, stratifyy ) clf LinearSVC(C1.0, class_weightbalanced) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(classification_report(y_test, y_pred)) joblib.dump(vectorizer, vectorizer.pkl) joblib.dump(clf, sentiment_clf.pkl)min_df2表示词至少出现在 2 条评论里才保留过滤掉拼写错误和噪声。max_df0.9表示出现在 90% 以上评论里的词丢掉这类词没有区分度。class_weightbalanced很重要旅游评论里正面往往远多于负面不加这个参数模型会偏向多数类负面召回率惨不忍睹。stratifyy保证训练集和测试集类别比例一致。训练完用classification_report看每类的 precision、recall、f1重点看负面类的 recall它直接决定差评能不能被捞出来。3.3 把模型包装成 Django 可调用的预测服务模型训练是一次性的线上要的是随时能调。做法是在 Django app 里写一个predict.py启动时加载 pkl 文件对外暴露一个函数。注意不要在每次请求里重新加载模型那样每次都要几百毫秒接口直接崩。# comments/ml/predict.py import os import joblib import jieba from django.conf import settings _MODEL_DIR os.path.join(settings.BASE_DIR, comments, ml, artifacts) _vectorizer None _clf None def _load(): global _vectorizer, _clf if _vectorizer is None: _vectorizer joblib.load(os.path.join(_MODEL_DIR, vectorizer.pkl)) _clf joblib.load(os.path.join(_MODEL_DIR, sentiment_clf.pkl)) def predict_sentiment(text): _load() tokens .join(tokenize(text)) vec _vectorizer.transform([tokens]) label _clf.predict(vec)[0] return label用全局变量做懒加载第一次调用时读盘之后常驻内存。transform而不是fit_transform线上只能用训练时确定的词表绝不能重新拟合。如果模型文件更新了需要重启进程或加一个 reload 接口否则内存里还是旧模型。批量预测时把多条评论拼成一个列表一次transform比循环单条快一个数量级。4. Django 接口与 Vue 前端联调评论看板怎么跑起来4.1 DRF 接口设计与分页参数后端接口用 Django REST Framework。核心接口有三个评论列表支持按景点、情感筛选和分页、情感统计返回各情感数量、批量预测接收文本返回标签。分页用 DRF 自带的 PageNumberPaginationpage_size设 20允许前端通过?page翻页。# comments/views.py from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.pagination import PageNumberPagination from rest_framework.decorators import api_view from rest_framework.response import Response from django.db.models import Count from .models import Comment from .serializers import CommentSerializer from .ml.predict import predict_sentiment class CommentPagination(PageNumberPagination): page_size 20 page_size_query_param size max_page_size 100 class CommentViewSet(ReadOnlyModelViewSet): serializer_class CommentSerializer pagination_class CommentPagination def get_queryset(self): qs Comment.objects.select_related(scenic).all() scenic_id self.request.query_params.get(scenic) sentiment self.request.query_params.get(sentiment) if scenic_id: qs qs.filter(scenic_idscenic_id) if sentiment: qs qs.filter(sentimentsentiment) return qs.order_by(-created_at) api_view([GET]) def sentiment_stats(request): scenic_id request.query_params.get(scenic) qs Comment.objects.all() if scenic_id: qs qs.filter(scenic_idscenic_id) data qs.values(sentiment).annotate(countCount(id)) return Response({item[sentiment]: item[count] for item in data}) api_view([POST]) def predict(request): text request.data.get(text, ) if not text: return Response({error: text required}, status400) return Response({sentiment: predict_sentiment(text)})select_related(scenic)是关键优化评论列表要显示景点名不加这个每渲染一条就查一次景点表20 条就是 20 次额外查询典型 N1。page_size_query_paramsize允许前端自定义每页条数但max_page_size100兜底防止有人传 size100000 把数据库拖死。sentiment_stats用values().annotate()在数据库层聚合别在 Python 里循环计数。4.2 Vue 端调用接口与图表渲染前端用 Vue 3 的script setup写法请求用 axios。评论列表和情感饼图是两个核心组件。饼图用 ECharts数据来自sentiment_stats接口。// src/api/comment.js import axios from axios; const api axios.create({ baseURL: http://127.0.0.1:8000/api, timeout: 10000, }); export function fetchComments(params) { return api.get(/comments/, { params }); } export function fetchStats(scenicId) { return api.get(/sentiment-stats/, { params: { scenic: scenicId } }); } export function predictText(text) { return api.post(/predict/, { text }); }// src/components/SentimentPie.vue script setup import { ref, onMounted } from vue; import * as echarts from echarts; import { fetchStats } from /api/comment; const chartRef ref(null); const props defineProps({ scenicId: String }); onMounted(async () { const { data } await fetchStats(props.scenicId); const chart echarts.init(chartRef.value); chart.setOption({ tooltip: { trigger: item }, series: [{ type: pie, radius: [40%, 70%], data: [ { value: data.pos || 0, name: 正面 }, { value: data.neg || 0, name: 负面 }, { value: data.neu || 0, name: 中性 }, ], }], }); }); /script template div refchartRef stylewidth: 100%; height: 320px;/div /templateaxios 实例统一配baseURL和timeout避免每个请求重复写。onMounted里初始化 ECharts注意容器必须有明确高度否则图表高度为 0 什么都不显示这是新手最常见的翻车点。跨域问题在开发阶段用 Django 的django-cors-headers解决生产环境让 Nginx 同源代理别在前端硬编码 IP。4.3 前后端联调的三个必查项联调阶段问题集中在三处。第一请求发出去了但 404检查 Django 的urls.py有没有注册路由DRF 的 router 注册路径和前端 baseURL 是否对得上。第二返回 200 但数据是空数组多半是筛选参数名不一致前端传scenic后端读scenic_id这种。第三图表不显示打开浏览器控制台看有没有报错再看 ECharts 容器的clientHeight是不是 0。注意开发阶段前端跑在 5173 端口后端跑在 8000跨域是必然的。CORS_ALLOWED_ORIGINS要写完整协议和端口写localhost:5173不带http://是不生效的。5. 避坑与排查这套系统最容易翻车的五个地方5.1 模型准确率虚高但线上全是错现象离线测试集准确率 92%上线后运营反馈「差评被标成正面」。原因通常是训练数据里正面样本占 80% 以上模型学会了无脑猜正面测试集分布又和训练集一样所以指标好看。解决先看classification_report里负面类的 recall低于 0.7 就说明有问题。用class_weightbalanced或者对负面样本做上采样。更彻底的做法是重新采样让三类比例接近 1:1:1。5.2 分词把否定词切没了导致极性反转现象「不推荐」被预测成正面。原因停用词表里误删了「不」或者分词把「不推荐」切成了「不」和「推荐」而「不」被过滤掉只剩「推荐」。解决把否定词和程度词加入保留词表tokenize的过滤条件用「不在停用词 或 在保留词」。更稳的做法是用jieba的自定义词典把「不推荐」「不值得」整体加进去。5.3 Django 接口数据量一大就超时现象评论过万后列表接口要 5 秒以上。原因没建索引、没做分页、或者序列化时触发了 N1 查询。解决给scenic和sentiment建联合索引列表接口强制分页get_queryset里加select_related。用 Django Debug Toolbar 看每个请求执行了多少条 SQL超过 5 条就要警惕。5.4 Vue 打包后接口地址写死导致部署失败现象本地跑得好好的npm run build部署到服务器后所有请求 404。原因axios 的baseURL硬编码了127.0.0.1:8000打包后浏览器访问的是用户自己的机器。解决用环境变量区分.env.development写本地地址.env.production写/api配合 Nginx 反向代理转发到 Django。5.5 模型文件路径在部署环境找不到现象本地能预测服务器上报FileNotFoundError。原因settings.BASE_DIR拼接的相对路径在部署后目录结构变了或者 pkl 文件没被打包进去。解决模型文件路径用绝对路径配置或者放进 Django 的STATIC_ROOT之外的固定目录部署脚本里显式拷贝。启动时加一个文件存在性检查不存在就打印明确日志别让它静默失败。6. 让这套系统真正可用批量预测与效果验证的实操技巧模型训完、接口通了离「能用」还差一步批量回填和效果验证。新抓的一批评论入库时sentiment是空的你需要写一个管理命令批量预测并回填而不是一条条调接口。# comments/management/commands/fill_sentiment.py from django.core.management.base import BaseCommand from comments.models import Comment from comments.ml.predict import predict_sentiment class Command(BaseCommand): help 批量回填评论情感标签 def add_arguments(self, parser): parser.add_argument(--batch, typeint, default500) def handle(self, *args, **options): qs Comment.objects.filter(sentiment) total qs.count() batch options[batch] for i in range(0, total, batch): chunk list(qs[i:i batch]) for c in chunk: c.sentiment predict_sentiment(c.content) Comment.objects.bulk_update(chunk, [sentiment]) self.stdout.write(f已处理 {min(i batch, total)}/{total})bulk_update一次更新一批比逐条save()快几十倍。--batch参数控制每批大小内存紧张就调小。跑之前先备份数据库跑完抽查 50 条人工核对算一个抽样准确率。验证环节我一般做两件事。一是混淆矩阵看负面被误判成正面多还是中性多前者危害大要优先优化。二是关键词云对比把预测为负面的评论分词后取 Top 50 词看是不是集中在「贵」「坑」「排队」这些真实痛点词上。如果负面词云里出现大量中性词说明模型边界模糊需要补充标注数据重训。验证指标合格线不达标时的动作负面类 recall≥ 0.75加 class_weight 或上采样负面样本整体 accuracy≥ 0.85检查分词和停用词表单条预测耗时≤ 50ms确认模型是常驻内存而非每次加载批量回填 1 万条≤ 3 分钟调大 batch 或改用多进程最后说个血泪经验别在项目初期追求模型多先进先把数据链路和接口跑通用最简单的 TF-IDF SVM 出一个能看的版本让运营先用起来。真实反馈会告诉你哪些评论被分错了这些错例才是下一轮训练最值钱的标注数据。我见过太多人卡在调模型上结果前端和部署一塌糊涂项目根本没法演示。先把整条链路打通再回头优化准确率这个顺序别搞反。希望帮到你。本文还有配套的精品资源点击获取
返回列表