Python

简易 Python 脚本查询嵊泗船票

2020-05-19  本文已影响0人  章光辉_数据

夏天来了,这颗躁动的心啊,想去嵊泗玩几天~

现在上海去嵊泗要上微信公众号或者官网买票,工作日还好,但是周末了不太容易抢到票了,又不能没事就刷手机(这太没有程序员范儿了)。

所以,看看能不能用 Python 写个爬虫脚本定时帮我搜索呢?

不想看罗里吧嗦分析的可以直接跳到文末。

1. 请求接口分析

1.1 URL

分析了一下网站,发现是用Vue写的;接口设计也很简单粗暴,非常好懂,同时发现没有任何反爬措施,可能也根本不需要吧。

购票页面

看购票页面里,票是分两种类型的——载人,以及载车(顺带载人)。

这是因为,有的客船(如高速客船)只支持载人,开得快,但是票价稍微贵上5~15块;有的客船(如客滚船)同时支持载人、载车(方便自驾游),所以开得慢一些,但是票价也便宜一点。

看后端接口的 URL,也能发现两者查询余票的接口是不一样的:

1.2 传参

尽管是两个接口,但是传递的参数都是一样的

传递参数

码头编号可以通过航线查询接口 https://www.ssky123.com/api/v2/line/port/all 获取到,真的是很方便了……

航线接口

这里只要看 lineList 就可以了,因为这个结果不仅告诉我们起点码头编号和终点码头编号,还告诉我们两两之间是否有可达的航线。

1.3 Header

这里是我最想吐槽的,token 的值居然是 undefined,这是彻底放弃抵抗的意思吧,我估计连 User-Agent 都不用模拟,接口也是可以调通的(事实证明确实如此)。

Header

2. 返回结果分析

我随便以沈家湾为起点,泗礁为终点,查询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 的结果,不同操作系统的提示方式不一样,所以我就先注释掉了。

运行一下,完美~

上一篇下一篇

猜你喜欢

热点阅读