夏天來(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è)面里,票是分兩種類型的——載人,以及載車(順帶載人)。
這是因?yàn)?,有的客船(如高速客船)只支持載人,開(kāi)得快,但是票價(jià)稍微貴上5~15塊;有的客船(如客滾船)同時(shí)支持載人、載車(方便自駕游),所以開(kāi)得慢一些,但是票價(jià)也便宜一點(diǎn)。
看后端接口的 URL,也能發(fā)現(xiàn)兩者查詢余票的接口是不一樣的:
1.2 傳參
盡管是兩個(gè)接口,但是傳遞的參數(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í)如此)。

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

這個(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)行一下,完美~
