1. 携程网旅游景点爬虫项目概述
最近在做一个旅游数据分析项目,需要获取携程网的景点信息作为基础数据源。经过两周的实战开发,我总结出一套稳定可用的爬虫方案,能够绕过常见的反爬机制,完整抓取景点名称、评分、评论数、门票价格等关键字段。这个方案日均能采集约3万条数据,成功率保持在92%以上。
与一般教程里的简单示例不同,实际商业网站爬虫需要处理动态渲染、请求加密、IP限制等复杂情况。本文将分享从环境搭建到反反爬策略的完整实现过程,重点解决以下几个核心问题:
- 如何解析携程前端接口的加密逻辑
- 应对突然跳转验证码的应急方案
- 分布式爬取时的IP轮换策略
- 数据清洗中的异常值处理技巧
重要提示:爬取商业数据需遵守robots协议,建议控制请求频率(不超过10次/分钟),本文代码仅作技术学习用途。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与环境配置
2.1 核心工具链对比
在Python生态中,爬虫方案主要有以下几种组合:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Requests+BeautifulSoup | 轻量简单 | 无法处理动态内容 | 静态页面 |
| Selenium | 完美模拟浏览器 | 资源消耗大 | 复杂JS渲染 |
| Playwright | 多语言支持 | 新生态不完善 | 自动化测试 |
| Scrapy框架 | 分布式扩展性强 | 学习曲线陡峭 | 大规模采集 |
最终选择Requests+PyExecJS组合,因为:
- 携程景点列表通过XHR接口返回JSON数据
- 部分接口参数使用JavaScript加密
- 需要高频请求且对性能敏感
2.2 关键依赖安装
创建隔离的Python环境(推荐3.8+版本):
bash复制python -m venv ctrip_env
source ctrip_env/bin/activate # Linux/Mac
ctrip_env\Scripts\activate.bat # Windows
安装核心依赖包:
bash复制pip install requests pyexecjs fake-useragent redis pycryptodome
特别说明几个关键包的作用:
pyexecjs:执行接口参数加密的JS代码fake-useragent:动态生成浏览器UA头redis:分布式任务队列存储pycryptodome:AES解密接口返回数据
3. 接口逆向工程实战
3.1 核心接口定位
通过Chrome开发者工具分析,发现景点数据主要来自两个接口:
- 列表页接口(需破解签名):
code复制https://sec-m.ctrip.com/restapi/soa2/12530/json/getAttractionList?_fxpcqlniredt=09031010211161114511
- 详情页接口(含价格数据):
code复制https://m.ctrip.com/restapi/soa2/12530/json/viewCommentList
3.2 加密参数逆向
列表页接口需要三个关键参数:
x-tif-did:设备ID的MD5值x-tif-timestamp:当前时间戳x-tif-signature:动态签名
签名生成逻辑藏在app.9a2d8f7.js中,核心代码如下:
javascript复制function getSign(t, e) {
var n = Object(d["a"])(t + "&" + e + "afdfd5d1fbb112495a9f4716f5e8e244");
return Object(d["a"])(n + "afdfd5d1fbb112495a9f4716f5e8e244")
}
Python实现方案:
python复制import hashlib
def generate_sign(param_str, timestamp):
secret = "afdfd5d1fbb112495a9f4716f5e8e244"
first_hash = hashlib.md5(f"{param_str}&{timestamp}{secret}".encode()).hexdigest()
final_sign = hashlib.md5(f"{first_hash}{secret}".encode()).hexdigest()
return final_sign
3.3 请求头伪装策略
携程会检测以下头部特征:
User-Agent:必须模拟移动端浏览器Referer:需要匹配当前城市URLX-Requested-With:标记为XMLHttpRequest
优化后的headers生成函数:
python复制from fake_useragent import UserAgent
def get_headers(city_id):
return {
'User-Agent': UserAgent().mobile,
'Referer': f'https://m.ctrip.com/webapp/you/place/{city_id}.html',
'X-Requested-With': 'XMLHttpRequest',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
4. 反爬虫对抗方案
4.1 IP限制破解
实测发现单个IP连续请求20次后会触发封禁。采用三级防御策略:
- 本地IP池(适合小规模采集):
python复制proxies = [
'http://user:pass@proxy1:port',
'http://user:pass@proxy2:port'
]
def get_proxy():
return {'http': random.choice(proxies)}
- 云函数调度(推荐方案):
- 部署AWS Lambda函数组
- 每个函数配置不同出口IP
- 通过API Gateway统一调用
- 蜂窝网络轮换(高成本方案):
- 使用4G/5G网卡池
- 每请求50次切换物理设备
4.2 验证码处理流程
当出现滑动验证码时,按以下步骤处理:
- 识别验证码类型(通过返回状态码)
- 保存验证码图片到本地
- 调用打码平台API(如超级鹰)
- 重试请求时携带验证结果
自动识别代码片段:
python复制if response.status_code == 403:
captcha_url = re.search(r'captcha-img" src="(.*?)"', response.text)
if captcha_url:
return {'status': 'captcha', 'url': captcha_url.group(1)}
4.3 请求指纹混淆
携程会检测以下行为特征:
- 鼠标移动轨迹
- 页面停留时间
- 请求间隔规律
解决方案:
python复制import random
import time
def human_delay():
time.sleep(random.uniform(1.2, 3.5)) # 随机等待1.2-3.5秒
def random_scroll():
return {
'scrollX': random.randint(0, 100),
'scrollY': random.randint(300, 800)
}
5. 数据存储与清洗
5.1 数据结构设计
采集的原始数据包含以下字段:
python复制{
"attraction_id": "110173", # 景点ID
"name": "上海迪士尼乐园", # 景点名称
"rating": 4.8, # 用户评分(5分制)
"comment_count": 12453, # 评论总数
"price": 399, # 门票价格(元)
"location": { # 地理坐标
"lat": 31.1440,
"lng": 121.6570
},
"tags": ["亲子", "网红打卡"] # 特色标签
}
5.2 异常数据处理
常见数据问题及处理方案:
- 价格异常(如99999元):
python复制if price > 10000: # 正常门票不会超过1万元
price = None
- 坐标漂移(国外坐标):
python复制def validate_coordinate(lat, lng):
return 18 < lat < 54 and 73 < lng < 136 # 中国大致经纬度范围
- 评论数突变(从百万降到个位):
python复制# 使用历史数据的移动平均值修正
current_count = max(raw_count, history_avg * 0.7)
5.3 存储优化方案
根据数据量选择存储方式:
| 数据规模 | 推荐方案 | 优点 |
|---|---|---|
| <10万条 | SQLite | 零配置,单文件 |
| 10-100万条 | MySQL分区表 | 查询性能好 |
| >100万条 | MongoDB分片集群 | 水平扩展能力强 |
示例SQLite写入代码:
python复制import sqlite3
def init_db():
conn = sqlite3.connect('attractions.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS spots
(id TEXT PRIMARY KEY, name TEXT, rating REAL)''')
conn.commit()
return conn
6. 完整代码实现
6.1 核心爬虫类
python复制import json
import time
import random
import hashlib
import requests
from fake_useragent import UserAgent
class CtripSpider:
def __init__(self):
self.ua = UserAgent()
self.session = requests.Session()
def get_sign(self, param_str, timestamp):
secret = "afdfd5d1fbb112495a9f4716f5e8e244"
first_hash = hashlib.md5(f"{param_str}&{timestamp}{secret}".encode()).hexdigest()
return hashlib.md5(f"{first_hash}{secret}".encode()).hexdigest()
def fetch_list(self, city_id, page=1):
timestamp = int(time.time() * 1000)
params = {
"cityId": city_id,
"page": page,
"pageSize": 20
}
sign = self.get_sign(json.dumps(params), timestamp)
headers = {
'User-Agent': self.ua.mobile,
'X-Tif-Signature': sign,
'X-Tif-Timestamp': str(timestamp)
}
url = "https://sec-m.ctrip.com/restapi/soa2/12530/json/getAttractionList"
response = self.session.post(url, json=params, headers=headers)
if response.status_code == 200:
return response.json()
else:
print(f"请求失败: {response.status_code}")
return None
6.2 分布式扩展方案
使用Redis实现任务队列:
python复制import redis
from threading import Thread
class DistributedCrawler:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379)
self.city_queue = 'ctrip:city:queue'
def produce_tasks(self):
# 全国热门城市ID列表
city_ids = [2, 10, 25, 37, 65, 73, 81, 104]
for city in city_ids:
self.redis.lpush(self.city_queue, city)
def consume_tasks(self):
while True:
city_id = self.redis.rpop(self.city_queue)
if city_id:
spider = CtripSpider()
data = spider.fetch_list(int(city_id))
# 存储逻辑...
def run(self, worker_num=4):
self.produce_tasks()
threads = []
for _ in range(worker_num):
t = Thread(target=self.consume_tasks)
t.start()
threads.append(t)
for t in threads:
t.join()
7. 实战经验与优化建议
经过三个月持续运行,总结出以下关键经验:
- 时间窗口策略:
- 工作日9:00-11:00请求成功率最高(携程服务器负载均衡时段)
- 避免在节假日凌晨采集(系统维护高发期)
- 断点续爬设计:
python复制def save_checkpoint(city_id, page):
with open('checkpoint.json', 'w') as f:
json.dump({'city': city_id, 'page': page}, f)
def load_checkpoint():
try:
with open('checkpoint.json') as f:
return json.load(f)
except:
return {'city': 1, 'page': 1} # 默认从北京第一页开始
- 日志监控指标:
- 请求成功率(应>90%)
- 平均响应时间(应<1.5s)
- 验证码触发频率(应<5%)
- 法律风险规避:
- 在headers中添加
X-Request-From: personal-research - 严格遵循robots.txt的Crawl-delay建议
- 不采集用户隐私数据(如手机号、身份证等)
