简介:面向计算机相关专业学生及毕业设计开发者,这份资源围绕携程酒店评论构建了从爬虫采集、数据清洗预处理到情感分类分析的可视化完整链路。基于Python与Jupyter Notebook实现,覆盖评论分词、情感词典构建、特征值提取、模型分析与预测,并产出词云、热力图等可视化结果,对文本挖掘与情感分析入门实践有较高参考价值。资源包共23个文件,约15.87MB,核心包括py脚本、ipynb分析笔记、txt说明与数据、zip压缩的爬虫/预处理结果、HTML可视化页面及docx报告,目录划分清晰,便于按阶段学习。目前已有638人学习浏览,项目均通过运行验证,功能正常,适合作为课程设计、大作业或初期立项演示,可直接参考或扩展二次开发。 说真的,酒店评论是情感分析最适合练手的场景之一:文本短、表达直接、正负情感词特别密集,而且用户关心的问题高度集中。我把携程的酒店评论爬下来,走完清洗、分词、情感打分、可视化这一整套流程,最后用热力图把“评论情感得分”和“用户打分”的关系呈现出来,整个过程比想象中坑多,但跑通之后非常有成就感。这篇文章就把这个项目的完整思路、关键代码和踩坑记录整理出来,适合正在学爬虫、NLP或者数据分析的读者参考。
项目里包含了可运行的爬虫脚本、数据预处理模块、基于情感词典的情感分类代码、可视化分析脚本,以及一份写得很详细的说明文档。如果只想快速跑通流程,按说明装好库就能出结果;如果想弄懂背后的原理,下面每一节都是按照实际执行顺序写的,照着走一遍基本能复现整个项目。
1. 为什么选携程评论做情感分析:项目目标与技术组合
1.1 从评论里能读到什么
携程的酒店评论有个很典型的特点:用户会在一条评论里同时提到位置、卫生、服务、价格、噪音等多个维度,而且情感倾向往往是混合的。比如“房间很干净,但隔音太差,楼下酒吧吵到半夜”——前半句是正向,后半句是负向。如果只用“用户给了4分”就判定为满意,会丢失大量信息;但直接训练一个深度学习模型又需要标注数据,对个人项目来说成本太高。
所以这个项目定了一个更务实的目标:不追求把每条评论识别得100%准确,而是用情感词典给每条评论打一个“情感倾向分数”,然后结合用户评分、评论长度、酒店星级等结构化字段做对比分析。这样既能看出“哪些酒店虽然评分高但评论情感偏向负面”,也能横向比较不同地段、不同星级酒店的舆论口碑。
1.2 为什么不用现成API而用情感词典
市面上的情感分析API确实方便,传一段文本就返回正负向概率,但问题也很明显:一是调用量有限制,爬几千条评论可能就触发限流;二是黑盒模型不容易解释结果,不知道它为什么把“卫生间太小”判成中性;三是无法针对酒店领域定制词表,像“隔音”“床垫”“前台”这类词在通用模型里权重不明显。
情感词典方案不需要训练,逻辑透明,还能根据酒店评论的特点不断扩充词典。配合否定词、程度副词规则,准确率在短文本场景下足够用。这也是很多商业舆情系统早期采用的方案。所以这个项目选择了“情感词典+规则加权”的技术路线,核心是可控、可解释、可迭代。
2. 携程评论爬虫:从页面到结构化数据
2.1 评论接口的定位与参数构造
携程酒店详情页的评论数据并不是静态HTML里一次性输出的。打开开发者工具,切到Network面板,刷新评论列表,能看到一个XHR请求,返回的是JSON格式的评论数据。这个接口的URL里有几个关键参数:酒店ID、页码、评论类型(全部/好评/差评)、排序方式等。实际写爬虫时,只需要先手动在页面上翻一页,复制出请求URL和表单数据,用requests库模拟发送就行。
这里不要自己去拼完整URL,因为携程的接口参数经常调整。更稳定的做法是:先用浏览器复制出完整的请求头,再在代码里保留关键字段。我当时保存的请求头大概长这样:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://hotels.ctrip.com/", "Accept": "application/json, text/plain, */*", }参数构造时,先把固定参数写成一个字典,每翻一页改变pageIndex的值。这样代码看起来清晰,也方便后续对接别的酒店ID。注意,有些接口的评论内容会做Unicode转义,直接请求返回的JSON里可能是\u597d这样的字符,需要调用json.loads解析,不能直接当字符串处理。
2.2 请求伪装与频率控制
携程的反爬策略不算特别激进,但也不傻。单线程连续快速请求,很容易被识别为脚本,轻则返回验证码页面,重则封IP。我的建议是:请求之间至少间隔3到5秒,最好用time.sleep(random.uniform(3, 5))做随机延时。如果爬取量比较大,可以准备一个代理IP池轮换,但个人学习项目用本地IP慢慢爬就够了。
另外一定要设置超时时间。因为部分请求可能被服务器丢弃,不设超时的话,脚本会一直卡住。我的习惯是requests.get(url, headers=headers, params=params, timeout=10),失败就记录日志并重试,连续失败三次就跳过当前页。这样哪怕中途断网,重跑时也不会从头开始。
2.3 评论字段提取与存储
评论JSON里能拿到的字段很多,但这个项目只保留了最核心的几个:用户名、评分、评论内容、评论时间、房型。把这些字段整理成表格,后续做分析和可视化就方便了。提取时要注意,有些字段是嵌套字典,比如“酒店回复”可能不存在,需要用get方法加默认值,避免KeyError。
存储格式我选了CSV而不是数据库,因为数据量在几千到几万条,CSV用pandas读起来非常快,也方便直接进入预处理阶段。写CSV时要指定utf-8-sig编码,不然用Excel打开会乱码。每爬完一页就把DataFrame追加到文件里,而不是等全部爬完再写,这样能减少内存占用,也避免中途失败导致数据全丢。
3. 数据预处理:把评论变成能用的语料
3.1 数据清洗:去重、去空、去HTML标签
爬下来的评论不会干干净净。首先会有重复数据,可能是接口返回了同一评论,也可能是我们自己重试造成的。用pandas的drop_duplicates(subset="content_id")按评论ID去重最稳妥,没有ID就按“用户+时间+内容”组合去重。
然后是空评论和噪声。携程的部分评价只有评分没有文字,这类数据在做情感分析时没有价值,直接丢掉。评论内容里偶尔会出现HTML标签、换行符、特殊字符,用正则清理:
import re def clean_text(text): if not isinstance(text, str): return "" text = re.sub(r"<[^>]+>", "", text) # 去HTML标签 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\s]", "", text) # 保留中文英文数字空格 text = re.sub(r"\s+", " ", text) # 合并空白 return text.strip()这一步特别重要。如果不清理,后面分词时会把“房间
很干净”切出奇怪的结果,甚至把表情符号当成词干,直接影响情感打分。
3.2 分词与停用词处理
中文分词我用的是jieba,因为它在社区普及度最高,安装简单,不开箱即用。默认分词对普通文本效果不错,但酒店评论里有很多品牌名、地域名、俗称,比如“外滩”“亚朵”“全季”,这些词默认可能会被切分成“外/滩”或者“亚/朵”。所以需要提前加载自定义词典,把专有名词固定下来:
import jieba jieba.load_userdict("hotel_dict.txt")hotel_dict.txt每一行是“词语 词频 词性”,比如“全季酒店 100 nf”“外滩 50 ns”。这样分词时就不会拆散这些词。
停用词表也非常关键。中文里“的”“了”“是”“而且”这类词出现频率极高,但对情感判断没有帮助,不删除的话会让后续的词频统计失真。哈工大停用词表、百度停用词表都可以在网上找到,把它们合并起来,再额外加入“酒店”“房间”“入住”这类虽然和酒店相关但情感中性的通用词,避免它们霸占词云。
3.3 自定义词典与领域词补充
酒店评论里有一些特殊表达,比如“隔音差”“位置好找”“性价比高”。如果用通用情感词典,“隔音差”里的“差”会被识别为负向词,这个没问题;但“性价比高”里的“高”在通用词典里是中性词,它和“性价比”组合后其实变成了正向表达。这类情况需要靠领域词表和规则弥补。
我维护了一个“酒店领域词典”,包含两部分:一部分是领域概念词,如“床垫”“淋浴”“前台”“早餐”“停车”等,用于分词后提取主题;另一部分是领域情感词,如“安静”“干净”“便捷”“贴心”“温馨”等,标注了情感极性。这些词可以自己在爬下来的评论里统计高频词后手动整理,也可以用网上的酒店评论情感词表做底,再按实际数据增删。
4. 基于情感词典的评论情感打分实现
4.1 情感词典的构建与扩充
项目的情感词典采用一个简单的格式:词、极性、权重。极性用1表示正向,-1表示负向;权重表示情感强烈程度,比如“喜欢”权重1,“超级喜欢”权重2,“讨厌”权重-2。网上的通用情感词典可以直接用,比如BosonNLP情感词典、大连理工情感词汇本体库,但需要注意覆盖率和领域偏差。
比如通用词典里没有“吵”“脏”“旧”这类在酒店场景高频出现的负面词,如果漏了它们,一条“房间很吵很脏很旧”的评论可能会被判成中性。处理办法是:先拿通用词典跑一遍爬下来的评论,把所有情感分数为0但用户评分很低的评论筛出来,查看里面出现了哪些词典里没有的词,再把这些词按人工标注加入领域词典。这样迭代两三次,覆盖率就能达到不错的效果。
4.2 否定词、程度副词与感叹号的加权规则
光有词典还不够,中文的否定和程度表达会翻转或加强情感。比如“不满意”和“满意”就差一个“不”字,如果只是逐词累计分值,就会把“不满意”判断成正向。所以打分时要做窗口判断:先对句子分词,然后遍历每个词,如果遇到情感词,就向前看紧邻的几个词,如果存在否定词(不、没、别、未、莫),就把情感值取反;如果存在程度副词(很、太、超、非常、极其),就把情感值乘以对应的权重。
具体实现可以是这样的伪代码:
def score_sentence(words, sentiment_dict, neg_words, degree_words): total = 0 for i, word in enumerate(words): if word in sentiment_dict: score = sentiment_dict[word] * sentiment_dict_weight.get(word, 1) if i > 0 and words[i-1] in neg_words: score = -score if i > 1 and words[i-2] in degree_words: score = score * degree_words[words[i-2]] total += score return total实际代码中还要考虑否定词与程度副词的组合顺序,比如“不是太好”,程序应该识别出“不是”后面的“好”被打折,而不是完全反转。这个可以通过设置否定词只对最近一个情感词生效来近似。另外,评论中的感叹号会强化情感,我给每条评论的感叹号数量乘了一个小系数,加进总分。
4.3 情感得分与情感倾向的转化
每条评论最终会得到一个数值分数。把所有评论的分数画出来,基本呈一个双峰分布,正向集中在较高区间,负向集中在较低区间。可以设定一个经验阈值,比如大于0.5为正向,小于-0.5为负向,中间为中性。但这个阈值最好根据数据分布动态调整,我用的是正负分数均值的中间点再加一个偏移,这样能减少“用户打了高分但情感分不高”的系统偏差。
这里有一个技巧:把用户评分也纳入校准。用户给5分的评论,如果情感分却低于-1,说明这条评论很可能是在“表扬中夹杂强烈批评”,值得单独拿出来看。这类评论往往比一面倒的差评更有分析价值。
5. 数据可视化:热力图、词云与情感分布
5.1 用户评分与情感得分的关系热力图
热力图是这个项目里最直观的一个输出。我用seaborn.heatmap画了一张二维密度热力图,横轴是用户评分(1-5分),纵轴是评论情感得分区间,颜色越深代表该组合下的评论数量越多。图出来之后能明显看到,大部分评论集中在“5分-高情感分”区域,但“5分-低情感分”区域也有一条明显的拖尾,那就是“好评但情感负面”的特殊群体。
画热力图之前,先要把连续的情感得分离散化,比如每0.5分一个桶。然后建一个透视表,行是情感得分区间,列是用户评分,值是评论数量。这样直接传给heatmap即可:
import seaborn as sns import matplotlib.pyplot as plt pivot = df.pivot_table(index=emotion_bins, columns=user_rating, values="count", aggfunc="sum") sns.heatmap(pivot, cmap="YlOrRd", annot=True, fmt="d") plt.show()这个图比散点图好读,因为酒店评论数量大,散点会重叠到看不清分布。用热力图能快速看出评论情感与评分的匹配程度,也是项目报告里最有说服力的图表。
5.2 高频词与情感词云
词云是另一张能快速展示特征的图。我把清洗后的评论分词、去停用词,用Counter统计词频,然后喂给wordcloud.WordCloud生成词云。但单纯的全量词云信息量太低,大家都会做。我建议按情感分组:正向评论组和负向评论组分别生成词云,对比看。正向词云里会出现“干净”“方便”“热情”“好吃”,负向词云里会出现“噪音”“隔音”“脏”“态度差”“排队”。这种对比比单张词云更能说明问题。
生成词云时注意两点:一是要设置font_path,指定中文字体,否则中文全显示成方块;二是wordcloud默认会过滤掉单字词,但有些单字情感词比如“脏”“吵”反而是重要信号,可以用max_words和collocations微调,或者直接传入自定义词频字典。
5.3 时间维度与酒店维度的情感趋势
除了热力图,我还画了“情感得分随评论时间的变化”折线图。按周聚合计算平均情感得分,能看出酒店的口碑是持续向好还是下滑。这个对用户选酒店很有参考价值。比如某家酒店近三个月平均情感分从1.2降到-0.5,那就说明近期体验变差了,即使总评分还是4.5分也要谨慎。
如果爬了多家酒店,还可以画一个“酒店ID-情感得分”的箱线图,把不同酒店的情感分布并列展示,比单纯比评分更有意义。因为用户评分的主观性太强,有些人习惯打5分,有些人习惯打3分,但评论内容里的情感词更能反映真实体验。
6. 完整源码运行说明与踩坑记录
6.1 项目文件结构与运行步骤
整个zip解压后大概是这样的目录:
project/ ├── crawler/ # 爬虫相关 │ ├── ctrip_spider.py │ └── headers.json ├── data/ # 原始数据与清洗后数据 │ ├── raw_comments.csv │ └── clean_comments.csv ├── preprocessing/ # 数据预处理 │ ├── clean.py │ ├── segment.py │ └── stopwords.txt ├── sentiment/ # 情感分析 │ ├── sentiment_dict.txt │ ├── degree_words.txt │ └── score.py ├── analysis/ # 可视化 │ ├── heatmap.py │ ├── wordcloud.py │ └── trend.py ├── requirements.txt └── README.md运行顺序是:先跑ctrip_spider.py爬数据,再跑clean.py清洗,然后segment.py分词,接着score.py情感打分,最后运行analysis目录下的脚本出图。requirements.txt里主要是requests、pandas、jieba、matplotlib、seaborn、wordcloud,安装命令就是pip install -r requirements.txt。
6.2 爬虫返回乱码或者字段缺失怎么办
第一次跑爬虫时,我遇到的典型问题是返回的JSON里评论内容字段不确定。有时候是content,有时候是comment,还有嵌套在extInfo里。不要硬编码字段名,而是先把整个返回JSON用pprint打印出来,人工确认一层结构,再写提取逻辑。如果服务器返回了Gzip压缩的数据,requests会自动解压,但如果手动设置了Accept-Encoding,可能需要自己gzip.decompress。
如果出现乱码,检查response.encoding,通常需要显式设置成utf-8,或者从响应头里获取charset。最保险的做法是用response.text前先response.encoding = "utf-8"。
6.3 分词结果不理想怎么办
一个常见坑是:没加载自定义词典,导致“星级酒店”被切成“星级/酒店”。如果分词后发现大量专有名词被拆开,需要用jieba.suggest_freq提高某个词出现的频率,比如jieba.suggest_freq("全季酒店", True)。另一个坑是停用词表过滤掉了太多个性化词,导致句子里只剩下一堆动词和名词。这时候要回看停用词表,把“什么”“怎么”这类疑问词保留下,因为“服务不怎么样”里的“怎么样”是情感判断的关键。
6.4 情感词典覆盖不全的自我迭代方法
没有哪个现成情感词典是完美适配酒店评论的。我在项目里写了一个辅助脚本:把情感得分为0但用户评分极低或极高的评论导出,人工扫描这些评论中出现的高频词,再补充进情感词典。比如“衣架”本身是中性词,但“没有衣架”就是明确的负面信号;通用情感词典不会把“衣架”标为负,所以只能靠领域规则。我的做法是在情感词典里加入诸如“没有X”“X差”“X坏”的短语模式,用正则先做一层匹配,再交给词典打分。
这种半自动迭代的方式虽然费时间,但每轮迭代之后准确率提升都很明显。如果你也要做类似项目,建议把这一步当成正式流程,而不是一次性用例。最后,按照我个人的实际经验,不要指望情感分数和用户评分完全一致,两者不一致的地方反而才是值得深挖的亮点。你可以把“高评分-低情感分”和“低评分-高情感分”这两类评论单独打印出来读一读,往往能发现很多有趣的数据洞察。
本文还有配套的精品资源,点击获取