ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python爬虫实战:携程酒店评论数据采集与反爬解析

Python爬虫实战:携程酒店评论数据采集与反爬解析 简介这是一份基于Java实现的携程酒店评论定向爬虫项目面向具备基础Java开发与HTTP协议理解能力的学习者和数据采集实践者旨在解决公开平台酒店评论数据的自动化获取与结构化存储问题。压缩包共34个文件包含10个核心Java类、9个XML配置文件涵盖Spring框架配置与Mapper映射、2个Properties配置文件及1个示例Excel模板整体体积仅149KB轻量易部署。项目采用标准Maven结构含完整pom.xml依赖管理、src/main源码目录及.idea开发配置便于IDE导入与二次开发。已有321人学习下载读者可直接复用其URL调度逻辑、HTML解析策略结合Jsoup或XPath、反爬应对思路如User-Agent轮换及MySQL/Excel双通道存储模块快速构建合规、可调优的垂直领域爬虫系统。1. 这个爬虫项目要解决什么问题携程评论数据到底值不值得抓先说个实际场景。我之前在做酒店消费偏好分析的时候需要大量真实用户点评作为样本找来找去发现携程的酒店评论是最合适的数据源之一覆盖城市广、酒店数量多、评论字段也相对完整。但我总不可能手动一条条复制粘贴于是就有了这个“携程酒店评论爬虫”项目。用 Python 爬虫抓取携程酒店评论本质上是把浏览器里动态加载的评论数据通过程序自动采集下来最后整理成结构化的表格文件方便做统计、分析和可视化。这个项目适合三类人刚学完 requests 基础语法想找真实案例练手的 Python 初学者、需要酒店点评数据做课题或商业分析的研究者、以及想搞懂动态网页数据是怎么加载和解析的爬虫爱好者。这个爬虫项目的定位按照爬虫行业的常规分类属于垂直型爬虫因为它只针对携程这一个垂直领域的评论数据做定向采集如果按采集频率来分它又是一个批量型爬虫一次性把某个酒店或某个城市的评论历史数据尽可能完整地抓下来。如果你后续想持续跟踪酒店口碑变化也可以在这个基础上改造成增量型爬虫只抓新增评论。这个后面我会详细讲。再补充一点很多人一听到“爬虫”就觉得很高深其实普通业务级爬虫的技术栈就三块请求requests、解析BeautifulSoup 或正则、存储csv / Excel / 数据库。这个项目没有用到 Scrapy、Selenium 这些重型框架因为携程的评论接口返回的是结构化的 JSON 数据用 requests 加 json 解析就够了性能和稳定性在单机小规模场景下完全够用。2. 动手前先抓包携程评论的数据到底是怎么加载出来的写爬虫最忌讳的事情就是拿到网址就直接对着 HTML 一顿正则匹配。携程这类大型网站的前端页面大量使用 JavaScript 异步加载你看到的评论内容根本不在初始 HTML 源码里。如果不先搞清楚数据加载方式代码写得再漂亮也抓不到数据。2.1 为什么评论内容不在页面源码里打开携程酒店详情页你会看到评论区域是一块一块显示的用户滚动页面或者点击“查看更多”之后评论才慢慢加载出来。这就是典型的 Ajax 异步加载。评论数据是通过浏览器向服务器发了一个单独的接口请求获取的拿到的是 JSON 数据然后前端 JavaScript 再把 JSON 渲染成页面上你看到的评论卡片。所以如果直接用 requests.get 去抓酒店详情页的 HTML你只能拿到酒店的房型、地址、设施等基础信息评论列表基本是空的或者只有一个“评论加载中”的占位符。2.2 抓包找到真正的评论接口我用 Chrome 的开发者工具来演示这个操作应该人人都应该会是爬虫基本功。先在浏览器里打开一个携程酒店详情页按 F12 打开开发者工具切到Network网络面板。在筛选栏里选择XHR或者Fetch/XHR因为评论数据走的是 Ajax 异步请求不是页面跳转。然后点击页面里的“查看全部点评”或者往下滚动触发评论加载。这时候你会看到网络面板里刷出来一堆请求逐个点开看Preview或Response重点找返回内容里包含评论字段比如 comment、content、评分的请求。通常情况下携程的评论接口地址是类似下面这种 RESTful API 风格https://m.ctrip.com/restapi/soa2/XXXXX/json/getCommentList它用的是 POST 请求请求体里传的是一个 JSON 字符串包含酒店 ID、分页信息等参数。这里我故意把地址里的关键 ID 部分用 XXXXX 模糊处理因为不同时期接口路径会有变化你需要通过自己的抓包来确定实际地址不要刻舟求剑。2.3 接口参数拆解在Payload选项卡里你能看到评论接口提交的参数。携程这种大厂接口的参数有几十个是很正常的并不需要全部理解抓核心的几个就行参数名作用类型说明poiId酒店唯一标识int对应某一家酒店是评论查询的核心条件pageIndex页码int从 1 开始每次翻页加 1pageSize每页评论数量int一般设置 10 或 20设置太大会被服务端拒绝tagId评论文本分类int0 表示全部评论channel数据来源渠道int固定值需要照抄抓包里的其他扩展参数校验、追踪字段不定照抄抓包里的默认值就行不要乱改除了请求体参数之外Headers 里的几个字段也至关重要。User-Agent要设置成浏览器标识Referer一般设置为当前酒店详情页的地址Cookie更是必不可少。携程对未登录用户也能看评论但接口层面通常会对 Cookie 做校验不带 Cookie 的话大概率会返回数据异常或直接弹安全验证。2.4 一个反直觉的规律实际抓包你会发现接口请求次数比你想象得多。携程的评论接口不止一个酒店详情页首屏加载时会同时请求“精选点评”“游客印象”“全部点评概览”等多个接口。你要区分清楚哪个是包含完整评论正文和用户信息的哪个只是统计汇总值。我当时的判断标准就是看响应数据里有没有content评论文本这个字段有就是对的。3. requests 直连实现代码逐段拆解与参数说明抓包搞清楚之后写代码就顺理成章了。这个项目我用的是 Python 3.9 环境核心库只有三个requests、BeautifulSoup4、pandas。没错连 BeautifulSoup4 都只是备用因为评论数据是 JSON直接用内置 json 模块就能解析。3.1 环境准备pip install requests beautifulsoup4 pandas建议在虚拟环境里装别污染系统 Python。3.2 构建请求函数评论接口是 POST 请求所以我们要构造一个session来保持会话状态这样 Cookie 可以自动携带不需要每次请求都手动复制 Cookie。import requests import json import time import random import pandas as pd # 你通过抓包拿到的完整请求头 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/hotels/detail/?hotelId12345, Content-Type: application/json, Origin: https://hotels.ctrip.com, Cookie: 换成你自己的完整Cookie字符串, } def get_comment_list(session, poi_id, page_index, page_size10): 获取某一页评论列表返回JSON对象 url https://m.ctrip.com/restapi/soa2/XXXXX/json/getCommentList payload { poiId: poi_id, pageIndex: page_index, pageSize: page_size, tagId: 0, # 其他字段照抄抓包里的固定参数 } try: resp session.post(url, headersHEADERS, jsonpayload, timeout10) resp.raise_for_status() return resp.json() except requests.exceptions.RequestException as e: print(f第{page_index}页请求失败: {e}) return None这里有一个关键点session.post的json参数会自动把字典序列化成 JSON 字符串同时设置好Content-Type。有的接口要求请求体是纯字符串格式比如 JSON.stringify 之后的结果如果遇到这种情况就改成datajson.dumps(payload)发送。3.3 解析返回数据携程评论接口的返回值结构一般是嵌套的字典大致长这样{ data: { commentList: [ { content: 位置很好离地铁站近房间干净, roomInfo: 高级大床房, score: 4.8, userInfo: {userName: 飞***, level: 3}, checkInDate: 2024-11-20, travelType: 1, hotelReply: ... } ], totalPage: 12, totalCount: 116 } }解析代码就是一层层取字段注意要加判断防止某个字段缺失导致KeyErrordef parse_comment(item): 从单条评论中提取目标字段 return { 酒店ID: str(item.get(poiId, )), 用户昵称: item.get(userInfo, {}).get(userName, ) if item.get(userInfo) else , 评分: item.get(score, ), 评论文本: item.get(content, ).strip(), 房型信息: item.get(roomInfo, ), 入住日期: item.get(checkInDate, ), 出游类型: item.get(travelType, ), 酒店回复: item.get(hotelReply, ).strip(), 有用数: item.get(usefulCount, 0), }3.4 分页和整站抓取逻辑评论是有分页的JSON 里返回了totalPage所以一个完整的抓取函数应该是def crawl_hotel_comments(poi_id, max_pages5): session requests.Session() all_comments [] # 先请求第一页拿到总页数 first_page get_comment_list(session, poi_id, 1) if not first_page: return all_comments total_page first_page.get(data, {}).get(totalPage, 1) total_page min(total_page, max_pages) # 防止页数太多先限制 print(f共 {total_page} 页评论开始抓取...) for page in range(1, total_page 1): data get_comment_list(session, poi_id, page) comment_list data.get(data, {}).get(commentList, []) for item in comment_list: all_comments.append(parse_comment(item)) print(f第 {page}/{total_page} 页完成累计 {len(all_comments)} 条) # 随机延时尽量模拟人类操作 time.sleep(random.uniform(1.5, 3.5)) return all_comments这里有个经验之谈max_pages参数是我特意加的。很多酒店评论有几十页但数据分析并不一定需要全部历史评论抓前几页最新评论往往就够用了。先小批量跑通再扩大范围这个思路能帮你省掉大量调试时间。3.5 入口函数if __name__ __main__: hotel_id 12345 # 替换成你要采集的酒店ID comments crawl_hotel_comments(hotel_id, max_pages3) if comments: df pd.DataFrame(comments) df.to_csv(hotel_comments.csv, indexFalse, encodingutf-8-sig) print(f成功保存 {len(comments)} 条评论到 hotel_comments.csv) else: print(没有抓取到数据检查Cookie或接口地址)到这一步一个最小可用的携程酒店评论爬虫就已经跑通了。但我必须提醒真实运行过程中你大概率会遇到各种反爬干扰下一段我把可能遇到的坑和应对方式完整列出来。4. 被限制后怎么处理反爬干扰与应对思路跑爬虫的人没有不遇到反爬的。尤其携程这种体量的平台反爬策略是分层的先给你一点甜头让你抓几页数据然后突然开始异常校验。我在真正跑通之前就至少撞上了三类问题。4.1 访问频率控制最基础也最容易踩的坑很多初学者拿到接口就兴奋直接用 for 循环连续请求几百页结果到第 20 页左右就开始收到异常响应。这不是因为你代码写错了而是请求频率太高触发了服务端的频率限制。解决方案很简单也有效每请求一页随机等待 2 到 5 秒。上面的代码里已经写了time.sleep(random.uniform(1.5, 3.5))这个延时区间不是随便定的——数据量大的页面响应时间在 0.5 秒到 1.5 秒之间如果请求间隔比页面渲染时间还短说明对面一定不是人类这个规律在爬虫圈几乎是通识。注意延时不是越大越好。设成 10 秒确实不会被封但抓 100 页评论要花 20 分钟太浪费时间。1.5 到 3.5 秒是单机小规模采集的平衡值如果你的需求量级是几万条以上就该考虑用合法合规的数据服务了。4.2 Cookie 失效与登录状态校验携程的评论接口对 Cookie 有校验但校验不是每次都触发。有时候第一页能请求成功到第二页突然要求登录。表现就是返回的 JSON 里data为 null或者出现 “token 校验失败” 之类的错误。当时我排查了好久最后发现是 Cookie 里的某个临时 token 过期了。它有一个有效期过期后需要重新在浏览器里打开页面、退出登录再重新登录然后从 DevTools 里复制新的 Cookie。为了避免来回手动复制我写了一个从本地文件读取 Cookie 的逻辑过期时只改文件内容就行不用动代码。with open(cookie.txt, r, encodingutf-8) as f: cookie_str f.read().strip() HEADERS[Cookie] cookie_str你可能会问为什么不用 requests 的 cookies 参数因为携程服务端校验的不只是 Cookie 字段里的键值对还有 Cookie 字符串本身的格式和顺序。直接把整段字符串塞进 Headers 里是最稳的。4.3 IP 被限制的表现与处理思路如果请求频率控制得比较好IP 一般不会出事。但如果你在短时间内跨城市抓了很多酒店触发了更严格的策略返回内容就会变成一个安全验证页面而不是 JSON 数据。这时候resp.json()会直接抛异常因为返回的是 HTML。判断方法也很简单在get_comment_list里加一个校验如果返回内容不是 JSON 就打印响应文本的前 200 个字符手动确认是不是验证页。应对方式第一选择是停止脚本等一段时间再跑。注意不要立刻换 IP 重试因为频繁更换 IP 在携程侧反而是一种风险信号。如果你确实需要大规模采集去用正规的代理服务商或者干脆走官方数据合作渠道别自己硬碰硬。4.4 参数加密和签名校验有些网站会在请求参数里加入sign、timestamp这类动态计算字段服务端会校验这些参数是否匹配。如果你抓包时发现同一个接口每次请求的参数都有变化比如多了一个deviceInfo或者traceId而且每次都不一样那说明你直接复制固定参数是不行的需要在代码里模拟生成逻辑。携程的部分接口也有类似机制但好在评论列表接口在实测中只要 Cookie 有效、频率正常复制抓包里的固定参数就能跑通。如果真的遇到动态签名技术复杂度会上升不少优先考虑是不是可以用页面渲染的方式替代比如使用 Playwright。这个不是本文主题我先不展开。4.5 反爬问题排查总表现象大概率原因处理办法返回 JSON 里 data 为 nullCookie 失效或参数缺失重新复制 Cookie检查 payload 是否完整返回 HTML 安全验证页面请求频率过高立即停脚本等待 30 分钟以上第一页成功第二页失败频率过快触发风控增大 sleep 区间加随机波动某些评论字段缺失解析路径写错在 DevTools 里对照 JSON 路径抓取的评论数对不上接口返回的不是完整评论列表换接口找包含 content 字段的那个5. 保存与清洗Excel中文乱码和评论去重问题爬虫抓到的原始数据是不能直接拿去分析的必须经过清洗。这一段我把我当时处理数据时的几个典型问题列出来每个都是实际踩过坑的。5.1 中文乱码问题用 pandas 保存 csv 的时候如果直接encodingutf-8用 Excel 打开文件极大概率会出现中文乱码。原因不是数据有问题而是 Excel 默认使用 GBK 编码解析 CSV导致 UTF-8 编码的中文变成乱码。解决办法是用utf-8-sig编码写入它会在文件开头加一个 BOM 头Excel 就能正确识别df.to_csv(hotel_comments.csv, indexFalse, encodingutf-8-sig)这是爬虫数据保存最容易踩的一个坑也是很多初学者最困惑的点。我看到过有人因此把 pandas 版本都重装了其实根本不是环境问题。5.2 评论去重和字段清洗多次运行爬虫或者评论接口因为分页原因重复返回数据会导致最终数据表里有重复行。清洗时以“用户昵称 评论文本 入住日期”三个字段联合去重df.drop_duplicates(subset[用户昵称, 评论文本, 入住日期], inplaceTrue)如果接口返回里有唯一的评论 ID 字段那更简单直接用这个 ID 去重。另外原始 JSON 里的评分是浮点数比如 4.8但有些酒店评论区混合了“4.5分”这种带文案的格式需要统一转换成数值型。出游类型字段返回的是数字编码对应亲子、商务、情侣等你可以自己映射成中文也可以保持原始编码视分析需求而定。5.3 结构化后的数据分析示例数据清洗完之后一个小小的数据表就已经可以做很多分析了。我当时做的第一件事是统计评分分布然后用一段简单的 Python 代码做了个词频统计看看大家评价里提到最多的是什么from collections import Counter def extract_keywords(text_series, top_n20): 简单词频统计生产环境建议用 jieba 分词 words [] for text in text_series.dropna(): # 这里只是示例实际需要先分词 words.extend(str(text).split()) return Counter(words).most_common(top_n) df[评论文本].str.len().describe()拿到词频之后你可以很直观地发现某个酒店被提到最多的是“早餐”“位置”“前台”这些词这就能反馈到消费者的预订决策里了。6. 完整跑通后的几点体会与合规提醒项目跑通之后我有几句真心话想说不是客套话。6.1 爬虫项目里最耗时间的不是写代码是定位接口我修复这个项目的时间分配大概是抓包定位接口占了 60%调试 Cookie 失效占了 20%真正写解析代码只占 10%剩下 10% 是数据清洗。很多新手把精力花在背 requests 用法上觉得代码写得越炫酷越高级。实际上拿到一个网站能不能在 10 分钟内定位到正确的接口、理解参数含义、判断该用 GET 还是 POST这才是核心能力。6.2 为什么选了 requests 而不是 Scrapy评论接口是 JSONrequests 一个 session 就够了不需要上 Scrapy 这种重型框架。Scrapy 的优势在于大规模分布式部署、管道化处理、中间件扩展但对于“抓某一个酒店几百条评论”这个需求Scrapy 反而把简单问题复杂化了。选型标准不是“越高级越好”而是“够用、可控、能快速排查问题”。6.3 合规提醒是必须说透的爬虫是个敏感话题作为从业者我必须把边界讲清楚。个人学习和技术研究用途抓取公开评论数据在合理频率下一般是没有问题的但要遵守几个底线遵守目标网站的 robots 协议和服务条款不抓取需要登录权限才能看到的非公开内容。控制请求频率不要对目标服务器造成负担。抓取的数据仅用于个人学习研究不用于商业用途不批量传播。不涉及用户隐私信息比如手机号、邮箱等。这个项目我只抓取评论内容、昵称和评分不触碰任何身份敏感信息字段设计的时候已经把这些考虑进去了。6.4 后续扩展方向爬虫跑通一批数据之后你可以顺着这三个方向继续深挖增量型改造记录已抓取的最新评论时间或评论 ID后续定期运行脚本只抓新增评论实现酒店口碑的长期跟踪。多酒店批量采集维护一个酒店 ID 列表循环调用crawl_hotel_comments保存到同一个 CSV 文件。分析层用 jieba 分词做中文评论文本的情感分析和主题聚类甚至可以做“酒店吐槽点自动归纳”。我个人在实际操作中的体会是爬虫项目最值钱的不是那一百多行代码而是你愿意花时间去蹲点抓包、去理解对方的数据组织方式、去处理各种边界情况。把这套方法练熟了换任何一个网站你都能在一小时内定位到核心接口。如果你也想练手建议按这个思路自己动手走一遍看到最终生成的 CSV 文件里躺着几百条结构化评论的那个瞬间你会觉得前面所有折腾都值了。本文还有配套的精品资源点击获取
返回列表