简易 Python 脚本查询嵊泗船票
夏天来了,这颗躁动的心啊,想去嵊泗玩几天~
现在上海去嵊泗要上微信公众号或者官网买票,工作日还好,但是周末了不太容易抢到票了,又不能没事就刷手机(这太没有程序员范儿了)。
所以,看看能不能用 Python 写个爬虫脚本定时帮我搜索呢?
不想看罗里吧嗦分析的可以直接跳到文末。
1. 请求接口分析
1.1 URL
分析了一下网站,发现是用Vue写的;接口设计也很简单粗暴,非常好懂,同时发现没有任何反爬措施,可能也根本不需要吧。
购票页面看购票页面里,票是分两种类型的——载人,以及载车(顺带载人)。
这是因为,有的客船(如高速客船)只支持载人,开得快,但是票价稍微贵上5~15块;有的客船(如客滚船)同时支持载人、载车(方便自驾游),所以开得慢一些,但是票价也便宜一点。
看后端接口的 URL,也能发现两者查询余票的接口是不一样的:
1.2 传参
尽管是两个接口,但是传递的参数都是一样的
传递参数- startDate: 出发日期,String
- startPortNo: 起点码头编号,Int
- endPortNo: 终点码头编号,Int
码头编号可以通过航线查询接口 https://www.ssky123.com/api/v2/line/port/all 获取到,真的是很方便了……
航线接口- startPortList: 起点码头编号列表
- endPortList: 终点码头编号列表
- lineList: 有效航线列表
这里只要看 lineList 就可以了,因为这个结果不仅告诉我们起点码头编号和终点码头编号,还告诉我们两两之间是否有可达的航线。
1.3 Header
这里是我最想吐槽的,token 的值居然是 undefined,这是彻底放弃抵抗的意思吧,我估计连 User-Agent 都不用模拟,接口也是可以调通的(事实证明确实如此)。
Header2. 返回结果分析
我随便以沈家湾为起点,泗礁为终点,查询2020年5月21号的剩余票数,接口部分截图如下:
接口部分数据这个 pubCurrentCount 应该就是我们要的剩余票数了,localPrice 代表了船票单价,但是 originPrice 不知是合意,真的贵。。
这里要稍微注意的是,两种船票的余票数据,是放在不同字段下的——对于查询乘客的余票,我们看的是 seatClasses 列表里的结果;对于查询载车的余票,我们要看 driverSeatClass 列表。
脚本
简单的看完以后,就可以快速地写个脚本啦。
import requests
import time
import sys
import os
def get_lines(start, end):
"""
基于最新的航线数据,查询目标航线是否存在,并获取起点和终点码头编号
:param start: 起点码头名称,中文
:param end: 终点码头名称,中文
"""
lines = requests.get('https://www.ssky123.com/api/v2/line/port/all').json()['data']['lineList']
startPortNum, endPortNum = None, None
for line in lines:
if start in line['startPortName'] and end in line['endPortName']:
startPortNum, endPortNum = line['startPortNum'], line['endPortNum']
break
if startPortNum and endPortNum:
print('起点: ' + start + str(startPortNum))
print('终点: ' + end + str(endPortNum))
else:
print('未找到本航线')
return startPortNum, endPortNum
def get_sale_info(startPortNum, endPortNum, with_car, date):
"""
查询余票信息,返回各开船时间下的有效剩余船票(若无票则不记录)
:param startPortNum: 起点码头编号
:param endPortNum: 终点码头编号
:param with_car: 是否开车上船
:param date: 出发日期
"""
url = 'https://www.ssky123.com/api/v2/line/ferry/enq' if with_car else 'https://www.ssky123.com/api/v2/line/ship/enq'
data = {
'endPortNo': endPortNum,
'startDate': date,
'startPortNo': startPortNum
}
result = requests.post(url, json=data).json()
data = {}
for info in result['data']:
class_name = 'driverSeatClass' if with_car else 'seatClasses' # 字段名
sail_time = info['sailTime'] # 开船时间
left_num = sum([cls['pubCurrentCount'] for cls in info[class_name]]) # 剩余票数
if left_num > 0:
data[sail_time] = left_num
return data
def main(start, end, date, with_car=False, max_search=10000, stop_when_find=True):
"""
主体脚本,基于用户输入进行循环
:param start: 起点码头名称
:param end: 起点码头名称
:param date: 出发日期,String 格式(如 '2020-05-22' ),如果要一次搜索多个日期,则用列表(如 ['2020-05-22', ...])
:param with_car: 是否开车上船
:param max_search: 最大循环次数
:param stop_when_find: 发现有余票后,是否停止循环
"""
# 查询航线是否存在
startPortNum, endPortNum = get_lines(start, end)
if not startPortNum or not endPortNum:
return
count = 0
while True:
find = False # 是否找到
dates = [date] if isinstance(date, str) else date
for date_ in dates:
data = get_sale_info(startPortNum, endPortNum, with_car, date_)
count += 1
if data:
find = True
for sail_time, left_num in data.items():
text = sail_time + ' 还有 ' + str(left_num) + ' 张票'
# os.system('say ' + text)
print(text)
# 是否停止搜索
if find and stop_when_find or count >= max_search:
break
else:
sys.stdout.write('第 ' + str(count) + ' 次搜索完毕\r')
time.sleep(30)
if __name__ == '__main__':
main(
start='沈家湾',
end='泗礁',
date=['2020-05-21'],
with_car=False, # 是否带车
stop_when_find=True, # 是否找到就停止
)
上面我注释了一行代码:os.system('say ' + text)
,这是执行苹果系统的命令,调用系统声音来提示我,防止我没有及时看到 print 的结果,不同操作系统的提示方式不一样,所以我就先注释掉了。
运行一下,完美~