Python3.x 抓取12306车次信息,表格详情显示,让你学会思路,分析网站特点,爬取数据。12306车票查看器!

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

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的起步阶段,又没有人练手项目的人。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 看到网上有很多火车票查询的小脚本,参考一下,发现很多都已经不能再运行了,据说12306接口返回的数据格式更新比较快...
    lexyhp阅读 3,695评论 1 3
  • 前几天看了一个爬取12306来获得火车票信息的教程,发现12306官网的存储车票信息的 Json 数据格式已经变了...
    LiuHDme阅读 2,020评论 0 12
  • 项目简介:使用Python3抓取12306网站信息提供一个命令行的火车票查询工具。通过该项目的实现,可以熟悉Pyt...
    海人为记阅读 1,605评论 0 1
  • 盖被子 2017.8.31 一直觉得自己对宝爸不够上心,在他面前既不贤惠也不体贴,宝爸会有一些抱怨,我自己也有些自...
    Cici清清阅读 214评论 0 0
  • 复习指南 金融衍生工具(李国华) 期货市场的产生,发展,特点,功能交易流程,结算流程,期货套期保值,期货投机,利率...
    千张卷卷阅读 312评论 0 0