簡(jiǎn)易 Python 腳本查詢嵊泗船票

夏天來(lái)了,這顆躁動(dòng)的心啊,想去嵊泗玩幾天~

現(xiàn)在上海去嵊泗要上微信公眾號(hào)或者官網(wǎng)買票,工作日還好,但是周末了不太容易搶到票了,又不能沒(méi)事就刷手機(jī)(這太沒(méi)有程序員范兒了)。

所以,看看能不能用 Python 寫(xiě)個(gè)爬蟲(chóng)腳本定時(shí)幫我搜索呢?

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

1. 請(qǐng)求接口分析

1.1 URL

分析了一下網(wǎng)站,發(fā)現(xiàn)是用Vue寫(xiě)的;接口設(shè)計(jì)也很簡(jiǎn)單粗暴,非常好懂,同時(shí)發(fā)現(xiàn)沒(méi)有任何反爬措施,可能也根本不需要吧。

購(gòu)票頁(yè)面

看購(gòu)票頁(yè)面里,票是分兩種類型的——載人,以及載車(順帶載人)。

這是因?yàn)?,有的客船(如高速客船)只支持載人,開(kāi)得快,但是票價(jià)稍微貴上5~15塊;有的客船(如客滾船)同時(shí)支持載人、載車(方便自駕游),所以開(kāi)得慢一些,但是票價(jià)也便宜一點(diǎn)。

看后端接口的 URL,也能發(fā)現(xiàn)兩者查詢余票的接口是不一樣的:

1.2 傳參

盡管是兩個(gè)接口,但是傳遞的參數(shù)都是一樣的

傳遞參數(shù)
  • startDate: 出發(fā)日期,String
  • startPortNo: 起點(diǎn)碼頭編號(hào),Int
  • endPortNo: 終點(diǎn)碼頭編號(hào),Int

碼頭編號(hào)可以通過(guò)航線查詢接口 https://www.ssky123.com/api/v2/line/port/all 獲取到,真的是很方便了……

航線接口
  • startPortList: 起點(diǎn)碼頭編號(hào)列表
  • endPortList: 終點(diǎn)碼頭編號(hào)列表
  • lineList: 有效航線列表

這里只要看 lineList 就可以了,因?yàn)檫@個(gè)結(jié)果不僅告訴我們起點(diǎn)碼頭編號(hào)和終點(diǎn)碼頭編號(hào),還告訴我們兩兩之間是否有可達(dá)的航線。

1.3 Header

這里是我最想吐槽的,token 的值居然是 undefined,這是徹底放棄抵抗的意思吧,我估計(jì)連 User-Agent 都不用模擬,接口也是可以調(diào)通的(事實(shí)證明確實(shí)如此)。

Header

2. 返回結(jié)果分析

我隨便以沈家灣為起點(diǎn),泗礁為終點(diǎn),查詢2020年5月21號(hào)的剩余票數(shù),接口部分截圖如下:

接口部分?jǐn)?shù)據(jù)

這個(gè) pubCurrentCount 應(yīng)該就是我們要的剩余票數(shù)了,localPrice 代表了船票單價(jià),但是 originPrice 不知是合意,真的貴。。

這里要稍微注意的是,兩種船票的余票數(shù)據(jù),是放在不同字段下的——對(duì)于查詢乘客的余票,我們看的是 seatClasses 列表里的結(jié)果;對(duì)于查詢載車的余票,我們要看 driverSeatClass 列表。

腳本

簡(jiǎn)單的看完以后,就可以快速地寫(xiě)個(gè)腳本啦。

import requests
import time
import sys
import os


def get_lines(start, end):
    """
    基于最新的航線數(shù)據(jù),查詢目標(biāo)航線是否存在,并獲取起點(diǎn)和終點(diǎn)碼頭編號(hào)
    :param start: 起點(diǎn)碼頭名稱,中文
    :param end: 終點(diǎn)碼頭名稱,中文
    """
    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('起點(diǎn): ' + start + str(startPortNum))
        print('終點(diǎn): ' + end + str(endPortNum))
    else:
        print('未找到本航線')
    
    return startPortNum, endPortNum

def get_sale_info(startPortNum, endPortNum, with_car, date):
    """
    查詢余票信息,返回各開(kāi)船時(shí)間下的有效剩余船票(若無(wú)票則不記錄)
    :param startPortNum: 起點(diǎn)碼頭編號(hào)
    :param endPortNum: 終點(diǎn)碼頭編號(hào)
    :param with_car: 是否開(kāi)車上船
    :param date: 出發(fā)日期
    """
    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']  # 開(kāi)船時(shí)間
        left_num = sum([cls['pubCurrentCount'] for cls in info[class_name]])  # 剩余票數(shù)
        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):
    """
    主體腳本,基于用戶輸入進(jìn)行循環(huán)
    :param start: 起點(diǎn)碼頭名稱
    :param end: 起點(diǎn)碼頭名稱
    :param date: 出發(fā)日期,String 格式(如 '2020-05-22' ),如果要一次搜索多個(gè)日期,則用列表(如 ['2020-05-22', ...])
    :param with_car: 是否開(kāi)車上船
    :param max_search: 最大循環(huán)次數(shù)
    :param stop_when_find: 發(fā)現(xiàn)有余票后,是否停止循環(huán)
    """
    
    # 查詢航線是否存在
    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),這是執(zhí)行蘋(píng)果系統(tǒng)的命令,調(diào)用系統(tǒng)聲音來(lái)提示我,防止我沒(méi)有及時(shí)看到 print 的結(jié)果,不同操作系統(tǒng)的提示方式不一樣,所以我就先注釋掉了。

運(yùn)行一下,完美~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容