1. 项目概述
最近在做一个火车票余票监控的小工具,目标是实现去哪儿网多日期火车票余票的异步查询。这个需求源于实际生活中的痛点——春运期间抢票实在太难了,手动刷新不仅效率低,还容易错过放票时间点。
通过Python异步爬虫技术,我们可以实现:
- 同时查询多个日期的余票情况
- 自动监控特定车次的余票变化
- 设置余票阈值自动提醒
- 避开人工刷新的繁琐操作
2. 技术选型与架构设计
2.1 为什么选择异步爬虫?
传统同步爬虫在查询多个日期余票时,需要顺序发送请求,等待每个响应返回后才能继续下一个。而异步爬虫可以同时发起多个请求,显著提升查询效率。
实测对比:
- 同步方式查询5个日期的余票:约8-12秒
- 异步方式同样任务:仅需2-3秒
2.2 核心组件设计
系统分为三个主要模块:
- 请求层:异步HTTP客户端
- 解析层:HTML/JSON数据处理
- 存储层:数据持久化
python复制class TicketMonitor:
def __init__(self):
self.session = aiohttp.ClientSession()
self.parser = TicketParser()
self.storage = DataStorage()
async def fetch_dates(self, dates):
tasks = [self.fetch_one_date(date) for date in dates]
return await asyncio.gather(*tasks)
3. 环境准备
3.1 基础环境要求
- Python 3.7+(必须支持async/await语法)
- aiohttp 3.8+(异步HTTP客户端)
- beautifulsoup4 4.11+(HTML解析)
- pandas 1.3+(数据处理)
安装命令:
bash复制pip install aiohttp beautifulsoup4 pandas
3.2 代理配置建议
为避免IP被封禁,建议配置代理池:
- 轮换多个代理IP
- 设置合理的请求间隔
- 模拟正常用户行为模式
python复制proxy = "http://user:pass@proxy_ip:port"
conn = aiohttp.TCPConnector(limit=30, force_close=True)
session = aiohttp.ClientSession(connector=conn)
4. 核心实现
4.1 请求参数构造
去哪儿网火车票API的关键参数:
- departDate:出发日期(格式YYYY-MM-DD)
- fromStation:出发站代码
- toStation:到达站代码
- trainNo:车次编号(可选)
python复制def build_params(date, from_station, to_station):
return {
"departDate": date,
"fromStation": from_station,
"toStation": to_station,
"channel": "pc"
}
4.2 异步请求实现
使用aiohttp实现并发请求:
python复制async def fetch_one_date(self, date):
url = "https://train.qunar.com/api/train/trains"
params = self.build_params(date, self.from_station, self.to_station)
try:
async with self.session.get(url, params=params) as resp:
if resp.status == 200:
data = await resp.json()
return self.parser.parse(data)
except Exception as e:
print(f"Error fetching {date}: {str(e)}")
return None
4.3 响应数据解析
典型响应数据结构:
json复制{
"data": {
"trains": [
{
"trainNo": "G123",
"departTime": "08:00",
"arriveTime": "12:30",
"duration": "4小时30分",
"seats": [
{
"type": "二等座",
"price": "553",
"count": "5"
}
]
}
]
}
}
解析逻辑:
python复制def parse(self, data):
results = []
for train in data.get("data", {}).get("trains", []):
item = {
"train_no": train["trainNo"],
"depart_time": train["departTime"],
"seats": {}
}
for seat in train.get("seats", []):
item["seats"][seat["type"]] = {
"price": seat["price"],
"count": seat["count"]
}
results.append(item)
return results
5. 数据存储与展示
5.1 数据存储方案
推荐两种存储方式:
- CSV文件:适合小规模数据
- SQLite数据库:适合长期监控
python复制def save_to_csv(self, data, filename):
df = pd.DataFrame(data)
df.to_csv(filename, index=False, encoding="utf_8_sig")
5.2 结果可视化
使用pandas进行简单分析:
python复制def analyze_results(results):
df = pd.DataFrame(results)
print(df.groupby("train_no")["seats"].value_counts())
# 找出有余票的车次
available = df[df["seats"].apply(lambda x: any(int(s["count"])>0 for s in x.values()))]
return available
6. 高级技巧与优化
6.1 请求头优化
模拟浏览器行为的关键headers:
python复制headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Referer": "https://train.qunar.com/",
"Accept-Encoding": "gzip, deflate, br"
}
6.2 异常处理机制
完善的错误处理应包括:
- 网络异常重试
- 频率限制检测
- 数据格式校验
python复制async def fetch_with_retry(self, url, params, max_retries=3):
for attempt in range(max_retries):
try:
async with self.session.get(url, params=params) as resp:
if resp.status == 200:
return await resp.json()
elif resp.status == 429:
await asyncio.sleep(2 ** attempt) # 指数退避
except Exception:
await asyncio.sleep(1)
return None
7. 常见问题排查
7.1 请求返回空数据
可能原因:
- 车站代码不正确
- 日期格式错误
- IP被限制
解决方案:
- 验证车站代码(可通过页面源码查找)
- 检查日期格式是否为YYYY-MM-DD
- 更换IP或增加请求间隔
7.2 异步任务卡住
调试技巧:
- 设置超时时间
- 使用asyncio.wait_for
- 限制并发数量
python复制async def safe_fetch(self, task, timeout=10):
try:
return await asyncio.wait_for(task, timeout=timeout)
except asyncio.TimeoutError:
print("Request timeout")
return None
8. 项目扩展思路
8.1 监控告警功能
可以集成邮件/短信通知:
python复制def send_alert(train_info):
msg = f"有余票!车次:{train_info['train_no']},时间:{train_info['depart_time']}"
# 实现邮件或短信发送逻辑
8.2 分布式扩展
使用Redis作为任务队列:
- 生产者:生成查询任务
- 消费者:执行爬取任务
- 结果汇总中心
9. 法律合规提示
重要注意事项:
- 查询频率不宜过高(建议≥5秒/次)
- 不要用于商业倒票等非法用途
- 尊重网站robots.txt规定
- 建议在个人学习研究范围内使用
提示:本文技术方案仅用于学习交流,请勿用于任何违反相关法律法规的用途。实际应用中请合理控制请求频率,避免对目标网站造成过大负担。