姚國任
(淮南師范學院 計算機學院,安徽 淮南 232038)
隨著用戶出行服務的需求個性化,電子客票的查詢訂閱越來越凸顯出重要性,尤其表現在法定節假日、高峰春運、集中的寒暑假、旺盛的旅游季.針對出行服務信息的社交媒介化、思維數據化的改變,傳統的線下服務逐步被個人的App訂閱所取代,私人定制無疑受到了許多人的熱捧.很多時候,訂閱者在查詢余票的時候因檢索手段的差異顯示的結果也不盡相同,無法訂購自己想要的票并非因為客票資源不足,很可能是普通訂閱者無法掌握線上數據運營的動力,無法改善現有票源的碎片集成功能[1].鐵路實行的是市場化的開放運營方案,從“四縱四橫”到“八縱八橫”,目的都是避免列車的運力浪費.最佳購票方案一直都是用戶通過手動檢索進行對比判斷,針對大數據時代,個性化的最佳出行方案順勢而行.
鑒于上述情況,一種基于Python爬蟲技術[2]提出了很好的解決方案,尤其是在數據挖掘[3]方面精準獲取數據,以形成電子客票為目的,利用瀏覽器的調試工具分析URL種子,以requests獲取相關接口,結合腳本語言挖掘車次、日程、余票等信息進行比對、解析,模擬神經網絡模型[4]進行優化、拼接、分段處理,將碎片化的余票信息形成數據集合,摸排那種查詢無票而實際有票的假信息,為查詢或者獲取其他相關大數據關聯信息[5]爭取時間和機會.該方案從數據爬取、解析數據、算法設計、信息推送4個層次線性順序進行架構.
推薦用Google Chrome或者Mozilla Firefox瀏覽器登錄中國鐵路12306官網:https://www.12306.cn/index/,借助瀏覽器自身的DevTools調試插件或者類似與網絡爬蟲抓包等工具判斷當前瀏覽器窗口的網絡請求,以多次查詢余票的請求可以判斷不是AJAX方式,進而可判定能用Selenium實現瀏覽器的模擬,利用python語言的第三方庫requests的一些函數就能實現爬蟲請求[6].
在調試模式下查看請求結果獲取查詢接口,地址返回的信息屬于JSON串,對其他請求沒有限制屬于典型的Get請求,后期將方便通過python構造這一Get方法,尋找一些有效的request請求,以2021年1月26日查詢從合肥到鄭州的車次情況為例,以下3項為爬蟲的幾項關鍵技術[7].
1.2.1 爬取接口地址
繼承了urllib2全部特征的requests庫,支持http協議的連接池,支持用cookie維持會話[8],通過分析網頁調試模式用requests爬取火車票余票相關信息的接口地址,包括出行日期、用英文字母代碼表示出發地與目的地:https://kyfw.12306.cn/otn/leftTicket/queryY?leftTicketDTO.train_date=2021-01-26&leftTicketDTO.from_station=HFH&leftTicketDTO.to_station=ZZF&purpose_codes=ADULT,這個接口地址對于后期的程序設計調試至關重要,地址也會因為日期的更新而刷新生成,上述地址測試時間是2021年1月26日以前的,但2021年1月27日的當日地址用到的是:https://kyfw.12306.cn/otn/leftTicket/queryZ?,通過Preview result展示如圖1所示.

圖1 Preview result展示
result展示的共38條記錄,正好與合肥到鄭州(測試時間:2021年1月26日17:20)的車次類型(GC-高鐵/城際、D-動車、Z-直達、T-特快、K-快速、其他)顯示的38個車次完全吻合.
1.2.2 提取重要參數
利用requests庫的requests.head()方法獲取HTML網頁頭部信息,依據Request URL GET請求需要傳入4個參數:日期、出發地、目的地、乘客類型正好對應以下字段leftTicketDTO.train_date、leleftTicketDTO.from_station、leftTicketDTO.to_station、purpose_codes,與前臺輸入查詢條件信息完全一致,如圖2所示,其中HFH與ZZF信息需要在后面的數據分析后進行解析.

圖2 Request URL請求參數
1.2.3 爬取碼表信息
爬取車站的碼表信息,名稱為station_name.js?station_version=1.9183對應的js文件:https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9183,請求該js文件后,在新的頁面打開標簽鏈接,獲取一部分結果如圖2所示,圖中碼表與12306網站上鐵路所有站點正好完全匹配.

