思路
12306車票的信息抓取來(lái)還是比較簡(jiǎn)單的造锅,其實(shí)難的如何用Python搶票袄简,12306有一個(gè)很“特殊”的驗(yàn)證碼逊桦,即使是人識(shí)別起來(lái)都有一定難度谈喳。但其實(shí)這種難度也恰是它薄弱的地方粹舵,這意味著此類驗(yàn)證碼的數(shù)量是有限的钮孵。我自己沒(méi)有抓過(guò),但我之前看過(guò)一篇文章眼滤,據(jù)說(shuō)這種碼一共是9000個(gè)左右(我記得好像作者把驗(yàn)證碼抓下來(lái)用md5加密后保存下來(lái)巴席,存到9000多個(gè)就存不下去了)。假設(shè)我們把每個(gè)驗(yàn)證碼圖片的答案都記下來(lái)诅需,理論上就可以完成自動(dòng)登陸了漾唉。聽(tīng)起來(lái)操作很費(fèi)力。
如果只是要查詢車票信息和價(jià)錢(qián)的話堰塌,就比較簡(jiǎn)單了赵刑。只有幾個(gè)需要注意的點(diǎn),這里講一下场刑。
獲得站名和其編號(hào)的對(duì)應(yīng)關(guān)系
在所有請(qǐng)求中般此,有一個(gè)js文件很重要,名字叫station_name.js?station_version=1.9053
,后面的是版本號(hào)牵现。這個(gè)文件會(huì)返回一個(gè)字符串铐懊,類似于:
@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京東|BOP|beijingdong|bjd|1..........
可以看得出來(lái),站與站之間用@
分隔瞎疼,然后每個(gè)站的信息字段用|
分隔科乎。具體哪個(gè)對(duì)應(yīng)哪個(gè)可以看我下面的代碼。其中最重要的是第三個(gè)字段贼急,之后查詢車票信息的時(shí)候喜喂,就是用這個(gè)字段的值的。我不僅獲取并解析了這個(gè)字符串竿裂,同時(shí)也把信息寫(xiě)入了sqlite3數(shù)據(jù)庫(kù)里。
查詢剩余車票信息
查詢余票信息的是一條XHR請(qǐng)求:
https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2018-05-24&leftTicketDTO.from_station=BJP&leftTicketDTO.to_station=TJP&purpose_codes=ADULT
其中train_data和purpose_codes很好理解照弥,兩個(gè)station用的就是上一節(jié)中所說(shuō)的teleCode腻异,因此要模擬這個(gè)請(qǐng)求,你需要先根據(jù)站點(diǎn)名字(如‘北京北’)從數(shù)據(jù)庫(kù)中得到其編號(hào)(如‘VAP')这揣。
這條請(qǐng)求的響應(yīng)中悔常,有三部分有用:
- status字段,標(biāo)識(shí)了這條請(qǐng)求是否成功给赞。
- data字段中的map字段机打,里面包好了響應(yīng)中使用的tecoCode與其對(duì)應(yīng)的站點(diǎn)名字,這樣我們就不需要再利用數(shù)據(jù)庫(kù)將編號(hào)轉(zhuǎn)會(huì)站臺(tái)名了片迅。因?yàn)椴捎玫哪:樵儾醒詍ap中數(shù)據(jù)可能不止兩個(gè),比如說(shuō)搜索’北京‘可能會(huì)返回’北京‘和’北京南‘。
- data字段中的result字段芥挣,這個(gè)字段就包含了所有的票價(jià)信息驱闷。但是它也是經(jīng)過(guò)編碼的,同樣用
|
分隔空免。這里最難的地方在于這些字段實(shí)在是太多了空另,完全不知道哪個(gè)是哪個(gè)。
一個(gè)例子:
"null|預(yù)訂|240000D3110K|D311|VNP|SHH|VNP|TXP|21:11|22:19|01:08|N|0Xz%2FAQnjcOjRAf%2FlTLDixMnMJwxcpe7x|20180524|3|P2|01|02|0|0||||無(wú)||||||||||無(wú)|F040|F4|0"
前面幾個(gè)還好理解蹋砚,到了后半部分代表座位剩余數(shù)量的字段就顯得一臉懵逼了。
怎么辦坝咐?除了一個(gè)一個(gè)猜循榆,我們可以從網(wǎng)頁(yè)源代碼和js文件里找出一點(diǎn)線索。在queryLeftTicket_end_js.js?scriptVersion=1.9085
文件中的2814行畅厢,可以看到:
這已經(jīng)可以看出很多東西了冯痢,如果還有點(diǎn)迷惑,再看網(wǎng)頁(yè)的源代碼:
通過(guò)審查元素框杜,你可以知道哪個(gè)id對(duì)應(yīng)哪個(gè)字段浦楣,然后再?gòu)纳蠄D上找出其對(duì)應(yīng)的字段偏移量。
查詢票價(jià)
查詢票價(jià)使用的是另外一條XHR請(qǐng)求:
https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice?train_no=24000C22290F&from_station_no=01&to_station_no=03&seat_types=O9OMP&train_date=2018-05-24
這幾個(gè)參數(shù)在查詢余票的響應(yīng)中都包含了咪辱,分別是第2振劳、16、17和35個(gè)字段油狂。但是難點(diǎn)是找出seat_types中幾個(gè)值分別代表了什么座位历恐。這個(gè)我沒(méi)找到線索,但是我在網(wǎng)上找到了別人代碼里寫(xiě)的對(duì)應(yīng)關(guān)系专筷,但是現(xiàn)在找不到那份代碼了弱贼。。這里我直接給出對(duì)應(yīng)關(guān)系磷蛹。
座位類型 | 編號(hào) | 座位類型 | 編號(hào) |
---|---|---|---|
商務(wù)座 | A9 | 特等座 | P |
一等座 | M | 二等座 | O |
高級(jí)軟臥 | A6 | 軟臥 | A4 |
動(dòng)臥 | -- | 硬臥 | A3 |
軟座 | A2 | 硬座 | A1 |
無(wú)座 | WZ | 其他 | -- |
一條余票查詢的請(qǐng)求會(huì)對(duì)應(yīng)很多條票價(jià)查詢的請(qǐng)求吮旅,因此如果余票類型很充足,會(huì)消耗很長(zhǎng)時(shí)間(可以關(guān)閉)味咳。
代碼
除了余票數(shù)量和票價(jià)的功能代碼庇勃,我還使用了PrettyTable
和colorama
庫(kù),支持彩色表格打印槽驶,同時(shí)增加是否支持身份證出入站
和始發(fā)站责嚷、終點(diǎn)站、經(jīng)過(guò)站
的標(biāo)識(shí)掂铐。代碼如下罕拂,不包括數(shù)據(jù)庫(kù)接口和配置文件揍异,其中t12306_init()
只要執(zhí)行一次(省資源,執(zhí)行多次不會(huì)出錯(cuò)):
12306.py
import requests
import ast
import json
from random import choice
from prettytable import PrettyTable
from colorama import init, Fore, Style
import Configure as Configs
import Sqlite3api as Sqlite3
init()
header = {}
header['user-agent'] = choice(Configs.FakeUserAgents)
header['Referer'] = "https://kyfw.12306.cn/otn/leftTicket/init"
def t12306_init():
conn = Sqlite3.sqlite3_init()
url = "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9053"
content = None
ret = Sqlite3.sqlite3_execute(conn, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='t12306'")[0][0]
if ret == 1:
Sqlite3.sqlite3_execute(conn, "DROP TABLE t12306")
Sqlite3.sqlite3_execute(conn, "CREATE TABLE t12306 (stationId real, stationName text, teleCode text, pinYin text, pinYinHead text)")
Sqlite3.sqlite3_execute(conn, "CREATE UNIQUE INDEX idx_follow_stationId on t12306(stationId)")
try:
response = requests.get(url, headers=header)
if response.status_code == requests.codes.ok:
content = response.text
except Exception as e:
print (e)
data = content[:-1].split('=')[1][2:]
for station in data.split('@'):
fields = station.split('|')
Sqlite3.sqlite3_execute(conn, "INSERT INTO t12306 VALUES (?,?,?,?,?)", (fields[5],fields[1],fields[2],fields[3],fields[0],))
#print (fields[5],fields[1],fields[2],fields[3],fields[0])
Sqlite3.sqlite3_close(conn)
def check_left_ticket(train_date, from_station, to_station, purpose_codes, need_price=False):
url = "https://kyfw.12306.cn/otn/leftTicket/query"
payload = {
'leftTicketDTO.train_date': train_date,
'leftTicketDTO.from_station': from_station,
'leftTicketDTO.to_station': to_station,
'purpose_codes': purpose_codes
}
content = ''
try:
response = requests.get(url, headers=header, params=payload)
if response.status_code == requests.codes.ok:
response.encoding = 'utf-8'
content = response.text
except Exception as e:
print (e)
data = json.loads(content)
#print (data)
if data.get('status') == False:
print ("獲取數(shù)據(jù)失敗聂受。")
return
# 站點(diǎn)編號(hào)->站點(diǎn)名字 的Map
name_map = data.get('data').get('map')
table = PrettyTable()
table.field_names = ["車次", "出發(fā)/到達(dá)", "出發(fā)/到達(dá)時(shí)間", "歷時(shí)", "可否網(wǎng)購(gòu)", "商務(wù)座", "特等座", "一等座", "二等座", "高級(jí)軟臥", "軟臥", "動(dòng)臥", "硬臥", "軟座","硬座", "無(wú)座", "其他"]
table.align["車次"] = "l"
table.align["出發(fā)/到達(dá)"] = "l"
for ticket_list in data.get('data').get('result'):
field = ticket_list.split('|')
flag_id = "[身]" if field[18]=='1' else "" # 是否支持身份證
flag_from = "[始]" if field[4] == field[6] else "[過(guò)]" # 是否始發(fā)站
flag_to = "[終]" if field[5] == field[7] else "[過(guò)]"# 是否終點(diǎn)站
price = {}
if need_price == True:
ret = query_ticket_price(field[2],field[16],field[17],field[35],train_date)
price = ret if ret else {}
table.add_row([
field[3] + (Fore.YELLOW + flag_id + Fore.RESET ) ,
'\n'.join([Fore.LIGHTGREEN_EX + flag_from + name_map.get(field[6]) + Fore.RESET,
Fore.LIGHTRED_EX + flag_to + name_map.get(field[7]) + Fore.RESET]),
'\n'.join([Fore.LIGHTGREEN_EX + field[8] + Fore.RESET,
Fore.LIGHTRED_EX + field[9] + Fore.RESET]),
field[10],
"是" if field[10] else "否",
# 商務(wù)座
"{0:s}{1:s}".format(field[32] if field[32] else "--", ("\n" + Style.BRIGHT + price.get('A9')) + Style.RESET_ALL if price.get('A9') else ""),
# 特等座
"{0:s}{1:s}".format(field[25] if field[25] else "--", ("\n" + Style.BRIGHT + price.get('P')) + Style.RESET_ALL if price.get('P') else ""),
# 一等座
"{0:s}{1:s}".format(field[31] if field[31] else "--", ("\n" + Style.BRIGHT + price.get('M')) + Style.RESET_ALL if price.get('M') else ""),
# 二等座
"{0:s}{1:s}".format(field[30] if field[30] else "--", ("\n" + Style.BRIGHT + price.get('O')) + Style.RESET_ALL if price.get('O') else ""),
# 高級(jí)軟臥
"{0:s}{1:s}".format(field[21] if field[21] else "--", ("\n" + Style.BRIGHT + price.get('A6')) + Style.RESET_ALL if price.get('A6') else ""),
# 軟臥
"{0:s}{1:s}".format(field[23] if field[23] else "--", ('\n' + Style.BRIGHT + price.get('A4')) + Style.RESET_ALL if price.get('A4') else ""),
# 動(dòng)臥
field[33] if field[33] else "--",
# 硬臥
"{0:s}{1:s}".format(field[28] if field[28] else "--", ("\n" + Style.BRIGHT + price.get('A3')) + Style.RESET_ALL if price.get('A3') else ""),
# 軟座
"{0:s}{1:s}".format(field[24] if field[24] else "--", ("\n" + Style.BRIGHT + price.get('A2')) + Style.RESET_ALL if price.get('A2') else ""),
# 硬座
"{0:s}{1:s}".format(field[29] if field[29] else "--", ("\n" + Style.BRIGHT + price.get('A1')) + Style.RESET_ALL if price.get('A1') else ""),
# 無(wú)座
"{0:s}{1:s}".format(field[26] if field[26] else "--", ("\n" + Style.BRIGHT + price.get('WZ')) + Style.RESET_ALL if price.get('WZ') else ""),
# 其他
field[22] if field[22] else "--"
])
print (table)
def query_ticket_price(train_no, from_station_no, to_station_no, seat_types, train_date):
url = "https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice"
#print (train_no, from_station_no, to_station_no, seat_types, train_date)
payload = {
'train_no': train_no,
'from_station_no': from_station_no,
'to_station_no': to_station_no,
'seat_types': seat_types,
'train_date': train_date,
}
try:
response = requests.get(url, headers=header, params=payload)
if response.status_code == requests.codes.ok:
#response.encoding = 'utf-8'
content = response.text
except Exception as e:
print (e)
data = json.loads(content)
if data.get('status') == False:
print ("獲取數(shù)據(jù)失敗蒿秦。")
return None
return data.get('data')
if __name__ == "__main__":
#t12306_init()
conn = Sqlite3.sqlite3_init()
date = input("請(qǐng)輸入乘車時(shí)間(YYYY-MM-DD): ")
from_station = input("請(qǐng)輸入出發(fā)車站: ")
to_station = input("請(qǐng)輸入到達(dá)車站: ")
purpose_codes = input("請(qǐng)輸入類型(1-成人票): ")
from_s = to_s = ticket_type = None
ret = Sqlite3.sqlite3_execute(conn, "SELECT teleCode FROM t12306 WHERE stationName ='{0:s}'".format(from_station))#[0][0]
from_s = ret[0][0]
ret = Sqlite3.sqlite3_execute(conn, "SELECT teleCode FROM t12306 WHERE stationName ='{0:s}'".format(to_station))#[0][0]
to_s = ret[0][0]
Sqlite3.sqlite3_close(conn)
if purpose_codes == 1:
ticket_type = 'ADULT'
check_left_ticket(date, from_s, to_s, ticket_type, True)