實時車票查詢及登陸CTC
- 2019 年 10 月 6 日
- 筆記

實時車票查詢及登陸CTC
0.說在前面
1.項目架構
2.模擬登陸
2.1 登陸分析
2.2 登陸實現
3.余票查詢
3.1 查詢分析
3.2 查詢實現
4.運行展示
5.作者的話
0.說在前面
又是一年國慶節,祝各位國慶節快樂,玩的開心!
從大學至今,唯一一個宅的國慶,讓自己多點思考,少點外出。
這幾天也沒更文,忙於之前的小遊戲pygame的開發,這方面的軟文,隨後幾日更新。
前兩天老表發了個12306軟文,忽然想起,自己的公眾號也好久沒更新爬蟲系列了,今天就開始琢磨一下,本次的爬蟲主要有兩大方面的功能。
【第一】 如何登陸12306
【第二】 如何做到實時車票查詢
當你們在排隊等候伺服器響應的時候,我已經買下票了;
當你們在搶購最後一張車票的時候,已經沒了;
當你們在等待放票的時候,我已經調整好買票方案了。
哈哈,有點難拉仇恨。。那麼沒事,學好接下來的操作,會有助於你解決車票麻煩。
車票查到了,離心中的遠方還遠?
Close To Close
1.項目架構

項目架構圖
login_spider # 登陸類 用於12306全局登陸與管理 downloadCode # 用於下載驗證碼 verifi_Code # 用於驗證驗證碼是否輸入成功 main_Login # 用於賬戶登陸 get_Tk # 登陸不成功的uamtk獲取 tk_Auth # uamtk驗證 Login # 真實登陸的跳轉頁面 main # 對上述程式碼的調用 ticker_spider get_StationName_En # 獲取出發站(抵達站)的字母簡寫 search_Ticket # 余票查詢 get_StationName # 獲取真實的中文表示的站點 print_TicketInfo # 列印余票查詢結果
2.模擬登陸
2.1 登陸分析
【驗證碼】
分為以下幾種情況:
第一種情況:驗證碼失敗,會發現如下圖校驗結果,並且沒有login的相關資訊。

驗證碼失敗圖
第二種情況:用戶名或密碼錯誤,驗證碼正確,此時會出現login的資訊

用戶名登錄請求圖

用戶名錯誤圖
第三種情況:登陸成功

登陸成功圖
綜上對於登陸的流程為,先下載驗證碼,手動驗證,然後傳入正確的用戶名與密碼,再進行登陸。
在登陸之前,12306會對你的驗證碼做校驗,如果失敗了,則直接不用管你的用戶名與密碼,所以先對驗證碼進行手動驗證。然後再去用賬戶名與密碼進行POST提交。
就這麼簡單?
當然不是,在你登陸後,最後會發現並未成功,搜索你的姓名並未發現,那麼就得繼續抓包。最後發現頁面上需要uamtk驗證,然後才可以進行正常的爬取操作。
接下來我們進入實戰環節。
2.2 登陸實現
上述的頁面訪問較多,未了更方便的操作,本次採用requests裡面的Session統一進行管理Cookie!
【封裝】
import requests class login_spider(object): def __init__(self): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' } sess = requests.Session() self.headers=headers self.sess = sess
【驗證碼識別】
當我們把圖中的八個驗證碼進行點擊的時候,會出現如下的坐標位置,那麼我們只要將上述的坐標放進list中,當出現是哪個數據時候,就輸入相應位置即可。輸入的範圍未(0~7) ,index從0開始!並且當驗證碼中有多個滿足條件時候,輸入一定要連著輸入。

