12306 半自动刷票

*** 说明***:
这不是黑科技,并不是自动购票,请根据自己的需求使用,建议使用抢票 APP 靠谱一些,360 抢票也比这个好,至少它能智能识图!!!
只是自动查询你想要的车次,并自动点击预订,自动填写用户名和密码,但是图片验证码还需要自己点击(这个无解)。
本文先说使用方法,再讲解。

使用方法

  • 首先,打开 12306 的 车票预定页面

  • 摁 F12 键打开浏览器控制台(Chrome 浏览为例),选择 console,如下图所示:

    12306 半自动刷票_第1张图片
    console.png

  • 将配置好的代码粘贴到图中区域,按 Enter 键回车就行。可以在 Network 里面看到已经在刷了:

12306 半自动刷票_第2张图片
log.png
  • 建议运行时关闭控制台 或者 将控制台放到右侧之类,不然小屏幕下登陆框会看不全。点击控制台右侧的三个竖点选择:
    12306 半自动刷票_第3张图片
    setRight.png

可能的结果

第一种情况是需要重新登录一次,这种很常见。用户名密码都自动帮你填好了,然后自己再填这个坑爹的验证码吧(ˉ▽ˉ;)...,示意图如下:

12306 半自动刷票_第4张图片
needLogin.png

另一种情况是直接跳到购买页面,你需要自己勾选乘客和点击提交订单即可:

12306 半自动刷票_第5张图片
noLogin.png

无论是哪种结果,都需要重新运行代码。在控制台按向上箭头,再按回车键就行(当然重新粘贴也行)。



配置代码

先粘代码:

var WISH = {
        train_date: '2017-01-24', // 乘车日期
        from_station_telecode: 'HGH', // HGH - 杭州东
        to_station_telecode: 'NXG', // NXG - 南昌西
        purpose_codes: 'ADULT', // ADULT - 成年人,0X00 - 学生

        station_train_code: [ // 想买的车次,排前面的优先
            'G2365',
            'G1417',
            'G1341'
        ],
        setType: [ // 座位类型,不填 - 不限制; yz_num - 硬座; rz_num - 软座; yw_num - 硬卧; rw_num - 软卧; gr_num - 高级软卧; zy_num - 一等座; ze_num - 二等座; tz_num - 特等座; wz_num - 无座; qt_num - 其它; swz_num - 商务座
            "zy_num", // 一等座
            "ze_num", // 二等座
            "wz_num" // 无座
        ]
        
    },
    USER = {
        name: '[email protected]', // 用户名称
        password: 'xxx' // 用户密码
    },
    SEARCH_RATE = 5000, // 刷新频率,5000 毫秒

    timer = null,
    matchTicket = {},
    availableTicketsMap = {};

/**
 * Ajax
 * @param  {Object}   data     搜索参数
 * @param  {Function} callback 回调函数,用于处理返回的数据
 */
function queryAjax(data, callback) {
    var ajaxData = {
        'leftTicketDTO.train_date': data.train_date,
        'leftTicketDTO.from_station': data.from_station_telecode,
        'leftTicketDTO.to_station': data.to_station_telecode,
        'purpose_codes': data.purpose_codes
    };

    // log, it's no use for me
    $.ajax({
        type: "GET",
        isTakeParam: false,
        beforeSend: function(xhr) {
            xhr.setRequestHeader("If-Modified-Since", "0");
            xhr.setRequestHeader("Cache-Control", "no-cache");
        },
        url: "/otn/leftTicket/log",
        data: ajaxData,
        timeout: 15000,
        success: function(res) {}
    });

    // query
    $.ajax({
        type: 'GET',
        isTakeParam: false,
        beforeSend: function(xhr) {
            xhr.setRequestHeader('If-Modified-Since', '0');
            xhr.setRequestHeader('Cache-Control', 'no-cache');
        },
        url: '/otn/leftTicket/queryA',
        data: ajaxData,
        timeout: 10000,
        success: function(res) {
            if (res.status) {
                callback(res.data);
            }
        }
    });
}

/**
 * 处理返回的数据
 * @param  {Array} data  返回的所有车次信息
 * @return {Object}      可购买的车次 map
 */
