我在之前寫過一次12306網站的爬蟲,當時實現了模擬登陸和查詢車票,可是感受還不太夠,因此對以前的代碼加以修改,還實現了一個訂購車票的功能。git
在使用Selenium作模擬登陸12306網站的時候,須要將登陸成功後的Cookie保存下來,這個Cookie在後面是必需的。而後就是在12306網站上查票訂票,同時使用Fiddler軟件進行抓包,經過分析獲得訂票所需的十多個請求,只要依次發送這些請求,在請求成功以後就可以訂到票。github
以前的代碼已經基本實現了模擬登陸的功能,可是還無法獲得想要的Cookie,因此須要對以前的代碼進行改進。雖然Selenium模塊提供了get_cookies()方法,可是使用這個方法獲得的是當前會話的Cookie,也就是Selenium開啓的瀏覽器中當前頁面的Cookie,這個Cookie和本地瀏覽器中的Cookie是不一樣的。以下是從本地Chrome中拷貝的Cookie,其中以_jc_save開頭的字段都是以前查詢車票的記錄,而其他字段都是生成的:正則表達式
JSESSIONID=A318817EEE594DE954CE352761DF4CD7;json
_jc_save_fromStation=%u6B66%u6C49%2CWHN;瀏覽器
_jc_save_wfdc_flag=dc;cookie
_jc_save_toStation=%u4E0A%u6D77%2CAOH;dom
RAIL_EXPIRATION=1560095439082;ide
RAIL_DEVICEID=P2wunHEkKFe9MgTM56h-NxsWiIGNkK6JLCOVaG0DHzRm-RxYa7YnDwftPoumiZ0wL7GPsQ93YBHRHgMgB_GLWwZ9Vb65tNiVuwaIOytW8lVG7B1KopI4pSyUr1u06RWpKPhvExBg3FA7ed87WxO3E-68Wg-hXZLl;工具
_jc_save_fromDate=2019-06-30;post
_jc_save_toDate=2019-06-06;
_jc_save_showIns=true;
route=495c805987d0f5c8c84b14f60212447d;
BIGipServerotn=300941834.24610.0000;
BIGipServerpool_passport=250413578.50215.0000
下面是使用Selenium模塊的get_cookies()方法獲得的Cookie,能夠看到和瀏覽器中的Cookie有很大不一樣,缺乏了不少字段:
[{'domain': 'kyfw.12306.cn', 'httpOnly': False, 'name': 'JSESSIONID', 'path': '/otn', 'secure': False, 'value': '672BAF8C694C50C49D3EFFCF9913A745'},
{'domain': 'kyfw.12306.cn', 'httpOnly': False, 'name': 'route', 'path': '/', 'secure': False, 'value': 'c5c62a339e7744272a54643b3be5bf64'},
{'domain': 'kyfw.12306.cn', 'httpOnly': False, 'name': 'BIGipServerotn', 'path': '/', 'secure': False, 'value': '1139802634.24610.0000'}]
解決辦法是使用add_cookie()方法向Selenium開啓的Chrome中添加Cookie,達到模擬本地瀏覽器的效果,最終就能登陸成功。在登陸成功以後,要獲取此時的Cookie,除了使用get_cookies()方法或者get_cookie()方法,還可使用以下語句:
cookie = browser.execute_script("return document.cookie;")
不過爲了驗證是否真的登陸成功了,還須要進行一下測試,驗證是否登陸成功的方法以下代碼,這段代碼會發送一個請求,請求的結果中包含了是否登陸信息(即is_login)和用戶名等信息:
1 def get_name(self): 2 """
3 獲取用戶姓名 4 :return: 5 """
6 url = "https://kyfw.12306.cn/otn/login/conf"
7 res = requests.post(url, headers=self.headers) 8 is_login = res.json()['data']['is_login'] 9 if is_login == 'Y': 10 self.name = res.json()['data']['name'] 11 print("歡迎用戶:{}".format(self.name)) 12 else: 13 print("未登陸!請先登陸。")
因爲查詢車票以前就已經作過了,因此這裏就再也不贅述。這裏就說查詢車票以後的操做,首先是在12306網站上查餘票,而後選擇一個車次點擊預訂,就會跳轉到以下頁面:
在這個頁面上能夠選擇乘客、選擇座位類型,而後再提交訂單。這裏雖然咱們可使用開發者工具而後刷新頁面來抓包,可是爲了不遺漏掉某些請求,因此我選擇使用Fiddler軟件抓包,最終通過分析實踐獲得12個請求,其url和對應的含義以下圖所示:
這裏我並不打算把全部的請求都說一遍,我會將幾個重要的請求拿出來描述,這些請求所使用的headers都是同樣的,其中包含了登陸後的Cookie,若是Cookie失效就會致使訂票失敗。
首先是initDc這個請求,在這個請求的結果中包含了後面請求所必需的一個參數--token(以下圖),獲取的方法也比較簡單,能夠直接使用正則表達式進行匹配:
初始化頁面獲取token的代碼以下:
1 # 初始化,獲取token值 2 def init_dc(): 3 global token 4 url = "https://kyfw.12306.cn/otn/confirmPassenger/initDc" 5 data = { 6 "_json_att": "" 7 } 8 res = requests.post(url, headers=headers, data=data) 9 result = re.findall(" var globalRepeatSubmitToken = '(.*?)';", res.text) 10 # print(result) 11 if len(result): 12 token = result[0] 13 else: 14 raise Exception("Error init")
其次是提交車票預訂信息,在Fiddler中點擊Inspectors,而後選擇WebForms,能夠看到以下圖所示信息,其中包含了出發城市、目的城市、出發日期等:
須要注意的是secretStr這個加密字符串,其來源於查詢車票時的結果,在結果中每一條車次信息中都包含了一個字符串,不過這兩個字符串並不徹底同樣。以下圖所示就是兩個字符串的對比,要獲得加密字符串只須要使用unquote()方法:
在選擇完車次、座位類型、乘客以後會生成一個訂單,而後就會發送一個確認訂單信息的請求,其中包含了不少重要的信息。這裏我放上該部分的代碼:
1 # 確認訂單信息
2 def check_order_info(name, uid, mobile, type_id): 3 # 商務座,一等座,二等座,軟臥,硬臥,硬座
4 type_str = ["9,0,1,", "M,0,1,", "O,0,1,", "4,0,1,", "3,0,1,", "1,0,1,"][type_id] 5 url = "https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo"
6 data = { 7 "_json_att": "", 8 "bed_level_order_num": "000000000000000000000000000000", 9 "cancel_flag": "2", 10 "oldPassengerStr": name + ",1," + uid + ",1_", 11 "passengerTicketStr": type_str + name + ",1," + uid + "," + mobile + ",N", 12 "REPEAT_SUBMIT_TOKEN": token, 13 "randCode": "", 14 "tour_flag": "dc", 15 "whatsSelect": "1"
16 } 17 res = requests.post(url, headers=headers, data=data) 18 # print(res.text)
這個方法包含了四個參數,name、uid和mobile分別表示乘客的姓名、身份證號和電話號碼,這三個值都是在獲取乘客信息時獲得的,第四個參數是座位類別id。在這個請求攜帶的參數中有一個REPEAT_SUBMIT_TOKEN,這就是前面說過的token,因爲我已經將token設置爲了全局變量,因此這裏就不用做爲參數傳到方法裏了。要注意的是每一個座位類別對應的字符是不一樣的,我經過在頁面上選擇元素獲得了每一個座位類型對應的字符,最後生成一個列表,而後經過改變座位類別id就能完成選擇座位類別的功能。
在確認訂單以後就是提交預訂請求,仍是在Fiddler軟件中找到這個請求,而後查看其攜帶的參數,以下圖所示:
其中包含了車次編碼、出發站編碼、目的站編碼、token等信息,這些編碼信息均可以在查詢車票的結果中獲得,須要注意的是train_date,能夠看到這是一個日期信息,並且是一個格林威治標準時間,要獲得這個時間可使用以下方法,這就能將日期轉變成格林威治標準時間:
train_date = datetime.datetime.strptime(train_date, "%Y-%m-%d").date()
this_date = train_date.strftime("%a+%b+%d+%Y")
在提交預訂請求以後,須要檢查提交狀態,這個請求包含了不少參數,其中一些參數的值都包含在提交預訂請求的結果中,除此以外這些參數還有乘客姓名、身份證號、乘客電話、token等。這個請求返回的結果中有一個submitStatus,須要提取出來,該值代表了提交是否成功。該部分的代碼以下所示:
1 # 檢查提交狀態
2 def confirm(key_check, left_ticket, passenger_name, passenger_id, passenger_mobile, location, type_id): 3 # 商務座,一等座,二等座,軟臥,硬臥,硬座
4 type_str = ["9,0,1,", "M,0,1,", "O,0,1,", "4,0,1,", "3,0,1,", "1,0,1,"][type_id] 5 url = "https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue"
6 data = { 7 "choose_seats": "", 8 "dwAll": "N", 9 "key_check_isChange": key_check, 10 "leftTicketStr": left_ticket, 11 "oldPassengerStr": passenger_name + ",1," + passenger_id + ",1_", 12 "passengerTicketStr": type_str + passenger_name + ",1," + passenger_id + "," + passenger_mobile + ",N", 13 "purpose_codes": "00", 14 "randCode": "", 15 "REPEAT_SUBMIT_TOKEN": token, 16 "roomType": "00", 17 "seatDetailType": "000", 18 "train_location": location, 19 "whatsSelect": "1", 20 "_json_att": "", } 21 res = requests.post(url, headers=headers, data=data) 22 try: 23 js = json.loads(res.text) 24 status = js["data"]["submitStatus"] 25 # print(status)
26 return status 27 except Exception as e: 28 print(e) 29 raise Exception("Confirm Error!")
當咱們的訂單提交成功以後,就須要排隊等待了,此時會發送一個請求,該請求中攜帶了一個時間戳參數(random),以下圖所示:
這是一個十三位的時間戳,在Python中可使用 int(time() * 1000) 獲得十三位時間戳。須要注意的是排隊等待的結果是不肯定的,正確的結果以下圖所示:
其中有一個orderId,這個值是咱們須要的。若是返回的結果中不包含orderId,就須要從新發送請求。
在獲得orderId以後,就能夠請求預訂結果了,請求無誤的話就可以成功訂到票了。下圖是在Fiddler軟件中截到的圖,其中EF73361481就是前面獲得的orderId:
下圖是在Pycharm中的運行截圖,在登陸成功以後查詢餘票,將查詢的結果顯示出來:
查詢車票以後就是預訂車票,須要輸入車次名稱、座位類別和選擇乘客,而後提交訂單,最終成功訂到火車票。
訂票成功以後,進入12306網站進行查看,能夠看到成功訂到票了, 以下圖所示:
完整代碼已上傳到GitHub!