ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的旅游景点评论分析系统:从爬虫到可视化全流程实战

基于Python的旅游景点评论分析系统:从爬虫到可视化全流程实战 简介这是一套面向数据分析初学者与旅游行业数字化实践者的Python评论分析系统聚焦于从海量旅游平台用户评论中提取情感倾向、关键词热度与主题分布解决景区口碑监测与游客需求洞察的实际问题。资源包共102个文件含32个核心Python脚本涵盖数据爬取、清洗、TF-IDF特征提取、LDA主题建模及情感分类、10个Vue前端页面实现分析结果可视化展示、6个PNG图表与3个HTML报告模板配合JSON配置、JS交互逻辑及SCSS样式文件构成前后端分离的完整分析闭环压缩包大小为47.7MB。已有675人学习下载提供可直接运行的工程结构、带注释的算法实现、预置测试数据集及清晰的README说明特别适合课程设计、毕业项目或文旅类数据分析实战训练开箱即用且便于二次开发。 之前接需求时经常要把某景区、某乐园的海量评论从头翻到尾判断设施好不好、服务到不到位。刚开始几万条评论还能靠肉眼硬刷刷到后面整个人都是麻的——评价里有夸的、有骂的、有阴阳怪气的还有纯纯打广告的根本不是一个好评率86%能概括的。后来我把这套调研流程沉淀成了基于 Python 的旅游景点评论分析系统爬取评论、清洗噪声、中文分词、情感打分、主题聚类最后叠一层可视化界面从想法到落地跑通也就一个周末的事。这篇东西的目标读者不是算法工程师而是手里攒了一堆景点评论、想快速摸清游客真实反馈的运营、产品、独立开发者。你不需要懂太深的理论照着下面的链路一步步走就能把很多条评论变成几个能看懂的结论。1. 系统总体设计先想清楚分析评论到底要做什么很多初学者拿到这个题目第一反应是去写爬虫把携程、去哪儿、马蜂窝的评论一股脑抓下来存进 CSV 就认为项目结束了。但分析系统的重点从来不是数据有多少而是你能从数据里提炼出什么。我的做法是先拆解需求把整个项目分成四个层次每层只干一件事。第一层是采集层。这个层需要解决的是评论从哪里来、用什么方式抓、怎么保存。第二层是清洗层。真实评论的噪声比你想象得多平台会插推荐语、用户会刷短句、表情符号和 HTML 标签混在一起这一层要做的是把有效的中文文本抽出来。第三层是分析层也是整个系统的灵魂。这里要做中文分词、情感判断、关键词提取和主题聚类让机器能看懂每句话到底在夸什么、骂什么。第四层是展示层把分析结果变成图表、词云、报表甚至可以做成一个简单的 Web 界面让团队里不懂代码的人也能去操作。这个四层结构对应到实际代码就是一套标准的 Python 数据工作流。爬虫部分用requests加BeautifulSoup数据处理用pandas中文分词用jieba情感分析可以结合SnowNLP和自建词典主题建模用gensim的 LDA可视化用matplotlib、wordcloud和pyecharts。如果你想把项目做成服务后端可以考虑 FastAPI 或 Flask如果只是想快点看到效果直接用 Streamlit 更省事。项目目录我建议这样组织scenic_comment_system/ ├── crawler/ │ ├── spider.py # 采集评论 │ └── comments_raw.csv # 原始评论存储 ├── analysis/ │ ├── cleaner.py # 数据清洗 │ ├── segmentation.py # jieba分词 │ ├── sentiment.py # 情感分析 │ └── topic_model.py # LDA主题建模 ├── visualization/ │ ├── wordcloud.py # 词云 │ ├── charts.py # 统计图表 │ └── app.py # Streamlit界面 ├── data/ │ ├── custom_dict.txt # 领域词典 │ ├── stopwords.txt # 停用词表 │ └── comments_clean.csv # 清洗后的数据 └── requirements.txt这个结构不是我拍脑袋定的而是踩了坑之后形成的习惯。早期我把所有代码写在同一个 Jupyter Notebook 里爬虫、清洗、分析混在一起数据和逻辑边界不清。后来数据量上来了每次重新运行都要从头爬一遍接口一变就要改好几处代码维护成本极高。拆成独立模块之后每一层都能单独调试爬虫挂了不影响已经清洗好的历史数据分词词典调整了也不用重跑一遍网络请求。这里有个很关键的选型问题为什么分词一定要用 jieba而不是正则去匹配好评差评喜欢这种关键词因为用户评论是自然语言性价比高和价格不便宜都是关于价格的看法但前者是正向、后者是偏中性的表达排队两小时和排队时间不长都涉及排队但体验完全相反。单纯靠关键词正则你会漏掉大量隐含情感。而分词只是为了把句子切成有意义的词真正判断情感还需要配合情感词典或模型这一步后面会详细展开。2. 评论采集从静态页面到异步接口的爬虫实战2.1 先看页面结构再写爬虫写爬虫最忌讳一上来就复制别人的代码。不同的旅游平台页面结构差别很大有的评论直接渲染在 HTML 里用BeautifulSoup就能解析有的评论是前端 JS 异步加载的得去模拟接口。所以我拿到一个网站第一步永远是打开浏览器开发者工具切到 Network 面板然后手动翻一页评论看网络请求发了什么。如果是静态 HTML你会看到一个网页文档请求里面包含评论内容。这种最简单直接解析即可。如果是异步加载你会找到一个 JSON 接口里面返回的是评论的数组这种反而更适合爬——JSON 结构化程度高不用做太多解析。但异步接口通常有签名参数比如把请求时间和密钥拼接后做 MD5再附带到 URL 上这就比较麻烦。实际操作中我见过用requests直接请求接口、把返回的 JSON 丢进pandas.DataFrame的做法代码量最短成功率也最高。假设我们处理的是静态 HTML基础采集代码可以这样写import requests import pandas as pd from bs4 import BeautifulSoup from time import sleep headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, } def fetch_comment_page(url, page_num): params {page: page_num} resp requests.get(url, headersheaders, paramsparams, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) comments [] for item in soup.select(.comment-item): content_tag item.select_one(.comment-content) if content_tag: comments.append(content_tag.get_text(stripTrue)) return comments all_comments [] for page in range(1, 6): try: page_comments fetch_comment_page(https://example.com/scenic/1001, page) all_comments.extend(page_comments) print(f第 {page} 页采集 {len(page_comments)} 条) sleep(1.5) # 控制请求频率 except Exception as e: print(f第 {page} 页采集失败: {e}) comments_df pd.DataFrame({comment: all_comments, source: example.com}) comments_df.to_csv(data/comments_raw.csv, indexFalse, encodingutf-8-sig)这段代码里有两个容易忽略的细节。一个是resp.encoding utf-8很多新手不设置编码抓下来的评论全是乱码——因为部分服务器的响应头里没有指定 charsetRequests 会按照默认编码去猜测。另一个是sleep(1.5)这是给自己留的礼貌缓冲避免短时间内请求过多触发封锁。真要说起来控制频率的优先级高于伪装 Headers你要是每秒发几十个请求换什么 UA 都没用。2.2 清洗层把评论从噪声里捞出来原始评论拿到手后第一件事不是分析而是清洗。旅游平台的数据质量其实不高常见噪声包括噪声类型示例处理方式HTML标签或控制字符p环境不错/p、换行符用正则或 BeautifulSoup 清除平台插入的推荐语我的旅行愿望清单根据固定文案匹配删除纯标点/纯表情、按长度过滤重复评论同一用户复制多条哈希去重广告或导流加微信 xxx关键词过滤我自己的清洗函数大概长这样import re import hashlib def clean_comment(text): if not isinstance(text, str): return None # 去HTML标签 text re.sub(r[^], , text) # 去网址 text re.sub(rhttps?://\S|www\.\S, , text) # 只保留中英文、数字、常用标点 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、~#%……*], , text) text re.sub(r\s, , text) # 过滤过短内容 if len(text) 4: return None return text.strip() comments_df[clean_comment] comments_df[comment].apply(clean_comment) comments_df comments_df.dropna(subset[clean_comment]) comments_df comments_df.drop_duplicates(subset[clean_comment])去重这里我多说一句很多人直接对文本去重但平台上同一个用户在不同时间发的相同内容、或者不同用户转载的相同文案匹配度很高。更稳妥的做法是对清洗后的文本做 MD5 哈希再按哈希去重这样既降低内存开销也避免长文本对比的性能问题。代码里hashlib模块就是干这个的先把 DataFrame 里的clean_comment字段变成哈希值存一列再drop_duplicates比直接文本去重要快得多。清洗完的数据建议存成 UTF-8 带 BOM 的 CSV 格式encodingutf-8-sig这个编码方案在 Windows 的 Excel 里打开不会乱码。很多人在这一步偷懒用默认的utf-8结果后续拿给运营看报表的时候所有中文都变乱码技术上看是小事体验上却非常掉链子。3. 中文评论的分词与情感判断从字到义的关键一跳3.1 为什么中文分析必须先分词中文和英文最直观的区别是没有空格一句话连在一起这家店环境很好但价格偏高如果不用分词机器看到的是一整串字符。情感分析、关键词提取、主题聚类都建立在一个前提下语义的最小单位是词而不是字。所以分词是水龙头后面的分析都是在喝水。jieba是目前最常用的中文分词库它基于前缀词典实现高效的词图扫描再通过动态规划查找最大概率路径。名词解释一大堆但实际使用很简单import jieba text 这家店环境很好但价格偏高排队时间有点久 words jieba.lcut(text) print(words) # 输出[这家, 店, 环境, 很好, 但, 价格, 偏高, , 排队, 时间, 有点, 久]但对于旅游场景jieba的默认词典存在一个明显短板它不认识景点专属名词。云上草原会被切成云上/草原松赞林寺可能切成松/赞/林寺。问题在于用户评论里景点名出现频率很高切碎了会导致后续统计和情感分析结果失真。解决方案是加载自定义词典jieba.load_userdict(data/custom_dict.txt)custom_dict.txt的格式每行三个字段用空格隔开词、词频、词性。词频可以不填但填了效果更稳定。比如云上草原 20 nz 长隆欢乐世界 20 nz 松赞林寺 20 nz 外滩观景台 20 nz我在实际项目中维护的词典一般有两类词一类是景点名称和园区内设施名另一类是评论里反复出现的口碑词比如出片遛娃避雷等新潮表达。自定义词典能让jieba在切分时优先把这些词作为整体输出准确率提升非常明显。3.2 情感分析专属词典打分 vs 预训练模型情感分析的目的是给每条评论打一个正负向分数。我试过两条技术路线一条是用现成的SnowNLP直接算sentiments另一条是自己构建情感词典做加权打分。两条路各有优劣我的最终方案是两者结合。先看SnowNLP的用法from snownlp import SnowNLP text 这里的风景太美了孩子玩得很开心 s SnowNLP(text) print(s.sentiments) # 0.99表示非常正向SnowNLP底层是一个朴素贝叶斯分类器用购物评论的数据训练过开箱即用对通用语料效果尚可。但它有个致命问题面向购物领域的模型真的不理解旅游场景。风景优美它判断为正向没问题但山路有点陡它可能给个负向而实际上游客可能只是在中性描述。人很多在旅游评论里往往是负面信息在购物评论里却可能被理解为中性。所以我更推荐自己搭一套情感词典打分器规则透明、可调可控也方便解释。我的情感打分器核心逻辑是这样的维护一份正向情感词集合比如赞、漂亮、震撼、值得、划算、舒适、推荐。维护一份负向情感词集合比如差、坑、失望、贵、破、脏、排队久。引入否定词和程度副词作为权重修正。否定词如不、没、别会把情感方向反转程度副词如很、超级、特别、非常会放大情感强度。import jieba positive_words set([赞, 漂亮, 震撼, 值得, 划算, 舒适, 推荐, 方便, 干净, 热情, 好玩, 喜欢, 满意, 美]) negative_words set([差, 坑, 失望, 贵, 破, 脏, 排队, 拥挤, 态度差, 后悔, 不值, 浪费时间, 无聊]) negation_words set([不, 没, 无, 别, 不太, 不是很]) degree_dict {很: 1.6, 非常: 2.2, 特别: 2.0, 超: 2.0, 太: 1.8, 有点: 0.7, 稍微: 0.8, 比较: 1.2} def sentiment_score(text): words jieba.lcut(text) total_score 0.0 i 0 while i len(words): word words[i] weight 0 if word not in positive_words and word not in negative_words: i 1 continue if word in positive_words: weight 1.0 elif word in negative_words: weight -1.0 # 回溯前面最多3个词寻找否定词和程度副词 neg_factor 1.0 degree_factor 1.0 j i - 1 while j 0 and j i - 3: if words[j] in negation_words: neg_factor * -1.0 if words[j] in degree_dict: degree_factor max(degree_factor, degree_dict[words[j]]) j - 1 total_score weight * degree_factor * neg_factor i 1 return total_score这个规则的准确性不如深度学习模型但它最大的好处是阈值可控。比如我可以把所有分数大于 0.2 的评论定义为正面小于 -0.2 的定义为负面中间的是中性。业务方要调整口径只需要改词典或者调阈值比让模型重新训练要快得多。3.3 抽一批人工标注验证情感结果的靠谱度情感分析跑完后一定要做抽检。我自己踩过几次坑就是算法跑完直接上报表结果运营反馈这个景区好评率怎么比OTA平台高这么多——原因就是词典里漏了当地方言或特定表达。比如某些地区游客常用巴适绝绝子上头这类词不提前加入词典模型就会把它们当成中性导致好评率虚高。抽样验证的做法很简单从清洗后的数据里随机抽 300 条人工打标 1正面、0中性、-1负面再和算法的打分对比计算准确率。如果准确率低于 80%就要回头去补词典。这步虽然麻烦但在真实项目里是让系统可信的必经之路。毕竟老板看的不是你的代码而是你给出来的结论准不准。4. 游客关注点挖掘让干净整洁和排队久自动聚成不同主题4.1 高频词统计先看大家都在聊什么情感分析回答了游客对整体是褒是贬但没回答游客到底在关心什么。要回答这个问题先做高频词统计。把分词后的结果去掉停用词统计词频取前 30 个词画柱状图基本就能看出一个景区的口碑关键词。停用词表要单独维护不能直接把网上下的通用中文停用词表拿来就用否则会漏掉一些关键信息。比如景区这个词在通用停用词表里没有但在几乎所有评论里都出现如果不加进停用词词频榜第一永远是景区毫无分析价值。我处理词频的时候通常还会做一个额外的过滤只保留长度大于等于 2 的词同时把我们、你们、可以、还是这类虚词塞进停用词表。4.2 LDA 主题建模把数千条评论压缩成几个可解释的话题高频词只能给我们零散的印象风景、门票、排队、孩子各自频率很高但它们之间是什么关系哪些词经常一起出现在同一类评论里这正是主题建模的用武之地。我用的 LDALatent Dirichlet Allocation是一种经典的概率主题模型它假设每篇文档一条评论是由若干主题混合而成每个主题又是一组词的概率分布。拿到结果后人只需要看每个主题的高概率词就能判断噢这个话题讲的是交通这个话题讲的是亲子设施。代码实现如下from gensim import corpora, models import jieba def load_stopwords(path): with open(path, r, encodingutf-8) as f: return set([line.strip() for line in f]) stopwords load_stopwords(data/stopwords.txt) texts [] for comment in comments_df[clean_comment].tolist(): words jieba.lcut(comment) words [w for w in words if w not in stopwords and len(w.strip()) 1 and not w.isdigit()] texts.append(words) dictionary corpora.Dictionary(texts) dictionary.filter_extremes(no_below2, no_above0.8) corpus [dictionary.doc2bow(t) for t in texts] lda_model models.LdaModel( corpuscorpus, id2worddictionary, num_topics5, passes15, random_state42, ) topics lda_model.print_topics(num_words8) for topic in topics: print(topic)num_topics5是我个人常用的起点不是拍脑袋。主题数设置太少会把排队久和卫生差硬揉成一个主题设置太多每个主题的词都没什么区分度。实操里我习惯用可解释性优先的方法分别跑 3、5、8、10 个主题看一眼每个主题的高频词是否像在描述同一个话题。如果某个主题的词是风景、门票、排队、吃饭、孩子这种乱七八糟的内容说明主题数太少如果两个主题词重叠严重说明主题数太多。等模型收敛之后再把主题映射回原始评论看每个主题下最典型的几条评论做最后一轮人工核验。4.3 负面评论专项挖掘光看整体主题还不够我更建议把负面评论单独切出来做专项挖掘。做法是用情感打分器筛出评分最低的 20% 评论再对这些评论做 LDA 主题聚类。这样得到的主题通常更尖锐比如停车场离入口太远雨季道路泥泞缆车排队时间过长。这一步对业务方最有价值因为正面口碑大家凭直觉就能说个大概但负面槽点往往很分散不靠数据很难系统发现。我做过一个景区项目整体好评率超过 85%但负面评论单独聚类后发现步行街商业化过重商家重复收款是两大高频槽点这两个问题藏在整体好评率的数字里根本看不出来。5. 从数据到界面可视化与轻量级系统呈现5.1 词云与统计图表分析结果如果只存在 CSV 里价值有限。让不懂代码的同事也能看懂必须可视化。词云是最直观的展示方式但中文词云有个经典的坑必须指定中文字体路径否则画出来全是方块。WordCloud 的font_path参数要指向系统里的一个中文字体Windows 下一般是simhei.ttf或msyh.ttcmacOS/Linux 下需要找本机的字体文件。import matplotlib.pyplot as plt from wordcloud import WordCloud word_freq {} # {词: 频次}由前面词频统计得到 wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width800, height600, background_colorwhite, max_words100, colormapviridis, ) wc.generate_from_frequencies(word_freq) plt.figure(figsize(10, 8)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(visualization/wordcloud.png, dpi200)词云之外我还会画三个基础图表情感分布饼图、评论量时间趋势折线图、主题占比柱状图。这三张图配合词云基本覆盖了游客情绪、客流趋势、关注热点三个最常用的分析维度。5.2 用 Streamlit 快速搭一个分析系统界面如果你想把这个项目转化成团队可用的系统而不是留在自己电脑里的脚本我推荐用 Streamlit。它能用 Python 直接写交互界面不需要前端知识改完代码保存浏览器自动刷新。下面是一段极简展示代码import streamlit as st import pandas as pd from collections import Counter from snownlp import SnowNLP st.set_page_config(page_title旅游景点评论分析系统, layoutwide) st.title(旅游景点评论分析系统) df pd.read_csv(data/comments_clean.csv) # 侧边栏 st.sidebar.header(筛选条件) source_filter st.sidebar.multiselect(数据来源, optionsdf[source].unique(), defaultdf[source].unique()) sentiment_threshold st.sidebar.slider(情感阈值, -1.0, 1.0, 0.0) filtered_df df[df[source].isin(source_filter)] # 情感分析展示 st.subheader(评论情感分布) sentiments [] for text in filtered_df[clean_comment].astype(str).head(1000): sentiments.append(SnowNLP(text).sentiments) filtered_df[sentiment] sentiments st.bar_chart(filtered_df[sentiment].value_counts(bins5).sort_index()) # 高频词展示 st.subheader(高频关键词Top20) all_words [] for text in filtered_df[clean_comment].astype(str): all_words.extend(text.split()) word_counter Counter(all_words) st.write(word_counter.most_common(20))Streamlit 的组件是自上而下顺序渲染的用户每次操作滑块或多选框脚本都会重新运行。这个特性既降低了开发成本也意味着数据处理逻辑要尽量轻量化否则每次交互都要等很久。如果数据量大建议事先把预处理结果以 parquet 或 CSV 存在本地Streamlit 直接读结果而不是每次现算。6. 实测中的深坑与排查复盘6.1 爬虫频率过高导致 IP 被限制第一次完整跑采集脚本时我用了 10 个线程并行抓取每秒钟发出 20 多个请求结果不到 3 分钟目标页面直接返回 403。排查过程是先看响应状态码发现所有请求都被拒绝拿浏览器测试又能正常打开基本确定是 IP 被临时限制。后来把并行改成串行请求间隔提高到 2 秒并且随机在 1 到 3 秒之间取一个值问题解决。这个教训告诉我对目标站点要保持合理克制采集量够用即可不要追求极致的抓取速度。尤其是做分析系统样本量达到几千条、覆盖主要时间段就能得出结论没必要把平台所有历史评论都薅下来。6.2 jieba 分词错误导致词频统计失真有一次我在统计某景区评论的高频词漂流好玩被 jieba 切成了漂流/好玩这个没问题但玻璃栈道被切成了玻璃/栈/道导致栈道单独变成一个词词频排名虚高。这类问题不仔细看数据很难发现后来我学乖了每轮词频统计后随机抽 20 条原文和分词结果对比发现词典缺失就立刻补进自定义词典而不是等最后报表出来再返工。6.3 停用词表不是越全越好网上能下载到几千词的停用词表但我试过直接塞进去之后分析结果反而变差了。原因是一些停用词表把没什么不好不太这种带否定含义的短语也放进去了导致情感分析完全丢失否定信息。比如环境不太好如果被分词器切成了环境/不太好而不太好又被停用词表吃掉那这条评论在情感分析里就等于环境变中性了——这是很严重的错误。正确的做法是停用词表只清理的、了、很、在、是这类无实际语义的虚词带语义倾向的词一律不能进停用词表。6.4 主题数 K 的选择不能只依赖困惑度LDA 有一个经典的困惑度指标理论上困惑度越低模型越好但实际使用中困惑度最低的主题数往往不可解释。我跑过一次 10 个主题的模型困惑度很低但一半主题都围绕停车场、门票、排队打转几乎没有区分度。后来我采用人工可解释性优先的原则跑多个 K 值看每个主题的高频词选最贴近业务认知的那组参数。毕竟这个系统最终是给人看的机器指标再漂亮人看不出话题差异就没有意义。7. 如果你是第一次做这个项目可以按这个顺序推进我见过不少人在这个项目上卡住原因不是某个技术点有多难而是顺序错了。有人先搞了三个月的爬虫结果发现数据清洗和分析才是重点有人一开始就想上深度学习情感模型结果连基础的分词和统计都没做扎实。我的建议是先做一个最小闭环。只用 500 条评论走通采集 - 清洗 - 分词 - 情感打分 - 词云 - 简单报表这条链路哪怕界面只是一个 Jupyter Notebook 里的几张图也算跑通了。然后在这个基础上迭代扩充数据量、补充专属词典、加 LDA 主题分析、上 Streamlit 界面。这个顺序能帮你尽早暴露问题。比如你会很快发现情感词典里漏了多少表达你会发现某个景点的负面评论和停车场高度相关这是靠看原始数据永远总结不出来的规律你还会发现当你把系统交给非技术同事用他们最大的痛点不是分析准不准而是界面进入门槛高不高。所以系统的价值不在于算法参数调得多花哨而在于它能否稳定地回答三个问题游客觉得怎么样游客最关心什么哪些问题最需要优先解决把这三个问题答好这个系统的意义就远超一个好玩的 Python 项目了。本文还有配套的精品资源点击获取
返回列表