function getAvailableTicketsMap(data) {
    var i = 0,
        ticket = {},
        result = {};

    for (i = 0; i < data.length; i++) {
        ticket = {
            secretStr: data[i].secretStr,
            train_no: data[i].queryLeftNewDTO.train_no,
            start_time: data[i].queryLeftNewDTO.start_time,
            station_train_code: data[i].queryLeftNewDTO.station_train_code,
            to_station_telecode: data[i].queryLeftNewDTO.to_station_telecode,
            from_station_telecode: data[i].queryLeftNewDTO.from_station_telecode
        };

        if (ticket.secretStr !== '' && // or data[i].queryLeftNewDTO.canWebBuy === 'Y'
            ticket.to_station_telecode === WISH.to_station_telecode &&
            ticket.from_station_telecode === WISH.from_station_telecode &&
            hasSiteType(data[i].queryLeftNewDTO) ) {

            result[ticket.station_train_code] = {
                train_no: ticket.train_no,
                secretStr: ticket.secretStr,
                start_time: ticket.start_time,
                station_train_code: ticket.station_train_code,
                to_station_telecode: ticket.to_station_telecode,
                from_station_telecode: ticket.from_station_telecode
            };
        }
    }

    return result;
}

/**
 * 是否有想要的座位
 * @param  {[type]}  queryLeftNewDTO [description]
 * @return {Boolean}                 true-有,false-无
 */
function hasSiteType( queryLeftNewDTO ) {
    var i = 0,
        wantSetTypeArr = WISH.setType;

    if ( wantSetTypeArr.length === 0 ) {
        return true;
    }

    for (i = 0; i < wantSetTypeArr.length; i++) {
        if ( /有|[1-9]/.test(queryLeftNewDTO[ wantSetTypeArr[i] ]) ) {
            return true;
        }
    }

    return false;
}

/**
 * 匹配想要购买的车次
 * @param  {Object} ticketsMap 可购买的所有车次
 * @return {Boolean}           true-匹配到,false-没匹配到
 */
function matchYourTickets(ticketsMap) {
    var i = 0,
        ticket = {},
        wantTrains = WISH.station_train_code;

    console.log(ticketsMap);

    for (i = 0; i < wantTrains.length; i++) {
        ticket = ticketsMap[wantTrains[i]];

        if (typeof ticket !== 'undefined') {
            clearInterval(timer); // 清除定时器
            reserveTicket(ticket); // 预订车票
            setFormData(USER); // 填写用户信息

            return true;
        }
    }

    return false;
}

/**
 * 预订车票
 * @param  {Object} ticket 改车次信息
 */
function reserveTicket(ticket) {
    checkG1234(ticket.secretStr, ticket.start_time, ticket.train_no, ticket.from_station_telecode, ticket.to_station_telecode);
}

/**
 * 填写表单信息
 * @param {Object} user 用户信息对象
 */
function setFormData(user) {
    $('#username').val(user.name);
    $('#password').val(user.password);
}

/**
 * 初始化
 */
function init() {
    // 一开始执行就查询匹配一次
    queryAjax(WISH, function(data) {
        availableTicketsMap = getAvailableTicketsMap(data);
        matchYourTickets(availableTicketsMap);
    });

    // 定时器
    timer = setInterval(function() {
        queryAjax(WISH, function(data) {
            availableTicketsMap = getAvailableTicketsMap(data);
            matchYourTickets(availableTicketsMap);
        });
    }, SEARCH_RATE);
}

init();

// 用于下一个页面
// function buy() {
//  $( '#normalPassenger_0' ).click();
//  $( '#submitOrder_id' ).click();
// }

// buy();

需要配置的信息就是代码开头的 WISH 部分。



城市 code 查询

至于城市 code 怎么查询,请在官方代码中查找,代码很长很长...链接地址
使用浏览器的 Ctrl + F 查找你所想查找的城市,比如 “杭州东”(注意:杭州和杭州东的 code 不一样),紧接着杭州东后面的字母就是对应的 code :

12306 半自动刷票_第6张图片
stationCode.png

建议使用可搜索的城市区间,在官网测试过的;注意大小写是区分的



思路分析——搜索功能

好了,接下来是思路分析。

先填写搜索条件,点击搜索,查看控制台 Network 里面的 XHR 记录,也就是发送的 Ajax 了:

12306 半自动刷票_第7张图片
searchLog.png

可以发现,每次点击搜索,都会发送两个 Ajax 请求:

  • /otn/leftTicket/log:这个看名字像是记录搜索日志,不是很清楚
  • /otn/leftTicket/queryA: 这个就是查询了

