简介一套面向毕业设计场景的基于Python的旅游景点评论情感分析系统项目源码聚焦携程、马蜂窝两大平台用户评论的采集与情感倾向识别适合需要完成类似课题或研究爬虫、NLP结合应用的开发者参考。包内共有122个文件以Python源码、pyc编译文件、Vue前端组件、配置说明和HTML页面为主压缩包整体47.72MB目录下细分出主程序、Web展示、图片资源、算法代码压缩包等模块便于按需学习与二次开发。目前已有78人学习下载。内容包含双平台评论爬虫、评论文本预处理、情感分类模型以及基于Vue的可视化界面覆盖从数据抓取、特征提取到结果展示的完整流程同时附有README编写说明和算法代码zip可快速理解项目结构并复现Python 3.9.11环境下的运行效果对旅游平台进行用户舆情管理也有一定参考价值。1. 旅游景点评论情感分析系统先定边界再谈“设计与实现”“旅游景点评论情感分析系统”这类题目在 Python 入门项目里出现频率极高网上能搜到的 demo 多半只做到“调用 SnowNLP 返回一个正负分数”就结束了。真按这套做下去交付时要么所有短评论都判成中性要么换个景点类型准确率就大幅下跌。一个能落地的系统至少要在做之前回答清楚输入是一条评论还是整份评论列表输出是三分类还是五档评分模型是通用情感倾向还是针对景区场景定制。带着这些边界再动手你才知道第一版该做什么、哪些部分值得投入训练自己的数据。这条路线适合正在做毕业设计、课程设计或者想给景区运营搭一个内部评论监控工具的 Python 开发者按能演示、能用、能迭代的标准来写。2. 第一版情感分析能跑通把评论变成情感分数的最小实现第一版的目标不是追求最高准确率而是打通一条从原始文本到情感分数的链路。只要这条链路稳定后续换模型、调阈值都有地方可改。我先讲清楚三个边界再给一个能直接运行的数据处理和情感打分实现。2.1 先给“情感分析”定边界对象、粒度、输出评论对象指的是数据来源——第三方 OTA 平台的游客点评、景区留言板评论、问卷里的开放性回答。旅游评论和电商评论有明显差异平均长度短大量出现地名、时间、票价、排队这些事实词情绪表达经常藏在叙述里。比如“索道检修白走一小时”没有明显情感词却是实打实的负面。再比如“云海绝了人间值得”是正面“排队两小时游玩五分钟”没有骂人但运营必须看到它是负面。粒度是我见过最容易出问题的地方。一句“景色很美但缆车排队两小时”按整条评论做篇章级情感分析会得到中性或模糊结果如果按子句切分前半句是正面、后半句是负面。对于初版系统建议先做篇章级结果统一成“正面/中性/负面”三个标签等有足够标注数据后再升级到方面级情感分析。输出要能解释。只返回正负 0.8 这种数字业务方看不懂。更常见的是输出两个值一个三分类标签一个 0 到 1 的置信度分数。这样系统既可以直接展示结论也可以给后续基于阈值做二次筛选。边界定完后代码层的关键在于把“原始文本-情感分数”做成一个可重复调用的函数。下面一节给数据来源这是很多人卡住的位置。2.2 数据从哪来爬虫、公开数据集、自造样本三选一一个情感分析系统没有数据就谈不上训练和验证。常见做法是以下三选一。第一种是 python爬虫对景区评论页做定向采集。这个方案最贴近真实但要特别注意合规和反爬要求不要高频请求、不要采集个人隐私字段用于课程设计时数据只要验证系统能工作即可没必要为了“看起来真实”去冒风险。第二种是直接用公开中文情感数据集比如各平台整理过的酒店、餐饮评论它们不是景区场景但足以跑通流程。第三种是自己构造小样本集写 200 到 500 条模拟评论覆盖景色、交通、餐饮、排队、性价比几个维度人工标注好正负。课程设计我更推荐第三种标签干净周期短答辩被问到“标签怎么来”时也能直接打开文件说清楚。无论走哪条路最终都整理成下面这种结构最简单的表格。字段示例作用text景区很美但周末停车场严重不够模型输入label1监督标签1正面、0负面、2中性star4弱标签用于自动生成labelsourcemanual/ota/online标识数据来源便于分批验证表格中 star 来自平台评分如果只有评论文本没有评分就人工标注 label。把这份 CSV 放进 data 目录后面所有脚本都从这里读数据。2.3 预处理、分词、去停用词的最小实现import re import jieba import pandas as pd # 加载停用词每行一个词 def load_stopwords(pathdata/stopwords.txt): with open(path, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) STOPWORDS load_stopwords() # 只保留中文、英文和数字去掉 HTML 标签和多余空白 def clean_text(text: str) - str: if not isinstance(text, str): text str(text) text re.sub(r.*?, , text) text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) text re.sub(r\s, , text) return text.strip() # 分词并过滤停用词、单字词 def tokenize(text: str) - list: return [ w for w in jieba.cut(text) if w.strip() and w not in STOPWORDS and len(w.strip()) 1 ]这段代码做三件事清理噪声、分词、移除停用词。clean_text 里的正则顺序是先删除 HTML 标签再把非中文、非英文字母、非数字的字符统一替换成空格最后把多个连续空白压成一个。需要注意正则\u4e00-\u9fa5是常用汉字的 Unicode 范围如果评论里包含 emoji在这一步会被删掉。对初版模型这是可以接受的折中。分词采用 jiebatokenize 中的len(w) 1会把“的、也、了”这类单字过滤但也会把有价值的单字情感词“美”“烂”滤掉因此这个过滤条件只适用于逻辑回归这类特征模型。若后续换成 BERT 类的预训练模型停用词过滤基本可以去掉。这里还有一个血泪教训网上通用停用词表可能会把“不”“没”“别”删掉结果“不累”直接变成“累”否定信息完全丢失。生成停用词表后至少手动确认这几个否定词不在集合里。这一段是训练和预测公用的预处理逻辑我习惯把它放在 text_utils.pyFlask 接口和训练脚本都 import 同一个模块避免训练用了清洗、上线却忘记清洗的翻车。2.4 用 SnowNLP 快速得到第一版结果from snownlp import SnowNLP def predict_with_snownlp(text: str) - dict: cleaned clean_text(text) s SnowNLP(cleaned) score s.sentiments # 0~1越接近1越正面 if score 0.6: label 1 elif score 0.4: label 0 else: label 2 return {label: label, score: round(score, 4)} # 示例 print(predict_with_snownlp(景色很美值得专程来一次)) print(predict_with_snownlp(停车场太远走了二十分钟))SnowNLP 内置了一个基于电商评论训练的情感模型sentiments 返回 0 到 1 的倾向概率。第一版拿它跑 demo 非常快但它对景区文本的领域适配很差把“暴晒”“爬得腿软”这种描述判成中性甚至正面都是常见现象。这就是下一章要自建训练数据的原因。阈值 0.6 和 0.4 是我常用的中性区间不要把切分点直接设成 0.5否则短评稍微带点情绪就左右乱跳。如果你要映射成 5 档评分也可以把 0.4~0.6 映射成 3 星0.2~0.4 映射成 2 星初版完全够用。这个最小实现跑通后系统就有了一个可运行的基线下一步的核心工作就是把这个通用情感概率替换成基于景区样本训练出来的分类模型。3. 用训练数据替换通用情感库TF-IDF 加逻辑回归的建模流程把 SnowNLP 换成自己训练的模型是系统从“能跑”走向“能交付”的第一道分水岭。选型上我优先逻辑回归不用 LSTM 或 BERT 打头阵因为旅游评论训练集通常只有几百到一两千条复杂模型容易过拟合还不好解释。逻辑回归加 TF-IDF在这个规模下足够稳定。3.1 为什么不满足于通用情感库行业的“正面”标准不一样游客写“今天很幸运看到云海”在 SnowNLP 里是中性的概率很高因为“云海”“缆车”“栈道”这些词在电商语料里几乎没出现过写“免费接驳车好评”通用情感库根本不知道“接驳车”和好评有什么关系。通用情感库满足不了两个要求一是景区专用名词的权重二是事实描述和情感倾向之间的映射。另一个关键点是正面标准因景区类型而不同。同一句“风景不错就是爬了三千级台阶”放在老年人占多数的古迹景区大概率是负面放在户外徒步爱好者聚集的登山景区可能只是中性陈述。如果全程只有一个模型这些差异会被全部抹平。第一版可以先不做细分但要在数据里记录 scenic_type 字段为后面建模留后路。3.2 自己标注数据5 档评分怎么转成二分类从 OTA 平台拿到的评论通常带 1 到 5 星评分。评分是最天然的弱标签但不能直接拿来当情感标签要按业务口径映射。常见做法是 4 星以上为正面2 星以下为负面3 星及部分 4 星留作中性。如果只做二分类就把 4、5 星当正面1、2 星当负面3 星样本丢弃。我不建议丢因为线上真实评论里“还行”这种中性描述占比很高训练时完全不管上线后它们会成群出现在预测结果里反而拉低体验。def star_to_label(star: int) - int: if star 4: return 1 # 正面 if star 2: return 0 # 负面 return 2 # 中性如果手头只有纯文本没有评分就人工标注。标注时的约定比数量更重要明确“事实性不满”算负面比如“排队两小时”“客观描述”算中性比如“门票 80 元”“没有明确情绪词的赞美”算正面比如“出片率高”。没有这些约定两个人标出来的 y 对不上模型学习的就是噪声。3.3 用 TF-IDF 加逻辑回归训练一个可解释基线模型from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split from text_utils import clean_text, tokenize # 复用 2.3 的预处理 df pd.read_csv(data/labeled_reviews.csv) X_train, X_test, y_train, y_test train_test_split( df[text], df[label], test_size0.2, random_state42, stratifydf[label] ) model Pipeline([ (tfidf, TfidfVectorizer( max_features5000, ngram_range(1, 2), preprocessorclean_text, tokenizertokenize )), (clf, LogisticRegression(max_iter1000, class_weightbalanced)) ]) model.fit(X_train, y_train) print(dev acc:, model.score(X_test, y_test))这个 Pipeline 把文本清洗、分词、TF-IDF 向量化和分类器串成一条链。predict 的时候直接 model.predict([text])内部会自动执行 clean_text 和 tokenize避免在接口里重复拼装步骤。TfidfVectorizer 的 preprocessor 接收文本返回字符串tokenizer 接收字符串返回词表两者与 text_utils 里的函数一一对应。参数怎么调max_features5000 限制词表大小避免景区地名、日期等低频词全部进模型造成维度膨胀ngram_range(1, 2) 同时取单词和相邻两词能抓住“不差”“性价比高”这类短语这是单个单词模型很难学到的class_weightbalanced 用于修正中性样本过多的问题让逻辑回归不会为了整体准确率把所有样本都往中性上推。max_iter1000 保证收敛默认的 100 在这个数据规模下通常也够调大不亏。如果数据量超过几千条可以加 GridSearchCV 调 c 和 ngram_range数据量小时先跑这一版再谈调参。这里不需要把 label 做 one-hotsklearn 的逻辑回归原生支持多分类直接传 0、1、2 即可。3.4 评估指标准确率、F1 与混淆矩阵怎么看from sklearn.metrics import classification_report, ConfusionMatrixDisplay import matplotlib.pyplot as plt y_pred model.predict(X_test) print(classification_report(y_test, y_pred, target_names[负面, 正面, 中性])) ConfusionMatrixDisplay.from_estimator( model, X_test, y_test, display_labels[负面, 正面, 中性] ) plt.title(Confusion Matrix) plt.savefig(report/confusion_matrix.png, dpi200)只看准确率在情感分析里会骗人。假设中性占 50%全预测中性也有 50% 准确率。我习惯先看每类的 F1尤其是负面类的召回率游客给了差评但系统漏成中性比把好评误判成中性的代价更高因为差评被漏掉会直接影响运营处理投诉的优先级。混淆矩阵要关注两个格子负面被判成中性和中性被判成负面。前者是漏报后者是误报。出现大量漏报时优先补充负面情绪的训练样本而不是急着换模型。训练集里负面样本不到 20% 时F1 上不去的可能性非常大先做数据平衡再谈算法优化。如果当前模型在验证集上 F1 达到 0.75 以上就可以接进系统。为了把 F1 从 0.75 提升到 0.8可能要花两周做标注和调参而第一版 0.7 多数情况下已经能辅助人工处理评论。4. 系统设计与实现把模型装进 Flask 接口与批量脚本模型训练完成只是解决了“有没有”的问题题目的另一半“系统设计与实现”要解决的是“怎么用”。我习惯把代码拆成数据层、模型层、接口层三部分模型训练和接口调用互不干扰以后替换模型也只动中间一层。4.1 系统总体结构数据层、模型层、接口层数据层只负责读取和写回读原始 CSV 或数据库中的评论表跑完预测写回 label、score、model_version 三个字段。模型层是核心资产放训练的 train.py、评估脚本 evaluate.py、训练产物 pkl 文件和一个 config.yaml。接口层有两条出口一条是 Flask 实时接口给前端或小程序调用另一条是离线批处理脚本每天定时分析新增评论。分层的理由是课程设计时有人把训练代码和 Flask 路由写在同一个文件里每次启动接口都重新训练一遍既慢又会让模型状态不一致。分层后模型文件由训练脚本输出接口只加载不训练。这样系统的可维护性和可演示性都强很多。模型层和接口层之间靠文件约定模型保存为 models/sentiment_v1.pkl接口读取相同的路径。如果后面要更新 v2只需要训练并保存 sentiment_v2.pkl改 config 指向它Flask 代码一行不用改。4.2 用 Flask 提供情感分析接口import joblib from flask import Flask, request, jsonify app Flask(__name__) model joblib.load(models/sentiment_v1.pkl) LABEL_NAMES {0: 负面, 1: 正面, 2: 中性} app.route(/api/sentiment, methods[POST]) def sentiment(): data request.get_json(silentTrue) or {} text (data.get(text) or ).strip() if not text: return jsonify({code: 400, msg: text 不能为空}), 400 if len(text) 500: return jsonify({code: 400, msg: text 长度不能超过500}), 400 label_id int(model.predict([text])[0]) proba model.predict_proba([text])[0].tolist() return jsonify({ code: 0, label: label_id, label_name: LABEL_NAMES[label_id], proba: proba, model_version: v1 }) if __name__ __main__: app.run(host0.0.0.0, port8000, debugFalse)接口使用 POSTbody 传 JSONget_json(silentTrue) 保证解析失败时不会直接把 500 抛给前端而是走后面的空值校验。长度限制 500 字防止有人把整篇游记传进来拖垮处理速度。predict_proba 返回三个类别的概率前端可以拿它画情感分布也可以做阈值兜底。不要把 debugTrue 带上生产环境。debug 模式会开启代码热重载和调试器在局域网环境下容易出安全风险开发时用可以交付时务必关掉。这里的预处理调用链看起来很短因为清洗和分词都被封装在 Pipeline 里。训练模块和接口模块不能各写一套把 2.3 节的 text_utils 抽出来共用接口翻车概率就低很多。4.3 批量导入与实时分析并存实时接口适合单条咨询离线脚本负责整表分析。景区每天新增的评论往往成百上千条用接口循环请求又慢又容易被限流正确姿势是一次性喂给模型。import pandas as pd import joblib model joblib.load(models/sentiment_v1.pkl) df pd.read_csv(data/daily_reviews.csv) df[label] model.predict(df[text].tolist()) df.to_csv(output/daily_reviews_labeled.csv, indexFalse) summary df.groupby(place_id)[label].agg(lambda s: (s 1).mean()) summary.to_csv(output/daily_place_summary.csv)这里把整个 DataFrame 的 text 列一次性传给 model.predict而不是循环逐条调用。model.predict 接收字符串列表一次调用就完成清洗、分词、预测效率远高于单条循环。summary 计算每个景区的正面比例结果可以画在报表上正面比例掉了就提醒运营去人工查看差评。如果数据量大于 1 万条批处理时也可以用 predict_proba 取负面概率比只看硬标签更平滑输出到报表时信息密度更高。4.4 参数配置与模型版本别“走死路”一个反复被踩的坑模型文件叫 model.pkl下次训练直接覆盖。上线后有人反馈新版本不如旧版你已经没有后悔药了。所以我从 v1 开始就给模型编号目录长这样models/ sentiment_v1.pkl sentiment_v2.pkl config/ config.yaml report/ confusion_matrix.pngmodel: path: models/sentiment_v1.pkl max_text_len: 500 version_note: 用2024年6月标注数据训练中性F1偏低 data: raw_path: data/raw_reviews.csv labeled_path: data/labeled_reviews.csv训练脚本和接口都读 config.yaml。换成 v2 时只改 path 和 version_note接口里返回的 model_version 也读这里这样前后端对模型版本有同一份记录。每次训练保存模型时我习惯在文件名前面加日期比如 20250112_sentiment_v2.pkl并把 F1 记在 version_note。这样翻旧账时能快速定位当年这批样本是怎么标、效果如何。你也许用不上但真到需要回滚的那天它就是后悔药。注意模型文件要按版本号命名并保留训练记录。否则覆盖之后想回滚只能重训时间和标注成本都不可控。5. 情感分析落地的 5 个常见问题与排查现象、原因、解决下面这五个问题是我在类似项目里反复见过的按翻车频率排序可以直接当排查清单用。5.1 短评全被判定为中性现象上线后拿真实评论一看“地方不错”“有点失望”“再也不来了”全部预测成中性正面负面一个都没有。原因有两个。第一短文本特征太少TF-IDF 向量里几乎没有有效情感词第二中性阈值画得太宽0.4 到 0.6 之间把所有低置信度样本全吞了。解决把中性区间从 0.4~0.6 收窄到 0.45~0.55同时加一条规则兜底命中“失望、踩雷、坑、不值、后悔”强制负面命中“值得、推荐、惊喜、再来”强制正面。规则不是万能的但能快速把短文本分类丢失的红利找回来。另一个做法是在训练时对 5 字以内的短句做数据增强把“不错”“挺好”这类高频词复制多份让模型有机会学到短文本里的强情感词。5.2 否定句式识别错误现象“不排队还行”被预测成负面“没那么差”被预测成中性甚至正面。原因特征抽取只用了 unigram词序信息全丢分词后的“不/排队/还行”从单个词来看“排队”偏负面模型自然把分数拉低。解决把 ngram_range 从 (1,1) 改成 (1,2)并保证停用词表里不要删除“不、没、别、无、非”这几个否定词。前者让“不排队”成为一个特征后者保留组成该特征的基本词。如果数据里否定句式很多光靠 bigram 也不够因为否定词和目标词可能隔了几个字比如“不是特别满意”。这时可以在 text_utils 里单独维护一个否定词表对匹配到的文本片段做极性翻转用后处理规则补模型的短板这不属于重新训练范围。5.3 模型在训练集上很好、上线后变差现象训练集 F1 0.85上线抽检准确率 0.6差评还经常漏。原因最常见的是训练分布和线上分布不一致。很多人拿星级映射标签而高星评论文本大多是“非常满意、下次还会来”线上真实评论里却是大量“一般、凑合、没想象中好”这些表达并没有出现在弱标签训练集里。解决上线前从原始评论里按景区类型和字数分层抽样 200 条人工标注成验证集用它替代 sklearn 随机切出的 test set你会发现 F1 马上变真实。上线后按周记录预测分布定期把新样本追加进训练数据而不是让模型永远停在 v1。模型漂移是常态定期更新才符合业务变化。5.4 不同景点类型标准不统一现象同一个模型在古城类景区表现不错换到主题乐园类景区负面召回率掉 20 个点。原因主题乐园的正面评论大量出现“排队”“项目”“刺激”古城景区却说“商业化”“同质化”词语差异比想象中大。解决能接受维护多个模型就按 scenic_type 各训练一个不想维护多个模型就把 scenic_type 当作特征拼进样本告诉模型当前评论来自哪种景区。更轻量的方案是全局模型之外再加一个类型词表预测结果按景区类型加权融合。这个思路在答辩里也更容易讲清楚你不是只调了个黑匣子而是按业务场景拆了系统。5.5 接口并发一上去就超时现象本地一条评论 50 毫秒压测 20 个并发开始排队100 个并发直接超时。原因Flask 自带 server 是单进程模型模型 predict 和 jieba 分词都会抢 CPU高并发时互相拖累。解决生产环境用 gunicorn 起 4 个 workergunicorn -w 4 -b 0.0.0.0:8000 app:app另外把 max_text_len500 加上前面 Flask 代码里的长度校验就是这个用途。如果还慢把 jieba 的缓存打开在 worker 启动时调用 jieba.initialize() 预加载避免第一个请求被分词初始化拖住三秒钟。还有一个隐藏问题每个 worker 都会 joblib.load 一次模型。逻辑回归这种小模型没问题如果升级成 BERT就要考虑共享内存或专门的模型服务不要再继续塞在 Flask 里。6. 验证与进阶让情感分析系统从“能跑”到“能信”6.1 上线前做一次人工一致性验证先准备 200 条未参与训练的线上真实评论人工标好标签用系统跑一遍按标签分别统计 P/R/F1形成下面这样一张表标签精确率召回率F1正面0.820.790.80负面0.760.700.73中性0.640.710.67这张表比整体准确率有用得多中性 F1 低说明阈值偏宽负面 F1 低说明该返回负面的样本没召回。把预测错的 20 条列出来人工翻一眼错误集中在哪几个场景直接补规则或补样本即可。6.2 把多模态情感分析作为下一个扩展点文本模型稳定之后可以再考虑视频人物情感分析、多模态情感分析这类进阶方向。旅游评论里本来就有大量图片和 emoji人脸表情、景色光线都能辅助判断。但多模态不是第一版必须做的事如果文本情感还不能覆盖大部分场景先解决文本再谈融合。我通常把多模态作为系统演进路线中“等数据积累够了”的那一步而不是为了追热点强行上。6.3 一个保底技巧把结果做成可视化报告最朴实的交付是让系统输出能看懂的图表而不只是接口返回 JSON。这一步本质上是 Python 数据分析与可视化的落地不用追求图表花哨而是让周均值曲线能直接暴露差评波动。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(output/daily_reviews_labeled.csv) df[date] pd.to_datetime(df[post_time]) weekly df.set_index(date).resample(W)[label].mean() weekly.plot(titleWeekly Sentiment Score) plt.savefig(report/weekly_sentiment.png, dpi200)这里的 label 是 0 负面、1 正面、2 中性周均值在 0.9 以下就说明负面反馈增多该触发人工审核。把这样的输出配上训练脚本、接口代码和 README就是一个能拿得出手的完整系统交付。我的最后一个习惯是每次模型迭代都留一个错误案例集把测试集里搞错的文本单独存成一个 CSV下次改进完先跑这个集合看老问题是否复发。这个习惯帮我避免了很多次“修好一个又弄坏一个”的返工。以上是我做旅游景点评论情感分析系统时的完整落地路线希望帮到你。本文还有配套的精品资源点击获取