var station_names ='@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京東|BOP|beijingdong|bjd|1@bji|北京|BJP|bei-jing|bj|2@bjn|北京南|VNP|beijingnan|bjn|3@bjx|北京西|BXP|beijingxi|bjx|4@gzn|廣州南|IZQ|guangzhounan|gzn|5@cqb|重慶北|CUW|chongqingbei|cqb|6@cqi|重慶|CQW|chongqing|cq|7@cqn|重慶南|CRW|chongqingnan|cqn|8@cqx|重慶西|CXW|chongqingxi|cqx|9@gzd|廣州東|GGQ|guangzhoudong|gzd|10@sha|上海|SHH|shang-hai|sh|11@shn|上海南|SNH|shanghainan|shn|12@shq|上海虹橋|AOH|shanghaihongqiao|shhq|13@shx|上海西|SXH|shanghaixi|shx|14@tjb|天津北|TBP|tianjinbei|tjb|15@tji|天津|TJP|tianjin|tj|16@tjn|天津南|TIP|tianjin-nan|tjn|17@tjx|天津西|TXP|tianjinxi|tjx|18@xgl|香港西九龍|XJA|hkwestkowloon|xgxjl|19@cch|長春|CCT|changchun|cc|20@ccn|長春南|CET|changchunnan|ccn|21@ccx|長春西|CRT|changchunxi|ccx|22@cdd|成都東|ICW|chengdudong|cdd|23@cdn|成都南|CNW|chengdunan|cdn|24@cdu|成都|CDW|chengdu|cd|25@cdx|成都西|CMW|chengduxi|cdx|26@csh|長沙|CSQ|changsha|cs|27@csn|長沙南|CWQ|changshanan|csn|28@dmh|大明湖|JAK|daminghu|dmh|29@fzh|福州|FZS|fuzhou|fz|30@fzn|福州南|FYS|fuzhounan|fzn|31@gya|貴陽|GIW|guiy-ang|gy|32@gzh|廣州|GZQ|guangzhou|gz|33@gzx|廣州西|GXQ|guangzhouxi|gzx|34@heb|哈爾濱|HBB|haerbin|heb|35@hed|哈爾濱東|VBB|haerbindong|hebd|36@hex|哈爾濱西|VAB|haerbinxi|hebx|37@hfe|合肥|HFH|hefei|hf|38@hhd|呼和浩特東|NDC|huhehaotedong|hhhtd|39@hht|呼和浩特|HHC|huhehaote|hhht|40@hkd|海口東|HMQ|haikoudong|hkd|42@hko|海口|VUQ|haikou|hk|43@hzd|杭州
從頁面上爬取到的數據,一般都不能作為數據直接使用,都需要進行信息的預處理[9-10],根據前面網頁分析的Response的URL與參數,只需要分析返回字段的實際意義,比對Response Json與頁面結果,以當前網站查詢到G3168次列車為例:
比對1(Response Json)如圖3所示.

RoNIVloVyUljqnZQODdWI6GgaXSKYkNlGNUH34ZXGY8CNVoX5tWNohNb87FqR0yxOTrD4fvs56V4%0A0zTphvOEl5TCnHsh8U3oKJTlWfnbz8cgomMViGezz0wTbwXHj3IdQ11oDqIgZ1qeNWA%2FH7d2vXgB%0Acx5CVckoo3VOjlm5BQ0X3JBnBfbdJ%2FsV0yRb31WDTMVfSzi5DH5P%2B%2BD%2FZ6%2BBsdsKETFnvBDKFTS0%0AMe9cHvTbKcSgCku%2F6W9krPfQNAcZtRmThzhCgTY0daebKx0b5pC4snKFEMVk%2FyhkORg8qePXztV7%0AZfkZZw%3D%3D|預訂|5i000G316801|G3168|ENH|EAY|ENH|ZAF|0700|1036|0336|Y|CTlQYgG6jmyaVJ%2Fs6SSVvw7gn72UnoTMKDP%2Fr1ulbsyHy%2F19|20210126|3|H3|01|10|1|0|||||||||||有|17|5||O0M090|OM9|0|0||O028950021M0465000179088550005|0|||||1|#1#0
比對2(查詢頁面)如圖5所示.

圖5 查詢頁面信息
比對結果:比對1中的車次(已標注粗體)、車出發時間與到達時間(已標注粗體、傾斜)、日期(已標注粗體)、一等座與二等座有無車票(已標注粗體、傾斜)與比對2返回頁面中的字段完全匹配,代碼編寫解析的時候只需要將Json result用符號“|”切割,這些數據清洗[11]以后才能存儲,需要刪除一些重復或者無用的數據.
在爬取map過程中的“合肥”字段用“HFH”表示,“鄭州”字段用“ZZF”表示,探究圖2中的碼表,排除無效的消息,發現車站站點的基本規律是“漢字+|+三位的英文字符”,利用正則表達式表示為“([u4e00-u9fa5]+)|([A-Z]+)”,其中“u4e00-u9fa5”是漢字unicode編碼的范圍,通過解析后獲取的碼表就有6236條記錄,截取一部分如圖6所示.

