1. 项目背景与核心价值
最近在帮朋友做出行规划时,发现手动查询多日期的火车票余票情况特别麻烦。每次都要反复输入出发地、目的地和日期,效率极低。于是萌生了开发一个自动化查询系统的想法,目标是实现多日期、多车次的余票实时监控。
去哪儿网作为国内主流OTA平台,其火车票查询接口稳定且数据准确。但直接调用官方API存在频率限制和反爬机制,需要设计合理的异步请求策略。这个项目就是基于Python异步编程技术,实现对去哪儿网火车票API的高效调用,构建一个稳定可靠的多日期余票查询系统。
2. 技术方案设计
2.1 整体架构设计
系统采用生产者-消费者模式,主要分为三个模块:
- 请求生成器:负责构造合法的API请求参数
- 异步请求引擎:使用aiohttp实现并发请求
- 数据处理中心:解析响应并存储有用信息
async def main(): # 初始化任务队列 task_queue = asyncio.Queue() # 启动生产者协程 producers = [asyncio.create_task(producer(task_queue)) for _ in range(3)] # 启动消费者协程 consumers = [asyncio.create_task(consumer(task_queue)) for _ in range(10)] await asyncio.gather(*producers) await task_queue.join() for c in consumers: c.cancel()2.2 关键技术选型
- 异步框架:aiohttp + asyncio组合,相比requests同步库能大幅提升IO密集型任务的效率
- 数据解析:使用parsel库(Scrapy底层解析库)处理HTML,比BeautifulSoup性能更好
- 存储方案:SQLite轻量级数据库,适合中小规模数据存储
- 反反爬策略:随机User-Agent + 代理IP池 + 请求间隔控制
提示:去哪儿网的API接口有严格的频率限制,单个IP请求间隔建议控制在3-5秒
3. 核心实现细节
3.1 请求参数构造
去哪儿网火车票查询API需要以下关键参数:
- departCity:出发城市编码
- arriveCity:到达城市编码
- departDate:出发日期(YYYY-MM-DD)
- trainType:列车类型(可多选)
城市编码需要通过先验知识获取,我们整理了一份包含300+城市的编码对照表:
city_map = { "北京": "BJP", "上海": "SHH", "广州": "GZQ", # ...其他城市 }3.2 异步请求实现
使用aiohttp的ClientSession实现并发请求,关键代码如下:
async def fetch(session, url, params): try: async with session.get(url, params=params, headers=random_header(), proxy=random_proxy()) as resp: if resp.status == 200: return await resp.text() else: logging.warning(f"请求失败: {resp.status}") return None except Exception as e: logging.error(f"请求异常: {str(e)}") return None3.3 数据解析策略
响应数据是HTML片段,需要提取以下关键信息:
- 车次编号
- 出发/到达时间
- 历时
- 各席别余票情况
- 票价信息
使用parsel的XPath解析示例:
def parse_trains(html): selector = parsel.Selector(html) trains = [] for item in selector.xpath('//div[@class="train-list"]/div'): train = { 'number': item.xpath('.//div[@class="number"]/text()').get(), 'depart_time': item.xpath('.//div[@class="time"][1]/text()').get(), 'arrive_time': item.xpath('.//div[@class="time"][2]/text()').get(), 'duration': item.xpath('.//div[@class="duration"]/text()').get(), 'seats': { seat.xpath('./@type').get(): seat.xpath('./text()').get() for seat in item.xpath('.//div[@class="seat"]') } } trains.append(train) return trains4. 系统优化策略
4.1 性能优化
- 连接池复用:保持TCP长连接,减少握手开销
- DNS缓存:使用aiodns加速DNS解析
- 响应压缩:启用gzip压缩减少传输量
- 结果缓存:对相同查询参数的结果缓存5分钟
4.2 稳定性保障
- 重试机制:对失败请求实现指数退避重试
- 熔断机制:当错误率超过阈值时暂停请求
- 心跳检测:定期检查代理IP可用性
- 监控告警:实现Prometheus监控指标采集
5. 常见问题与解决方案
5.1 请求被屏蔽
现象:返回403状态码或验证码页面解决方案:
- 降低请求频率至5秒/次
- 更换高质量代理IP
- 模拟浏览器行为(添加Referer、Cookies等)
5.2 数据解析失败
现象:XPath无法定位到元素解决方案:
- 检查页面结构是否变更
- 使用更宽松的XPath表达式
- 添加try-catch块处理解析异常
5.3 异步任务堆积
现象:内存持续增长解决方案:
- 限制并发协程数量
- 实现背压控制
- 使用asyncio.Semaphore限制并发度
6. 扩展功能实现
6.1 邮件通知功能
集成SMTP协议实现余票提醒:
async def send_email(receiver, subject, content): message = MIMEText(content, 'plain', 'utf-8') message['Subject'] = subject message['From'] = sender message['To'] = receiver async with aiosmtplib.SMTP(hostname='smtp.xxx.com', port=465) as smtp: await smtp.login(username, password) await smtp.send_message(message)6.2 可视化展示
使用Pyecharts生成余票趋势图:
def draw_ticket_chart(data): line = ( Line() .add_xaxis(data['dates']) .add_yaxis("二等座", data['second_class']) .add_yaxis("一等座", data['first_class']) .set_global_opts(title_opts=opts.TitleOpts(title="余票趋势")) ) return line.render_notebook()7. 部署与运行
推荐使用Docker容器化部署:
FROM python:3.8-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "main.py"]启动命令:
docker build -t ticket-monitor . docker run -d --name monitor ticket-monitor在实际使用中,我发现设置合理的请求间隔是关键。初期过于激进的高频请求导致IP被封,后来调整为动态间隔(3-8秒随机)后稳定性大幅提升。另外建议对解析逻辑添加单元测试,因为页面结构变化是常见问题。