標籤:

從零實現一款12306搶票軟體(二)

從零實現一款12306搶票軟體(二)

本文接上一篇文章《張小方:從零實現一款12306搶票軟體(一)》。

當然,這裡需要說明一下的就是,由於全國的火車站點信息文件比較大,我們程序解析起來時間較長,加上火車站編碼信息並不是經常變動,所以,我們我們沒必要每次都下載這個station_name.js,所以我在寫程序模擬這個請求時,一般先看本地有沒有這個文件,如果有就使用本地的,沒有才發http請求向12306伺服器請求。這裡我貼下我請求站點信息的程序代碼(C++代碼):

/** * 獲取全國車站信息 * @param si 返回的車站信息 * @param bForceDownload 強制從網路上下載,即不使用本地副本 */ bool GetStationInfo(vector<stationinfo>& si, bool bForceDownload = false);#define URL_STATION_NAMES "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9053"bool Client12306::GetStationInfo(vector<stationinfo>& si, bool bForceDownload/* = false*/){ FILE* pfile; pfile = fopen("station_name.js", "rt+"); //文件不存在,則必須下載 if (pfile == NULL) { bForceDownload = true; } string strResponse; if (bForceDownload) { if (pfile != NULL) fclose(pfile); pfile = fopen("station_name.js", "wt+"); if (pfile == NULL) { LogError("Unable to create station_name.js"); return false; } CURLcode res; CURL* curl = curl_easy_init(); if (NULL == curl) { fclose(pfile); return false; } //URL_STATION_NAMES curl_easy_setopt(curl, CURLOPT_URL, URL_STATION_NAMES); //響應結果中保留頭部信息 //curl_easy_setopt(curl, CURLOPT_HEADER, 1); curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); //設定為不驗證證書和HOST curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10); res = curl_easy_perform(curl); bool bError = false; if (res == CURLE_OK) { int code; res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); if (code != 200) { bError = true; LogError("http response code is not 200, code=%d", code); } } else { LogError("http request error, error code = %d", res); bError = true; } curl_easy_cleanup(curl); if (bError) { fclose(pfile); return !bError; } if (fwrite(strResponse.data(), strResponse.length(), 1, pfile) != 1) { LogError("Write data to station_name.js error"); return false; } fclose(pfile); } //直接讀取文件 else { //得到文件大小 fseek(pfile, 0, SEEK_END); int length = ftell(pfile); if (length < 0) { LogError("invalid station_name.js file"); fclose(pfile); } fseek(pfile, 0, SEEK_SET); length++; char* buf = new char[length]; memset(buf, 0, length*sizeof(char)); if (fread(buf, length-1, 1, pfile) != 1) { LogError("read station_name.js file error"); fclose(pfile); return false; } strResponse = buf; fclose(pfile); } /* 返回結果為一個js文件, var station_names = @bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京東|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2" */ //LogInfo("recv json = %s", strResponse.c_str()); OutputDebugStringA(strResponse.c_str()); vector<string> singleStation; split(strResponse, "@", singleStation); size_t size = singleStation.size(); for (size_t i = 1; i < size; ++i) { vector<string> v; split(singleStation[i], "|", v); if (v.size() < 6) continue; stationinfo st; st.code1 = v[0]; st.hanzi = v[1]; st.code2 = v[2]; st.pingyin = v[3]; st.simplepingyin = v[4]; st.no = atol(v[5].c_str()); si.push_back(st); } return true;}

這裡用了一個站點信息結構體stationinfo,定義如下:

//var station_names = @bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京東|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2struct stationinfo{ string code1; string hanzi; string code2; string pingyin; string simplepingyin; int no;};

因為我們這裡目的是為了模擬http請求做買火車票相關的操作,而不是技術方面本身,所以為了快速實現我們的目的,我們就使用curl庫。這個庫是一個強大的http相關的庫,例如12306伺服器返回的數據可能是分塊的(chunked),這個庫也能幫我們組裝好;再例如,伺服器返回的數據是使用gzip格式壓縮的,curl也會幫我們自動解壓好。所以,接下來的所有12306的介面,都基於我封裝的curl庫一個介面:

/** * 發送一個http請求 *@param url 請求的url *@param strResponse http響應結果 *@param get true為GET,false為POST *@param headers 附帶發送的http頭信息 *@param postdata post附帶的數據 *@param bReserveHeaders http響應結果是否保留頭部信息 *@param timeout http請求超時時間 */ bool HttpRequest(const char* url, string& strResponse, bool get = true, const char* headers = NULL, const char* postdata = NULL, bool bReserveHeaders = false, int timeout = 10);

函數各種參數已經在函數注釋中寫的清清楚楚了,這裡就不一一解釋了。這個函數的實現代碼如下:

bool Client12306::HttpRequest(const char* url, string& strResponse, bool get/* = true*/, const char* headers/* = NULL*/, const char* postdata/* = NULL*/, bool bReserveHeaders/* = false*/, int timeout/* = 10*/){ CURLcode res; CURL* curl = curl_easy_init(); if (NULL == curl) { LogError("curl lib init error"); return false; } curl_easy_setopt(curl, CURLOPT_URL, url); //響應結果中保留頭部信息 if (bReserveHeaders) curl_easy_setopt(curl, CURLOPT_HEADER, 1); curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); //設定為不驗證證書和HOST curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false); //設置超時時間 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, timeout); curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout); curl_easy_setopt(curl, CURLOPT_REFERER, URL_REFERER); //12306早期版本是不需要USERAGENT這個欄位的,現在必須了,估計是為了避免一些第三方的非法刺探吧。 //如果沒有這個欄位,會返回 /* HTTP/1.0 302 Moved Temporarily Location: http://www.12306.cn/mormhweb/logFiles/error.html Server: Cdn Cache Server V2.0 Mime-Version: 1.0 Date: Fri, 18 May 2018 02:52:05 GMT Content-Type: text/html Content-Length: 0 Expires: Fri, 18 May 2018 02:52:05 GMT X-Via: 1.0 PSshgqdxxx63:10 (Cdn Cache Server V2.0) Connection: keep-alive X-Dscp-Value: 0 */ 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"); //不設置接收的編碼格式或者設置為空,libcurl會自動解壓壓縮的格式,如gzip //curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip, deflate, br"); //添加自定義頭信息 if (headers != NULL) { //LogInfo("http custom header: %s", headers); struct curl_slist *chunk = NULL; chunk = curl_slist_append(chunk, headers); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); } if (!get && postdata != NULL) { //LogInfo("http post data: %s", postdata); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata); } LogInfo("http %s: url=%s, headers=%s, postdata=%s", get ? "get" : "post", url, headers != NULL ? headers : "", postdata!=NULL?postdata : ""); res = curl_easy_perform(curl); bool bError = false; if (res == CURLE_OK) { int code; res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); if (code != 200 && code != 302) { bError = true; LogError("http response code is not 200 or 302, code=%d", code); } } else { LogError("http request error, error code = %d", res); bError = true; } curl_easy_cleanup(curl); LogInfo("http response: %s", strResponse.c_str()); return !bError;}

正如上面注釋中所提到的,瀏覽器在發送http請求時帶的一些欄位,我們不是必須的,如查票介面瀏覽器可能會發以下http數據包:

GET /otn/leftTicket/query?leftTicketDTO.train_date=2018-05-30&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=BJP&purpose_codes=ADULT HTTP/1.1Host: kyfw.12306.cnConnection: keep-aliveCache-Control: no-cacheAccept: */*X-Requested-With: XMLHttpRequestIf-Modified-Since: 0User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36Referer: https://kyfw.12306.cn/otn/leftTicket/initAccept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9,en;q=0.8Cookie: 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,其值是一串非常奇怪的東西: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-2。

在這串字元中有一個JSESSIONID,在不需要登錄的查票介面,我們可以傳或者不傳這個欄位值。但是在購票以及查詢常用聯繫人這些需要在已經登錄的情況下才能進行的操作,我們必須帶上這個數據,這是伺服器給你的token(驗證令牌),而這個令牌是在剛進入12306站點時,伺服器發過來的,你後面的登錄等操作必須帶上這個token,否則伺服器會認為您的請求是非法請求。我第一次去研究12306的買票流程時,即使在用戶名、密碼和圖片驗證碼正確的情況下,也無法登錄就是這個原因。這是12306為了防止非法登錄使用的一個安全措施。

二、登錄與拉取圖片驗證碼介面

我的登錄頁面效果如下:

12306的圖片驗證碼一般由八個圖片組成,像上面的「龍舟」文字,也是圖片,這兩處的圖片(文字圖片和驗證碼)都是在伺服器上拼裝後,發給客戶端的,12306伺服器上這種類型的小圖片有一定的數量,雖然數量比較大,但是是有限的。如果你要做驗證碼自動識別功能,可以嘗試著下載大部分圖片,然後做統計規律。所以,我這裡並沒有做圖片自動識別功能。有興趣的讀者可自行嘗試。

先說下,拉取驗證碼的介面。我們打開Chrome瀏覽器12306的登錄界面:kyfw.12306.cn/otn/login,如下圖所示:

可以得到拉取驗證碼的介面:

我們可以看到發送的http請求數據包格式是:

GET /passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.7520968747611347 HTTP/1.1Host: kyfw.12306.cnConnection: keep-aliveUser-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36Accept: image/webp,image/apng,image/*,*/*;q=0.8Referer: https://kyfw.12306.cn/otn/login/initAccept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9,en;q=0.8Cookie: _passport_session=badc97f6a852499297796ee852515f957153; _passport_ct=9cf4ea17c0dc47b6980cac161483f522t9022; 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; BIGipServerpassport=837288202.50215.0000

這裡也是一個http GET請求,Host、Referer和Cookie這三個欄位是必須的,且Cookie欄位必須帶上上文說的JSESSIONID,下載圖片驗證碼和下文中各個步驟也必須在Cookie欄位中帶上這個JSESSIONID值,否則無法從12306伺服器得到正確的應答。後面會介紹如何拿到這個這。這個拉取圖片驗證碼的http GET請求需要三個參數,如上面的代碼段所示,即login_site、module、rand和一個類似於0.7520968747611347的隨機值,前三個欄位的值都是固定的,module欄位表示當前是哪個模塊,當前是登錄模塊,所以值是login,後面獲取最近聯繫人時取值是passenger。這裡還有一個需要注意的地方是,如果您驗證圖片驗證碼失敗時,重新請求圖片時,必須也重新請求下JSESSIONID。這個url是kyfw.12306.cn/otn/login。http請求和應答包如下:

請求包:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9,en;q=0.8Cache-Control: max-age=0Connection: keep-aliveCookie: 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; BIGipServerpassport=837288202.50215.0000Host: kyfw.12306.cnReferer: https://kyfw.12306.cn/otn/passport?redirect=/otn/login/loginOutUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36

應答包:

HTTP/1.1 200 OKDate: Sun, 20 May 2018 02:23:53 GMTContent-Type: text/html;charset=utf-8Transfer-Encoding: chunkedSet-Cookie: JSESSIONID=D5AE154D66F67DE53BF70420C772158F; Path=/otnct: C1_217_101_6Content-Language: zh-CNContent-Encoding: gzipX-Via: 1.1 houdianxin184:4 (Cdn Cache Server V2.0)Connection: keep-aliveX-Dscp-Value: 0X-Cdn-Src-Port: 46480

這個值在應答包欄位Set-Cookie中拿到:

Set-Cookie: JSESSIONID=D5AE154D66F67DE53BF70420C772158F; Path=/otn

所以,我們每次請求圖片驗證碼時,都重新請求一下這個JSESSIONID,代碼如下:

#define URL_LOGIN_INIT "https://kyfw.12306.cn/otn/login/init"bool Client12306::loginInit(){ string strResponse; if (!HttpRequest(URL_LOGIN_INIT, strResponse, true, "Upgrade-Insecure-Requests: 1", NULL, true, 10)) { LogError("loginInit failed"); return false; } if (!GetCookies(strResponse)) { LogError("parse login init cookie error, url=%s", URL_LOGIN_INIT); return false; } return true;}bool Client12306::GetCookies(const string& data){ if (data.empty()) { LogError("http data is empty"); return false; } //解析http頭部 string str; str.append(data.c_str(), data.length()); size_t n = str.find("

"); string header = str.substr(0, n); str.erase(0, n + 4); //m_cookie.clear(); //獲取http頭中的JSESSIONID=21AC68643BBE893FBDF3DA9BCF654E98; vector<string> v; while (true) { size_t index = header.find("
"); if (index == string::npos) break; string tmp = header.substr(0, index); v.push_back(tmp); header.erase(0, index + 2); if (header.empty()) break; } string jsessionid; string BIGipServerotn; string BIGipServerportal; string current_captcha_type; size_t m; OutputDebugStringA("
response http headers:
"); for (size_t i = 0; i < v.size(); ++i) { OutputDebugStringA(v[i].c_str()); OutputDebugStringA("
"); m = v[i].find("Set-Cookie: "); if (m == string::npos) continue; string tmp = v[i].substr(11); Trim(tmp); m = tmp.find("JSESSIONID"); if (m != string::npos) { size_t comma = tmp.find(";"); if (comma != string::npos) jsessionid = tmp.substr(0, comma); } m = tmp.find("BIGipServerotn"); if (m != string::npos) { size_t comma = tmp.find(";"); if (comma != string::npos) BIGipServerotn = tmp.substr(m, comma); else BIGipServerotn = tmp; } m = tmp.find("BIGipServerportal"); if (m != string::npos) { size_t comma = tmp.find(";"); if (comma != string::npos) BIGipServerportal = tmp.substr(m, comma); else BIGipServerportal = tmp; } m = tmp.find("current_captcha_type"); if (m != string::npos) { size_t comma = tmp.find(";"); if (comma != string::npos) current_captcha_type = tmp.substr(m, comma); else current_captcha_type = tmp; } } if (!jsessionid.empty()) { m_strCookies = jsessionid; m_strCookies += "; "; m_strCookies += BIGipServerotn; if (!BIGipServerportal.empty()) { m_strCookies += "; "; m_strCookies += BIGipServerportal; } m_strCookies += "; "; m_strCookies += current_captcha_type; return true; } LogError("jsessionid is empty"); return false;}#define URL_GETPASSCODENEW "https://kyfw.12306.cn/passport/captcha/captcha-image"bool Client12306::DownloadVCodeImage(const char* module){ if (module == NULL) { LogError("module is invalid"); return false; } //https://kyfw.12306.cn/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.06851784300754482 ostringstream osUrl; osUrl << URL_GETPASSCODENEW; osUrl << "?login_site=E&module="; osUrl << module; //購票驗證碼 if (strcmp(module, "passenger") != 0) { osUrl << "&rand=sjrand&"; } //登錄驗證碼 else { osUrl << "&rand=randp&"; } double d = rand() * 1.000000 / RAND_MAX; osUrl.precision(17); osUrl << d; string strResponse; string strCookie = "Cookie: "; strCookie += m_strCookies; if (!HttpRequest(osUrl.str().c_str(), strResponse, true, strCookie.c_str(), NULL, false, 10)) { LogError("DownloadVCodeImage failed"); return false; } //寫入文件 time_t now = time(NULL); struct tm* tblock = localtime(&now); memset(m_szCurrVCodeName, 0, sizeof(m_szCurrVCodeName));#ifdef _DEBUG sprintf(m_szCurrVCodeName, "vcode%04d%02d%02d%02d%02d%02d.jpg", 1900 + tblock->tm_year, 1 + tblock->tm_mon, tblock->tm_mday, tblock->tm_hour, tblock->tm_min, tblock->tm_sec);#else sprintf(m_szCurrVCodeName, "vcode%04d%02d%02d%02d%02d%02d.v", 1900 + tblock->tm_year, 1 + tblock->tm_mon, tblock->tm_mday, tblock->tm_hour, tblock->tm_min, tblock->tm_sec);#endif FILE* fp = fopen(m_szCurrVCodeName, "wb"); if (fp == NULL) { LogError("open file %s error", m_szCurrVCodeName); return false; } const char* p = strResponse.data(); size_t count = fwrite(p, strResponse.length(), 1, fp); if (count != 1) { LogError("write file %s error", m_szCurrVCodeName); fclose(fp); return false; } fclose(fp); return true;}

我們再看下驗證碼去伺服器驗證的介面kyfw.12306.cn/passport/

請求頭:

POST /passport/captcha/captcha-check HTTP/1.1Host: kyfw.12306.cnConnection: keep-aliveContent-Length: 50Accept: application/json, text/javascript, */*; q=0.01Origin: https://kyfw.12306.cnX-Requested-With: XMLHttpRequestUser-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36Content-Type: application/x-www-form-urlencoded; charset=UTF-8Referer: https://kyfw.12306.cn/otn/login/initAccept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9,en;q=0.8Cookie: _passport_session=3e39a33a25bf4ea79146bd9362c11ad62327; _passport_ct=c5c7940e08ce44db9ad05d213c1296ddt4410; 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; BIGipServerpassport=837288202.50215.0000

這是一個POST請求,其中POST數據帶上的輸入的圖片驗證碼選擇的坐標X和Y值:

answer: 175,58,30,51login_site: Erand: sjrand

這裡我選擇了兩張圖片,所以有兩組坐標值,(175,58)是一組,(30,51)是另外一組,這個坐標系如下:

因為每個圖片的尺寸都一樣,所以,我可以給每個圖片設置一個坐標範圍,當選擇了一個圖片,給一個在其中的坐標即可,不一定是滑鼠點擊時的準確位置:

//刷新驗證碼 登錄狀態下的驗證碼傳入」randp「,非登錄傳入」sjrand「 具體參看原otsweb中的傳入參數struct VCodePosition{ int x; int y;};const VCodePosition g_pos[] ={ { 39, 40 }, { 114, 43 }, { 186, 42 }, { 252, 47 }, { 36, 120 }, { 115, 125 }, { 194, 125 }, { 256, 120 }};//驗證碼圖片八個區塊的位置struct VCODE_SLICE_POS{ int xLeft; int xRight; int yTop; int yBottom;};const VCODE_SLICE_POS g_VCodeSlicePos[] = { {0, 70, 0, 70}, {71, 140, 0, 70 }, {141, 210, 0, 70 }, {211, 280, 0, 70 }, { 0, 70, 70, 140 }, {71, 140, 70, 140 }, {141, 210, 70, 140 }, {211, 280, 70, 140 }};//8個驗證碼區塊的滑鼠點擊狀態bool g_bVodeSlice1Pressed[8] = { false, false, false, false, false, false, false, false};

驗證的圖片驗證碼的介面代碼是:

int Client12306::checkRandCodeAnsyn(const char* vcode){ string param; param = "randCode="; param += vcode; param += "&rand=sjrand"; //passenger:randp string strResponse; string strCookie = "Cookie: "; strCookie += m_strCookies; if (!HttpRequest(URL_CHECKRANDCODEANSYN, strResponse, false, strCookie.c_str(), param.c_str(), false, 10)) { LogError("checkRandCodeAnsyn failed"); return -1; } ///** 成功返回 //HTTP/1.1 200 OK //Date: Thu, 05 Jan 2017 07:44:16 GMT //Server: Apache-Coyote/1.1 //X-Powered-By: Servlet 2.5; JBoss-5.0/JBossWeb-2.1 //ct: c1_103 //Content-Type: application/json;charset=UTF-8 //Content-Length: 144 //X-Via: 1.1 jiandianxin29:6 (Cdn Cache Server V2.0) //Connection: keep-alive //X-Cdn-Src-Port: 19153 //參數無效 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"0","msg":""},"messages":[],"validateMessages":{}} //驗證碼過期 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"0","msg":"EXPIRED"},"messages":[],"validateMessages":{}} //驗證碼錯誤 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"1","msg":"FALSE"},"messages":[],"validateMessages":{}} //驗證碼正確 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"1","msg":"TRUE"},"messages":[],"validateMessages":{}} Json::Reader JsonReader; Json::Value JsonRoot; if (!JsonReader.parse(strResponse, JsonRoot)) return -1; //{"validateMessagesShowId":"_validatorMessage", "status" : true, "httpstatus" : 200, "data" : {"result":"1", "msg" : "TRUE"}, "messages" : [], "validateMessages" : {}} if (JsonRoot["status"].isNull() || JsonRoot["status"].asBool() != true) return -1; if (JsonRoot["httpstatus"].isNull() || JsonRoot["httpstatus"].asInt() != 200) return -1; if (JsonRoot["data"].isNull() || !JsonRoot["data"].isObject()) return -1; if (JsonRoot["data"]["result"].isNull()) return -1; if (JsonRoot["data"]["result"].asString() != "1" && JsonRoot["data"]["result"].asString() != "0") return -1; if (JsonRoot["data"]["msg"].isNull()) return -1; //if (JsonRoot["data"]["msg"].asString().empty()) // return -1; if (JsonRoot["data"]["msg"].asString() == "") return 0; else if (JsonRoot["data"]["msg"].asString() == "FALSE") return 1; return 1;}

同理,這裡也給出驗證用戶名和密碼的介面實現代碼:

int Client12306::loginAysnSuggest(const char* user, const char* pass, const char* vcode){ string param = "loginUserDTO.user_name="; param += user; param += "&userDTO.password="; param += pass; param += "&randCode="; param += vcode; string strResponse; string strCookie = "Cookie: "; strCookie += m_strCookies; if (!HttpRequest(URL_LOGINAYSNSUGGEST, strResponse, false, strCookie.c_str(), param.c_str(), false, 10)) { LogError("loginAysnSuggest failed"); return 2; } ///** 成功返回 //HTTP/1.1 200 OK //Date: Thu, 05 Jan 2017 07:49:53 GMT //Server: Apache-Coyote/1.1 //X-Powered-By: Servlet 2.5; JBoss-5.0/JBossWeb-2.1 //ct: c1_103 //Content-Type: application/json;charset=UTF-8 //Content-Length: 146 //X-Via: 1.1 f186:10 (Cdn Cache Server V2.0) //Connection: keep-alive //X-Cdn-Src-Port: 48361 //郵箱不存在 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{},"messages":["該郵箱不存在。"],"validateMessages":{}} //密碼錯誤 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{},"messages":["密碼輸入錯誤。如果輸錯次數超過4次,用戶將被鎖定。"],"validateMessages":{}} //登錄成功 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"otherMsg":"",loginCheck:"Y"},"messages":[],"validateMessages":{}} //WCHAR* psz1 = Utf8ToAnsi(strResponse.c_str()); //wstring str = psz1; //delete[] psz1; Json::Reader JsonReader; Json::Value JsonRoot; if (!JsonReader.parse(strResponse, JsonRoot)) return 2; //{"validateMessagesShowId":"_validatorMessage", "status" : true, //"httpstatus" : 200, "data" : {"otherMsg":"", loginCheck : "Y"}, "messages" : [], "validateMessages" : {}} if (JsonRoot["status"].isNull()) return -1; bool bStatus = JsonRoot["status"].asBool(); if (!bStatus) return -1; if (JsonRoot["httpstatus"].isNull() || JsonRoot["httpstatus"].asInt() != 200) return 2; if (JsonRoot["data"].isNull() || !JsonRoot["data"].isObject()) return 2; if (JsonRoot["data"]["otherMsg"].isNull() || JsonRoot["data"]["otherMsg"].asString() != "") return 2; if (JsonRoot["data"]["loginCheck"].isNull() || JsonRoot["data"]["loginCheck"].asString() != "Y") return 1; return 0;}

這裡還有個注意細節,就是通過POST請求發送的數據需要對一些符號做URL Encode,這個我在上一篇文章《從零實現一個http伺服器》也詳細做了介紹,還不清楚的可以參見上一篇文章。所以對於向圖片驗證碼坐標信息中含有的逗號信息就要進行URL編碼,從

answer=114,54,44,46&login_site=E&rand=sjrand

變成

answer=114%2C54%2C44%2C46&login_site=E&rand=sjrand

所以,在http包頭中指定的Content-Length欄位的值應該是編碼後的字元串長度,而不是原始的長度,這個地方特別容易出錯。

如果驗證成功後,接下來就是查票和購票了。

由於知乎字數限制,您可以繼續閱讀下一篇《從零實現一款12306搶票軟體(三)》。

歡迎關注公眾號『easyserverdev』。如果有任何技術或者職業方面的問題需要我提供幫助,可通過這個公眾號與我取得聯繫,此公眾號不僅分享高性能伺服器開發經驗和故事,同時也免費為廣大技術朋友提供技術答疑和職業解惑,您有任何問題都可以在微信公眾號直接留言,我會儘快回復您。

weixin.qq.com/r/DS_qsp3 (二維碼自動識別)


推薦閱讀:

購票者邊罵邊用,第三方搶票服務為什麼讓人又愛又恨?
購票者的心焦,搶票軟體的利益江湖
回家的路,一年一度春運大遷徙來臨

TAG:搶票 | C | 編程 |