北京北|VAP北京東|BOP北京|BJP北京南|VNP北京西|BXP廣州南|IZQ重慶北|CUW重慶|CQW重慶南|CRW重慶西|CXW廣州東|GGQ……
部分代碼實現如下所示:
def station_info():
#中國鐵路12306官網的城市名稱與城市代碼對應js文件的url:
url='https://kwfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9183'
rs=requests.get(url,verify=False)
#“u4e00-u9fa5”為漢字unicode碼范圍
sn=u'([u4e00-u9fa5]+)|([A-Z]+)' #按正則表達式進行匹配:
result=re.findall(sn,rs.text)
stationinfo=dict(result)
return stationinfo
該設計思想是通過算法選擇時間最短、距離最短、降低換乘次數的最優出行路線供查詢者參考.模擬一條線路,始發站標識為T1,終點站標識為Tn,在始發站與終點站之間站點線性依次標識為T1、T2、T3、……、Tn,站點匹配成碼表后將建立數據庫,具體策略如下所示:
(1)建立碼表鏈接
將T1->T2、T1->T3、T1->T4、T1->T5、……、T1->Tn-1、T1->Tn標識為S12、S13、S14、S15、……、S1(n-1)、S1n,信息包括是否有車次、剩余票數、無票等字段.
依次建立T2->T3、T2->T4、T2->T5、T2->T6、……、T2->Tn-1、T2->Tn標識為S23、S24、S25、S26、……、S2(n-1)、S2n,信息依然包括是否有車次、剩余票數、無票等字段.
用同樣辦法建立始發站是T3、T4、T5、……Tn-1、Tn的碼表鏈接.
建立Tn-1->Tn的標識為S(n-1)n,包含信息同上.
(2)數據拼接
第1次站點換乘:在有票源的前提下,判斷S1i+Sin是否可行,設Ti是同一列車換乘中轉站點對應的中轉碼表.
第2次站點換乘:在有票源的前提下,判斷S1i+Sij+Sjn是否可行,設Ti、Tj是列車換乘中轉站點對應的中轉碼表.
用同樣的辦法多個站點換乘:在有票源的前提下,判斷S1i+Sij+ Sjk+……+Svn是否可行,設Ti、Tj、Tk、……、Tv是列車換乘中轉站點對應的中轉碼表.
(3)在上述執行過程中無法滿足的情況下,考慮換位換乘,即購買同一車次的中轉票,將Sa標識(商務座/特等座)、Sb標識(一等座)、Sc標識(二等座)、Sd標識(硬座)、Se標識(無座),在數據拼接的過程中實現換座切換,可能實現的是:Sa1i+Sbij+Scjk+……+Sdvn,其中Ti、Tj、Tk、……、Tv是列車換乘中轉站點對應的中轉碼表.
(4)在上述執行(3)過程中無法滿足的情況下,可以考慮多購買1~2個站點,即有1~2個站點的重疊,比如Sa1(i+1)與Sb(i-1)j就會最少有一個站點的重復,可能實現的是:
Sa1(i+1)+Sb(i-1)j+……Sckv +Sdvn,Ti、Tj、Tk、Tv等是列車中轉站點對應的碼表.
(5)在上述執行(4)過程中無法滿足的情況下,從理論上可以考慮利用最短距離的補票換乘,即先購買一張站票,可以利用最少重復站的換乘,比如Sa1(i-1)+S b(i-1)(i+1)+Sc(i+1)j+……+Sdkv+ Sdvn.
(6)算法中始終以直達票為首先,同車換乘為優選,每個算法方案均是遞進的關系,在(5)的算法無法遞進的時候,就要考慮2車或者更多次列車的換乘方案,換乘后再用上述的同車換乘實現整個流程的分段.
以上算法也是有缺陷的,在拼接的過程中未將列車晚點等不確定因素考慮進去.
3.2.1 構造API URL
生成可查詢的URL是整個程序的入口與關鍵,所有的城市名稱按照字典的方式設計,按照{城市名稱:城市代碼}生成,將城市名稱轉換生成城市代碼,構造API URL如下: url=('https://kyfw.12306.cn/otn/leftTicket/queryY?'
#該地址會因為網站改版或者日期的變化會動態刷新:
'leftTicketDTO.train_date={}&' # train_date為列車的出發時間;
'leftTicketDTO.from_station={}&' # train_date為出發站的城市代碼;
'leftTicketDTO.to_station={}&' # train_date為到達站的城市代碼;
'purpose_codes=ADULT').format(date,from_station,to_station).
3.2.2 設計獲取列車車次信息
以從合肥到鄭州為例
deftrain_query(url,text):
try:
rs=requests.get(url,verify=False)
#查詢json信息data字段result值
train_infos=rs.json()['data']['result']
for i in train_infos:
db=i.split('|') # 遍歷所有列車信息;
train_no=db[3] # 車次代碼 ;
from_station_code=db[6] # 出發站;
from_station_name=text['合肥'];
to_station_code=db[7] # 達到站;
to_station_name=text['鄭州'];
starttime=db[8] # 發站時間;
arrivetime=db[9] # 到站時間;
fulltime=db[10] # 發站與到站時間間隔;
firstseat=db[31] or '--' # 一等座剩余信息;
secondseat=db[30] or '--' # 二等座剩余信息;
softsleeper=db[23] or '--' # 軟臥剩余信息;
hardsleeper=db[28] or '--' # 硬臥剩余信息;
hardseat=db[29] or '--' # 硬座剩余信息;
noseat=db[26] or '--' # 顯示無座信息;
info=( '查詢車次:{} 始發站:{} 終點站:{} 始發時間:{}抵達時間:{} 歷時:{} 座位剩余情況: 一等座剩余:「{}」 二等座剩余:「{}」 軟臥剩余:「{}」 硬臥剩余:「{}」 硬座剩余:「{}」 無座:「{}」 '.format(train_no,from_station_name,to_station_name,starttime,arrivetime,fulltime,firstseat,secondseat,softsleeper,hardsleeper,hardseat,noseat)) # 供查詢顯示的信息.
3.2.3 實現刷新頻率程序部分代碼如下:
text=station_info()
print(text)
url= urlinfo_query(text) #調用生成可查詢的URL.
#循環查詢,查詢終止條件為查到必須有的車次,
while True:
time.sleep(1) #查票刷新頻率
if train_query(url,text):
break
12306購票成功后,信息通知渠道有很多,手機短信、騰訊QQ、個人郵箱、微信等,但是尚未購票成功的客戶想通過提前查詢余票的結果推送功能卻不具備,借助第三方一款方便使用的工具Server醬即可滿足很好的推送功能,Server醬即可實現程序員與服務器之間的通信[12].
Server醬(ServerChan)本身就是一個擁有GET接口可編程的接收器,信息可以通過微信推送至客戶,鑒于Server醬SCKEY與UserID一對一的關系,如果實現一對多信息的服務推送就必須要用到PushBear,相對于Server醬也就是高級版本.ServerChan配置過程需要3個步驟:(1)登錄步驟:注冊GitHub賬號,獲取1個SCKEY,SCKEY將在發送信息的頁面使用.(2)綁定步驟:單擊按鈕“微信推送”,掃碼關注即可完成綁定請求.(3)信息發送步驟:向URL頁面發送Get或者Post請求,而URL將接受sendkey、text、desp3個參數,其中sendkey參數為通道,屬于必填寫項;text參數為消息標題,屬于長度不超過256的必填項;desp參數為信息的內容,可以為空,支持MarkDown.以上實時查詢余票信息后,可以調用Server醬,推送至客戶的微信,部分定義代碼實現如下:
def send_Information(title,info):
url='https://pushbear.ftqq.com/sub?sendkey=此處為個人注冊后生成的SCKEY值&text=%s&desp=%s'%(title,info)
requests.get(url)


圖7 控制臺運行結果

圖8 Server醬推送信息
測試結果:控制臺輸出的結果與Server醬公眾號推送的信息完全一致,而這兩個結果與圖9網頁實際查詢的結果也是完全一致的.

圖9 網頁查詢結果
該系統設計基于python的爬蟲技術,經過算法篩選,用Python的Requests模塊與JSON解析方法[13-14]爬取了電子客票的相關信息.調用Server醬推送功能獲取了最佳出行方案,解決了官網中以5 s作為刷新的固定頻率,可以實時獲取想要的數據,事實證明該方案行之有效,對其他相關系統有一些應用參考價值[15-17],但系統設計還有一些可以改進的地方,比如沒有通過更換USER-Agent或者IP代理進行防爬處理,算法設計中模糊了車次晚點的實際情況等.