两个 Ajax 的参数都是一致的:

12306 半自动刷票_第8张图片
queryString.png

就是我们所填写的搜索参数,格式化后如下:

{
    'leftTicketDTO.train_date': '2017-01-24', // 出发日
    'leftTicketDTO.from_station': 'HGH', // 出发地
    'leftTicketDTO.to_station': 'NXG', // 目的地
    'purpose_codes': 'ADULT' // 普通(成年人)
}

所以我们可以使用这些参数来模拟搜索。

Tips:
并且发现查询结果只和 出发日期出发地目的地乘客类型(普通、学生) 有关,和车次的筛选条件无关:

不填写筛选条件(返回 92 条数据)

12306 半自动刷票_第9张图片
noFilter.png

填写筛选条件(返回 92 条数据)
12306 半自动刷票_第10张图片
hasFilter.png

所以车次的过滤,是在浏览器端完成的



思路分析——预订

找个可以预订的车次,查看 预定 按钮信息(就是控制台左上角的箭头,先点击它,再去点按钮):

12306 半自动刷票_第11张图片
viewEle.png

可以看到使用的是 onClick 事件,执行函数是 checkG1234(顺带多观察了几个可预定的车次,发现预订按钮点击执行的函数名都叫做 checkG1234):

12306 半自动刷票_第12张图片
clickHandleFunction.png

checkG1234 格式化:

checkG1234(
    'c7JA6pPfpAnWCp5RcvN8Ks8WYk68Wv0ZxdytFe8c8vmN64VfPKCIdxXSLTCBd1Gc%2F9zXT5i7gaZy%0ATr4SWbEOeZwttRjRaGMfOHoKAnxoDJwtiEm8Ittvm7BZZu1qaJcZqFqg2EbdT%2FyissJoFkqEzenT%0AA%2F1UTQV%2BHLjKKEM6MT9SsmljBbjFH3SAhSavWoWUzjQlLsWofENBOoRg2Hu%2F%2FxSqKmWoLwQ%2F4Ku%2B%0AOZNWfWel2HJQ1Oqn5obNR5mTv8sNkJGaSCO459N4zSjH5Fnv29Nbw207GxdKQGFCVQ%3D%3D',
    '00:35',
    '56000K429760',
    'HZH',
    'NCG'
)

一共有四个参数,前三个不知道是啥,后两个是始发地和目的地。先不管,先去扒扒搜索返回的数据。



思路分析——返回数据分析

假设 K4297 在查询结果中对应的数据为 data,则将可以将预订函数参数分解:
找两条数据对比一下:

{
    "queryLeftNewDTO": {
        "train_no": "56000K429760",
        "station_train_code": "K4297", // 车次
        "start_station_telecode": "HZH",
        "start_station_name": "杭州",
        "end_station_telecode": "NCG",
        "end_station_name": "南昌",
        "from_station_telecode": "HZH",
        "from_station_name": "杭州",
        "to_station_telecode": "NCG",
        "to_station_name": "南昌",
        "start_time": "00:35",
        "arrive_time": "10:12",
        "day_difference": "0",
        "train_class_name": "",
        "lishi": "09:37",
        "canWebBuy": "Y",
        "lishiValue": "577",
        "yp_info": "0%2BhvfBij8EeRbc3N5OhdLWdJS%2F%2FutFvI",
        "control_train_day": "20300303",
        "start_train_date": "20170124",
        "seat_feature": "W010",
        "yp_ex": "1010",
        "train_seat_feature": "0",
        "train_type_code": "4",
        "start_province_code": "08",
        "start_city_code": "0904",
        "end_province_code": "11",
        "end_city_code": "1104",
        "seat_types": "11",
        "location_code": "H1",
        "from_station_no": "01",
        "to_station_no": "07",
        "control_day": 29,
        "sale_time": "1030",
        "is_support_card": "0",
        "controlled_train_flag": "0",
        "controlled_train_message": "正常车次,不受控",
        "yz_num": "--", // 硬座
        "rz_num": "--", // 软座
        "yw_num": "--", // 硬卧
        "rw_num": "--", // 软卧
        "gr_num": "--", // 高级软卧?
        "zy_num": "有", // 一等座
        "ze_num": "有", // 二等座
        "tz_num": "--", // 特等座
        "gg_num": "--", // ?
        "yb_num": "--", // ?
        "wz_num": "--", // 无座
        "qt_num": "--", // 其它?
        "swz_num": "11" // 商务座
    },
    "secretStr": "'c7JA6pPfpAnWCp5RcvN8Ks8WYk68Wv0ZxdytFe8c8vmN64VfPKCIdxXSLTCBd1Gc%2F9zXT5i7gaZy%0ATr4SWbEOeZwttRjRaGMfOHoKAnxoDJwtiEm8Ittvm7BZZu1qaJcZqFqg2EbdT%2FyissJoFkqEzenT%0AA%2F1UTQV%2BHLjKKEM6MT9SsmljBbjFH3SAhSavWoWUzjQlLsWofENBOoRg2Hu%2F%2FxSqKmWoLwQ%2F4Ku%2B%0AOZNWfWel2HJQ1Oqn5obNR5mTv8sNkJGaSCO459N4zSjH5Fnv29Nbw207GxdKQGFCVQ%3D%3D",
    "buttonTextInfo": "预订"
}

