大数跨境
0
0

+从零实现一款12306刷票软件1.2

+从零实现一款12306刷票软件1.2 CppGuide
2018-05-21
1
导读:逢年过节,中华大地,一票难求,本文将教你如何一步步地实现一款个人的12306抢票软件。第二篇。

咱们接着上一篇《从零实现一款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


其中像ConnectionCache-ControlAcceptIf-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为了防止非法登录使用的一个安全措施。

由于微信公众号文章字数限制,您可以继续阅读下一篇《从零实现一款12306刷票软件1.3》。


欢迎关注公众号『easyserverdev』。如果有任何技术或者职业方面的问题需要我提供帮助,可通过这个公众号与我取得联系,此公众号不仅分享高性能服务器开发经验和故事,同时也免费为广大技术朋友提供技术答疑和职业解惑,您有任何问题都可以在微信公众号直接留言,我会尽快回复您。



【声明】内容源于网络
0
0
CppGuide
专注于高质量高性能C++开发,站点:cppguide.cn
内容 1260
粉丝 0
CppGuide 专注于高质量高性能C++开发,站点:cppguide.cn
总阅读919
粉丝0
内容1.3k