项目实战一 12306火车票余票查询软件

1.安装docopt、urllib、requests

2.实现程序基础框架

# -*- coding:utf-8 -*-

"""
Train tickets query program.

Usage:
    crawl12306 [-dgktz] <from> <to> <date>

Options:
    -h --help            Show this screen
    -d                   动车
    -g                   高铁
    -k                   快速
    -t                   特快
    -z                   直达
"""

from docopt import docopt

def crawler():
    arguments = docopt(__doc__,version = "v1.0")
    print(arguments)                                         #可以测试出是否能从终端获取参数,返回字典
    from_station = arguments.get('<from>')                   #从字典查值
    to_station = arguments.get('<to>')
    date = arguments.get('<date>')
    url = ''                                                 #这个时候应该会发现url不知道怎么写


if __name__ == '__main__':
    crawler()

3.测试一下当前效果,尤其是docopt的效果

4.找出request中url的构造规则

容易发现,12306余票查询的页面使用了Ajax,可以找到其header中的request url

可以发现,这个url的基础是'https://kyfw.12306.cn/otn/leftTicket/query?',后面的一系列都是参数,包括时间、始发站、终点站、成人等。而车站的名称都是字母码表示的。要想构造完整的url,必须知道站名和字母码的关系表。

5.找出站名-字母码关系表

找到12306出发站与目标站的地名对应文件,鼠标右键->copy link address,可以看到目前使用的文件外链是:https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9061,点进去可看到json数据

6.实现站名到字母码的转换函数

import re
import requests


def parse_stations():
    url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9061'
    response = requests.get(url,verify=False)
    # print(response.text)            #测试用
    stations = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)', response.text)       #\u4e00-\u9fa5是Unicode汉字编码范围
    # print(type(stations))           #测试用
    # print(stations)                 #测试用
    stationsDict = dict(stations)
    # print(stationsDict)             #测试用
    # print(type(dict(stations)))     #测试用
    return stationsDict

可以测试一下,测试的时候取消“#”注释:

7.完善②中的url构造

from docopt import docopt
from urllib.parse import urlencode


def generate_url(staionDict):
    arguments = docopt(__doc__,version = "v1.0")                            #可以测试出是否能从终端获取参数,返回字典
    print(arguments)                                                       
    from_station = stationsDict.get(arguments.get('<from>'), None)
    to_station = stationsDict.get(arguments.get('<to>'), None)
    date = arguments.get('<date>')
    standard_date = date[0:4]+'-'+date[4:6]+'-'+date[6:]
    print(standard_date)
    url = 'https://kyfw.12306.cn/otn/leftTicket/query?'+\
                urlencode({'leftTicketDTO.train_date': standard_date,
                            'leftTicketDTO.from_station': from_station,
                            'leftTicketDTO.to_station': to_station,
                            'purpose_codes': 'ADULT'})
    print(url)
    return url

8.完善一下逻辑,测试一下:

# -*- coding:utf-8 -*-

"""
This is a train tickets query program.
                                                          #必须有空行,不然参数不识别的好吗?这个bug真心难找……
Usage:
    crawl12306 [-dgktz] <from> <to> <date>

Options:
    -h --help            Show this screen
    -d                   动车
    -g                   高铁
    -k                   快速
    -t                   特快
    -z                   直达

Examples:
    crawl12306 北京 上海 20180808
    crawl12306 -dg 成都 广州 20180808
"""


from docopt import docopt
from urllib.parse import urlencode


def generate_url(staionDict):
    arguments = docopt(__doc__,version = "v1.0")                                #可以测试出是否能从终端获取参数,返回字典
    print(arguments)                                                             
    from_station = stationsDict.get(arguments.get('<from>'), None)
    to_station = stationsDict.get(arguments.get('<to>'), None)
    date = arguments.get('<date>')
    standard_date = date[0:4]+'-'+date[4:6]+'-'+date[6:]
    print(standard_date)
    url = 'https://kyfw.12306.cn/otn/leftTicket/query?'+\
                urlencode({'leftTicketDTO.train_date': standard_date,
                            'leftTicketDTO.from_station': from_station,
                            'leftTicketDTO.to_station': to_station,
                            'purpose_codes': 'ADULT'})
    print(url)
    return url


def crawler(url):
    pass


import re
import requests


def parse_stations():
    url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9061'
    response = requests.get(url,verify=False)
    # print(response.text)
    stations = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)', response.text)  #\u4e00-\u9fa5是Unicode汉字编码范围
    # print(type(stations))
    # print(stations)
    stationsDict = dict(stations)
    # print(stationsDict)
    # print(type(dict(stations)))
    return stationsDict


