首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >+从零实现一款12306刷票软件1.2

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

作者头像
范蠡
发布2018-07-25 16:09:23
9540
发布2018-07-25 16:09:23
举报

咱们接着上一篇《从零实现一款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为了防止非法登录使用的一个安全措施。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能服务器开发 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
验证码
腾讯云新一代行为验证码(Captcha),基于十道安全栅栏, 为网页、App、小程序开发者打造立体、全面的人机验证。最大程度保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时,提供更精细化的用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档