咱们接着上一篇《从零实现一款12306刷票软件1.1》继续介绍。
当然,这里需要说明一下的就是,由于全国的火车站点信息文件比较大,我们程序解析起来时间较长,加上火车站编码信息并不是经常变动,所以,我们我们没必要每次都下载这个station_name.js,所以我在写程序模拟这个请求时,一般先看本地有没有这个文件,如果有就使用本地的,没有才发http请求向12306服务器请求。这里我贴下我请求站点信息的程序代码(C++代码):
1/**
2 * 获取全国车站信息
3 * @param si 返回的车站信息
4 * @param bForceDownload 强制从网络上下载,即不使用本地副本
5 */
6bool GetStationInfo(vector<stationinfo>& si, bool bForceDownload = false);
1#define URL_STATION_NAMES "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9053"
1bool Client12306::GetStationInfo(vector<stationinfo>& si, bool bForceDownload/* = false*/)
2{
3 FILE* pfile;
4 pfile = fopen("station_name.js", "rt+");
5 //文件不存在,则必须下载
6 if (pfile == NULL)
7 {
8 bForceDownload = true;
9 }
10 string strResponse;
11 if (bForceDownload)
12 {
13 if (pfile != NULL)
14 fclose(pfile);
15 pfile = fopen("station_name.js", "wt+");
16 if (pfile == NULL)
17 {
18 LogError("Unable to create station_name.js");
19 return false;
20 }
21 CURLcode res;
22 CURL* curl = curl_easy_init();
23 if (NULL == curl)
24 {
25 fclose(pfile);
26 return false;
27 }
28 //URL_STATION_NAMES
29 curl_easy_setopt(curl, CURLOPT_URL, URL_STATION_NAMES);
30 //响应结果中保留头部信息
31 //curl_easy_setopt(curl, CURLOPT_HEADER, 1);
32 curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
33 curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);
34 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);
35 curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);
36 curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
37 //设定为不验证证书和HOST
38 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false);
39 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false);
40 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10);
41 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);
42 res = curl_easy_perform(curl);
43 bool bError = false;
44 if (res == CURLE_OK)
45 {
46 int code;
47 res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
48 if (code != 200)
49 {
50 bError = true;
51 LogError("http response code is not 200, code=%d", code);
52 }
53 }
54 else
55 {
56 LogError("http request error, error code = %d", res);
57 bError = true;
58 }
59 curl_easy_cleanup(curl);
60 if (bError)
61 {
62 fclose(pfile);
63 return !bError;
64 }
65 if (fwrite(strResponse.data(), strResponse.length(), 1, pfile) != 1)
66 {
67 LogError("Write data to station_name.js error");
68 return false;
69 }
70 fclose(pfile);
71 }
72 //直接读取文件
73 else
74 {
75 //得到文件大小
76 fseek(pfile, 0, SEEK_END);
77 int length = ftell(pfile);
78 if (length < 0)
79 {
80 LogError("invalid station_name.js file");
81 fclose(pfile);
82 }
83 fseek(pfile, 0, SEEK_SET);
84 length++;
85 char* buf = new char[length];
86 memset(buf, 0, length*sizeof(char));
87 if (fread(buf, length-1, 1, pfile) != 1)
88 {
89 LogError("read station_name.js file error");
90 fclose(pfile);
91 return false;
92 }
93 strResponse = buf;
94 fclose(pfile);
95 }
96 /*
97 返回结果为一个js文件,
98 var station_names = '@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京东|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2"
99 */
100 //LogInfo("recv json = %s", strResponse.c_str());
101 OutputDebugStringA(strResponse.c_str());
102 vector<string> singleStation;
103 split(strResponse, "@", singleStation);
104 size_t size = singleStation.size();
105 for (size_t i = 1; i < size; ++i)
106 {
107 vector<string> v;
108 split(singleStation[i], "|", v);
109 if (v.size() < 6)
110 continue;
111 stationinfo st;
112 st.code1 = v[0];
113 st.hanzi = v[1];
114 st.code2 = v[2];
115 st.pingyin = v[3];
116 st.simplepingyin = v[4];
117 st.no = atol(v[5].c_str());
118 si.push_back(st);
119 }
120 return true;
121}
这里用了一个站点信息结构体stationinfo,定义如下:
1//var station_names = '@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京东|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2
2struct stationinfo
3{
4 string code1;
5 string hanzi;
6 string code2;
7 string pingyin;
8 string simplepingyin;
9 int no;
10};
因为我们这里目的是为了模拟http请求做买火车票相关的操作,而不是技术方面本身,所以为了快速实现我们的目的,我们就使用curl库。这个库是一个强大的http相关的库,例如12306服务器返回的数据可能是分块的(chunked),这个库也能帮我们组装好;再例如,服务器返回的数据是使用gzip格式压缩的,curl也会帮我们自动解压好。所以,接下来的所有12306的接口,都基于我封装的curl库一个接口:
1/**
2 * 发送一个http请求
3 *@param url 请求的url
4 *@param strResponse http响应结果
5 *@param get true为GET,false为POST
6 *@param headers 附带发送的http头信息
7 *@param postdata post附带的数据
8 *@param bReserveHeaders http响应结果是否保留头部信息
9 *@param timeout http请求超时时间
10 */
11 bool HttpRequest(const char* url, string& strResponse, bool get = true, const char* headers = NULL, const char* postdata = NULL, bool bReserveHeaders = false, int timeout = 10);
函数各种参数已经在函数注释中写的清清楚楚了,这里就不一一解释了。这个函数的实现代码如下:
1bool Client12306::HttpRequest(const char* url,
2 string& strResponse,
3 bool get/* = true*/,
4 const char* headers/* = NULL*/,
5 const char* postdata/* = NULL*/,
6 bool bReserveHeaders/* = false*/,
7 int timeout/* = 10*/)
8{
9 CURLcode res;
10 CURL* curl = curl_easy_init();
11 if (NULL == curl)
12 {
13 LogError("curl lib init error");
14 return false;
15 }
16 curl_easy_setopt(curl, CURLOPT_URL, url);
17 //响应结果中保留头部信息
18 if (bReserveHeaders)
19 curl_easy_setopt(curl, CURLOPT_HEADER, 1);
20 curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
21 curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);
22 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);
23 curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);
24 curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
25 //设定为不验证证书和HOST
26 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false);
27 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false);
28 //设置超时时间
29 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, timeout);
30 curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
31 curl_easy_setopt(curl, CURLOPT_REFERER, URL_REFERER);
32 //12306早期版本是不需要USERAGENT这个字段的,现在必须了,估计是为了避免一些第三方的非法刺探吧。
33 //如果没有这个字段,会返回
34 /*
35 HTTP/1.0 302 Moved Temporarily
36 Location: http://www.12306.cn/mormhweb/logFiles/error.html
37 Server: Cdn Cache Server V2.0
38 Mime-Version: 1.0
39 Date: Fri, 18 May 2018 02:52:05 GMT
40 Content-Type: text/html
41 Content-Length: 0
42 Expires: Fri, 18 May 2018 02:52:05 GMT
43 X-Via: 1.0 PSshgqdxxx63:10 (Cdn Cache Server V2.0)
44 Connection: keep-alive
45 X-Dscp-Value: 0
46 */
47 curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36");
48 //不设置接收的编码格式或者设置为空,libcurl会自动解压压缩的格式,如gzip
49 //curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip, deflate, br");
50 //添加自定义头信息
51 if (headers != NULL)
52 {
53 //LogInfo("http custom header: %s", headers);
54 struct curl_slist *chunk = NULL;
55 chunk = curl_slist_append(chunk, headers);
56 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
57 }
58 if (!get && postdata != NULL)
59 {
60 //LogInfo("http post data: %s", postdata);
61 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata);
62 }
63 LogInfo("http %s: url=%s, headers=%s, postdata=%s", get ? "get" : "post", url, headers != NULL ? headers : "", postdata!=NULL?postdata : "");
64 res = curl_easy_perform(curl);
65 bool bError = false;
66 if (res == CURLE_OK)
67 {
68 int code;
69 res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
70 if (code != 200 && code != 302)
71 {
72 bError = true;
73 LogError("http response code is not 200 or 302, code=%d", code);
74 }
75 }
76 else
77 {
78 LogError("http request error, error code = %d", res);
79 bError = true;
80 }
81 curl_easy_cleanup(curl);
82 LogInfo("http response: %s", strResponse.c_str());
83 return !bError;
84}
正如上面注释中所提到的,浏览器在发送http请求时带的某些字段,不是必须的,我们在模拟这个请求时可以不添加,如查票接口浏览器可能会发以下http数据包:
1GET /otn/leftTicket/query?leftTicketDTO.train_date=2018-05-30&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=BJP&purpose_codes=ADULT HTTP/1.1
2Host: kyfw.12306.cn
3Connection: keep-alive
4Cache-Control: no-cache
5Accept: */*
6X-Requested-With: XMLHttpRequest
7If-Modified-Since: 0
8User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36
9Referer: https://kyfw.12306.cn/otn/leftTicket/init
10Accept-Encoding: gzip, deflate, br
11Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
12Cookie: JSESSIONID=ACD9CB098169C4D73CDE80D6F6C38E5A; RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_wfdc_flag=dc; route=c5c62a339e7744272a54643b3be5bf64; BIGipServerotn=1708720394.50210.0000; _jc_save_fromDate=2018-05-30; _jc_save_toDate=2018-05-20
其中像Connection、Cache-Control、Accept、If-Modified-Since等字段都不是必须的,所以我们在模拟我们自己的http请求时可以不用可以添加这些字段,当然据我观察,12306服务器现在对发送过来的http数据包要求越来越严格了,如去年的时候,User-Agent这个字段还不是必须的,现在如果你不带上这个字段,可能12306返回的结果就不一定正确。当然,不正确的结果中一定不会有明确的错误信息,充其量可能会告诉你页面不存在或者系统繁忙请稍后再试,这是服务器自我保护的一种重要的措施,试想你做服务器程序,会告诉非法用户明确的错误信息吗?那样不就给了非法攻击服务器的人不断重试的机会了嘛。
需要特别注意的是:查票接口发送的http协议的头还有一个字段叫Cookie,其值是一串非常奇怪的东西:
1JSESSIONID=ACD9CB098169C4D73CDE80D6F6C38E5A;
2RAIL_EXPIRATION=1526978933395;
3RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8;
4_jc_save_fromStation=%u4E0A%u6D77%2CSHH;
5_jc_save_toStation=%u5317%u4EAC%2CBJP;
6_jc_save_wfdc_flag=dc;
7route=c5c62a339e7744272a54643b3be5bf64;
8BIGipServerotn=1708720394.50210.0000;
9_jc_save_fromDate=2018-05-30;
10_jc_save_toDate=2018-05-2
注意:原代码中各个字段都是连在一起的,我这里为了读者方便阅读,将各个字段单独放在一行。在这串字符中有一个JSESSIONID,在不需要登录的查票接口,我们可以传或者不传这个字段值。但是在购票以及查询常用联系人这些需要在已经登录的情况下才能进行的操作,我们必须带上这个数据,这是服务器给你的token(验证令牌),而这个令牌是在刚进入12306站点时,服务器发过来的,你后面的登录等操作必须带上这个token,否则服务器会认为您的请求是非法请求。我第一次去研究12306的买票流程时,即使在用户名、密码和图片验证码正确的情况下,也无法登录就是这个原因。这是12306为了防止非法登录使用的一个安全措施。