if __name__ == '__main__':
    stationsDict = parse_stations()
    url = generate_url(stationsDict)
    crawler(url)

在这里,特地打印出了构造的url,对比④中的request url,可以认为是合理的。

9.完成车次信息爬取

有了正确的request url,可以看一下返回的数据的构造

可以修改crawler函数如下:

def crawler(url):
    response = requests.get(url,verify= False)                 
    trians_info = response.json()['data']['result']    #转化为json,再通过索引查找所需信息部分
    print(trians_info)

可以发现打印出好长一串字符(如果日期不是设置在今天且没有车次了的话:)),这些字符串应该是可以用json解析的,不然12306的界面显示肯定会混乱不堪,对吧?初步怀疑是两个json中的一个,先打开第一个。注意图中标记的{}可以prettyprint此json,不然多难看不是?

美观多了,对吧?

鼠标放在json文本窗口中,ctrl+f打开搜索功能,搜索t-list,这样就能减少阅读工作量,至于为什么要搜t-list,也算是一种基于经验的判断:

搜索发现疑似目标

复制出一段来,以备修改:

            var cq = ct[cr].split("|");
            cw.secretHBStr = cq[36];
            cw.secretStr = cq[0];
            cw.buttonTextInfo = cq[1];
            var cu = [];
            cu.train_no = cq[2];
            cu.station_train_code = cq[3];
            cu.start_station_telecode = cq[4];
            cu.end_station_telecode = cq[5];
            cu.from_station_telecode = cq[6];
            cu.to_station_telecode = cq[7];
            cu.start_time = cq[8];
            cu.arrive_time = cq[9];
            cu.lishi = cq[10];
            cu.canWebBuy = cq[11];
            cu.yp_info = cq[12];
            cu.start_train_date = cq[13];
            cu.train_seat_feature = cq[14];
            cu.location_code = cq[15];
            cu.from_station_no = cq[16];
            cu.to_station_no = cq[17];
            cu.is_support_card = cq[18];
            cu.controlled_train_flag = cq[19];
            cu.gg_num = cq[20] ? cq[20] : "--";
            cu.gr_num = cq[21] ? cq[21] : "--";
            cu.qt_num = cq[22] ? cq[22] : "--";
            cu.rw_num = cq[23] ? cq[23] : "--";
            cu.rz_num = cq[24] ? cq[24] : "--";
            cu.tz_num = cq[25] ? cq[25] : "--";
            cu.wz_num = cq[26] ? cq[26] : "--";
            cu.yb_num = cq[27] ? cq[27] : "--";
            cu.yw_num = cq[28] ? cq[28] : "--";
            cu.yz_num = cq[29] ? cq[29] : "--";
            cu.ze_num = cq[30] ? cq[30] : "--";
            cu.zy_num = cq[31] ? cq[31] : "--";
            cu.swz_num = cq[32] ? cq[32] : "--";
            cu.srrb_num = cq[33] ? cq[33] : "--";
            cu.yp_ex = cq[34];
            cu.seat_types = cq[35];
            cu.exchange_train_flag = cq[36];

通过文本替换,修改为符合python语法的格式,也可猜到部分标记的意思:

data_list = i.split("|")
train_no = data_list[2]
station_train_code = data_list[3]
start_station_telecode = data_list[4]            #始发站代码
end_station_telecode = data_list[5]              #终点站代码
from_station_telecode = data_list[6]             #出发站代码
to_station_telecode = data_list[7]               #到达站代码
start_time = data_list[8]                        #出发时间
arrive_time = data_list[9]                       #到达时间
lishi = data_list[10]                            #历时
canWebBuy = data_list[11]
yp_info = data_list[12]
start_train_date = data_list[13]
train_seat_feature = data_list[14]
location_code = data_list[15]
from_station_no = data_list[16]
to_station_no = data_list[17]
is_support_card = data_list[18]
controlled_train_flag = data_list[19]
gg_num = data_list[20] or "--"                    
gr_num = data_list[21] or "--"                    #高级软卧
qt_num = data_list[22] or "--"                    #其他
rw_num = data_list[23] or "--"                    #软卧
rz_num = data_list[24] or "--"                    #软座
tz_num = data_list[25] or "--"                    
wz_num = data_list[26] or "--"                    #无座
yb_num = data_list[27] or "--"                    
yw_num = data_list[28] or "--"                    #硬卧
yz_num = data_list[29] or "--"                    #硬座
ze_num = data_list[30] or "--"                    #二等座
zy_num = data_list[31] or "--"                    #一等座
swz_num = data_list[32] or "--"                   #商务座/特等座
srrb_num = data_list[33] or "--"                  #动卧
yp_ex = data_list[34]
seat_types = data_list[35]
exchange_train_flag = data_list[36]

