实时车票查询及登陆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.作者的话
最后,您如果觉得本公众号对您有帮助,欢迎您多多支持,转发,谢谢!