ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

携程景点评价爬虫与情感分析实战:从抓取到综合评分

2026/9/17 3:05:31 拓冰建站 浏览量
携程景点评价爬虫与情感分析实战:从抓取到综合评分 做这个项目的起因其实特别实际五一那会儿朋友约我出去玩大家在携程上翻景点评价看着动辄上千条点评谁也没耐心看完。我当时刚把Python爬虫学了个七七八八就想着能不能用Python把某个景点的全部评价抓下来再做一轮情感评分分析看看大家到底是真心推荐还是被评分表给骗了。项目本身不复杂但一路做下来踩了不少坑从接口分析、翻页去重到中文分词、情感打分每一步都有值得记录的细节。这篇就围绕“携程景点评价爬虫与情感评分分析”把整个项目的思路、核心代码和坑点一次性说清楚适合刚入门Python爬虫、想拿真实数据分析练手的朋友。1. 项目怎么拆才不踩坑整体设计与边界1.1 为什么拿携程景点评价开刀很多人学爬虫喜欢挑豆瓣电影、电商商品评论练手我偏偏选了携程景点评价原因有三个。第一页面结构相对规整。景点评价不像微博那样夹杂大量短链和嵌套转发也不像淘宝那样有复杂的模板装修评价内容、星级、日期、用户名这些字段基本都是固定结构对新手非常友好。就算页面改版底层接口的数据字段变动也没那么剧烈。第二中文评价的情感表达特别丰富。同样一个“还不错”放在“人少景美还不错”和“排队两小时还不错个头”里完全是两个意思非常适合用来做情感评分分析的练习。而且携程的星级评分是1到5分平台分和用户真实情绪往往存在错位这种错位就是情感分析出价值的地方。第三数据量适中跑起来不费劲。单个热门景点的评价通常几千到几万条用普通requests加单线程完全能跑完不需要一上来就整分布式爬虫那套正好适合做技术验证。这个项目的核心价值不光是“把数据抓下来”而是给每一条评价算出一个可量化的情感分再把所有情感分聚合起来跟平台星级分做对比。如果你发现自己想去的地方评论里全是“风景不错但管理混乱”那你心里就有数了。1.2 整体流程怎么拆整个项目我拆成五个环节顺序非常固定接口定位通过浏览器开发者工具找到评价数据的真实请求地址搞清楚返回的是HTML还是JSON。数据采集用requests库模拟请求循环翻页把评价原文、评分、用户名、点评时间全部保存下来。数据清洗处理空评论、去掉重复抓取的数据、清理表情符号和特殊字符。情感分析对每条中文评价做分词和情感计算得到0到1之间的情感倾向值。结果对比把情感得分和平台星级放在一起算综合推荐指数最后用图表呈现差异。看起来每步好像都不难但真正做起来每一步都有细节。比如接口返回的数据里评论文本可能是经过Unicode转义的JSON里嵌入了一堆\uXXXX又比如翻页的时候如果直接改页码参数部分页面会返回同样的数据必须用“用户ID评价内容时间”去重。1.3 项目边界与合规意识这里必须先说清楚边界爬虫只是学习Python和数据分析的切入方式不是让你去薅平台数据的生产工具。我个人做这个项目时全程只测了少数几个景点的公开评价请求频率非常低抓取页面之间都sleep了2到3秒对目标服务器几乎没有任何压力。提示如果你要在真实项目里做同类事情请务必遵守目标平台的用户协议和Robots协议只抓公开数据、控制请求量、不商用、不传播。这篇博文所有代码都保留基础频率控制也建议你把这种习惯刻进DNA里。2. 工具选型解析requests、解析库、情感分析库怎么搭2.1 请求库选型requests够用非要并发也想清楚再上Python发HTTP请求的库多得是requests是绝大多数人入门的第一选择这个项目我用的就是它。原因很简单API足够人性化params参数可以直接传字典headers能按需伪装最关键是社区资料多遇到问题搜一下就有答案。很多人看完热搜里的“并发设计到底哪个好”就手痒想上异步、想上线程池。我的真实体验是在单机、低频、抓几千条评价的场景下单线程加time.sleep(2)反而最稳。为什么因为爬虫最大的瓶颈通常不是CPU也不是解析速度而是请求频率触发了服务端的访问限制。你开20个线程冲上去可能10秒就被封了然后整半天都在跟IP限制作斗争完全得不偿失。如果你确实想把抓取速度提上来我的建议是用ThreadPoolExecutor加信号量把最大并发数控制在3到5之间同时每次请求前随机sleep 1到2秒。异步方案在这个场景下收益不明显还容易把代码绕晕。2.2 数据解析方案JSON优先别硬啃HTML携程的评价接口返回的是JSON数据这一点对爬虫来说是最大的幸运。JSON是结构化数据字段清晰直接用resp.json()就能拿到Python字典比正则表达式抠HTML代码舒服太多了。我见过很多人一上来就用requests.get(url)然后拿着返回的HTML文本傻了眼——浏览器里看到的评价内容HTML源码里根本没几个字。这是因为当代网站大量使用JavaScript动态渲染数据评价内容是通过异步请求后续加载的这就是为什么第一步必须做接口定位。解析上我的建议是接口返回JSON就老老实实解析JSON只有碰到纯页面渲染且没法找接口的情况才考虑XPath或Selenium。用XPath抓HTML虽然有时候也能凑合但页面结构一变你的解析代码就废了维护成本很高。2.3 情感分析库怎么选SnowNLP是快速起跑线中文情感分析目前主流有几种思路直接调用大模型API做情感判断精度高但成本高不适合几百上千条数据的批量调试。基于情感词典计算正负向得分可解释性强但需要自己维护一个大词典工作量不小。用现成工具库比如SnowNLP和jieba上手快对短评文本有一定效果。我最后选的是SnowNLP。它的sentiments属性返回一个0到1之间的值大于0.5算正向小于0.5算负向使用门槛几乎为零。不过要注意SnowNLP默认语料偏向购物评论用在旅游点评上会有一定偏差这跟“默认参数不一定适配所有场景”是一个道理。后面我会专门讲怎么对它做针对性调整。3. 携程景点评价爬虫实操接口、翻页与数据清洗3.1 定位携程评价接口这一步是整个项目最关键的地方思路比代码重要。我以一个模拟的点评列表页为例说明操作过程实际地址换成你自己的目标景点即可。打开浏览器开发者工具F12切到Network面板勾选Fetch/XHR过滤条件然后在页面上点击“查看全部点评”或者翻到下一页。正常情况下Network面板会冒出一堆请求其中总有一个名字类似comment/list或review/list的接口点开Preview预览如果能看到结构化数据里面正好有评论文本、评分、时间这些字段那它就是你要找的接口。拿到接口后需要构造请求头。至少要带User-Agent和Referer前者用来让服务器认为你的请求来自浏览器后者用来表示你是从景点详情页跳转过来的。有的情况下还需要Cookie但只抓公开评价数据时通常不需要登录所以多数场景可以不加。一般接口的查询参数大概长这样params { poiId: 12345, # 景点ID pageno: 1, # 页码 pagesize: 10, # 每页条数 tagId: 0, # 评价标签 textFilter: # 文本过滤条件 }这些参数名可能随平台改版而变化但核心逻辑是固定的poiId定位景点pageno控制翻页pagesize控制单页数量。第一次跑的时候我建议只请求一页数据把响应内容先打印出来确认能拿到数据再去写循环。3.2 单页抓取与字段提取确认接口可访问之后就是写代码抓单页数据。下面是简化版的核心逻辑完全可以直接跑通import requests import json import time def fetch_comments(poi_id, page): url https://example.com/comment/list headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/scenic/12345 } params { poiId: poi_id, pageno: page, pagesize: 10, tagId: 0, textFilter: } resp requests.get(url, headersheaders, paramsparams, timeout10) resp.raise_for_status() data resp.json() comments data.get(comments, []) rows [] for item in comments: row { user: item.get(userName), rating: item.get(star, 0), content: item.get(content, ), date: item.get(date, ) } rows.append(row) return rows if __name__ __main__: result fetch_comments(12345, 1) for r in result: print(r)注意几个坑接口返回的JSON结构通常不是平铺的可能套了一层data又可能评论文本在commentList里你打印完再取字段更稳妥。resp.raise_for_status()一定要写它可以帮你在HTTP 4xx/5xx时直接抛异常省得后面拿一堆错误页面死磕。有的接口会对请求频率做限制如果你发现连续请求后突然返回空数据先别改代码等两分钟再试。3.3 翻页、去重与数据落地单页没问题后翻页就简单了核心是循环改pageno。但这里有个很隐蔽的问题部分接口在你请求过快时会在第5页、第10页重复返回之前的数据而不是报错。如果我不做去重后续情感分析会把同一条评论算两遍直接影响统计结果。我的去重方案是这样的把user content date拼成字符串作为唯一标识存到一个set里每抓一条就检查一次seen set() all_comments [] for page in range(1, 11): rows fetch_comments(12345, page) for row in rows: key f{row[user]}|{row[content]}|{row[date]} if key in seen: continue seen.add(key) all_comments.append(row) time.sleep(2)数据落地我选了CSV而不是数据库。原因很直白这个项目的数据量大概几千条CSV用Excel也能打开看结果方便。如果你后续要继续做更复杂的聚合分析再换SQLite也不迟迁移成本很低。写CSV的时候别忘了加utf-8-sig编码否则Windows上打开会中文乱码import csv with open(comments.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[user, rating, content, date]) writer.writeheader() writer.writerows(all_comments)3.4 清洗工作不能省爬下来的数据大概率带各种杂质比如带图评价会拼进图片描述文本、评论内容里残留多余空格、评价表情在JSON里显示成\ue056这类私有字符。这些内容不做清洗后面分词和情感计算都会受影响。我的清洗流程按顺序做三件事去掉空白字符把\n、\t、连续空格替换成单个空格。剔除冗余前缀有些评价内容是“来自iPhone客户端风景不错”这种客户端来源和正文混一起了需要先把“来自某某客户端”即匹配到就删除。过滤内容过短的样本少于4个字的评价通常信息量很少比如“好”“不错”这类文本情感倾向过于单一统计分析时容易把结果带偏。4. 情感评分分析从中文分词到综合得分4.1 数据预处理与中文分词情感分析的第一步还是处理文本。SnowNLP内部会自己做分词不用你额外调jieba但输入文本越干净分词效果越好。我在实际使用中发现评价里的“的”“了”“而且”这类停顿词不会太影响情感分值真正影响大的是否定词和程度副词比如“太差了”“特别好”这类表达SnowNLP本身能处理一部分但肯定不够完美。为了让结果更稳我把文本处理封装成一个函数先做清洗再判断长度最后交给SnowNLP。如果评论是纯表情包或者全部由数字组成就直接丢弃不进入情感分析流程。4.2 情感分FLOW怎么算SnowNLP的用法很简单from snownlp import SnowNLP text 风景很美但排队时间太长了 s SnowNLP(text) print(s.sentiments) # 0.23整体偏负向sentiments返回的是值越接近1越正向越接近0越负向0.5是分界线。但我们不能直接把0.5当唯一标准原因是中文评价里大量句子是“还行”“一般”“无功无过”这类中性表达它们的sentiments可能落在0.4到0.6之间但你不敢说它是好评还是差评。我实际处理时把区间拉大了情感分大于0.6算正向小于0.4算负向中间这段归为中性。这么做的好处是聚合统计时不会被那些“还行”的评价干扰判断。你想如果一家景点有40%的“还行”平台星级显示4.5分这种数据就很有迷惑性。4.3 综合评分模型设计平台星级是1到5的整数情感分析得出的是0到1的小数两个数据直接相加或比较没有意义必须先归一化到同一个尺度。我用的模型是这样的把平台星级分除以5转成0到1区间的标准化分数。把每条评价的SnowNLP情感分单独记录。综合推荐指数 平台星级标准化 × 0.5 平均情感分 × 0.5最后再乘5还原成5分制。说白了就是让平台分和文本情绪各占一半权重。用公式说话import csv from snownlp import SnowNLP def sentiment_score(text): if len(text) 4: return None return SnowNLP(text).sentiments rows [] with open(comments.csv, encodingutf-8-sig) as f: for line in csv.DictReader(f): score sentiment_score(line[content]) if score is None: continue rows.append({ rating: int(line[rating]), sentiment: score }) avg_rating sum(r[rating] for r in rows) / len(rows) avg_sentiment sum(r[sentiment] for r in rows) / len(rows) composite (avg_rating / 5 * 0.5 avg_sentiment * 0.5) * 5 print(f平台平均分: {avg_rating:.2f}) print(f文本情感均分: {avg_sentiment:.3f}) print(f综合推荐指数: {composite:.2f})这个模型的优点是可解释性很强如果平台平均分明显高于综合推荐指数说明评论区的文字情绪并没有星级那么乐观如果反过来说明游客打了低分但文字内容里透露出“下次还来”的意思这种评价同样值得分析。4.4 可视化呈现让结论一眼能看出来分析完不能只停留在终端打印数字我用matplotlib画了一个景区对比图横轴是平台平均分纵轴是情感均分中间加一条对角线落在线下方的景区就是“星级虚高”落在线上的就是“口碑实在”。画词云也很有意思。把评价内容用jieba分词去掉停用词之后用wordcloud生成一张词云图。热门吐槽词会直接暴露问题比如“排队”“卫生”“管理”这些词出现频率高那不用看差评也能猜到游客的不满集中在哪。代码方面不需要多复杂import matplotlib.pyplot as plt plt.scatter(avg_rating, avg_sentiment) plt.xlabel(Platform Rating) plt.ylabel(Sentiment Score) plt.title(Rating vs Sentiment) plt.show()当然如果你的目标是把所有景点放在一起对比建议把结果整理成DataFrame使用groupby按景点聚合后统一出图比一个个画省事得多。5. 常见问题与排查技巧纪实这部分我本来没打算单独写但整个项目做下来发现几个问题是几乎每个朋友上手都会踩的干脆整理成速查表你遇到类似情况直接对症下药。5.1 评价数据突然为空现象第一页抓得好好的翻到第5页突然返回的列表是空的。排查思路先别怀疑数据没有下一页先看接口响应里有没有hasNextPage这类字段。有的接口不是靠返回空列表表示到底而是有一个显式的标志位。如果标志位显示还有下一页但列表为空大概率是请求频率触发风控了把time.sleep(2)调大到time.sleep(5)再试。另一种可能是指定页数过大部分接口单页页码上限是100页。5.2 请求频繁出现403现象抓几十条之后请求返回403 Forbidden。排查思路403基本都是服务端识别出了非浏览器访问。这时候优先检查两件事一是请求头是否完整二是请求频率是否太高。我试过最有效的缓解方式是给请求头补上完整的Accept、Accept-Language字段然后把单次请求间隔随机化让爬取节奏接近真人浏览。记住不要盲目堆并发特别是在没有实际需求的情况下。5.3 中文乱码和Unicode转义问题现象打印出来的评价内容是\u98ce\u666f而不是“风景”。排查思路看到\uXXXX别慌这说明返回的是Unicode转义字符串。如果用的是resp.json()Python会自动完成解码如果用的是resp.text需要手动encode(latin-1).decode(unicode_escape)或者直接json.loads处理。写CSV时记得用utf-8-sig这样Excel打开才不会乱码。5.4 情感分数全都在0.5附近现象跑了500条评价sentiments基本都在0.45到0.55之间区分度很差。排查思路这是SnowNLP默认模型对旅游类文本不适配的典型表现。我做了两件事来改善第一是收集一批已经标注好正负向的景点评论用SnowNLP的贝叶斯模型重新训练第二是对评价中的强情感词做规则补充比如出现“太差”“恶劣”“千万别去”就直接标负向出现“惊艳”“完美”“强烈推荐”直接标正向。5.5 增量抓取怎么做现象前一天抓完的数据第二天想看看有没有新增评价。排查思路不要重新全量抓一遍直接记录上次抓取的最后一条时间翻页时遇到小于这个时间的评价就停止。我本地是存了一个last_crawl_time.txt文件每次抓完把最新时间写进去下次读取作为截止条件。这个方案虽然土但足够可靠。写在最后一点个人体会整个项目从拆解到跑通我花了两个晚上真正花时间的不是写代码而是理解接口返回结构和对情感分结果的反复调校。做爬虫的人经常把注意力放在“能不能抓到数据”上可一旦抓到了真正的挑战才开始怎么清洗、怎么去噪、怎么把文本变成可量化指标每一步都在考验你对业务的理解。我个人的体会是爬虫只是获取数据的手段情感评分分析才是让数据产生价值的关键。如果你也打算用Python练手我建议别只停留在“抓下来存成表格”试着把情感分析和数据可视化做进去哪怕结果不完美过程中暴露的问题也是最好的老师。最后再分享一个小技巧跑数据前记得把所有字段先print一遍肉眼确认三条没问题再放手让它循环跑。看似耽误一分钟实际能帮你省掉后面清洗数据的三小时。