php实现查询火车票
闲来无事,做一个好玩的东西吧,也不是很难,也没啥技术难度,做来玩玩。做一个查询火车票的吧,再加上一个能循环查询的功能,这样就实现了简单的刷票程序。那就开始吧!
首先打开 12306 的网页。他有一个输入开始地址和到达地址的 选择框,根据全国的火车站可以选择的,那么这部分数据是怎么来的呢?打开火狐的 firebug 我们可以看到这么一个js请求
没错了,所有的候车站就在这里了。我们可以看到这个 地址是 https的,那么用file_get_contents,我们是无法获取数据的。但是不用怕,我们可以用curl来访问这个接口。看一下他的请求头:
注意看,这里 他有一个 host 和 referer 以及 cookie,12306的网站我们不登录也是可以进行车票查询的,因此可以确定 cookie不是必要值,但是带上了 referer和host 显然要进行来源的验证,那么
我们可以伪造一个 这样的头去请求这个接口就可以得到数据了。看一下获取的函数,这个函数在后面的查询火车票的地方也可以用到:
1 /** 2 * 采集数据 3 * @param $url 4 * @param $decode 5 */ 6 private function curlGet($url, $decode = true) 7 { 8 $ch = curl_init(); 9 $timeout = 5; 10 $header = [ 11 'Accept:*/*', 12 'Accept-Charset:GBK,utf-8;q=0.7,*;q=0.3', 13 'Accept-Encoding:gzip,deflate,sdch', 14 'Accept-Language:zh-CN,zh;q=0.8,ja;q=0.6,en;q=0.4', 15 'Connection:keep-alive', 16 'Host:kyfw.12306.cn', 17 'Referer:https://kyfw.12306.cn/otn/lcxxcx/init', 18 ]; 19 curl_setopt($ch, CURLOPT_URL, $url); 20 curl_setopt($ch, CURLOPT_HTTPHEADER, $header); 21 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 22 curl_setopt($ch, CURLOPT_ENCODING, "gzip"); //指定gzip压缩 23 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查 24 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在 25 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); 26 $result = curl_exec($ch); 27 curl_close($ch); 28 29 $decode && $result = json_decode($result, true); 30 31 return $result; 32 }
还需要注意一点的 是,从请求头可以看到,数据进行了gzip压缩,这样我们如果不做处理的话,返回的数据会成为乱码,很麻烦,不过用curl很容易解决这个问题。
数据得到了,那么根据规律很好拆分,首先根据 @ 查分一次,然后按照 | 查分一次即可。本项目中采用的是 boostrap-suggest, 这是一个非常强大且易用的输入联想
插件,我就降数据整理成 这个插件可以用的格式,代码如下:
1 /** 2 * 解析火车站信息 3 */ 4 private function parseStation() 5 { 6 $url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8992'; 7 $station = $this->curlGet($url, false); 8 9 if (empty($station)) { 10 throw new Exception('获取站点信息失败!'); 11 } 12 13 $delStr = "var station_names ='"; //需要截断的字符 14 $station = substr($station, strlen($delStr), strlen($station)); 15 16 $station = explode('@', $station); 17 $json = [ 18 'message' => '' 19 ]; 20 21 foreach ($station as $key => $vo) { 22 if (empty($vo)) continue; 23 24 $st = explode('|', $vo); 25 $json['value'][] = [ 26 'stationName' => $st['1'], 27 'shortName' => $st['3'], 28 'stationFlag' => $st['2'] 29 ]; 30 } 31 unset($station); 32 33 file_put_contents(ROOT_PATH . '/data/station.json', json_encode($json)); 34 }
好了,车站信息我们有了,那么怎慢查询具体的火车票呢?我们再在 firebug中看看,12306用的是哪个接口。很快看到了这个接口:
https://kyfw.12306.cn/otn/lcxxcx/query?purpose_codes=ADULT&queryDate=2016-12-26&from_station=NJH&to_station=XCH,没错就是这个了。
头信息基本上跟获取站点的一样。purpose_codes表示你要查询的票的类型,默认为成人票。queryDate 为出发日期,from_station 开始站点编号,to_station:到达站点
编号。只要我们拼装成这个样子,很容易获取到数据:
1 /** 2 * 入口函数 3 */ 4 public function run() 5 { 6 if (is_null($this->fromStation) || is_null($this->toStation)) 7 throw new Exception('起始站不能为空!'); 8 is_null($this->date) && $date = date('Y-m-d'); 9 10 $url = 'https://kyfw.12306.cn/otn/lcxxcx/query?purpose_codes=ADULT&queryDate=' . $this->date . '&from_station='; 11 $url .= $this->fromStation . '&to_station=' . $this->toStation; 12 13 $ticketInfo = $this->curlGet($url); 14 return $ticketInfo; 15 }
每步的代码看到了,那么整个类如下:
1 <?php 2 3 /** 4 * author: NickBai 5 * createTime: 2016/12/26 0026 上午 9:11 6 * 7 */ 8 class Tickets 9 { 10 public $fromStation = null; 11 public $toStation = null; 12 public $date = null; 13 14 public function __construct($fromStation = null, $toStation = null, $date = null) 15 { 16 if (!file_exists(ROOT_PATH . '/data/station.json')) { 17 $this->parseStation(); 18 } 19 20 $this->fromStation = $fromStation; 21 $this->toStation = $toStation; 22 $this->date = $date; 23 } 24 25 /** 26 * 入口函数 27 */ 28 public function run() 29 { 30 if (is_null($this->fromStation) || is_null($this->toStation)) 31 throw new Exception('起始站不能为空!'); 32 is_null($this->date) && $date = date('Y-m-d'); 33 34 $url = 'https://kyfw.12306.cn/otn/lcxxcx/query?purpose_codes=ADULT&queryDate=' . $this->date . '&from_station='; 35 $url .= $this->fromStation . '&to_station=' . $this->toStation; 36 37 $ticketInfo = $this->curlGet($url); 38 return $ticketInfo; 39 } 40 41 /** 42 * 解析火车站信息 43 */ 44 private function parseStation() 45 { 46 $url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8992'; 47 $station = $this->curlGet($url, false); 48 49 if (empty($station)) { 50 throw new Exception('获取站点信息失败!'); 51 } 52 53 $delStr = "var station_names ='"; //需要截断的字符 54 $station = substr($station, strlen($delStr), strlen($station)); 55 56 $station = explode('@', $station); 57 $json = [ 58 'message' => '' 59 ]; 60 61 foreach ($station as $key => $vo) { 62 if (empty($vo)) continue; 63 64 $st = explode('|', $vo); 65 $json['value'][] = [ 66 'stationName' => $st['1'], 67 'shortName' => $st['3'], 68 'stationFlag' => $st['2'] 69 ]; 70 } 71 unset($station); 72 73 file_put_contents(ROOT_PATH . '/data/station.json', json_encode($json)); 74 } 75 76 /** 77 * 采集数据 78 * @param $url 79 * @param $decode 80 */ 81 private function curlGet($url, $decode = true) 82 { 83 $ch = curl_init(); 84 $timeout = 5; 85 $header = [ 86 'Accept:*/*', 87 'Accept-Charset:GBK,utf-8;q=0.7,*;q=0.3', 88 'Accept-Encoding:gzip,deflate,sdch', 89 'Accept-Language:zh-CN,zh;q=0.8,ja;q=0.6,en;q=0.4', 90 'Connection:keep-alive', 91 'Host:kyfw.12306.cn', 92 'Referer:https://kyfw.12306.cn/otn/lcxxcx/init', 93 ]; 94 curl_setopt($ch, CURLOPT_URL, $url); 95 curl_setopt($ch, CURLOPT_HTTPHEADER, $header); 96 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 97 curl_setopt($ch, CURLOPT_ENCODING, "gzip"); //指定gzip压缩 98 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查 99 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在 100 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); 101 $result = curl_exec($ch); 102 curl_close($ch); 103 104 $decode && $result = json_decode($result, true); 105 106 return $result; 107 } 108 109 }
代码组织结构如下:
前台,依旧是采用 H+和layui 进行修改的页面,效果如下:
设置好 刷新频率,就可以愉快的刷票了。
代码分享地址:http://pan.baidu.com/s/1o8Apa5c
线上体验地址:http://www.baiyf.com/