美文网首页python专题大数据 爬虫Python AI Sql程序员
Python3.x 抓取12306车次信息,表格详情显示,让你学

Python3.x 抓取12306车次信息,表格详情显示,让你学

作者: 旧城城旧 | 来源:发表于2017-10-31 20:56 被阅读1455次

    我的例子都比较适合新手,那种老司机请绕道,谢谢!

    ps

    查询车票接口被更换了,就是多了一个O而已,不知道啥时候又要换成什么样?我tm能说是开发后台的那个逼输错单词了不https://kyfw.12306.cn/otn/leftTicket/queryO?leftTicketDTO.train_date=2017-10-31&leftTicketDTO.from_station=GIW&leftTicketDTO.to_station=ZIW&purpose_codes=ADULT

    前言

    最近学习Python,所以呢?跟大家一样,都是看看官网,看看教程,然后就准备搞一个小东西来试试,那么我使用的例子是实验楼中的12306火车票查询例子。但是那个是2.7版本的,并且那个实验楼的ubuntu系统老是一些包装不上,没办法就在我电脑上搞好了。

    结果展示:

    我在window上运行的结果

    下面这一段说明我是抄的,哈哈,因为我自己再怎么写还不是同样的内容。

    让我们先给这个小应用起个名字吧,既然及查询票务信息,那就tickets,其实 大家随意了,需要发布就需要起一个更好的名字,不然只要自己玩儿的懂,但是要有程序的特点,所以还是tickets相关的吧。方便阅读和自己记忆。

    我们希望用户只要输入出发站,到达站以及日期就让就能获得想要的信息,比如要查看10月31号贵阳-遵义西的火车余票, 我们只需输入:

    python3.5 lnlr.py 贵阳 遵义西 2017-10-31

    注意:上面的日期(包括后面的)是笔者写文章时确定的日期,当你在做这个项目的时候可能要根据当前时间做适当调整。

    转化为程序语言就是:

    python3.5 lnlr.py from to date

    另外,火车有各种类型,高铁、动车、特快、快速和直达,我们希望可以提供选项只查询特定的一种或几种的火车,所以,我们应该有下面这些选项:

    -g 高铁
    -d 动车
    -t 特快
    -k 快速
    -z 直达
    这几个选项应该能被组合使用,所以,最终我们的接口应该是这个样子的:

    python3.5 lnlr.py [options] from to date

    接口已经确定好了,剩下的就是实现它了。

    环境

    Centos 7 linux 系统
    Python3.5.2

    使用到的库

    docopt------>命令行解释器(把我玩儿死)
    colorama--->一个文本着色器
    requests --->爬虫必备,http请求库
    prettytable->表格显示

    安装库

    1. 未安装之前


      Linux上面目前的库

    安装库:

    pip3.5 install requests colorama docopt prettytble

    1. 安装之后


      安装完成之后的库列表

    ok,我们环境有了,库有了,那么应该干啥呢?

    爬虫个人分析:

    1. 制定爬取内容
    2. 选取目标
    3. 准备环境,上面就提前说了,因为这个本来就是在搞爬虫,所以...
    4. 分析该网站的html结构,得到url
    5. 爬取数据
    6. 分析数据
    7. 封装数据(组装数据),弄成自己想要的样子
    8. coding......

    那么我们开始吧

    第一步

    当然是打开12306的官网了,然后进行一个余票查询,当然首先你得按一下f12,打开控制台面板哦。
    我是查询的是:贵阳--遵义西 10-31号的车票

    f12之后,查票页面

    我先埋下一个伏笔:

    我看到的贵阳-遵义 10-31的车次一共是11个班次。

    第二步

    首先在控制台找到Network按钮,点击。然后选择XHR ---》找到请求到后台的接口,

    请求接口
    那么我们先看一个三个接口的请求和返回的数据:
    1.https://ad.12306.cn/sdk/webservice/rest/appService/getAdAppInfo.json?placementNo=0004&clientType=2&billMaterialsId=28e783cd2ec048ee8575cc3e502292c2
    查看返回的结果:
    貌似没有什么有用的信息。
    1. https://kyfw.12306.cn/otn/leftTicket/log?leftTicketDTO.train_date=2017-10-31&leftTicketDTO.from_station=GIW&leftTicketDTO.to_station=ZIW&purpose_codes=ADULT
      好像也看不到关于贵阳-遵义的任何信息

    3.https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2017-10-31&leftTicketDTO.from_station=GIW&leftTicketDTO.to_station=ZIW&purpose_codes=ADULT

    第三个接口返回的数据

    哈哈,在里面我看到了贵阳,遵义,那么大胆的猜测这个接口有可能就是我们需要的。
    我们继续点开看看有什么东西,这些数据都是以json的格式传递过来的。


    展开结果

    到这里我相信,聪明的人已经知道了这个就是我们所需要的接口了,而这些数据就绝对是车次信息的数据。
    由上面的我f12查看到的数据是11条,那么你们就没有点小激动么?
    说明这个接口就是我们所需要的接口无误。那么现在我们就要得到它的url咯。
    靠,双十一快到了,被女朋友抓去看了一会儿衣服,可能今天就不写了,明天接着写。

    那么这样:我就明天开始分析url,然后就coding
    请求的接口URL:

    https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2017-10-31&leftTicketDTO.from_station=GIW&leftTicketDTO.to_station=ZIW&purpose_codes=ADULT
    分析:
    1. 查看请求方式 POST/GET(常用的),这里使用的是GET请求
    2. 肯定带有参数了,并且GET请求是拼接到url之后的,我们查询的时候是输入了起始地点,目的地点,出发时间。那么url上也应该带有这三个。
    3. 得到参数名称:leftTicketDTO.train_date=2017-10-31,leftTicketDTO.from_station=GIW,leftTicketDTO.to_station=ZIW
    还有一个参数:purpose_codes=ADULT 根据ADULT的意思(成人,成年)大胆猜测这就是学生票和成人票。
    4.得到了四个参数,但是我们还不知道其中有两个GIW,ZIW是什么意思。
    因为我输入的是中文,但是出现的是字母代号。做过前后台交互的同学应该觉得这种是很常见的。目的是避免了中文传输导致的问题。
    

    分析参数的获取

    leftTicketDTO.train_date=2017-10-31 时间
    leftTicketDTO.from_station=GIW 出发地
    leftTicketDTO.to_station=ZIW 目的地
    同学们请注意:我们输入的是中文,出来的是地点代码,说明中间有一层转换,那么在常规的网站中,只有两种三种方式能这样处理?

    1. 将这个地点-地点代码字典写入js中,这个地方有可能,因为国内的地点太多,需要手动维护。
    2. 将地点-地点代码字典写入本地文件文件,做好缓存,每次读取文件,然后使用。
    3. 从远端服务器进行获取,在这里也没必要,因为每次都要去请求后台,增加服务器的压力,这个是没必要的,因为这个字典的话是基本不会变化的。

    ok经过上面的分析,我们就能很清楚的知道这个字典绝对是从js获取的,那么我们就也一样的使用发f12来检查到资源,找到该页面所引用的所有js,然后进行分析。

    该页面所加载的js文件
    上图中的矩形中的就是当前页面中的所有js,当然每个js的作用我就不一一的说了,各位需要帮助的可以email(leihfein@gmail.com)我,或者在下面留言。
    那么我直接查看各个的内容,发现有一个是: 包含了地点,地点代码js

    这样我们就得到了一个js的请求地址哦。https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9028

    浏览器中输入该URL看到的结果

    我们还可以来一个测试:
    我在查询的时候是输入了,贵阳-遵义,搜索一下看看吧。

    搜索

    同学们,看到这个你们觉得爽不爽,说明这个文件就是我们所需要的。

    ok,到这里,编码前期准备工作,所有的都昨晚了,我从一步一步的分析,然后截图。给大家思路,方法,步骤。希望大家能够更明白,更多的是学习到其中的分析思路哈。

    codingwars

    1. 首先爬取地点-代码code字典。
    #!/usr/bin/env python3
    # coding: utf-8
    
    import requests
    import re
    from pprint import pprint
    """
      获取到地点-地点code字典
    """
    def get_station():
            url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9028'
            response = requests.get(url,verify=False)
            station = re.findall(u'([\u4e00-\u95fa5]+)\|([A-Z]+)',response.text)
            pprint(dict(station),indent=4)
    
    if __name__=="__main__":
            get_station()
    
    在最后,我是使用pprint输出到屏幕的,大家可以进行拷贝,或者是使用Linux的重定向进行输出到别的文件 。
    python3.5 get_station_code.py > stations.py
    并且需要在stations.py文件中增加一个名字哦,因为输出来时也没有名字的。见下图
    
    在windows中就只能拷贝了,
    
    新增一个stations字典名称

    好的,到这里我们的地点-地点代码就得到了,那么我们就应该写那个爬取车次信息的py了。

    处理输出的代码:

    from prettytable import PrettyTable
    
    from stations import stations
    from colorama import init, Fore
    
    """
        处理爬取出来的车次信息,并进行表格输出
    """
    
    init()
    
    
    class TrickCollection(object):
        def __init__(self, available_trains, options):
            self.header = ('车次 车站 时间 历时 特等座 一等 二等 高级软卧 软卧 动卧 硬卧 '
                           + '软座 硬座 无座 备注').split()
            self.available_trains = available_trains
            self.options = options
    
        # 将历时转化为小时和分钟的形式
        def get_duration(self, raw_train):
            duration = raw_train[10].replace(':', '小时') + '分'
            if duration.startswith('00'):
                return duration[4:]
            if duration.startswith('0'):
                return duration[1:]
            return duration
    
    
    
        # 返回每个车次的基本信息
        def trains(self):
            for raw_train in self.available_trains:
                # 列车号
                train_no = raw_train[3]
                # 得到什么列车并小写
                initial = train_no[0].lower()
                # 反转station所对应的字典
                stations_re = dict(zip(stations.values(), stations.keys()))
                if not self.options or initial in self.options:
                    # 将车次的信息保存到列表中
                    # train 出发地
                    begin_station = stations_re.get(raw_train[4])
                    # train 目的地
                    end_station = stations_re.get(raw_train[5])
                    # your 出发地
                    from_station = stations_re.get(raw_train[6])
                    # your 目的地
                    to_station = stations_re.get(raw_train[7])
                    # 判断是起始还是经过
                    begin_flag = self.__check_equals(begin_station, from_station)
                    end_flag = self.__check_equals(end_station, to_station)
                    train = [
                        train_no,
                        '\n'.join([begin_flag + ' ' + self.__get_color(Fore.GREEN, from_station),
                                   end_flag + ' ' + self.__get_color(Fore.RED, to_station)]),
                        '\n'.join([self.__get_color(Fore.GREEN, raw_train[8]),
                                   self.__get_color(Fore.RED, raw_train[9])]),
                        # 时间
                        self.get_duration(raw_train),
                        # 历时
                        raw_train[32],
                        # 特等座
                        self.__show_color(raw_train[31]),
                        # 一等
                        self.__show_color(raw_train[30]),
                        # 二等
                        self.__show_color(raw_train[22]),
                        # 高级软卧
                        self.__show_color(raw_train[23]),
                        # 软卧
                        self.__show_color(raw_train[33]),
                        # 硬卧
                        self.__show_color(raw_train[28]),
                        # 软座
                        self.__show_color(raw_train[24]),
                        # 硬座
                        self.__show_color(raw_train[29]),
                        # 无座
                        self.__show_color(raw_train[26]),
                        # 备注
                        self.__show_color(raw_train[1])
                    ]
                    # 更改不运行车次的时间和历时
                    if raw_train[14] == 'null':
                        train[2] = '--\n--'
                    train[3] = '--'
                    # 将空字符串转化为‘--’
                    for i, item in enumerate(train):
                        if not item:
                            train[i] = '--'
                    yield train
    
        def __check_equals(self, from_station, to_station):
            """
            检查是否是始、过
                检查你的起始站是否为该车次的起始站
                检查你的终止站是否为该车次的终止站
            :param from_station:  出发位置
            :param to_station:     结束位置
            :return: 决定了是使用‘始' 还是 ’过‘
            """
            if from_station == to_station:
                return '始'
            else:
                return '过'
        def __get_color(self, color, content):
            """
            返回颜色内容组合,并且清除该内容之后的颜色
            :param color: 传递的颜色
            :param content: 内容
            :return: 返回值为拼接上颜色
            """
            return color + content + Fore.RESET
    
        def __show_color(self, content):
            """
            对内容进行颜色显示,并且只显示有,其余不上色
            :param content: 需要颜色显示的内容
            :return: 返回设置结果
            """
            if content == '有':
                return Fore.GREEN + content + Fore.RESET
            else:
                return content
    
        def pretty_print(self):
            """
                显示内容
            :return:
            """
            pt = PrettyTable()
            pt._set_field_names(self.header)
            for train in self.trains():
                pt.add_row(train)
            print(pt)
    
    

    成果展示:
    但是我在远端上使用xshell没有颜色,这个是什么鬼。最后发现是自己设置xshell而已。
    好的,下面就是我们的程序执行的结果。
    如果程序输入的地点在字典中查询不到,那么就不能查票。所以我们需要随时更新stations.py文件(地点-code字典表)


    成果展示

    ok,到这里的话,我们的所有的爬虫就抓取成功,并且输出成我们想要的结果了。我准备下一步就是搞那个抢票。试试吧。
    代码在后面我会给出github地址的。

    总结

    在本次实验中,我所遇到的问题简单说一下:

    1. optdoc的使用,这个是我遇到的最大的坑,(usage 下面命令之后必须空一行)
    2. 在Linux上的缩进问题
      3.就是对python还不是特别的熟。

    github地址

    12306火车票查看器git地址

    阅读到本教程的童鞋,喜欢请点个喜欢,不求赞赏,只求喜欢,顶上去。现在网上很多都是以前的接口的例子,已经跑不起来了,所以我希望让更多的人看到,能帮助更多还在py的起步阶段,又没有人练手项目的人。

    相关文章

      网友评论

      • 东东隆东抢:多个O,并不是拼写错误,是故意。之前还出现过queryZ呢。现在就是query
        旧城城旧:谢谢,我知道的,地址在随时变,就是防止刷票
      • e444e1c8dcfd:非常感感谢楼主分享,但是楼主代码是否未贴完整呢?:blush:
        旧城城旧:@LCQ_KK 嗯呢?但是要注意github上面的文件有点多。要注意使用对应的那个包里面的文件哦。
        e444e1c8dcfd:@旧城城旧 哦哦,好的
        旧城城旧:@LCQ_KK 看GitHub地址
      • 那一缕长发飘飘:请问出现Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)这是什么意思呢
        旧城城旧:@erleichtert 354571000
        erleichtert:老哥,有联系方式吗?有点问题想请教一下。
        旧城城旧:@那一缕长发飘飘 中文编码问题
      • 磨_4c80:更新了接口也没用
        旧城城旧:@磨_4c80 应该是可以的,等我下班之后我在试试
        磨_4c80:@旧城城旧 试过,不知道是我的电脑原因还是什么
        旧城城旧:@磨_4c80 我的这个不行了么?
      • 磨_4c80:老是显示不出来
      • 小白猿:同是爬12306的数据,这样的文章才十几个喜欢,太屈才了,楼主加油吧,Python自学党一枚,多谢楼主的文章
        旧城城旧:@小白猿 没关系的,自己学习到才是真的。我也是自学的。喜欢就点:heart:️。关注谢谢
      • 1199b66853a4:我想问下,为啥我用f12查不到js那些呢
        旧城城旧:@Yuan小江YJ leihfei@163.com
        1199b66853a4:@旧城城旧 方便留个email嘛我没选择xhr
        旧城城旧:@Yuan小江YJ 你是不是选择成xhr,这个就不能查看的哦
      • bahamutedean:非老司机,替楼主更正一下
        def trains(self):
        for raw_train in self.available_trains:
        # 列车号
        train_no = raw_train[3]
        # 得到什么列车并小写
        这里需要加一个raw_train.split('|'),不然遍历的只是一个长字符串哦
        旧城城旧:sorry,现在才回复你,很是抱歉。工作上的需要,导致都没去找py虐我了。你说的问题我看了一下,是不存在的,因为我在lnlr.py中已经使用availabel_trains = [i.split('|') for i in availabel_trains]
        来将抓取的信息分割了,所以在另一个文件中是不需要再次分割的,直接使用索引来获取即可。

      本文标题:Python3.x 抓取12306车次信息,表格详情显示,让你学

      本文链接:https://www.haomeiwen.com/subject/gsompxtx.html