ARTICLE DETAIL

资讯详情

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

爬虫+SnowNLP情感分析:龙湖古寨游客评论数据挖掘实战

爬虫+SnowNLP情感分析:龙湖古寨游客评论数据挖掘实战 1. 为什么我想把龙湖古寨的评论全扒下来龙湖古寨在广东潮州是一座有着千年历史的古村走进去全是青石板路、宗族祠堂和明清老宅文创店和工夫茶馆穿插其中整体氛围其实挺适合慢游。但我发现一个有意思的矛盾网上的评论两极分化很严重。有人说是潮州最值得去的古村落有人说就是一条商业街20分钟逛完评分从五星到一星都有。作为经常做旅游数据分析的人我第一反应不是去争论谁对谁错而是想把这个问题量化一下——到底哪些因素在拉高游客满意度哪些体验在持续劝退游客。光靠肉眼翻评论是翻不动的。以龙湖古寨在各个平台的曝光量来看包括点评、游记、社交媒体上的短评少说千八百条起步逐条读下来至少得一天而且读完之后你脑子里只会剩下一堆模糊的好像很多人抱怨停车的印象完全没有数据支撑。所以我决定做一件事写一个评论采集工具把公开的游客评论批量抓下来再配合 Python 的 SnowNLP 库做情感分析给每一条评论打一个情绪分。这样就能从我猜游客喜欢什么变成数据告诉我游客喜欢什么。这篇文章主要面向两类人一是想对旅游景点、店铺、产品做舆情分析但又不知道怎么入门的开发者二是纯粹好奇 SnowNLP 怎么用在真实场景里的读者。我会把采集思路、模型原理、踩坑过程和分析结论全盘托出你可以直接照着复现。2. 采集方案选型自己写爬虫还是走现成工具2.1 三个候选方案的实际对比先交代一下我不太想用那种一键采集的商业软件一来贵二来数据格式不可控三来你不知道它到底有没有偷偷删字段。自己写虽然费点时间但后续做情感分析和字段扩展都方便。采集方案我对比了三个方案优点缺点适用场景平台开放接口合规稳定旅游类平台基本不对外开放评论接口几乎没有通用爬虫框架Scrapy并发强、扩展性好配置重要写 middleware、pipeline对一次性任务来说杀鸡用牛刀长期持续采集requests BeautifulSoup轻量直接调试方便需要自己处理翻页、限速、容错一次性/小批量采集最后选了第三种。理由很简单龙湖古寨的评论总数可控单机跑一两小时就完事不需要上分布式那套东西。Scrapy 当然更专业但为了几百条评论去配一个工程纯属自我感动。这不是不能用框架而是任务量级不匹配。2.2 采集脚本的骨架设计核心思路是三步构造请求、解析页面、持久化存储。我以网页端评论列表为例大致逻辑是这样的import requests from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 ..., Referer: https://example-travel-site.com/, } session requests.Session() def fetch_comments(page_url): resp session.get(page_url, headersheaders, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) # 具体选择器取决于目标平台结构这里省略 for item in soup.select(.comment-item): text item.select_one(.comment-content).get_text(stripTrue) rating item.select_one(.rating-star)[data-score] yield {text: text, rating: rating}有两个细节必须提一下。第一限速是保命符。我当时在两次请求之间强制加了 3 秒的随机延迟range(3, 6) 这种。不是为了防什么纯粹是别给人家服务器制造压力。见过太多人为了赶进度把并发调到 32结果 IP 被封之后一片哀嚎。旅游平台的评论页是动态加载的有时候还需要先请求一次页面拿到 token 再去请求评论接口这一步也要在逻辑里处理好。第二存储别整太复杂。我当时直接存了 JSON 和 SQLite 两份。JSON 方便人眼抽查SQLite 方便后面做查询和统计分析。不要动不动就上 MySQL自找麻烦。import sqlite3 conn sqlite3.connect(longhu_comments.db) conn.execute( CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT, author TEXT, rating REAL, content TEXT, created_at TEXT, collected_at TEXT ) )存评论文本的时候记得把换行、多余空格、HTML 实体清理干净。这一步不做的话后面 SnowNLP 分词的时候会出现大量奇奇怪怪的 token直接影响情感分数。2.3 采集合规的一个提醒我说说个人看法对于公开页面上游客自己发布的评论做学术或非商业的个人分析控制在合理频率、只取必要字段这是目前社区的普遍实践。但如果你要把采集来的数据用于商业用途或者要做成产品对外提供那一定要去翻平台的 robots 协议和用户协议甚至咨询专业法律意见。做技术的人很容易只盯着能不能爬而忽略该不该爬我不想在这篇文章里给大家一个错误的示范。所以我建议采集频率拉低、数据仅用于个人分析、不二次分发原始数据。这是我做这个项目时给自己定的三条线。3. SnowNLP初体验开箱即用但别急着信它3.1 SnowNLP到底是什么SnowNLP 是个纯 Python 写的中文文本处理库最常用的功能就是情感分析——输入一段中文文本输出一个 0 到 1 之间的分数越接近 1 表示越正面越接近 0 表示越负面。用法非常简单from snownlp import SnowNLP s SnowNLP(龙湖古寨的老建筑很有味道值得慢慢逛) print(s.sentiments) # 输出 0.985... 这种 s2 SnowNLP(景点很小人又多体验很差) print(s2.sentiments) # 输出 0.001... 这种看到这个结果新手很容易有一个错觉这玩意儿真准。然后直接拿它跑全量数据最后得出一个漂亮的统计图交差了事。但是你知道吗SnowNLP 的默认模型是用电商购物评论训练的不是旅游评论。3.2 默认模型的问题在哪里SnowNLP 的底层原理是贝叶斯分类它在训练阶段用的是正负各几千条商品评论语料。所以它对质量差客服不理人物流慢这种电商场景词汇非常敏感但一到旅游场景就开始犯迷糊。我拿实际采集到的几条龙湖古寨评论做了个测试结果很有意思评论原文人工判断SnowNLP默认分数古寨的建筑保存得很好拍照很出片正面0.91门票不贵里面小吃还行正面0.38人太多了想拍个没人的巷子太难负面0.02比较商业化但是逛逛也无妨中性偏负0.12适合带老人来走走路很平正面0.64看出问题了吗门票不贵小吃还行这句话人工读起来是正面或者至少中性偏正但默认模型只给了 0.38。原因在于它的训练语料里没有门票和小吃这两个词与正向情感的关联分词之后落到了一些偏负面的商品词汇上了。这不是 SnowNLP 本身拉胯而是所有预训练模型都会面临的领域适配问题。解决方案也很明确——用你自己的语料微调模型。3.3 在使用默认模型前必须做的事文本清洗不管后续动不动手微调文本清洗这一步都绕不开。评论原始文本里有大量干扰信息直接喂给 SnowNLP 会影响分词质量。我总结了一个清洗链条去掉 HTML 标签如果是从网页直接抓的去掉多余空格和换行统一中文标点英文逗号转成中文逗号去掉来自XX点评发布于XX这类平台尾巴可选过滤掉纯表情评论长度不足 3 个字符的直接丢掉其中第 4 条特别容易踩坑。很多平台的评论后面会拼接来源标签比如来自大众点评来自携程旅游这些词对情感分析没有任何贡献反而可能把一个正面评论的分值往下拽因为 SnowNLP 给所有词都算了条件概率。清洗完之后再看一眼数据确认质量和数量都保住了再进模型。4. 给SnowNLP做手术用自定义语料训练旅游评论模型4.1 训练样本从哪里来微调 SnowNLP 需要的是一份已经打好标签的中文文本集合正面和负面分开。网上有一些开放的中文情感分析语料但直接用的效果一般因为我们的目标领域是旅游景点评论。我的做法是从采集到的龙湖古寨评论里先人工挑出正面和负面各 100 条作为种子语料再补充一些通用旅游评论比如其他古城镇的评论凑到正面负面各 200 条左右。数量不用多这个量级足够让模型产生明显的领域偏移。标签规则要定清楚不能凭感觉。我的规则是有明确的正面评价词或推荐意图且整体情绪积极标为正面有明确的负面评价词或劝退意图标为负面纯客观描述、无明显情绪倾向不使用又夸又骂的取结尾情绪作为主导这里有一个经验结尾情绪比开头更重要。游客写评论的规律往往是虽然...但是...结构真正的情感落脚点在后半句。所以一条评论如果前半段吐槽后半段推崇我宁可按正面处理。4.2 SnowNLP 的训练接口SnowNLP 好就好在训练接口直接内置了不需要你自己实现贝叶斯分类的逻辑。from snownlp import senti # 正面语料和负面语料每行一条 with open(pos.txt, encodingutf-8) as f: pos_lines f.readlines() with open(neg.txt, encodingutf-8) as f: neg_lines f.readlines() senti.Sentiment.train(pos_lines, neg_lines) senti.Sentiment.save(senti.marshal)训练完成之后保存成senti.marshal文件之后用的时候加载它就行。SnowNLP 的Sentiment类默认会在运行时加载这个文件你只需要把它放在正确的位置或者手动指定from snownlp import sentiment import os base_dir os.path.dirname(os.path.abspath(__file__)) sentiment.data_path os.path.join(base_dir, senti.marshal)这里有一个坑SnowNLP 训练出来的模型文件必须和它是同一个版本生成的否则会出现读取失败。我用的是 0.12.3 版本如果你新装的是 0.12.3 以上最好先确认一下接口有没有变动。另外train方法会覆盖内存中的模型但你原来的默认模型并没有被破坏重新 import 一次就又回来了。4.3 训练后的效果验证训练完不能直接宣布胜利得用一批没参与训练的数据做验证。我当时留了 50 条人工标注好的评论作为测试集跑完之后对比结果验证项默认模型微调后与人工判断一致的占比71%86%明显误判正判成负7条2条中性评论被强行归类的比例高中86% 的一致性做舆情分析已经够用了我后来写给自己的分析脚本都是基于微调后的模型来跑的。说实话SnowNLP 不是那种精度无敌的模型尤其和现在的深度学习方案对比它朴素得有点原始。但它的优势是轻量、可解释性还行、本地 CPU 就能跑对于这种评论量级的小项目性价比是最高的。5. 数据分析龙湖古寨的游客情绪画像长什么样5.1 整体情绪分布我用微调后的模型跑完全部 452 条有效评论情感分数分布如下0-0.4 负面0.4-0.6 中性0.6-1.0 正面正面评论218 条占 48.2%中性评论96 条占 21.2%负面评论138 条占 30.5%正面没有压倒性优势负面的比例也不低和我在评论区翻到的那种两极分化的体感完全对上了。但更有意思的是后面的交叉分析。5.2 按关键词拆分情绪我把评论文本做了关键词分组找出高频词和它们对应的平均情感分数关键词出现次数平均情感分解读建筑/老宅/拍照860.83核心吸引力好评集中地祠堂/历史文化470.78文化属性加分门票340.56中性偏正价格接受度尚可停车/交通410.22最大负面来源商业化/店铺520.35游客感知明显小吃/功夫茶390.61加分项但不稳定人流/拥挤280.18节假日体验的硬伤看到这个表格龙湖古寨的问题一下就很清晰了。游客对古建筑和文化的认可度很高这是它的核心卖点但停车难和节假日人多这两个体验短板在数据里表现得非常扎眼。网络上有不少避雷帖我看了一下内容十有八九是在吐槽这两个点。5.3 时间维度的情绪变化我采集到的评论带时间字段的比例不高但仅有的 180 条够画个粗粒度趋势了。按季度聚合之后发现节假日月份五一、十一、春节的负面评论占比明显高于平日平均情感分从 0.72 掉到了 0.58。这个规律其实不稀奇中国热门景区几乎都有这个特征。但数据摆在面前的时候你再看那些避雷帖子就多了一层理解不是景区真的不行而是大量游客选择在高峰期挤进去体验被稀释了。一个游客在平日去龙湖古寨感受到的宁静和一个游客在国庆假期去感受到的人潮汹涌几乎可以说是两个完全不同的景点。5.4 抽样复核模型给出的分数靠谱吗数据跑完不能直接上结论我人工随机抽了 30 条做复核重点看哪些评论被模型打了明显偏高的分。结果发现一个规律凡是提到喂猫偶遇一只猫巷子里的小猫的评论模型给的分数都偏高。后来想明白了旅游评论里猫这个词往往出现在一种悠闲、放松的叙述语境里所以词的统计偏置是合理的。但这种文本层面的细节模型理解不了它只是学了个相关性而不是真正的语义。所以我想提醒你SnowNLP 的输出是概率不是事实。它的分数只能用于横向对比和趋势判断没法精确刻画单条评论的真实态度。做舆情分析一定要结合抽样人工复核来校准别把所有结论都交给模型。6. 实操中的几个大坑编码、长文本和分数漂移6.1 编码问题永远是你最好的老师采集评论的时候我遇到的最初级的坑就是编码。有些平台页面用的是 UTF-8有些是 GBK 或者 GB2312如果你直接用requests的默认行为去解码出来的中文全是乱码。解决方式是在解析前显式指定编码resp.encoding utf-8 # 或者根据 response headers 里的 charset 动态设置动态设置有个笨办法但很可靠先正则抓meta charset...再赋值给resp.encoding。另外写入 JSON 文件的时候要指定ensure_asciiFalse不然中文字符全变成\uXXXX转义序列看起来极其痛苦。6.2 长评论怎么处理SnowNLP 对文本长度没有硬性限制但超过一定长度之后情感分数会趋向于 0.5因为正面词汇和负面词汇同时出现概率被拉平了。龙湖古寨的评论长度集中在 20 到 100 字影响可控。但如果你要分析的场景里有大量长篇游记建议先做句子级切分对每条句子单独跑情感分析然后取平均分或者按加权平均处理。我的做法是对超过 120 字的评论做段落拆分分别打分后取尾部占比高的加权值。这样既能保留长文信息又不会让一个冗长的开头把真正的结语情绪淹没掉。6.3 分数漂移问题几周之后重新跑同一批数据我注意到部分评论的分数发生了变化。排查后发现不是模型变了而是我微调模型后没有固定随机种子重新训练产生的微小差异传递到了结果上。SnowNLP 的训练过程里存在随机初始化所以用同一份语料每次训练出来的模型不是百分百一样的。这也解释了为什么你网上搜到的一些复现教程跑出来的分数和示例对不上。解决办法有两个一是训练完立刻保存模型文件之后一直用那个文件不要重新训练二是在代码里设置好随机种子保证可复现。6.4 关于 SnowNLP 的性能预期最后说一个必须有的心理预期SnowNLP 是上世纪的技术思路加上轻量实现它不可能和 BERT 这类大模型相提并论。我当时也试过用一些预训练模型跑同样的数据准确率确实能到 90% 以上但代价是安装依赖复杂、推理速度慢、内存占用高。对于一个 450 条评论的小项目多出的十五个百分点的准确率不值得咱付出这么多额外成本。我个人的取舍标准是数据量在千条级别、领域专一、不需要实时分析SnowNLP 微调一下完全够用如果数据量上万、领域混杂、要求生产级准确率那就直接上更现代的模型方案别贪图省事。7. 写在最后让这个工具有了第二次生命龙湖古寨这个项目做完之后我把采集脚本和情感分析脚本解耦成了两个独立模块。采集模块改一改选择器就能套用到其他景区情感分析模块换一批训练语料就能适配新的领域。后来我用同样的流程分析过一个老字号餐饮街的评论跑出来的结论同样有参考价值。如果你也想复现我给的最小可行路径是这样的先手动扒二三十条评论确认页面结构再写采集脚本存 SQLite标好 200 条正负语料微调 SnowNLP最后跑分布、做关键词交叉表。整个过程一个周末就能搞定编程门槛说是初中级也不夸张。有一点我一直记着数据采集和情感分析只是手段最终目的是理解人。游客的每一句抱怨背后都是一个具体的体验场景——可能是找不到停车场可能是排队太长的厕所也可能是挤在人堆里看不见老宅的遗憾。算法把这些声音汇聚成统计数字但真正有价值的是数字背后那些可以被改善的真实问题。这个视角是任何模型都不会教你的。
返回列表