假设这条数据叫做 data,可以发现如下对应关系:

'data.secretStr'  =>  'c7JA6pPfpAnWCp5RcvN8Ks8WYk68Wv0ZxdytFe8c8vmN64VfPKCIdxXSLTCBd1Gc%2F9zXT5i7gaZy%0ATr4SWbEOeZwttRjRaGMfOHoKAnxoDJwtiEm8Ittvm7BZZu1qaJcZqFqg2EbdT%2FyissJoFkqEzenT%0AA%2F1UTQV%2BHLjKKEM6MT9SsmljBbjFH3SAhSavWoWUzjQlLsWofENBOoRg2Hu%2F%2FxSqKmWoLwQ%2F4Ku%2B%0AOZNWfWel2HJQ1Oqn5obNR5mTv8sNkJGaSCO459N4zSjH5Fnv29Nbw207GxdKQGFCVQ%3D%3D',
'data.queryLeftNewDTO.start_time'  =>  '00:35',
'data.queryLeftNewDTO.train_no'  =>  '56000K429760',
'data.queryLeftNewDTO.from_station_telecode'  =>  'HZH',
'data.queryLeftNewDTO.to_station_telecode'  =>  'NCG'

好了,点击预订需要的参数都找全了。



模拟搜索

这个很简单,定义一个 queryAjax 的方法,传入搜索参数 data 和回调函数 callback:

/**
 * Ajax
 * @param  {Object}   data     搜索参数
 * @param  {Function} callback 回调函数,用于处理返回的数据
 */
function queryAjax( data, callback ) {
    var ajaxData = {
        'leftTicketDTO.train_date': data.train_date,
        'leftTicketDTO.from_station': data.from_station_telecode,
        'leftTicketDTO.to_station': data.to_station_telecode,
        'purpose_codes': data.purpose_codes
    };

    // log, it's no use for me
    $.ajax({
        type: "GET",
        isTakeParam: false,
        beforeSend: function( xhr ) {
            xhr.setRequestHeader("If-Modified-Since", "0");
            xhr.setRequestHeader("Cache-Control", "no-cache");
        },
        url: "/otn/leftTicket/log",
        data: ajaxData,
        timeout: 15000,
        success: function( res ) {}
    });

    // query
    $.ajax({
        type: 'GET',
        isTakeParam: false,
        beforeSend: function( xhr ) {
            xhr.setRequestHeader('If-Modified-Since', '0');
            xhr.setRequestHeader('Cache-Control', 'no-cache');
        },
        url: '/otn/leftTicket/queryA',
        data: ajaxData,
        timeout: 10000,
        success: function( res ) {
            if ( res.status ) {
                callback( res.data );
            }
        }
    });
}



处理返回的数据

为了便于后期查找出我们需要的,并且可购买的车次,这里使用键值对保存可购买的车次信息:

/**
 * 处理返回的数据
 * @param  {Array} data  返回的所有车次信息
 * @return {Object}      可购买的车次 map
 */
