/12306_spider

12306模拟登陆及余票查询

Primary LanguagePython

实时车票查询及登陆CTC

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,然后先将var='去掉,并将js的末尾字符去掉,保留中间需要的,然后通过split对字符串分割成list,直接找到list当中请求的中文站点名字对应的index,然后加1获取真实的英文字符,然后再去请求相应的url即可!

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.运行展示

验证上述查询结果,对比之后,正确!