但是,这里面有不少信息是我们的程序功能用不上的,可以筛选一下(不知道对应的是啥可以打印出来看看)。同时,由于需要用车站字母码反查车站名称,可以构造一个新的dict。

new_dict = {v: k for k, v in stationsDict.items()}

至此完整的crawler函数如下:

def crawler(url):
    response = requests.get(url)
    # print(response.text)      #测试用
    trians_info = response.json()['data']['result']         #先将网页的response转化为json,再通过索引查找所需的车次信息部分
    # print(trians_info)        #测试用
    for i in trians_info:
        data_list = i.split("|")
        train_no = data_list[2]                           #列车编号,如2400000G710Q
        station_train_code = data_list[3]                 #列车车次号,如G71
        from_station_telecode = data_list[6]              #出发站代码
        to_station_telecode = data_list[7]                #到达站代码
        start_time = data_list[8]                         #出发时间
        arrive_time = data_list[9]                        #到达时间
        lishi = data_list[10]                             #历时
        gr_num = data_list[21] or "--"                    #高级软卧
        rw_num = data_list[23] or "--"                    #软卧
        rz_num = data_list[24] or "--"                    #软座
        wz_num = data_list[26] or "--"                    #无座
        yw_num = data_list[28] or "--"                    #硬卧
        yz_num = data_list[29] or "--"                    #硬座
        ze_num = data_list[30] or "--"                    #二等座
        zy_num = data_list[31] or "--"                    #一等座
        swz_num = data_list[32] or "--"                   #商务座
        data = [                                          #上述信息有些放着好看,我们需要实现的功能用不上
        station_train_code,
        new_dict.get(from_station_telecode),              #通过出发车站字母码反查出发车站名称,查不到默认返回None
        new_dict.get(to_station_telecode),
        start_time,
        arrive_time,
        lishi,
        zy_num,
        ze_num,
        rw_num,
        yw_num,
        yz_num
        ]
        yield data

11.安装prettytable

12.完成信息打印函数

from prettytable import PrettyTable

def prettyPrint(datagenerator):
    pt = PrettyTable()
    pt._set_field_names('车次 出发站 到达站 出发时间 到达时间 历时 一等座 二等座 软卧 硬卧 硬座'.split())
    for i in datagenerator:                             #注意到i是list类型
        pt.add_row(i)
    print(pt)

再完善一下程序逻辑:

if __name__ == '__main__':
    stationsDict = parse_stations()
    url = generate_url(stationsDict)
    new_dict = {v: k for k, v in stationsDict.items()} 
    datagenerator = crawler(url)
    prettyPrint(datagenerator)

测试一下。小功告成!

14.解决一个问题

目前仍存在的问题是选项options还没有发挥作用。解决办法如下:

在generate_url()函数定义中,加入:

options = [k for k,v in arguments.items() if v == True]                    #获取options,构造成一个list

return url,options。

传入crawler()函数,函数定义中,在station_train_code = data_list[3]后加入判断:

if not options or '-'+station_train_code[0].lower() in options:
    #如果没有选项,默认全部查找,或者将车次如G71的G取出来,转化为g,构造成-g查看是否包含在选项中,是则执行车次信息获取

再完善一下程序逻辑:

if __name__ == '__main__':
    stationsDict = parse_stations()
    url,options = generate_url(stationsDict)
    new_dict = {v: k for k, v in stationsDict.items()} 
    datagenerator = crawler(url,options)
    prettyPrint(datagenerator)

执行一下:

15.打印界面美化

可使用prettyprint的功能使界面分行显示,效果如下,还可以安装使用colorama模块进行着色(此功能懒得实现了):

总结:

本程序实现了基于python3的12306火车票余票查询工具,主体思想参考的是实验楼公开课程,只是模块化更加明显,且使用了美妙的yield。

只是,车站-字母码可以生成一次便保存省得每次都重新生成,更进一步地,嫌找json解析数据太过麻烦不如使用scrapy对接spash/selenium实现所见即所得。

还可以使用python内建的input()函数替代docopy实现更适合新手的简化版,或者可以从面向类编程的角度实现进阶版。

参考:《【公开课】实验楼带你学爬虫——Python实现火车票爬虫工具

猜你喜欢

转载自blog.csdn.net/weixin_42353109/article/details/81512374