function getAvailableTicketsMap(data) {
    var i = 0,
        ticket = {},
        result = {};

    for (i = 0; i < data.length; i++) {
        ticket = {
            secretStr: data[i].secretStr,
            train_no: data[i].queryLeftNewDTO.train_no,
            start_time: data[i].queryLeftNewDTO.start_time,
            station_train_code: data[i].queryLeftNewDTO.station_train_code,
            to_station_telecode: data[i].queryLeftNewDTO.to_station_telecode,
            from_station_telecode: data[i].queryLeftNewDTO.from_station_telecode
        };

        if (ticket.secretStr !== '' && // or data[i].queryLeftNewDTO.canWebBuy === 'Y'
            ticket.to_station_telecode === WISH.to_station_telecode &&
            ticket.from_station_telecode === WISH.from_station_telecode &&
            hasSiteType(data[i].queryLeftNewDTO) ) {

            result[ticket.station_train_code] = {
                train_no: ticket.train_no,
                secretStr: ticket.secretStr,
                start_time: ticket.start_time,
                station_train_code: ticket.station_train_code,
                to_station_telecode: ticket.to_station_telecode,
                from_station_telecode: ticket.from_station_telecode
            };
        }
    }

    return result;
}

Tips
分析发现,如果车次可以购买,那么 secretStr 的值不为空,并且queryLeftNewDTO.canWebBuy = 'Y'



匹配座位类型

长度为 0 就是不限制,有卖就买,返回 true;不为 0 就是遍历期望的类型数组,找到有匹配的就返回 true,否则为 false:

/**
 * 是否有想要的座位
 * @param  {[type]}  queryLeftNewDTO [description]
 * @return {Boolean}                 true-有,false-无
 */
function hasSiteType( queryLeftNewDTO ) {
    var i = 0,
        wantSetTypeArr = WISH.setType;

    if ( wantSetTypeArr.length === 0 ) {
        return true;
    }

    for (i = 0; i < wantSetTypeArr.length; i++) {
        if ( /有|[1-9]/.test(queryLeftNewDTO[ wantSetTypeArr[i] ]) ) {
            return true;
        }
    }

    return false;
}



匹配需要的车次

接下来就是匹配我们需要的车次了,需要传入上述所有可购买的车次:

/**
 * 匹配想要购买的车次
 * @param  {Object} ticketsMap 可购买的所有车次
 * @return {Boolean}           true-匹配到,false-没匹配到
 */
function matchYourTickets( ticketsMap ) {
    var i          = 0,
        ticket     = {},
        wantTrains = WISH.station_train_code;

    console.log( ticketsMap );

    for (i = 0; i < wantTrains.length; i++) {
        ticket = ticketsMap[ wantTrains[i] ];

        if ( typeof ticket !== 'undefined' ) {
            clearInterval( timer ); // 清除定时器
            reserveTicket( ticket ); // 预订车票
            setFormData( USER ); // 填写用户信息

            return true;
        }
    }

    return false;
}



预订车票

就是调预订的那个方法,传入需要的参数而已:

/**
 * 预订车票
 * @param  {Object} ticket 改车次信息
 */
function reserveTicket( ticket ) {
    checkG1234(ticket.secretStr, ticket.start_time,ticket.train_no, ticket.from_station_telecode, ticket.to_station_telecode);
}



填写用户信息

这个就是查看登陆表单的结果了,选择器就直接写死了:

/**
 * 填写表单信息
 * @param {Object} user 用户信息对象
 */
function setFormData( user ) {
    $( '#username' ).val( user.name );
    $( '#password' ).val( user.password );
}

Tips:
点击查看元素可以看到表单的用户名、用户密码输入框的 id 名称分别为 'username'、'password'

12306 半自动刷票_第13张图片
form.png

图片验证码无解,不知道抢票软件咋弄的,有内部接口?貌似移动端有独立的接口,下次看看。



初始化

写个定时器自动查询匹配:

/**
 * 初始化
 */
function init() {
    // 一开始执行就查询匹配一次
    queryAjax(WISH, function( data ) {
        availableTicketsMap = getAvailableTicketsMap( data );
        matchYourTickets( availableTicketsMap );
    });

    // 定时器
    timer = setInterval(function() {
        queryAjax(WISH, function( data ) {
            availableTicketsMap = getAvailableTicketsMap( data );
            matchYourTickets( availableTicketsMap );
        });
    }, SEARCH_RATE);
}



总结

自己开双屏,一边写代码一边看着,挂着刷还行,当然手机 App 更好吧。春运的票好难抢,今天啥都没抢到,就看明天了。抢不到就拿这个挂个一天....

Good Night ~ ~ o(* ̄▽ ̄*)ブ
—— 2016/12/26 By Live

你可能感兴趣的:(12306 半自动刷票)