姚國任
(淮南師范學(xué)院 計算機(jī)學(xué)院,安徽 淮南 232038)
隨著用戶出行服務(wù)的需求個性化,電子客票的查詢訂閱越來越凸顯出重要性,尤其表現(xiàn)在法定節(jié)假日、高峰春運、集中的寒暑假、旺盛的旅游季.針對出行服務(wù)信息的社交媒介化、思維數(shù)據(jù)化的改變,傳統(tǒng)的線下服務(wù)逐步被個人的App訂閱所取代,私人定制無疑受到了許多人的熱捧.很多時候,訂閱者在查詢余票的時候因檢索手段的差異顯示的結(jié)果也不盡相同,無法訂購自己想要的票并非因為客票資源不足,很可能是普通訂閱者無法掌握線上數(shù)據(jù)運營的動力,無法改善現(xiàn)有票源的碎片集成功能[1].鐵路實行的是市場化的開放運營方案,從“四縱四橫”到“八縱八橫”,目的都是避免列車的運力浪費.最佳購票方案一直都是用戶通過手動檢索進(jìn)行對比判斷,針對大數(shù)據(jù)時代,個性化的最佳出行方案順勢而行.
鑒于上述情況,一種基于Python爬蟲技術(shù)[2]提出了很好的解決方案,尤其是在數(shù)據(jù)挖掘[3]方面精準(zhǔn)獲取數(shù)據(jù),以形成電子客票為目的,利用瀏覽器的調(diào)試工具分析URL種子,以requests獲取相關(guān)接口,結(jié)合腳本語言挖掘車次、日程、余票等信息進(jìn)行比對、解析,模擬神經(jīng)網(wǎng)絡(luò)模型[4]進(jìn)行優(yōu)化、拼接、分段處理,將碎片化的余票信息形成數(shù)據(jù)集合,摸排那種查詢無票而實際有票的假信息,為查詢或者獲取其他相關(guān)大數(shù)據(jù)關(guān)聯(lián)信息[5]爭取時間和機(jī)會.該方案從數(shù)據(jù)爬取、解析數(shù)據(jù)、算法設(shè)計、信息推送4個層次線性順序進(jìn)行架構(gòu).
推薦用Google Chrome或者M(jìn)ozilla Firefox瀏覽器登錄中國鐵路12306官網(wǎng):https://www.12306.cn/index/,借助瀏覽器自身的DevTools調(diào)試插件或者類似與網(wǎng)絡(luò)爬蟲抓包等工具判斷當(dāng)前瀏覽器窗口的網(wǎng)絡(luò)請求,以多次查詢余票的請求可以判斷不是AJAX方式,進(jìn)而可判定能用Selenium實現(xiàn)瀏覽器的模擬,利用python語言的第三方庫requests的一些函數(shù)就能實現(xiàn)爬蟲請求[6].
在調(diào)試模式下查看請求結(jié)果獲取查詢接口,地址返回的信息屬于JSON串,對其他請求沒有限制屬于典型的Get請求,后期將方便通過python構(gòu)造這一Get方法,尋找一些有效的request請求,以2021年1月26日查詢從合肥到鄭州的車次情況為例,以下3項為爬蟲的幾項關(guān)鍵技術(shù)[7].
1.2.1 爬取接口地址
繼承了urllib2全部特征的requests庫,支持http協(xié)議的連接池,支持用cookie維持會話[8],通過分析網(wǎng)頁調(diào)試模式用requests爬取火車票余票相關(guān)信息的接口地址,包括出行日期、用英文字母代碼表示出發(fā)地與目的地:https://kyfw.12306.cn/otn/leftTicket/queryY?leftTicketDTO.train_date=2021-01-26&leftTicketDTO.from_station=HFH&leftTicketDTO.to_station=ZZF&purpose_codes=ADULT,這個接口地址對于后期的程序設(shè)計調(diào)試至關(guān)重要,地址也會因為日期的更新而刷新生成,上述地址測試時間是2021年1月26日以前的,但2021年1月27日的當(dāng)日地址用到的是:https://kyfw.12306.cn/otn/leftTicket/queryZ?,通過Preview result展示如圖1所示.
圖1 Preview result展示
result展示的共38條記錄,正好與合肥到鄭州(測試時間:2021年1月26日17:20)的車次類型(GC-高鐵/城際、D-動車、Z-直達(dá)、T-特快、K-快速、其他)顯示的38個車次完全吻合.
1.2.2 提取重要參數(shù)
利用requests庫的requests.head()方法獲取HTML網(wǎng)頁頭部信息,依據(jù)Request URL GET請求需要傳入4個參數(shù):日期、出發(fā)地、目的地、乘客類型正好對應(yīng)以下字段leftTicketDTO.train_date、leleftTicketDTO.from_station、leftTicketDTO.to_station、purpose_codes,與前臺輸入查詢條件信息完全一致,如圖2所示,其中HFH與ZZF信息需要在后面的數(shù)據(jù)分析后進(jìn)行解析.
圖2 Request URL請求參數(shù)
1.2.3 爬取碼表信息
爬取車站的碼表信息,名稱為station_name.js?station_version=1.9183對應(yīng)的js文件:https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9183,請求該js文件后,在新的頁面打開標(biāo)簽鏈接,獲取一部分結(jié)果如圖2所示,圖中碼表與12306網(wǎng)站上鐵路所有站點正好完全匹配.
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|杭州
從頁面上爬取到的數(shù)據(jù),一般都不能作為數(shù)據(jù)直接使用,都需要進(jìn)行信息的預(yù)處理[9-10],根據(jù)前面網(wǎng)頁分析的Response的URL與參數(shù),只需要分析返回字段的實際意義,比對Response Json與頁面結(jié)果,以當(dāng)前網(wǎng)站查詢到G3168次列車為例:
比對1(Response Json)如圖3所示.
RoNIVloVyUljqnZQODdWI6GgaXSKYkNlGNUH34ZXGY8CNVoX5tWNohNb87FqR0yxOTrD4fvs56V4%0A0zTphvOEl5TCnHsh8U3oKJTlWfnbz8cgomMViGezz0wTbwXHj3IdQ11oDqIgZ1qeNWA%2FH7d2vXgB%0Acx5CVckoo3VOjlm5BQ0X3JBnBfbdJ%2FsV0yRb31WDTMVfSzi5DH5P%2B%2BD%2FZ6%2BBsdsKETFnvBDKFTS0%0AMe9cHvTbKcSgCku%2F6W9krPfQNAcZtRmThzhCgTY0daebKx0b5pC4snKFEMVk%2FyhkORg8qePXztV7%0AZfkZZw%3D%3D|預(yù)訂|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 查詢頁面信息
比對結(jié)果:比對1中的車次(已標(biāo)注粗體)、車出發(fā)時間與到達(dá)時間(已標(biāo)注粗體、傾斜)、日期(已標(biāo)注粗體)、一等座與二等座有無車票(已標(biāo)注粗體、傾斜)與比對2返回頁面中的字段完全匹配,代碼編寫解析的時候只需要將Json result用符號“|”切割,這些數(shù)據(jù)清洗[11]以后才能存儲,需要刪除一些重復(fù)或者無用的數(shù)據(jù).
在爬取map過程中的“合肥”字段用“HFH”表示,“鄭州”字段用“ZZF”表示,探究圖2中的碼表,排除無效的消息,發(fā)現(xiàn)車站站點的基本規(guī)律是“漢字+|+三位的英文字符”,利用正則表達(dá)式表示為“([u4e00-u9fa5]+)|([A-Z]+)”,其中“u4e00-u9fa5”是漢字unicode編碼的范圍,通過解析后獲取的碼表就有6236條記錄,截取一部分如圖6所示.
北京北|VAP北京東|BOP北京|BJP北京南|VNP北京西|BXP廣州南|IZQ重慶北|CUW重慶|CQW重慶南|CRW重慶西|CXW廣州東|GGQ……
部分代碼實現(xiàn)如下所示:
def station_info():
#中國鐵路12306官網(wǎng)的城市名稱與城市代碼對應(yīng)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]+)' #按正則表達(dá)式進(jìn)行匹配:
result=re.findall(sn,rs.text)
stationinfo=dict(result)
return stationinfo
該設(shè)計思想是通過算法選擇時間最短、距離最短、降低換乘次數(shù)的最優(yōu)出行路線供查詢者參考.模擬一條線路,始發(fā)站標(biāo)識為T1,終點站標(biāo)識為Tn,在始發(fā)站與終點站之間站點線性依次標(biāo)識為T1、T2、T3、……、Tn,站點匹配成碼表后將建立數(shù)據(jù)庫,具體策略如下所示:
(1)建立碼表鏈接
將T1->T2、T1->T3、T1->T4、T1->T5、……、T1->Tn-1、T1->Tn標(biāo)識為S12、S13、S14、S15、……、S1(n-1)、S1n,信息包括是否有車次、剩余票數(shù)、無票等字段.
依次建立T2->T3、T2->T4、T2->T5、T2->T6、……、T2->Tn-1、T2->Tn標(biāo)識為S23、S24、S25、S26、……、S2(n-1)、S2n,信息依然包括是否有車次、剩余票數(shù)、無票等字段.
用同樣辦法建立始發(fā)站是T3、T4、T5、……Tn-1、Tn的碼表鏈接.
建立Tn-1->Tn的標(biāo)識為S(n-1)n,包含信息同上.
(2)數(shù)據(jù)拼接
第1次站點換乘:在有票源的前提下,判斷S1i+Sin是否可行,設(shè)Ti是同一列車換乘中轉(zhuǎn)站點對應(yīng)的中轉(zhuǎn)碼表.
第2次站點換乘:在有票源的前提下,判斷S1i+Sij+Sjn是否可行,設(shè)Ti、Tj是列車換乘中轉(zhuǎn)站點對應(yīng)的中轉(zhuǎn)碼表.
用同樣的辦法多個站點換乘:在有票源的前提下,判斷S1i+Sij+ Sjk+……+Svn是否可行,設(shè)Ti、Tj、Tk、……、Tv是列車換乘中轉(zhuǎn)站點對應(yīng)的中轉(zhuǎn)碼表.
(3)在上述執(zhí)行過程中無法滿足的情況下,考慮換位換乘,即購買同一車次的中轉(zhuǎn)票,將Sa標(biāo)識(商務(wù)座/特等座)、Sb標(biāo)識(一等座)、Sc標(biāo)識(二等座)、Sd標(biāo)識(硬座)、Se標(biāo)識(無座),在數(shù)據(jù)拼接的過程中實現(xiàn)換座切換,可能實現(xiàn)的是:Sa1i+Sbij+Scjk+……+Sdvn,其中Ti、Tj、Tk、……、Tv是列車換乘中轉(zhuǎn)站點對應(yīng)的中轉(zhuǎn)碼表.
(4)在上述執(zhí)行(3)過程中無法滿足的情況下,可以考慮多購買1~2個站點,即有1~2個站點的重疊,比如Sa1(i+1)與Sb(i-1)j就會最少有一個站點的重復(fù),可能實現(xiàn)的是:
Sa1(i+1)+Sb(i-1)j+……Sckv +Sdvn,Ti、Tj、Tk、Tv等是列車中轉(zhuǎn)站點對應(yīng)的碼表.
(5)在上述執(zhí)行(4)過程中無法滿足的情況下,從理論上可以考慮利用最短距離的補(bǔ)票換乘,即先購買一張站票,可以利用最少重復(fù)站的換乘,比如Sa1(i-1)+S b(i-1)(i+1)+Sc(i+1)j+……+Sdkv+ Sdvn.
(6)算法中始終以直達(dá)票為首先,同車換乘為優(yōu)選,每個算法方案均是遞進(jìn)的關(guān)系,在(5)的算法無法遞進(jìn)的時候,就要考慮2車或者更多次列車的換乘方案,換乘后再用上述的同車換乘實現(xiàn)整個流程的分段.
以上算法也是有缺陷的,在拼接的過程中未將列車晚點等不確定因素考慮進(jìn)去.
3.2.1 構(gòu)造API URL
生成可查詢的URL是整個程序的入口與關(guān)鍵,所有的城市名稱按照字典的方式設(shè)計,按照{(diào)城市名稱:城市代碼}生成,將城市名稱轉(zhuǎn)換生成城市代碼,構(gòu)造API URL如下: url=('https://kyfw.12306.cn/otn/leftTicket/queryY?'
#該地址會因為網(wǎng)站改版或者日期的變化會動態(tài)刷新:
'leftTicketDTO.train_date={}&' # train_date為列車的出發(fā)時間;
'leftTicketDTO.from_station={}&' # train_date為出發(fā)站的城市代碼;
'leftTicketDTO.to_station={}&' # train_date為到達(dá)站的城市代碼;
'purpose_codes=ADULT').format(date,from_station,to_station).
3.2.2 設(shè)計獲取列車車次信息
以從合肥到鄭州為例
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] # 出發(fā)站;
from_station_name=text['合肥'];
to_station_code=db[7] # 達(dá)到站;
to_station_name=text['鄭州'];
starttime=db[8] # 發(fā)站時間;
arrivetime=db[9] # 到站時間;
fulltime=db[10] # 發(fā)站與到站時間間隔;
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=( '查詢車次:{} 始發(fā)站:{} 終點站:{} 始發(fā)時間:{}抵達(dá)時間:{} 歷時:{} 座位剩余情況: 一等座剩余:「{}」 二等座剩余:「{}」 軟臥剩余:「{}」 硬臥剩余:「{}」 硬座剩余:「{}」 無座:「{}」 '.format(train_no,from_station_name,to_station_name,starttime,arrivetime,fulltime,firstseat,secondseat,softsleeper,hardsleeper,hardseat,noseat)) # 供查詢顯示的信息.
3.2.3 實現(xiàn)刷新頻率程序部分代碼如下:
text=station_info()
print(text)
url= urlinfo_query(text) #調(diào)用生成可查詢的URL.
#循環(huán)查詢,查詢終止條件為查到必須有的車次,
while True:
time.sleep(1) #查票刷新頻率
if train_query(url,text):
break
12306購票成功后,信息通知渠道有很多,手機(jī)短信、騰訊QQ、個人郵箱、微信等,但是尚未購票成功的客戶想通過提前查詢余票的結(jié)果推送功能卻不具備,借助第三方一款方便使用的工具Server醬即可滿足很好的推送功能,Server醬即可實現(xiàn)程序員與服務(wù)器之間的通信[12].
Server醬(ServerChan)本身就是一個擁有GET接口可編程的接收器,信息可以通過微信推送至客戶,鑒于Server醬SCKEY與UserID一對一的關(guān)系,如果實現(xiàn)一對多信息的服務(wù)推送就必須要用到PushBear,相對于Server醬也就是高級版本.ServerChan配置過程需要3個步驟:(1)登錄步驟:注冊GitHub賬號,獲取1個SCKEY,SCKEY將在發(fā)送信息的頁面使用.(2)綁定步驟:單擊按鈕“微信推送”,掃碼關(guān)注即可完成綁定請求.(3)信息發(fā)送步驟:向URL頁面發(fā)送Get或者Post請求,而URL將接受sendkey、text、desp3個參數(shù),其中sendkey參數(shù)為通道,屬于必填寫項;text參數(shù)為消息標(biāo)題,屬于長度不超過256的必填項;desp參數(shù)為信息的內(nèi)容,可以為空,支持MarkDown.以上實時查詢余票信息后,可以調(diào)用Server醬,推送至客戶的微信,部分定義代碼實現(xiàn)如下:
def send_Information(title,info):
url='https://pushbear.ftqq.com/sub?sendkey=此處為個人注冊后生成的SCKEY值&text=%s&desp=%s'%(title,info)
requests.get(url)
圖7 控制臺運行結(jié)果
圖8 Server醬推送信息
測試結(jié)果:控制臺輸出的結(jié)果與Server醬公眾號推送的信息完全一致,而這兩個結(jié)果與圖9網(wǎng)頁實際查詢的結(jié)果也是完全一致的.
圖9 網(wǎng)頁查詢結(jié)果
該系統(tǒng)設(shè)計基于python的爬蟲技術(shù),經(jīng)過算法篩選,用Python的Requests模塊與JSON解析方法[13-14]爬取了電子客票的相關(guān)信息.調(diào)用Server醬推送功能獲取了最佳出行方案,解決了官網(wǎng)中以5 s作為刷新的固定頻率,可以實時獲取想要的數(shù)據(jù),事實證明該方案行之有效,對其他相關(guān)系統(tǒng)有一些應(yīng)用參考價值[15-17],但系統(tǒng)設(shè)計還有一些可以改進(jìn)的地方,比如沒有通過更換USER-Agent或者IP代理進(jìn)行防爬處理,算法設(shè)計中模糊了車次晚點的實際情況等.