“基于Python的旅游网站用户评论分析与旅游推荐系统”这个项目,我前前后后做了大概一个多月。起因很简单,我发现自己出门旅游前,在各大平台看景点评论看到头大,有人夸有人骂,短评里全是高频词,根本分不清一个景点到底值不值得去。后来我就想,能不能用Python把这些零散评论抓下来,做一轮情感分析和标签提取,再直接生成一个“懂你”的推荐列表。于是就有了这套系统。

这套系统到底能干什么?简单说,三件事:抓取旅游网站的景点评论数据,对评论文本做清洗、分词、情感打分和主题标签提取,最后基于用户历史行为构建推荐模型,输出Top-N景点推荐。适合谁看?如果你是Python学习者,想找一个综合了爬虫、文本挖掘、推荐算法的完整练手项目,或者你是产品经理、运营,想用技术手段快速了解用户对景点的真实评价,这篇文章都值得读完。我尽量把每一步都拆开讲清楚,包括踩过的坑和调参经验。

1. 项目要解决什么问题:一个构建想法到落地的全记录

1.1 为什么把评论分析和旅游推荐放在一起做

大部分旅游App都做推荐,但很多只是基于用户浏览行为或者简单评分。评分这种数据太粗糙了,打4分和打5分之间差距很大,评论里“人少景美”“设施陈旧”这样的信息,在分数里完全体现不出来。评论分析的价值,就是把非结构化的文本变成结构化标签,比如“适合亲子”“拍照出片”“排队严重”,这些才是用户真正关心的维度。

推荐系统的常见问题是冷启动,新用户没有行为记录,协同过滤根本跑不动。这时候评论分析就能顶上:新用户只要输入自己关心的关键词,比如“小众”“带娃”“爬山”,系统就能从景点评论的主题分布里找到匹配的候选集。也就是说,评论分析不只是为了分析,它还能作为推荐系统的特征来源,两者天然互补。这个思路,是我觉得整个项目最有价值的地方。

1.2 系统的核心功能与目标用户拆解

我先列一个功能清单,做项目前把边界划清楚很重要,不然需求会无限膨胀:

  • 评论数据采集:从旅游网站爬取热门景点评论,包含景点名称、评论内容、评分、用户名、评论时间。
  • 文本预处理:清洗HTML标签、去重、去除无意义短句、中文分词、去停用词。
  • 情感分析:对每条评论输出情感得分,区间0到1,越接近1越正向,并汇总成景点级情感指数。
  • 主题标签挖掘:通过TF-IDF和LDA主题模型,提取每个景点的热门主题词,人工映射成“亲子游”“性价比高”之类的标签。
  • 用户画像与推荐:基于用户对景点的历史评分(包括自己打的分和评论情感折算分)构建协同过滤模型,再融合内容特征的匹配分,输出最终推荐列表。
  • 可视化面板:用Flask搭一个简单的Web页面,展示景点情感分布、关键词云、推荐结果。

目标用户有两类:一类是普通游客,用系统的Web界面输入偏好词得到推荐;另一类是研究人员或运营同学,通过分析结果观察竞品景点的口碑变化。我当时主要是给自己用,顺便写成了一个可复用的项目结构。

2. 技术选型与整体架构:为什么用这套Python组合拳

2.1 爬虫与数据处理工具链

抓评论我用的是 requests + BeautifulSoup ,没有上Scrapy。原因很直接:目标站点数据量不大,我只需要几百个景点的评论,Scrapy的分布式、中间件这些能力用不上,反而增加学习成本。但如果你要抓整个平台的评论,日均几万条,建议直接上Scrapy,它的并发控制和去重机制更完善。

数据清洗和结构化用了 pandas ,这是Python数据分析的标配。pandas处理CSV、Excel、数据库读取都很方便,尤其做groupby聚合、merge连接这类操作,比手写循环快得多。我的原始评论表大约12万条,清洗后剩11万条左右,pandas处理起来毫无压力。

存储层面我选了SQLite,因为项目比较轻量。评论表存原始数据,景点表存景点静态信息,用户行为表存用户点击和评分记录。你如果用 pymysql ,改一下连接配置就能迁移到MySQL,并不复杂。

2.2 文本分析与推荐算法选型

