就算世界终结,也无所谓

愿你有一天,能与你最重要的人重逢

PHP 实现 12306 余票状况监控与告警

每年一到春运,火车票子就特别难买。到车站人工窗口排老长的队买票肯定不现实,偏偏 12306 的服务器又是个土豆。所以每年都有一群人用各种抢票软件拼尽全力也要买到一张回家的票,各种抢票工具的供应商通过提供「抢票加速包」等并没有什么卵用的附加服务赚的也是盆满钵满。

在泰宁待了三个多月,很闷,各种压力又大,情绪一直很不好,所以我打算寒假去福州散散心。我是打算从三明北转福州的,因为我还要去三明搞点事情。我是那种不喜欢做太详细的计划的人,习惯在出发前几个小时才买票。虽然说三明北到福州的动车的票还蛮好买的,但是泰宁到三明北的动车的余票都很紧张,说不准什么时候三明北到福州的票会不会也被抢完,所以我需要做一个余票状况监控,来让我决定要不要提前买票。

虽然网上已经有现成的轮子了,但是毕竟这种东西是你国特色,我不是很相信它们不会偷偷摸摸搞一些糟糕的事情出来,而且我又是那种不怎么关注邮箱的人,等到我看到告警邮件之后估计余票都卖完了,得要一个短信告警。所以我决定自己造一个轮子。

0x01 获取 12306 余票查询的 API 的地址和请求参数

12306 网站的余票查询是前端通过 API 查询余票情况,然后解析返回的数据再显示到页面上的。所以我们很容易就能抓出来 API 地址和请求参数。

浏览器的开发者工具打开,在 12306 网站上查询余票情况,就可以在开发者工具的 Network 选项卡中看到这样一个请求:

《PHP 实现 12306 余票状况监控与告警》

Preview 看一下,是一个 JSON,里面就包含了当前查询的所有车次的余票信息。右键 Copy link address,我们就获取到了 API 地址和请求参数。

0x02 使用 cURL 构造并发起请求

唔,这个应该不用我多说了吧。

这边最好设置一下请求 Header 里的 User Agent 和 Referrer,不然可能会出现玄学问题。具体内容就用浏览器的开发者工具抓到的请求里的好了。

0x03 解析数据

因为 12306 的 API 返回的是一个 JSON,所以我们可以直接 json_decode 解决掉。

通过目测可以了解到,我们请求的余票信息在 data.result 里,是一个数组。所以 foreach 遍历过去,然后利用 strpos 函数的返回值筛选出我们关注的车次就好了。

data.result 里的每个元素都对应了一次列车的多个数据,数据之间以 | 隔开,所以我们需要使用 explode 函数把它“炸”开。得到的数组总共有 38 个元素,至于哪个元素是哪个座次的余票信息,与其他车次的数据比对一下就能知道。

得到余票信息之后验证一下值,决定要不要发送告警信息。

需要注意的是 12306 的土豆服务器可能会响应超时(笑),所以在解析数据之前要先 is_array 验证一下我们 json_decode 出来的数据是不是一个数组,如果不是的话直接重新发送请求获取数据,直到正常获取到数据为止。

0x04 发送告警信息

这个就不好细说了,因为各大服务商提供的 API 和 SDK 都不一样。参考服务商提供的文档就好了。

Demo

这是我监控 01/31/2019 从三明北到福州的 D3303 次列车的二等座的余票数据的代码。当然这份代码经过脱敏了,是无法直接发送短信告警信息的。供各位参考。

<?php

/*
12306 余票紧张提醒 by Little_Qiu
*/

while(true) {
    // 获取当前时间戳
    $currentTime = time() + 28800; // 把 UTC+0 转换成 UTC+8 以方便计算

    // 如果当前时间戳大于开车时间的时间戳,就退出循环
    if($currentTime >= 1548909000 + 28800) {
        break;
    }

    // 每天 23:00 到次日 06:00 期间 12306 不可购票仅可查询,所以此时监控其实是没有意义的
    if(($currentTime % 86400 < 21600) || ($currentTime % 86400 > 82800)) {
        // 等待五分钟再试
        sleep(300);
        continue;
    }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array("User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", "Referer: https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc"));
    curl_setopt($ch, CURLOPT_URL, 'https://kyfw.12306.cn/otn/leftTicket/queryZ?leftTicketDTO.train_date=2019-01-31&leftTicketDTO.from_station=SHS&leftTicketDTO.to_station=FZS&purpose_codes=ADULT');
    $originData = json_decode(curl_exec($ch), true);
    curl_close($ch);

    if(is_array($originData)) {
        foreach($originData["data"]["result"] as $result) {
            $trainData = explode("|", $result);
            if($trainData[3] == "D3303") {
                $data = $trainData;
            }
        }
    } else {
        continue;
    }

    
    if($data[30] != "有") {
        // 发送告警信息
    }

    //等待五分钟再试
    sleep(300);
}

?>

效果

《PHP 实现 12306 余票状况监控与告警》

总结

这个监控讲道理写起来还蛮简单的,我这边算上注释和空行总共 60+ 行就写完了。

不过有坑真的很难受…我第一次运行的时候死活无法正常获取 API 返回的数据,然后去洗了个澡…就能获取到了…

还要感谢一下 @printempw 对我的指导,不然我永远都不会知道 12306 的 API 返回的数据有玄学问题…

这种监控一般来说也是每隔几分钟监控一次或者定时监控,你可以写个 crontab 来定时监控。我就比较懒了,直接写了个死循环然后 sleep(300),好孩子不要学我哦(

以上。

点赞
由于 LiveRe 使用的头像 API 不支持 HTTPS,现在已经禁用了 QQ、微信和微博的 OAuth 登录。
我也不想就这样因噎废食,但事实是我根本没有能力去搭建一个反代。
如果你发现了有评论被删除的痕迹,八成是这些评论使用的 OAuth 方式是 QQ 或者微信或者微博。