驗證碼全選圖
def verifi_Code(self): verifi_url = 'https://kyfw.12306.cn/passport/captcha/captcha-check' verifi_axis = ['36,46','109,44','181,47','254,44','33,112','105,116','186,116','253,115'] axis = input("請輸入驗證碼坐標>> ") verifi_list = [] for point in axis: verifi_list.append(verifi_axis[int(point)]) axis_pos = ','.join(verifi_list) post_data = { "answer": axis_pos, "login_site": "E", "rand": "sjrand", } res = self.sess.post(url=verifi_url,headers=self.headers,data=post_data) res_json = res.json() if not res_json['result_code']=='4': print("驗證失敗") return False print(res_json) return True
【登陸】
def main_Login(self): login_url = 'https://kyfw.12306.cn/passport/web/login' data_post = { "username":"輸入您的用戶名", "password": "輸入您的密碼", "appid": "otn" } res = self.sess.post(login_url, headers=self.headers, data=data_post) print(res.json())
【登陸後驗證】
def get_Tk(self): url_uamtk = 'https://kyfw.12306.cn/passport/web/auth/uamtk' data_uamtk = {"appid":"otn"} res = self.sess.post(url_uamtk,headers=self.headers,data=data_uamtk) print(res) res_json = res.json() data_verifi = {"tk":res_json["newapptk"]} return data_verifi def tk_Auth(self): uamauthclient_url = "https://kyfw.12306.cn/otn/uamauthclient" res = self.sess.post(uamauthclient_url,headers=self.headers,data=self.get_Tk()) print(res)
3.余票查詢
3.1 查詢分析
余票查詢可以使用之前的Session管理的cookie用賬戶許可權去抓取,也可以不用登陸就可以!
【難點】
- 查詢的結果在哪
- 結果如何處理
- 查詢途中的站點名字與字母簡寫如何處理
對於第一個難點,直接打開f12檢查即可,會發現,如下圖所示結果:

余票查詢圖
上圖中的result裡面的就是余票查詢結果!
但是問題來了,查詢出來的數據是這麼的亂,那麼怎麼處理呢?到底哪一塊表示始發站,硬座,軟卧等?
這個處理是直接打開12306隨機去是個查詢結果,然後到了這個頁面後,去搜索相應的車次,然後對應的一行就是顯示介面的數據,最後發現各條數據之前從|預訂|
開始後面所有的數據是很規則的,那麼前面的所有東西我直接通過正則匹配以|預訂|
分開,然後得到一個list,取index=1的數據即為我們需要的完整的數據,然後將其與頁面數據進行匹配,最後就可以鎖定哪個index表示硬座,軟卧等。
在前面去請求數據的時候,會發現請求的數據並不是你所輸入的中文,比如要查詢重慶到成都,那麼按照我們正常思路是直接用原字元串重慶與成都訪問,但是實際不是,如下圖:

真實請求圖
看到了沒,重慶對應CQW,成都對應CDW,中文又是怎麼變為這些英文字母的呢?
針對這個問題,想必又是js作祟,於是打開js篩選,找到了有關station_name的相關js,如下兩圖:

js請求圖

js內容圖
發現了js裡面中文後的下一個便是請求的英文字元串,那麼我可以不費吹灰之力便可以拿到頁面的js,然後先將var='
去掉,並將js的末尾字元去掉,保留中間需要的,然後通過split對字元串分割成list,直接找到list當中請求的中文站點名字對應的index,然後加1獲取真實的英文字元,然後再去請求相應的url即可!

js頭圖