文本分析方面,分词用的是 jieba ,我试过 pkuseg ,准确率更高但速度慢,对11万条评论来说太耗时。jieba支持自定义词典,我把景点名、旅游常用词“遛娃”“打卡”“出片”都加进去了,分词效果提升明显。情感分析用 SnowNLP ,它自带一个训练好的中文情感模型,开箱即用,但在旅游场景下会有偏差,我后面会讲怎么修正。

推荐算法用的是 surprise 库里的协同过滤,配合自写的基于内容的相似度计算。surprise库封装了SVD、KNN等多种算法,非常适合做离线实验。我最终选的是KNNBasic(皮尔逊相关系数),原因在于数据稀疏度高、可解释性强,适合给用户展示“因为你看过A,所以推荐B”。

2.3 系统模块划分与数据流

整个系统的数据流是这样的:爬虫模块先采集原始评论,存入评论表;然后预处理模块做清洗和分词,生成干净的文本特征文件;接着情感分析模块输出情感得分,LDA主题模型输出主题标签;推荐模块读取用户评分数据和景点标签数据,分别生成协同过滤推荐结果和内容匹配结果;最后Flask应用把结果展示在前端页面。

模块之间通过中间文件解耦,比如预处理完的数据存成 processed_comments.csv ,情感分析读完再输出 sentiment_results.csv ,这样任何一个模块挂了,不需要全部重跑,调试的时候特别省心。很多初学者写项目把所有逻辑堆在一个py文件里,改一处坏三处,我强烈建议按职责拆分开。

3. 评论数据采集与预处理:从零构建你的数据库

3.1 爬虫实现要点与反爬应对

爬取旅游网站评论,第一步要分析页面结构。我优先选择Json接口,很多Web站点的评论区是通过Ajax加载的,直接在返回的Json里就能拿到评论数据,效率比解析HTML高很多。你可以在浏览器开发工具里切到Network,刷新评论区,找XHR请求,一般能看到一个类似 getCommentList 的接口。

请求头要模拟完整,User-Agent、Referer、Cookie一个都不能少。我一开始只放了User-Agent,结果请求几次就被识别了。后来加上Referer和Cookie,再把请求间隔设成3到6秒随机值,稳定跑了一晚上没被封。这里有个所有人都该有的基本共识:爬虫要尊重网站的robots协议,只抓公开数据,控制请求频率,不要给目标服务器造成压力。

数据解析我用BeautifulSoup,代码片段大致是这样的:

import requests
from bs4 import BeautifulSoup

def parse_comments(html):
    soup = BeautifulSoup(html, 'html.parser')
    comment_items = soup.select('div.comment-item')
    data = []
    for item in comment_items:
        content = item.select_one('span.content').get_text(strip=True)
        rating = item.select_one('span.rating').get('data-score')
        user = item.select_one('a.username').get_text(strip=True)
        date = item.select_one('span.time').get_text(strip=True)
        data.append({
            'content': content,
            'rating': rating,
            'user': user,
            'date': date
        })
    return data

3.2 数据清洗不可忽视的细节

拿到原始评论后,清洗是最费时间的环节。常见的脏数据有几类:空评论、纯表情、重复评论、明显广告、超长短句异常值。我用pandas做去重,按 用户名+评论内容+评论时间 三个字段联合去重,避免误删同一个用户在不同时间的真实评价。

文本预处理方面,去除英文、数字、特殊符号,保留中文和基本标点。这里要注意,旅游评论里“5A景区”“3小时”是有信息量的数字,直接全删会丢失上下文。我的做法是把数字统一替换成 NUM 占位符,这样模型能识别“游玩时长”这类特征,又不会被具体数字带偏。

分词后要过滤停用词。网络上有很多现成的中文停用词表,但旅游领域需要自己补充,比如“景区”“景点”“地方”“感觉”这类词,在大部分场景下没有区分度。我维护了一个自定义停用词表,每次跑完分词结果扫一遍,发现问题就加进去,迭代三轮之后效果好了很多。

3.3 Jieba词典配置与额外优化技巧

jieba支持加载自定义词典,格式是一行一个词,后面可以跟词频和词性。我把“亲子游”“性价比”“自然风光”“玻璃栈道”“索道排队”这类词都加了进去,确保它们不被错误切分。比如“玻璃栈道”如果不加词典,可能被切成“玻璃/栈道”,对后续主题提取影响很大。

pip install jieba

自定义词典 custom_dict.txt 内容示例:

玻璃栈道 10 n
亲子游 20 n
性价比 15 n
打卡 20 v
出片 10 v

加载方式很简单:

import jieba
jieba.load_userdict("custom_dict.txt")

还有一个容易被忽略的优化:jieba的 cut 函数默认是全模式,会输出所有可能的分词结果,很多词是冗余的。我用的是 jieba.cut(text, cut_all=False) ,精确模式,再配合 jieba.lcut 直接返回列表。处理11万条评论时,再加 suggester 关闭、HMM开关调优,整体性能提升20%左右,实测几分钟就跑完。

4. 评论情感分析与主题标签挖掘:把文字变成数据

4.1 SnowNLP情感分析结果为何“偏负”,以及如何修正

SnowNLP其实很适合做初版情感判断,但它的训练语料偏社交媒体,用在旅游评论上有一个明显问题:对“还行”“一般”这类中性偏正向的表达判断过于保守,容易打低分。我抽样了200条评论人工标注,发现SnowNLP平均打分比人工低0.15左右,正向召回率不高。

修正方案有两个思路。一是自己重新训练模型,把爬下来的评论人工标注5000条,用SnowNLP的贝叶斯训练接口重新训练,效果会提升不少,但人工标注成本很高。二是做一个基于规则校准:如果评论里包含特定正面词(如“推荐”“值得”“美”“好吃”“方便”),且SnowNLP得分低于0.4,就向上修正到0.6;如果包含负面词(如“坑”“后悔”“脏”“差”),且得分高于0.7,就向下修正到0.3。我最终用的是第二种,修改简单且效果可解释。

4.2 景点级情感聚合与评分推断

每条评论得到情感得分后,就可以按景点聚合,计算情感均值。但直接平均有一个问题:评论数很少的景点,情感均值波动很大。我用的是贝叶斯平均的思路,把整体均值作为先验,样本量越少越向先验靠拢。这个处理方法在排行榜类场景非常实用,避免了“只有两条好评的小众景点冲上第一”的尴尬。

这里再引入一个关键点:用情感得分推算“隐性评分”。很多评论是游客不评星的,只有文字,怎么作为推荐模型的输入?我把情感得分按0.2为一档,映射成1到5星的整数评分。比如情感得分0.0到0.2映射为1星,0.8到1.0映射为5星。这样既扩展了评分数据量,也保持了评分尺度的一致性。

4.3 LDA主题模型与景点标签生成流程

标签挖掘我用的是TF-IDF加LDA。先对每个景点的所有评论合并成一篇“文档”,构建语料库,用TF-IDF过滤掉低频词和高频无意义词,再用LDA训练主题模型。主题数K的选择很讲究,我试过K=5,主题混杂度太高;最终K=8加上人工校准,效果最理想。

每个主题会输出若干个高概率词,比如主题A可能是“爬山”“风景”“累”“索道”,主题B可能是“孩子”“乐园”“亲子”“设施”。我会根据这些词人工命名成标签:“登山徒步”“亲子游乐”。然后计算每个景点在各个主题上的概率分布,取Top3作为该景点的最终标签,存入景点特征表。这套流程虽然要人工介入,但在可控成本内获得的质量比纯无监督高很多。

4.4 评论关键词云和情感趋势怎么做

可视化部分,我做了每个景点的Top20高频词云,以及评论量随月份的情感走势折线图。词云用的是 wordcloud 库,要注意中文字体路径设置,否则画出来全是方块。情感趋势分析对运营很有价值,比如一个景点某月情感值突然下降,多半是出了负面事件,可以及时预警。

5. 旅游推荐系统从0到1:协同过滤与混合推荐实战

5.1 构建用户-景点评分矩阵的细节

推荐系统的输入是用户对景点的评分矩阵。创建过程需要把用户名和景点名映射成数值ID。这张表的来源是两部分:用户主动打分和评论情感推算评分。我按7比3加权融合,主动打分的权重更高,比如最终评分 = 0.7 * 主动评分 + 0.3 * 情感推算评分。

矩阵构建完,一定要检查稀疏度。11万条评论对应1万个用户和300个景点,稀疏度99%以上是常态。稀疏度太高的后果是协同过滤找到的邻居不可靠。我的处理方式是过滤掉评论数少于3条的用户和评分人数少于10的景点,把矩阵缩小到可操作范围。虽然丢失了一部分数据,但推荐质量显著上升。

5.2 基于物品的协同过滤为什么更适合旅游场景

