尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python爬虫实战:逆向分析携程AJAX接口高效获取景点评论数据

Python爬虫实战:逆向分析携程AJAX接口高效获取景点评论数据 1. 项目概述与核心价值最近在做一个旅游数据分析的小项目需要获取大量景点的用户评价来做情感分析和口碑挖掘。我第一时间就想到了携程作为国内最大的在线旅游平台之一它的景点评论数据质量高、数量大是绝佳的数据源。但当我像往常一样准备用传统的页面解析方法去爬取时发现事情没那么简单——携程景点评论页面的翻页早就不是简单的?page2这种静态链接了。点开一个景点的评论页面比如“上海迪士尼乐园”你会发现点击“下一页”按钮时页面的URL根本没变但评论列表却刷新了。这明显是AJAX技术在背后搞鬼。对于爬虫开发者来说这既是挑战也是机会。挑战在于你不能再简单地遍历页码链接机会在于一旦你搞懂了它的AJAX请求机制数据获取会变得非常高效和稳定因为你是直接在和服务器“对话”跳过了渲染整个页面的开销。这个项目的核心就是逆向分析携程景点评论的AJAX接口实现一个能够稳定、高效爬取多页评论数据的爬虫。它不仅能帮你拿到结构化的评论数据包括用户昵称、评分、评论内容、出游时间等更重要的是掌握这套方法后你可以举一反三应用到其他大量使用AJAX动态加载数据的网站上。无论是电商平台的商品评价、内容社区的用户动态还是其他旅游、生活服务类网站其核心思路都是相通的。2. 核心思路与技术选型面对一个AJAX动态加载的网站我们的爬虫策略必须从“模拟浏览器点击”转向“直接请求数据接口”。整个思路可以拆解为以下几个关键步骤2.1 逆向工程从表象到本质首先我们需要使用浏览器的开发者工具Chrome DevTools 或 Firefox Developer Tools进行网络抓包分析。这是整个项目的基石。具体操作是打开目标景点的评论页面保持Network面板开启并勾选“XHR”或“Fetch”过滤器然后手动点击“下一页”按钮。这时Network面板中会立刻出现一个新的请求这个请求就是承载着下一页评论数据的AJAX请求。我们的任务就是仔细研究这个请求请求URL (Request URL)这是数据接口的地址。我们需要观察它的规律看页码参数是如何嵌入的。是作为查询字符串?page2还是作为请求体Payload的一部分请求方法 (Request Method)通常是GET或POST。请求头 (Request Headers)这里藏着很多“钥匙”。User-Agent用来伪装成浏览器Referer告诉服务器你从哪个页面跳转过来而最关键的往往是Cookie它包含了维持会话状态比如你是否登录的信息。对于携程这类网站Cookie是反爬的重点关照对象。请求参数 (Query String Parameters 或 Payload)这是翻页逻辑的核心。我们需要找到控制页码的那个参数。它可能叫page、pageIndex、offset也可能是其他名字甚至是一个需要计算的加密值。2.2 工具选型Python生态的黄金组合基于上述思路我选择了Python生态中久经考验的组合Requests库用于发送HTTP请求。它简单、强大是处理网络请求的事实标准。我们将用它来模拟浏览器发送AJAX请求。BeautifulSoup4 或 lxml用于解析HTML。虽然AJAX接口返回的数据可能是JSON格式更理想但有时接口返回的也可能是包含数据的HTML片段这时就需要用它们来提取信息。本次分析发现携程接口返回的是JSON所以解析会更简单。浏览器开发者工具这不是Python库但却是最重要的“工具”。手动分析阶段完全依赖它。为什么不直接用Selenium或Playwright它们能完美模拟浏览器操作确实可以绕过AJAX分析。但对于这种结构相对固定、只需获取数据的任务直接请求接口的效率要高几个数量级无需启动浏览器、渲染页面资源消耗也小得多更适合大规模、稳定的数据采集。3. 逆向分析与接口破解实录理论说再多不如实际操作一遍。我们以上海迪士尼乐园的评论页为例进行现场分析。3.1 定位核心AJAX请求打开携程景点页面进入评论部分打开浏览器开发者工具的Network面板。清空记录后点击“下一页”。你会看到列表中瞬间出现了一个新的请求其类型为XHR或Fetch名称可能类似于getcommentlist或包含ajax字样。点击这个请求查看其详细信息。这是最关键的一步。3.2 解码请求参数与Headers在Headers标签页下我们重点关注General确认请求方法是POST还是GET。我实测发现携程景点评论翻页是一个POST请求。Request Headers我们需要复制以下几项在代码中还原User-Agent: 模拟一个常见的浏览器例如Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...Referer: 这个值通常是当前景点页面的完整URL。它告诉服务器请求来源于合法的景点页面是常见的反爬校验。Content-Type: 对于POST请求如果是表单数据通常是application/x-www-form-urlencoded如果是JSON则是application/json。这决定了我们如何构造请求体。Cookie:这是重中之重携程会通过Cookie验证会话和用户状态。没有有效的Cookie请求很可能返回空数据或错误。这个Cookie需要你从当前已打开的浏览器中复制。Request Payload或Query String Parameters对于POST请求参数在Payload里对于GET请求在Query String里。我们需要找到翻页参数。经过分析携程的接口参数可能类似以下结构参数名仅为示例实际需要你现场分析{ poiId: 123456, // 景点的唯一ID从页面URL或源码中获取 pageIndex: 2, // 当前页码这就是我们要控制的翻页参数 pageSize: 10, // 每页显示多少条评论 orderType: 1, // 排序方式例如按时间排序 // ... 可能还有其他固定参数 }关键就是找到那个随着点击“下一页”而变化的参数它大概率就是pageIndex或类似名称。3.3 解析响应数据切换到Response或Preview标签页查看服务器返回的数据。理想情况下它是结构清晰的JSON。你会看到类似这样的结构{ status: success, data: { commentList: [ { userName: 旅行家小明, score: 5.0, content: 非常棒的体验孩子玩得很开心, travelDate: 2023-10-01, // ... 其他字段 }, // ... 更多评论 ], totalPage: 50 // 总页数这个信息对我们控制爬取范围非常有用 } }确认了响应格式是JSON我们就可以直接用Python的json库来解析比解析HTML简单得多。注意实际参数名和结构可能因携程前端代码更新而变化。以上仅为演示逻辑你必须以自己实时分析的结果为准。这是爬虫工作的常态——网站一变分析就要重来。4. 爬虫代码实现与核心环节分析清楚后我们就可以动手写代码了。整个过程分为几个模块请求参数构造、会话维持、数据解析和翻页循环。4.1 基础请求模块构建首先我们需要用requests.Session()来维持一个会话。Session对象可以自动处理Cookies在连续请求时保持登录状态非常方便。import requests import json import time from typing import Dict, List, Optional class CtripCommentSpider: def __init__(self): self.session requests.Session() # 设置请求头伪装成浏览器 self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Referer: https://you.ctrip.com/sight/shanghai2/111111.html, # 替换为实际景点URL Content-Type: application/x-www-form-urlencoded; charsetUTF-8, # 根据实际分析修改 # Cookie 不能写死最好通过登录或从浏览器复制后动态设置 } self.session.headers.update(self.headers) self.base_url https://you.ctrip.com/destinationsite/TourismSiteRestService # 示例接口地址需替换这里有个关键点Cookie。对于携程直接请求评论接口可能需要一个已登录的Cookie才能获取完整数据否则可能只返回前几页。你有几种选择手动从浏览器复制Cookie字符串临时赋给self.headers[Cookie]。适合小规模测试。编写模拟登录代码通过账号密码获取有效Cookie。这更自动化但需要处理登录验证可能有验证码。观察是否可以不登录获取全部数据。有时网站对未登录用户会限制访问页数。4.2 核心请求与解析函数接下来编写一个函数用于请求某一页的数据并解析。def fetch_one_page(self, poi_id: str, page_index: int) - Optional[Dict]: 获取单页评论数据 # 1. 构造请求参数 (Payload) # 这些参数需要你根据实际抓包分析得到 payload { poiId: poi_id, resourceId: poi_id, # 有时是另一个参数 pageIndex: str(page_index), # 翻页的核心参数 pageSize: 10, orderType: 1, starType: 0, # _ 或 callback 参数可能是时间戳用于防止缓存有时需要添加 # _: str(int(time.time() * 1000)) } try: # 2. 发送POST请求 # 注意根据抓包结果确认是form-data还是x-www-form-urlencoded # 如果是form-data用 files 或 data如果是urlencoded用 data response self.session.post(self.base_url, datapayload, timeout10) response.raise_for_status() # 检查请求是否成功 # 3. 解析响应 # 有时响应是JSONP格式包含在回调函数中需要先提取JSON部分 resp_text response.text if resp_text.startswith(callback() and resp_text.endswith()): resp_text resp_text[9:-1] # 去掉 callback( 和 ) data_dict json.loads(resp_text) # 4. 检查状态并提取数据 if data_dict.get(status) success or data_dict.get(returnCode) 0: # 提取评论列表和总页数 comment_list data_dict.get(data, {}).get(commentList, []) total_page data_dict.get(data, {}).get(totalPage, 1) return { comments: comment_list, total_page: total_page, current_page: page_index } else: print(f第{page_index}页请求失败返回状态异常: {data_dict.get(message)}) return None except requests.exceptions.RequestException as e: print(f第{page_index}页网络请求错误: {e}) return None except json.JSONDecodeError as e: print(f第{page_index}页响应JSON解析错误: {e}) print(f原始响应: {response.text[:200]}) # 打印前200字符辅助调试 return None这个函数完成了从构造参数到解析数据的完整链路。注意异常处理非常重要网络爬虫必须足够健壮以应对各种意外。4.3 实现自动翻页与数据存储有了单页抓取能力翻页就简单了——一个循环而已。但我们需要先获取总页数。def crawl_all_pages(self, poi_id: str, start_page1, max_pagesNone): 爬取所有页或指定页数的评论 all_comments [] # 先获取第一页从中得到总页数信息 first_page_data self.fetch_one_page(poi_id, start_page) if not first_page_data: print(获取第一页数据失败程序终止。) return all_comments all_comments.extend(first_page_data[comments]) total_page first_page_data[total_page] print(f景点ID: {poi_id}, 评论总页数: {total_page}) # 计算实际要爬取的最大页数 end_page total_page if max_pages is not None: end_page min(start_page max_pages - 1, total_page) # 循环爬取后续页面 for page in range(start_page 1, end_page 1): print(f正在爬取第 {page}/{end_page} 页...) page_data self.fetch_one_page(poi_id, page) if page_data: all_comments.extend(page_data[comments]) else: print(f第 {page} 页爬取失败跳过。) # 礼貌性延迟避免请求过快被封IP time.sleep(1.5 random.random()) # 随机延迟1.5~2.5秒 print(f爬取完成共获取 {len(all_comments)} 条评论。) return all_comments def save_to_file(self, comments: List[Dict], filenamectrip_comments.json): 将评论数据保存为JSON文件 with open(filename, w, encodingutf-8) as f: json.dump(comments, f, ensure_asciiFalse, indent2) print(f数据已保存至 {filename})最后写一个主函数把它们串起来def main(): spider CtripCommentSpider() # 关键步骤设置有效的Cookie这里需要你手动更新 # 方法1从浏览器手动复制临时测试用 # spider.session.headers.update({Cookie: 你的Cookie字符串}) # 方法2如果实现了登录函数调用它 # spider.login(your_username, your_password) # 景点ID需要从目标景点页面的URL或源码中提取 # 例如URL为 https://you.ctrip.com/sight/shanghai2/76551.html 则poi_id可能是76551 poi_id 76551 # 上海迪士尼乐园示例ID请替换 # 开始爬取 all_comments spider.crawl_all_pages(poi_id, start_page1, max_pages5) # 先测试爬5页 # 保存数据 if all_comments: spider.save_to_file(all_comments, fctrip_comments_{poi_id}.json) # 简单打印几条看看 for i, comment in enumerate(all_comments[:3]): print(f评论{i1}: 用户{comment.get(userName)}, 评分{comment.get(score)}, 内容{comment.get(content)[:30]}...) if __name__ __main__: main()5. 关键细节、反爬策略与实战心得代码框架搭好了但要让爬虫稳定、长期运行还需要处理很多细节和反爬机制。5.1 参数动态化与签名现代网站的AJAX接口参数往往不是明文传递pageIndex这么简单。你可能会遇到时间戳参数如_t或_其值是一个13位毫秒级时间戳。这主要用于防止缓存我们必须每次请求时动态生成。payload[_] str(int(time.time() * 1000))签名参数如sign或token。这是最强的反爬手段之一。服务器会使用一套算法通常涉及多个参数和密钥计算出一个签名随请求一起发送。服务器收到后会用同样算法验签不一致则拒绝。逆向签名算法是爬虫中最难的部分可能需要分析前端JavaScript代码。如果遇到你需要仔细研究Network中第一个请求的initiator调用栈找到生成签名的JS函数。5.2 Cookie与会话维持对于携程这类需要登录态的网站Cookie的管理是生命线。获取Cookie除了手动复制更可靠的方法是模拟登录。你需要分析携程的登录接口通常是另一个AJAX请求提交用户名、密码可能是加密后的以及可能的验证码。成功后Session会自动保存服务器返回的Cookie。Cookie过期Cookie会过期。一个健壮的爬虫需要检测“未登录”状态如接口返回特定错误码或跳转到登录页然后自动重新登录更新Cookie。多账号轮换如果数据量巨大考虑准备多个账号在Cookie失效或请求频率受限时自动切换。5.3 请求头Headers的完备性只设置User-Agent和Referer可能不够。有些网站会检查Accept、Accept-Language、Accept-Encoding、Origin等头部。最稳妥的办法是直接从浏览器开发者工具里把那个成功的AJAX请求的所有Headers除了可能过长的Cookie全部复制下来作为一个字典用在requests中。self.headers { Accept: application/json, text/javascript, */*; q0.01, Accept-Encoding: gzip, deflate, br, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, Host: you.ctrip.com, Origin: https://you.ctrip.com, Sec-Fetch-Dest: empty, Sec-Fetch-Mode: cors, Sec-Fetch-Site: same-origin, X-Requested-With: XMLHttpRequest, # 这是一个关键头表明是AJAX请求 # ... 以及之前提到的 User-Agent, Referer, Content-Type }5.4 频率控制与代理IP毫无节制地高速请求是找死。必须实施礼貌的爬取策略固定延迟在每次请求间加入time.sleep(interval)。随机延迟更好的是随机延迟time.sleep(1 random.random() * 2)让请求模式更接近人类。代理IP池当单IP请求频率过高被封锁时就需要使用代理IP。可以购买付费代理服务或者自建代理池。在requests中使用代理很简单proxies { http: http://your-proxy-ip:port, https: http://your-proxy-ip:port, } response self.session.post(url, datapayload, proxiesproxies, timeout10)5.5 数据清洗与存储拿到原始JSON数据后通常还需要清洗字段提取不是所有返回字段都需要提取你关心的如content,score,userNickName,travelDate。HTML标签清理评论内容content里可能包含br/、p等HTML标签需要用BeautifulSoup或正则表达式清理。时间格式化将时间字符串转换为统一的datetime对象便于分析。去重根据评论ID或用户时间内容哈希进行去重。存储除了存为JSON根据分析需求也可以存入CSV、数据库如SQLite、MySQL或数据仓库。6. 常见问题排查与调试技巧在开发过程中你肯定会遇到各种问题。下面是一些常见坑点和排查思路6.1 请求返回空数据或错误状态码问题代码逻辑没错但返回的commentList为空或者status不是success。排查检查Cookie这是最常见的原因。打印或检查你请求中携带的Cookie是否有效。可以尝试在浏览器中手动访问接口URL在地址栏输入完整的URL和参数看是否返回数据。检查参数仔细比对代码中的payload和浏览器抓包看到的Payload确保每个键值对都一致特别是那些看似随机的长字符串参数。检查请求头确保Content-Type、Referer、Origin等头部与浏览器一致。X-Requested-With: XMLHttpRequest这个头经常被服务器用来判断是否为AJAX请求。检查请求方法确认是GET还是POST用错了方法服务器可能不处理。6.2 只能爬到前几页数据问题爬取到第3页或第5页后返回的数据就和第一页一样或者提示“没有更多数据”。排查未登录限制很多网站对未登录用户只开放前几页评论。你必须解决登录问题获取有效登录态Cookie。参数依赖可能某一页的请求参数依赖于上一页响应中的某个token或key。你需要分析连续翻页时请求参数的变化规律不仅仅是pageIndex在变。频率限制请求太快被临时限制了。增加请求间隔加入随机延迟。6.3 遇到“请验证”或滑块验证码问题请求返回一段提示验证的HTML或JSON。排查行为检测你的请求频率、模式被识别为非人类。立即降低频率并确保请求头尽可能真实。IP被封当前IP地址被暂时或永久封禁。必须使用代理IP并考虑使用高质量高匿、稳定的代理服务。验证码破解对于复杂的图形或滑块验证码可以考虑使用第三方打码平台如超级鹰、图鉴进行识别但这会增加复杂度和成本。对于个人项目遇到验证码时最好暂停手动处理或更换IP/账号。6.4 调试技巧保存请求和响应在调试阶段将每次请求的URL、Headers、Payload以及响应的状态码、内容保存到日志文件中方便对比分析。import logging logging.basicConfig(levellogging.DEBUG, format%(asctime)s - %(levelname)s - %(message)s)使用Postman或Curl复现将浏览器中抓到的请求直接导入到Postman或转换成cURL命令。如果能复现成功说明问题出在你的代码参数构造上如果不能说明请求本身可能依赖浏览器上下文如特定的Cookie或动态生成的Token。打印关键信息在代码中打印出构造的payload、关键的headers与浏览器抓包的结果逐行对比。6.5 数据字段缺失或错位问题解析出来的数据某些字段为空或者字段对应关系错了。排查JSON结构变化网站前端更新可能导致接口返回的JSON结构微调。重新抓包分析最新的数据结构。编码问题确保保存文件时使用utf-8编码json.dump时设置ensure_asciiFalse。数据清洗逻辑错误检查你的数据提取和清洗代码特别是列表索引和字典键名确保它们与实际的JSON结构匹配。爬取AJAX网站的核心在于耐心和细致的逆向分析。每一个参数、每一个请求头都可能有其作用。没有一成不变的代码只有不断适应变化的思路。当你成功抓取到数据的那一刻你会发现之前所有的调试和排查都是值得的。这套方法不仅适用于携程更是你应对现代Web动态加载数据的通用钥匙。
返回列表