用Node.js做一个火车票查询小工具

github地址: https://github.com/KKDestiny/TrainTicket12306.git
演示地址:http://tickets.onelib.biz/


TrainTicket12306

这是一款用于在铁道部12306官网爬取车票等信息的node.js应用。

An app to query Tickects and other information from 12306 website by node.js

8.jpg

9.jpg


master branch

master分支为最新功能,所有功能都会最终同步到这里。


element branch

element分支为所有功能的原型


1.安装 Installation

  • 如果直接使用模块TrainTickects,则不需要任何依赖,直接引用此模块即可。
    Require TrainTickects directly if you just want to use TrainTickects module.

  • 如果你要查看网页Demo,则需要安装,直接执行 npm install 即可
    Execute npm install to install modules needed automaticly if you want to check website demo.


2.文件结构 FileStructure

主要文件结构如下:
Main Structure:

root
  |---TrainTickects.js   // TrainTickects Module
  |---test.js            // Debug file, you can use this file to test 
                         // each function in TrainTickects module
  |---package.json       // Dependences
  |---cert     
        |---srca_der.cer  // Certification
 
 

    3.更新 Updates

    V0.0.1

    首次提交于 2017/07/20,制作了 TrainTickects 模块,实现了三个简单功能:
    + 查询火车票
    + 查询中途停靠站列表
    + 收集所有火车站数据


    V1.0.0

    2017/07/20 18:38
    实现网页版小工具
    + 可以输入中文站名、日期,从而查询火车
    + 点击结果列表还可以查看本次列车的途经站点信息

    V1.1.0

    2017/07/21 14:00 完善网页版功能
    + 输入出发地、目的地时,出现下来选项,不必输入完整的名称;拼音(大小写均可)和中文均支持
    + 到达时间与出发时间不在同一天时,会给出提示,“+1”、“+2”或更多
    + 新增过滤功能:过滤车次、站名
    效果


    4.文档 Documents

    4.1 12306接口 Interface

    请求方式为:
    Request:
    https

    Host:
    kyfw.12306.cn/

    查询前缀为:
    Query Prefix
    /otn/

    已知的几个接口有:
    Known interfaces as follows:

    InterfacenameruleDemo
    leftTicket/query查询符合条件的车票
    Query tickets that satisfy query conditions
    leftTicketDTO.train_date=<date>
    &leftTicketDTO.from_station=<from_station>
    &leftTicketDTO.to_station=<to_station>
    &purpose_codes=ADULT
    leftTicketDTO.train_date=2017-07-21
    &leftTicketDTO.from_station=SZQ
    &leftTicketDTO.to_station=XMS
    &purpose_codes=ADULT
    czxx/queryByTrainNo查询某车次的途经站及时间
    Query the stations&time of a train passing by
    train_no=<train_no>
    &from_station_telecode=<from_station_telecode>
    &to_station_telecode=<to_station_telecode>
    &depart_date=<depart_date>
    train_no=6i000D40920A
    &from_station_telecode=SZQ
    &to_station_telecode=XMS
    &depart_date=2017-07-21
    resources/js/framework/station_name.js获取所有火车站数据
    Collection all stations data
    station_version=<version>station_version=1.8964

    4.2 OL_TrainTickects.QueryTickects()

    查询符合条件的车次、各类车票的剩余数量。

    此接口为 TrainTickects 模块提供的接口,需要传入两个参数:configcallback

    其中 config 为对象,其属性包括:

    • date : String, 日期, 格式”yyyy-mm-dd”
    • from_station : String, 始发站车站代码,如”SZQ”
    • end_station : String, 终到站车站代码,如”XMS”
    • print : boolean, 是否后台打印

    callback 为回调函数,允许为空。

    下面为一个简单的测试例子:

    var OL_TrainTickects = require('./TrainTickects');
    // 测试查询火车票
    var Config = {
            date         : '2017-07-24',    // 日期, 格式"yyyy-mm-dd"
            from_station : 'SZQ',           // 始发站车站代码,这里是深圳
            end_station  : 'XMS',           // 终到站车站代码,这里是厦门
            print        : true,            // 打印查询结果
        };
    OL_TrainTickects.QueryTickects(Config, function(err, tickects) {
       
       
        // console.log(tickects)
    });
     
     

      执行效果:
      upload.png


      4.3 OL_TrainTickects.QueryStations()

      用于查询某车次的停靠站点及到站、发车和停靠时间。

      此接口为 TrainTickects 模块提供的接口,需要传入两个参数:configcallback

      其中 config 为对象,其属性包括:

      • train_no: String, 列车编号, 如”6i000D40920A”
      • from_station : String, 始发站车站代码,如”SZQ”
      • end_station : String, 终到站车站代码,如”XMS”
      • date : String, 日期, 格式”yyyy-mm-dd”
      • print : boolean, 是否后台打印

      callback 为回调函数,允许为空。

      下面为一个简单的测试例子:

      var OL_TrainTickects = require('./TrainTickects');
      

      // 测试查询中途停靠站列表
      var Config = {
      train_no : ‘6i000D40920A’, // 列车编号
      from_station : ‘SZQ’, // 始发站车站代码
      end_station : ‘XMS’, // 终到站车站代码
      date : ‘2017-07-24’, // 日期, 格式"yyyy-mm-dd"
      print : true, // 是否打印
      };
      OL_TrainTickects.QueryStations(Config, function(err, sList) {
      // console.log(sList)
      });

        执行效果:
        upload.png


        4.4 OL_TrainTickects.CollectStations()

        手机全国所有火车站的数据。

        此接口为 TrainTickects 模块提供的接口,只需要传入回调函数callback即可,允许为空。

        下面为一个简单的测试例子:

        var OL_TrainTickects = require('./TrainTickects');
        

        // 测试收集所有火车站数据
        OL_TrainTickects.CollectStations(function(err, data) {
        var fs = require(‘fs’);
        // 写入js文件
        var dir = “./js/”
        if(!fs.existsSync(dir)) {
        fs.mkdirSync(dir);
        }
        fs.writeFile(dir+‘stations_py.js’, "var StationData = "+JSON.stringify(data));
        })

          执行效果:

          upload.png


          4.5 OL_TrainTickects.QueryPrice()

          查询火车票价格。

          此接口为 TrainTickects 模块提供的接口,需要传入两个参数:configcallback

          其中 config 为对象,其属性包括:

          • train_no : String, 列车编号, 如”650000Z23001”
          • from_station_no : String, 出发地车序,如”01”
          • to_station_no : String, 目的地车序,如”23”
          • seat_types : String, 如”113”
          • train_date : String, 乘车日期,如”2017-07-23”
          • print : boolean, 是否后台打印

          callback 为回调函数,允许为空。

          下面为一个简单的测试例子:

          // 测试查询票价
          var Config = {
                  train_no        : '650000Z23001',   // 列车编号
                  from_station_no : '01',             // 出发地车序
                  to_station_no   : '23',             // 目的地车序
                  seat_types      : '113',            // 如"113"
                  train_date      : '2017-07-23',     // 日期, 格式"yyyy-mm-dd"
              };
          OL_TrainTickects.QueryPrice(Config, function(err, tickects) {
             
             
              console.log(tickects)
          });
           
           

            5.一些问题

            5.1 余票查询的返回数据

            从12306获取到的原始数据结构如下:

            { 
              validateMessagesShowId: '_validatorMessage',
              status: true,
              httpstatus: 200,
              data:
              { 
                 result: [ '3ec5X9fRSYq5hiOeUulIj%2F6TbXHRQBiBhbLIFmE0GQj8rPSTYtSYZl6KDzSfChayvwsPRbgom0Fc%0Afi61kzfkhRhOqB6nkxQCUwijG2opA6FpNXyvejLtqlVrbqTiTVZMukqyucS5ldEcSsttYhaiTEdq%0ADS3WuQw4FUd8LazFoDT5eVXtAfyaNuuMtD9VR5dJKNAyMMBDI8T%2FeCJu0pPMIpxF%2FY%2FkWAK%2FANmK%0AqEKHximPszTr|预订|6i000D40920A|D4092|IOQ|XKS|IOQ|XKS|06:40|10:16|03:36|Y|yx1mCzhcHlabqde2UZwX9Y8XCtPt2nDYQaY%2BBGBmT33wpoaD|20170724|3|Q6|01|07|1|0|||||||有||||有|有|||O0M0O0|OMO', 
               ...
            
            ],
            

            flag: ‘1’,
            map: { XMS: ‘厦门’, IOQ: ‘深圳北’, XKS: ‘厦门北’ },
            messages: [],
            validateMessages: {}
            }

              对我们来说,有意义的数据只要两个:data.resultmap

              其中,data.result 是一个数组,数组的每一个元素都代表一个符合条件的车次,包含了车次、起止站、各坐席票数等信息。

              map是一个对象,属性名为火车站的代码,属性值为该代码对应的中文站名。

              map的作用在于,当我们查询 深圳厦门 的火车票时,可能出现 深圳北厦门北 这样的车站,它们和 深圳厦门不同,需要做好对应关系。

              关于如何使用这个映射关系,在5.3节会做介绍。


              5.2 关于余票信息的数据处理

              从12306获取到的每一个车次的余票信息的原始数据如下:

              upload.png

              可以看出,这些数据是以 | 为分割的,因此可以通过 split() 函数来切分原始数据:

              arr = raw_data.split("|");
              

              // Result
              [
              /* 0 / ‘uR7MTk54pjLluluefRzSndjJsuDNdhtgJK6caO246PF7XGyVDML5aJ6EgQZRE7yXyONObC8q4E5c%0ASfuGXEljez5PZuzJlFASnSuluBeLTzL06LR18feZ3AtLCG%2FjqeqBks3tpY168pxxeGdkPOr0rakC%0AhJ6gEXT%2Bbo0OSKVXn9fWm3pXsU16O9tZBcbD4LGLtUlynzNxM%2FB%2BN0rRNodacPi3VHx5wMIpgeaH%0AIF%2FkfW2ySCl1’,
              / 1 / ‘预订’,
              / 2 / ‘6i000D40920A’,
              / 3 / ‘D4092’,
              / 4 / ‘IOQ’,
              / 5 / ‘XKS’,
              / 6 / ‘IOQ’,
              / 7 / ‘XKS’,
              / 8 / ‘06:40’,
              / 9 / ‘10:16’,
              / 10 / ‘03:36’,
              / 11 / ‘Y’,
              / 12 / ‘VYa0L6Eo5FxIlzIT86a%2BZ1aAkg1iGV%2FwY8DzVKRX2iKk0iN%2B’,
              / 13 / ‘20170724’,
              / 14 / ‘3’,
              / 15 / ‘Q6’,
              / 16 / ‘01’,
              / 17 / ‘07’,
              / 18 / ‘1’,
              / 19 / ‘0’,
              / 20 / ‘’,
              / 21 / ‘’,
              / 22 / ‘’,
              / 23 / ‘’,
              / 24 / ‘’,
              / 25 / ‘’,
              / 26 / ‘有’,
              / 27 / ‘’,
              / 28 / ‘’,
              / 29 / ‘’,
              / 30 / ‘有’,
              / 31 / ‘有’,
              / 32 / ‘’,
              / 33 / ‘’,
              / 34 / ‘O0M0O0’,
              / 35 */ ‘OMO’ ]

                可以看出,每个元素都严格对应一个数据;但是由于缺少文档,我不清楚每个位置代表什么意思。

                不过,根据我们已知的情况, arr[3] 肯定是车次, arr[8] 肯定是发车时间, arr[9] 肯定是到达时间, arr[10] 肯定是总时间, arr[13] 肯定是乘车日期。

                另外,根据后来做 OL_TrainTickects.QueryStations() 的时候分析其接口时可以知道:arr[2] 肯定是列车编号,这个编号可以用在后面查询某车次中途停靠站信息中。

                因此源代码中,是这样处理的:

                ...
                

                tickect.train_no = temp[2]; // 火车编号
                tickect.tId = temp[3]; // Train ID

                tickect.sTime = temp[8]; // Start Time
                tickect.eTime = temp[9]; // End Time
                tickect.tTime = temp[10]; // Total Time
                tickect.date = temp[13];

                • 1
                • 2
                • 3
                • 4
                • 5
                • 6
                • 7
                • 8
                • 9
                • 10
                • 11


                那么,剩余的一些字段该如何解读呢?

                首先,我们看到了一些位置出现了 这个数值,但这并不足以让我们知道这些位置代表什么数据;我们看下面图中,一等座、二等座和无座三类席位都是 ,我们仅仅能知道第26 30 31个位置代表一等座、二等座和无座,并且顺序也不清楚。

                upload.png


                这里提供一种比较笨的方法:大量查找不同起止站的列车,推理出各类坐席剩余票数在原始数据中的位置。

                举个例子,我们在12306官网中查询一下深圳-郑州的列车:
                upload.png

                可以知道商务座、一二等座的票数分别为 16 12。那么我们同时查看余票的原始数据:

                [ '9IQVfT3yCLkzmM1ZghLmyzwq%2Fn4vNliAfmnnwzZ01FtloWX5j1WC%2B2eLlKI7MhrbrUUwKKRc9zh7%0APzQNPa6qbZxP0F
                9Sm4zin2w4d8sWSsB6ZlL8anu7LciQyVlV6XPMZqEMgcRW5T6tcH2MOmxuLsxH%0AE5L%2BJd8FmD029yCje%2F%2Bwir7yHyLyA
                M7oElbTsa2c5C%2BfouxQU1V6lb1bcL%2FueyAUsI1qd0OObjT2%0Afg%3D%3D',
                  '预订',
                  '6i00000G7210',
                  'G72',
                  'NZQ',
                  'BXP',
                  'NZQ',
                  'ZAF',
                  '07:43',
                  '14:56',
                  '07:13',
                  'Y',
                  'UM2Ja13aKKsc6qxZtVDIqImzPYBW8yoFun0JxcFfLjb8BFPl',
                  '20170724',
                  '3',
                  'Q6',
                  '01',
                  '10',
                  '0',
                  '0',
                  '',
                  '',
                  '',
                  '',
                  '',
                  '',
                  '',
                  '',
                  '',
                  '',
                  /* 30 */ '有',
                  /* 31 */ '12',
                  /* 32 */ '16',
                  '',
                  'O0M090',
                  'OM9' ]
                 
                 
                • 1
                • 2
                • 3
                • 4
                • 5
                • 6
                • 7
                • 8
                • 9
                • 10
                • 11
                • 12
                • 13
                • 14
                • 15
                • 16
                • 17
                • 18
                • 19
                • 20
                • 21
                • 22
                • 23
                • 24
                • 25
                • 26
                • 27
                • 28
                • 29
                • 30
                • 31
                • 32
                • 33
                • 34
                • 35
                • 36
                • 37
                • 38

                这样,很明显arr[30] 代表的是二等座,arr[31] 代表的是一等座,arr[32] 商务座。

                用同样的方式,再查找其他车次的车票数,只要不全是相同的值,我们就能够通过对比这两个信息得到各个位置代表什么坐席。

                根据多次尝试,得到以下结果(2017.07.20):

                tickect.ruanwo = temp[23];  // 软卧
                tickect.ruanzuo = temp[24]; // 软座
                tickect.wuzuo = temp[26];   // 无座
                tickect.yingwo = temp[28];  // 硬卧
                tickect.yingzuo = temp[29]; // 硬座
                

                tickect.scSeat = temp[30]; // 二等座
                tickect.fcSeat = temp[31]; // 一等座
                tickect.bcSeat = temp[32]; // 商务座 / 特等座

                tickect.dongwo = temp[33]; // 动卧

                • 1
                • 2
                • 3
                • 4
                • 5
                • 6
                • 7
                • 8
                • 9
                • 10
                • 11


                5.3 始发站/终点站 与 出发站/到达站

                5.1中的余票数据中,有四个站点代码:

                /* 4 */  'IOQ',
                /* 5 */  'XKS',
                /* 6 */  'IOQ',
                /* 7 */  'XKS',
                 
                 
                • 1
                • 2
                • 3
                • 4

                发现4和6、5和7是一样的代码。原因很简单,因为我们出发的火车站就是始发站、到达的火车站就是终点站。

                那么到底前两个是始发站/终点站,还是后两个是?我们可以在12306官网找一个例子:深圳-梅州的列车只有一趟,很好分析:
                upload.png

                所以我们可以更改一下查询条件:惠州-兴宁,结果为:

                // {HCQ: "惠州", ENQ: "兴宁"}
                

                /* 4 / ‘SZQ’,
                / 5 / ‘MOQ’,
                / 6 / ‘HCQ’,
                / 7 */ ‘ENQ’,

                • 1
                • 2
                • 3
                • 4
                • 5
                • 6
                • 7

                因此可以确定,56为此列车的始发站和终点站,67为实际的出发站和到达站。

                由此,我们也可以根据map信息来得到火车站代码对应的中文名:

                /*
                 * 根据车站code转换为中文名称
                 */
                function TMapStations(code, map) {
                   
                   
                    if(map) {
                        var name = map[code];
                        if(name) {
                            return name
                        }
                    }
                    return code;
                }
                

                // 解析余票信息部分代码
                tickect.fSation = TMapStations(temp[6], map); // From Station Name
                tickect.tSation = TMapStations(temp[7], map); // To Station Name

                • 1
                • 2
                • 3
                • 4
                • 5
                • 6
                • 7
                • 8
                • 9
                • 10
                • 11
                • 12
                • 13
                • 14
                • 15
                • 16
                • 17
                • 18
                • 19
                • 20


                5.4 出发地和目的地车序以及seat_types

                在获取车票价格的请求中,我们可以看到有至少3个字段:

                upload.png

                • from_station_no : 01
                • to_station_no : 23
                • seat_types : 113

                那么这三个数据是从哪里来的呢?

                我们先看一下获取到的车票的原始数据:

                 [ "8ug8apI2e0XX%2BolsYZc4z%2FDkjvClldv5Bg8q%2BZq9xkYTNdJQdu1X4NIxb2hWi7oSF%2FVMsfRnDUoV%0AtgGs1o9VzGfjN8Q4VUTKusWMtiwidqHG9lTBpTnN09xV6LOWlC0FK3ZicLhxc4E%2FwFNuCnlQUfhG%0Af9AM%2BdkPfPy755xwhHwVZz9rdjCJOjf8tAom4hJb27XWdlvZo2viDjCwfhYMkTbCSE6b0eV0XDhH%0AhdN1Z1Q%3D", 
                "预订", 
                "650000Z23001", 
                "Z230", 
                "SZQ",
                "WAR", 
                "SZQ", 
                "WAR", 
                "09:30", 
                "10:33", 
                "47:14", 
                "Y", 
                "4Qoh7OTkPpObyF7cv4441dPVJFE7XBu06AVmiPYDitUlwBcb", 
                "20170723", 
                "3", 
                "Q7", 
                /* 16 */ "01", 
                /* 17 */ "23", 
                "0", 
                "0", 
                "", 
                "", 
                "", 
                "", 
                "", 
                "", 
                "有",
                "", 
                "无", 
                "无", 
                "", 
                "", 
                "", 
                "", 
                "101030", 
                /* 35 */ "113"
                ]
                 
                 
                • 1
                • 2
                • 3
                • 4
                • 5
                • 6
                • 7
                • 8
                • 9
                • 10
                • 11
                • 12
                • 13
                • 14
                • 15
                • 16
                • 17
                • 18
                • 19
                • 20
                • 21
                • 22
                • 23
                • 24
                • 25
                • 26
                • 27
                • 28
                • 29
                • 30
                • 31
                • 32
                • 33
                • 34
                • 35
                • 36
                • 37

                可以知道,第16位、17位、35位分别代表了 from_station_noto_station_noseat_types


                余票返回数据格式总结

                arr = raw_data.split("|");
                

                // Result
                [
                /* 0 - ? / ‘uR7MTk54pjLluluefRzSndjJsuDNdhtgJK6caO246PF7XGyVDML5aJ6EgQZRE7yXyONObC8q4E5c%0ASfuGXEljez5PZuzJlFASnSuluBeLTzL06LR18feZ3AtLCG%2FjqeqBks3tpY168pxxeGdkPOr0rakC%0AhJ6gEXT%2Bbo0OSKVXn9fWm3pXsU16O9tZBcbD4LGLtUlynzNxM%2FB%2BN0rRNodacPi3VHx5wMIpgeaH%0AIF%2FkfW2ySCl1’,
                / 1 - 状态 / ‘预订’,
                / 2 - 列车编号 / ‘6i000D40920A’,
                / 3 - 车次*/ ‘D4092’,
                /* 4 - 始发站代码 / ‘IOQ’,
                / 5 - 终点站代码 / ‘XKS’,
                / 6 - 出发地代码 / ‘IOQ’,
                / 7 - 目的地代码 / ‘XKS’,
                / 8 - 发车时间 / ‘06:40’,
                / 9 - 到达时间 / ‘10:16’,
                / 10 - 运行时长 / ‘03:36’,
                / 11 - ?(canWebBuy) / ‘Y’,
                / 12 - ?(yp_info) / ‘VYa0L6Eo5FxIlzIT86a%2BZ1aAkg1iGV%2FwY8DzVKRX2iKk0iN%2B’,
                / 13 - 乘车日期 / ‘20170724’,
                / 14 - ?(train_seat_feature) / ‘3’,
                / 15 - ?(location_code) / ‘Q6’,
                / 16 - 出发地车序 / ‘01’,
                / 17 - 到达地车序 / ‘07’,
                / 18 - ?(is_support_card) / ‘1’,
                / 19 - ?(controlled_train_flag) / ‘0’,
                / 20 - ? / ‘’,
                / 21 - ? / ‘’,
                / 22 - ? / ‘’,
                / 23 - 软卧 / ‘’,
                / 24 - 软座 / ‘’,
                / 25 - ? / ‘’,
                / 26 - 无座 / ‘有’,
                / 27 - ? / ‘’,
                / 28 - 硬卧 / ‘’,
                / 29 - 硬座 / ‘’,
                / 30 - 二等座 / ‘有’,
                / 31 - 一等座 / ‘有’,
                / 32 - 商务座 / 特等座 / ‘’,
                / 33 - 动卧 / ‘’,
                / 34 - ?(yp_ex) / ‘O0M0O0’,
                / 35 - seat_type */ ‘OMO’ ]

                • 1
                • 2
                • 3
                • 4
                • 5
                • 6
                • 7
                • 8
                • 9
                • 10
                • 11
                • 12
                • 13
                • 14
                • 15
                • 16
                • 17
                • 18
                • 19
                • 20
                • 21
                • 22
                • 23
                • 24
                • 25
                • 26
                • 27
                • 28
                • 29
                • 30
                • 31
                • 32
                • 33
                • 34
                • 35
                • 36
                • 37
                • 38
                • 39
                • 40


                感谢

                感谢 落花落雨不落叶 的博文提供的帮助!
                http://www.cnblogs.com/hongrunhui/p/6284192.html

                • 0
                  点赞
                • 0
                  收藏
                  觉得还不错? 一键收藏
                • 0
                  评论
                评论
                添加红包

                请填写红包祝福语或标题

                红包个数最小为10个

                红包金额最低5元

                当前余额3.43前往充值 >
                需支付:10.00
                成就一亿技术人!
                领取后你会自动成为博主和红包主的粉丝 规则
                hope_wisdom
                发出的红包
                实付
                使用余额支付
                点击重新获取
                扫码支付
                钱包余额 0

                抵扣说明:

                1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
                2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

                余额充值