我最终采用的是Item-Based Collaborative Filtering。原因很简单:旅游景点的数量远小于用户数量,计算物品相似度的开销比用户相似度小得多,而且景点之间的相似关系相对稳定,不需要像用户相似度那样频繁更新。景区“张家界”和“黄山”的相似度高,是因为它们都被喜欢自然风光的人评价过,这个逻辑对用户解释起来也通顺。

代码上用surprise的KNNBasic,相似度度量选皮尔逊相关系数,代码如下:

from surprise import KNNBasic, Dataset, Reader
from surprise.model_selection import train_test_split

reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(ratings_df[['user_id', 'item_id', 'rating']], reader)
trainset, testset = train_test_split(data, test_size=0.2, random_state=42)

sim_options = {
    'name': 'pearson',
    'user_based': False  # 基于物品
}
algo = KNNBasic(sim_options=sim_options)
algo.fit(trainset)
predictions = algo.test(testset)

5.3 基于内容的推荐:解决冷启动问题的关键模块

协同过滤对新用户无解,这时候内容推荐顶上。我构建的景点特征向量,就是第4章提取的标签分布加TF-IDF词向量。用户进入系统时,如果还没有评分记录,就要求他选择3个兴趣标签,比如“自然风光”“亲子游”“美食”,然后计算用户偏好向量和景点特征向量的余弦相似度,推荐Top-N。

内容推荐的关键是特征向量构建。我用了两种特征拼接:标签独热向量(8维)和TF-IDF关键词向量(取Top50,共58维)。相似度计算要注意向量长度差异大带来的偏差,比如标签向量数值总体偏小,容易淹没在关键词向量里。我把两个相似度分别计算,再加权融合,权重各0.5,效果比直接拼接向量好很多。

5.4 混合推荐策略与Top-N生成

最终推荐采用加权融合。如果有用户行为数据,协同过滤得分占70%,内容匹配得分占30%;如果是新用户,就直接用内容匹配得分。融合前要把两套得分归一化到0到1之间,否则量纲不一致会导致某一部分主导结果。归一化我用的min-max方法,但要注意剔除异常值后再做,否则个别极端分数会把整体区间拉得很扁。

最后,推荐结果需要做多样性优化:如果Top10全是类似景点,用户体验很差。我加了简单的MMR(最大边际相关性)重排,在相关性和相似度之间取平衡。具体公式是 MMR = 相关性 - lambda * 与已选物品的最大相似度 ,lambda取0.5,这个值是拍出来的,不同场景可以微调。

6. 系统展示与效果评估:推荐结果到底准不准

6.1 Flask Web界面与交互流程

我用Flask搭了很简单的Web界面,一个输入页,一个结果页。输入页收集用户选择的历史景点(或兴趣标签),结果页展示推荐Top10,每个景点附带情感指数、Top3标签、近期评论摘要和推荐理由。推荐理由是从“相似用户喜欢过”或“标签匹配”里自动生成的,例如“因为你喜欢自然风光类景点,而黄山和张家界在同类标签下评价很高”。

前端我没花太多心思,就是原生HTML加Bootstrap,数据通过Jinja2模板直接渲染。你要做更复杂的交互,可以考虑加个Vue或者React前端,但这套系统的核心价值在数据和推荐逻辑上,界面够用就行。

6.2 离线评估指标与实验对比

离线评估我用了经典的留一法:对每个用户随机隐藏一条已评分的景点,然后让推荐系统给这个用户打分,检查被隐藏的景点是否出现在推荐列表里。核心指标是精确率、召回率、覆盖率和新颖度。

实测下来的数据是这样的:纯协同过滤精确率约11%,覆盖率只有38%;混合推荐精确率提升到15.6%,覆盖率达到61%。覆盖率提升主要来自内容推荐模块,它能发现协同过滤看不到的长尾景点。召回率提升不明显,这和数据稀疏度有关,冷门景点评分太少,模型很难学。

我还做了一个小实验,对比“只用主动评分”和“主动评分+情感推算评分”的效果,后者RMSE略有下降,精确率提升约3个百分点。这说明评论情感分析确实能补充行为数据,让推荐更贴近用户真实感受。

6.3 一个真实案例的回放:从评论到推荐链路

我拿“杭州西湖”举一个例子。爬取到的评论经过清洗后剩800条,情感分析均值得分0.74,贝叶斯修正后0.71,映射为4星。LDA主题分布显示“自然风光”概率最高,“亲子游”次之,“文化古迹”第三。所以西湖的推荐标签是“自然风光+亲子游+文化古迹”。