js尾圖
3.2 查詢實現
【封裝】
import re from prettytable import PrettyTable from login_spider import login_spider class ticker_Spider(object): def __init__(self): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' } search_url = 'https://kyfw.12306.cn/otn/index/init' ls = login_spider() self.headers = headers self.ls = ls self.search_url = search_url
【真實的站點名字】
def get_StationName_En(self,name): # 此處可以不需要session操作即可 url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9069' res = self.ls.sess.get(url,headers=self.headers).text # print(res) with open('name.txt', 'w', encoding='utf-8') as f: # 去掉js的開頭與結尾 res = res.replace('var station_names =', '').replace(''', '').replace(';', '') f.write(res) with open('name.txt', 'r', encoding='utf-8') as f: line = f.read() # print(line) sn_list = line.split('|') # print(sn_list) # print(sn_list.index(name)) name_index = sn_list.index(name) + 1 return sn_list[name_index]
【余票查詢】
相應位置對應的數據資訊表
''' 1-車次 checi 2/4-始發站 from_station 3/5-終點站 to_station 6-出發時間 from_time 7-到達時間 to_time 8-歷時 total_time 11-出發日期 from_datetime -16-高級軟卧 high_soft -14-軟卧 common_soft -11-無座 no_seat -4-動卧 move_down -5-商務座(特等座) special_seat -6-一等座 first_seat -7-二等座 second_seat -9-硬卧 hard_seat '''
def search_Ticket(self): self.ls.main() # 'leftTicketDTO.train_date=2018-10-04&leftTicketDTO.from_station=CQW&leftTicketDTO.to_station=SHH&purpose_codes=ADULT' print("時間輸入格式為>> 2018-10-02") raw_from_station = input("請輸入出發地>> ") raw_to_station = input("請輸入目的地>> ") train_date = input("請輸入出發日>> ") # back_train_date = input("請輸入返程日>> ") base_url = 'https://kyfw.12306.cn/otn/leftTicket/queryA?' from_station_En = self.get_StationName_En(raw_from_station) to_station_En = self.get_StationName_En(raw_to_station) url = base_url + 'leftTicketDTO.train_date=' + train_date + '&leftTicketDTO.from_station=' + from_station_En + '&leftTicketDTO.to_station=' + to_station_En + '&purpose_codes=ADULT' res = self.ls.sess.get(url,headers=self.headers).json() tick_res = res['data']['result'] print(len(tick_res)) search_res = len(tick_res) checi = [] from_station = [] to_station = [] from_time = [] to_time = [] total_time = [] from_datetime = [] no_seat = [] high_soft = [] common_soft = [] special_seat = [] move_down = [] first_seat = [] second_seat = [] hard_seat = [] for each in tick_res: print("-----") # print(i) # a = i.find('預訂') need_data = re.split(r'|預訂|', each)[1] need_data = need_data.split('|') print(need_data) checi.append(need_data[1]) from_station.append(self.get_StationName(need_data[2])) to_station.append(self.get_StationName(need_data[3])) from_time.append(need_data[6]) to_time.append(need_data[7]) total_time.append(need_data[8]) from_datetime.append(need_data[11]) high_soft.append(need_data[-16]) common_soft.append(need_data[-14]) no_seat.append(need_data[-11]) move_down.append(need_data[-4]) special_seat.append(need_data[-5]) first_seat.append(need_data[-6]) second_seat.append(need_data[-7]) hard_seat.append(need_data[-9]) return search_res,raw_from_station,raw_to_station,checi,from_station,to_station,from_time,to_time,total_time,from_datetime,high_soft,common_soft,no_seat,move_down,special_seat,second_seat,first_seat,hard_seat
【余票展示】
def get_StationName(self,name): with open('name.txt', 'r', encoding='utf-8') as f: line = f.read() # print(line) sn_list = line.split('|') # print(sn_list) # print(sn_list.index(name)) name_index = sn_list.index(name) - 1 return sn_list[name_index] def print_TicketInfo(self): search_res, raw_from_station, raw_to_station,checi, from_station, to_station, from_time, to_time, total_time, from_datetime, high_soft, common_soft, no_seat, move_down, special_seat, second_seat, first_seat, hard_seat = self.search_Ticket() pt = PrettyTable() print("---------從" + str(raw_from_station) + '到' + str(raw_to_station) + '共' + str(search_res) + '個車次'+ '---------') pt.add_column('車次', checi) pt.add_column('始發站', from_station) pt.add_column('終點站', to_station) pt.add_column('出發時間', from_time) pt.add_column('到達時間', to_time) pt.add_column('歷時', total_time) pt.add_column('出發日期', from_time) pt.add_column('高級軟卧', high_soft) pt.add_column('軟卧', common_soft) pt.add_column('無座', no_seat) pt.add_column('動卧', move_down) pt.add_column('商務座', special_seat) pt.add_column('一等座', first_seat) pt.add_column('二等座', second_seat) pt.add_column('硬卧', hard_seat) return pt
4.運行展示

驗證登陸圖

余票結果圖

余票官網圖
驗證上述查詢結果,對比之後,正確!
5.作者的話
最後,您如果覺得本公眾號對您有幫助,歡迎您多多支援,轉發,謝謝!