测试用户A的历史记录中有灵隐寺和西溪湿地,评分分别为5星和4星。协同过滤找到与灵隐寺相似度最高的5个景点,西湖排第一,相似度0.63。内容匹配部分,用户偏好向量与西湖标签向量相似度0.58。混合后西湖综合得分最高,成功进入推荐Top3。这个链路在代码里跑通时,我对整个系统的信心一下就上去了。

7. 避坑实录:数据、算法、工程三个维度的经验总结

7.1 数据采集与清洗阶段踩过的坑

第一个坑是爬虫返回的评论不完整。有些网站把长评论默认折叠,需要点击“展开”才能加载全文,如果不处理,拿到手的就是一串省略号。我后来发现评论接口有一个 full=true 的参数,直接传参数就能拿全文。建议你在分析接口时多观察参数。

第二个坑是评论清洗时漏掉了繁体字,导致同一句话被切成不同结果。我在分词前统一做了 zhconv 转简体,这样情感分析和主题模型的结果稳定多了。

第三个坑是评论时间字段格式不统一,有的“昨天”、有的“2024-03-15”。我需要把“昨天”“一周前”都换算成日期,写了一个时间归一化函数,避免后续分析时报错。

7.2 情感分析的“假阴性”问题排查

有个现象让我困惑了很久:一条评论写“景区人太多了,排队三小时,但风景确实值得”,SnowNLP给的情感得分是0.3,明显偏低。原因是“人太多”“排队三小时”这些负面词汇被模型捕捉到,但用户真正的满意度却是中偏正。

这种转语言现象很难靠单一模型解决。我的补充策略是引入“转折词分析”:如果评论里同时出现“但是”“不过”“然而”等转折词,就重点考察转折词后面的内容。实现起来就是用正则把评论拆成两个分句,对转折词后面的分句情感分加权到70%。这个规则在人工校验集上提高了9个百分点的准确率。

7.3 推荐模型稀疏矩阵和性能优化

协同过滤最怕稀疏矩阵。我一开始不敢过滤数据,精确率只有7%,推荐结果全是热门景点。加了最小评论数过滤后,精确率升到11%,但覆盖率和多样性又下降了。最后我引入基于物品的协同过滤加内容推荐混合,才在保证覆盖率的同时不过度牺牲精确率。

性能方面,surprise在训练KNN模型时,默认会用所有数据计算相似度,数据量大时内存容易爆。我的做法是设置 k=50 ,只保留每个物品最相似的50个邻居,训练时间从十几分钟降到两分钟,推荐效果几乎无损。

7.4 新手最该避开的三个“大坑”清单

  • 不要跳过数据清洗直接建模。文本数据里带HTML标签、多余空白、emoji表情,都会让分词和情感分析结果失真,这一步不做,后面全白搭。
  • 不要把情感分析和推荐完全割裂。这两块共用一套清洗后的数据,情感分能当评分用,主题标签能当特征用,割裂开来等于白白丢掉信息。
  • 不要盲目追求算法复杂度。我最后用的模型加起来不到150行代码,核心就是KNN加余弦相似度,但效果比开始用的SVD还好。在小数据集上,简单模型加好特征,往往赢过复杂模型加脏数据。

8. 最后再分享一个我实测有效的扩展方向

我在做完这套系统之后,发现还能往两个方向扩展。一个是把评论数据的实时性做起来,用定时任务每天增量抓最新评论,配合情感均值变化画趋势曲线,就能做出景区口碑预警。另一个是引入地理信息,计算用户所在城市和景点之间的距离,在推荐结果里增加“周边推荐”模块,对实际出行更有参考价值。

还有一个体验上的小细节值得做:推荐页给每个景点生成一句话评论摘要,从高赞好评和典型差评里各挑出一条,让用户对景点有更直观的印象。我用LDA选评论的典型性,再按情感得分排序选正负面代表,效果很自然,看起来比冷冰冰的评分真实得多。

我在实际使用这套系统的过程中最大的体会是, 数据质量决定效果上限,模型只是逼近这个上限的手段 。你花三天清洗出来的数据,作用可能超过花三天调参。这种以数据为中心的思路,放到任何推荐系统项目里都适用。如果你也准备做类似的方向,我的建议是:先把一条评论从爬取到展示的完整链路跑通,再逐步加模型、调指标,这个顺序踩坑最少。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