咱们接着上一篇《从零实现一款12306刷票软件1.2》继续介绍。
二、登录与拉取图片验证码接口
我的登录页面效果如下:
12306的图片验证码一般由八个图片组成,像上面的“龙舟”文字,也是图片,这两处的图片(文字图片和验证码)都是在服务器上拼装后,发给客户端的,12306服务器上这种类型的小图片有一定的数量,虽然数量比较大,但是是有限的。如果你要做验证码自动识别功能,可以尝试着下载大部分图片,然后做统计规律。所以,我这里并没有做图片自动识别功能。有兴趣的读者可自行尝试。
先说下,拉取验证码的接口。我们打开Chrome浏览器12306的登录界面:
https://kyfw.12306.cn/otn/login/init
如下图所示:
可以得到拉取验证码的接口:
我们可以看到发送的http请求数据包格式是:
1GET /passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.7520968747611347 HTTP/1.1
2Host: kyfw.12306.cn
3Connection: keep-alive
4User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36
5Accept: image/webp,image/apng,image/*,*/*;q=0.8
6Referer: https://kyfw.12306.cn/otn/login/init
7Accept-Encoding: gzip, deflate, br
8Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
9Cookie: _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是
1https://kyfw.12306.cn/otn/login/init
http请求和应答包如下:
请求包:
1Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
2Accept-Encoding: gzip, deflate, br
3Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
4Cache-Control: max-age=0
5Connection: keep-alive
6Cookie: 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
7Host: kyfw.12306.cn
8Referer: https://kyfw.12306.cn/otn/passport?redirect=/otn/login/loginOut
9Upgrade-Insecure-Requests: 1
10User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36
应答包:
1HTTP/1.1 200 OK
2Date: Sun, 20 May 2018 02:23:53 GMT
3Content-Type: text/html;charset=utf-8
4Transfer-Encoding: chunked
5Set-Cookie: JSESSIONID=D5AE154D66F67DE53BF70420C772158F; Path=/otn
6ct: C1_217_101_6
7Content-Language: zh-CN
8Content-Encoding: gzip
9X-Via: 1.1 houdianxin184:4 (Cdn Cache Server V2.0)
10Connection: keep-alive
11X-Dscp-Value: 0
12X-Cdn-Src-Port: 46480
这个值在应答包字段Set-Cookie中拿到:
1Set-Cookie: JSESSIONID=D5AE154D66F67DE53BF70420C772158F; Path=/otn
所以,我们每次请求图片验证码时,都重新请求一下这个JSESSIONID,代码如下:
1#define URL_LOGIN_INIT "https://kyfw.12306.cn/otn/login/init"
1bool Client12306::loginInit()
2{
3 string strResponse;
4 if (!HttpRequest(URL_LOGIN_INIT, strResponse, true, "Upgrade-Insecure-Requests: 1", NULL, true, 10))
5 {
6 LogError("loginInit failed");
7 return false;
8 }
9 if (!GetCookies(strResponse))
10 {
11 LogError("parse login init cookie error, url=%s", URL_LOGIN_INIT);
12 return false;
13 }
14 return true;
15}
16
17bool Client12306::GetCookies(const string& data)
18{
19 if (data.empty())
20 {
21 LogError("http data is empty");
22 return false;
23 }
24 //解析http头部
25 string str;
26 str.append(data.c_str(), data.length());
27 size_t n = str.find("\r\n\r\n");
28 string header = str.substr(0, n);
29 str.erase(0, n + 4);
30 //m_cookie.clear();
31 //获取http头中的JSESSIONID=21AC68643BBE893FBDF3DA9BCF654E98;
32 vector<string> v;
33 while (true)
34 {
35 size_t index = header.find("\r\n");
36 if (index == string::npos)
37 break;
38 string tmp = header.substr(0, index);
39 v.push_back(tmp);
40 header.erase(0, index + 2);
41 if (header.empty())
42 break;
43 }
44 string jsessionid;
45 string BIGipServerotn;
46 string BIGipServerportal;
47 string current_captcha_type;
48 size_t m;
49 OutputDebugStringA("\nresponse http headers:\n");
50 for (size_t i = 0; i < v.size(); ++i)
51 {
52 OutputDebugStringA(v[i].c_str());
53 OutputDebugStringA("\n");
54 m = v[i].find("Set-Cookie: ");
55 if (m == string::npos)
56 continue;
57 string tmp = v[i].substr(11);
58 Trim(tmp);
59 m = tmp.find("JSESSIONID");
60 if (m != string::npos)
61 {
62 size_t comma = tmp.find(";");
63 if (comma != string::npos)
64 jsessionid = tmp.substr(0, comma);
65 }
66 m = tmp.find("BIGipServerotn");
67 if (m != string::npos)
68 {
69 size_t comma = tmp.find(";");
70 if (comma != string::npos)
71 BIGipServerotn = tmp.substr(m, comma);
72 else
73 BIGipServerotn = tmp;
74 }
75 m = tmp.find("BIGipServerportal");
76 if (m != string::npos)
77 {
78 size_t comma = tmp.find(";");
79 if (comma != string::npos)
80 BIGipServerportal = tmp.substr(m, comma);
81 else
82 BIGipServerportal = tmp;
83 }
84 m = tmp.find("current_captcha_type");
85 if (m != string::npos)
86 {
87 size_t comma = tmp.find(";");
88 if (comma != string::npos)
89 current_captcha_type = tmp.substr(m, comma);
90 else
91 current_captcha_type = tmp;
92 }
93 }
94 if (!jsessionid.empty())
95 {
96 m_strCookies = jsessionid;
97 m_strCookies += "; ";
98 m_strCookies += BIGipServerotn;
99 if (!BIGipServerportal.empty())
100 {
101 m_strCookies += "; ";
102 m_strCookies += BIGipServerportal;
103 }
104 m_strCookies += "; ";
105 m_strCookies += current_captcha_type;
106 return true;
107 }
108 LogError("jsessionid is empty");
109 return false;
110}
1#define URL_GETPASSCODENEW "https://kyfw.12306.cn/passport/captcha/captcha-image"
1bool Client12306::DownloadVCodeImage(const char* module)
2{
3 if (module == NULL)
4 {
5 LogError("module is invalid");
6 return false;
7 }
8 //https://kyfw.12306.cn/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.06851784300754482
9 ostringstream osUrl;
10 osUrl << URL_GETPASSCODENEW;
11 osUrl << "?login_site=E&module=";
12 osUrl << module;
13 //购票验证码
14 if (strcmp(module, "passenger") != 0)
15 {
16 osUrl << "&rand=sjrand&";
17 }
18 //登录验证码
19 else
20 {
21 osUrl << "&rand=randp&";
22 }
23 double d = rand() * 1.000000 / RAND_MAX;
24 osUrl.precision(17);
25 osUrl << d;
26 string strResponse;
27 string strCookie = "Cookie: ";
28 strCookie += m_strCookies;
29 if (!HttpRequest(osUrl.str().c_str(), strResponse, true, strCookie.c_str(), NULL, false, 10))
30 {
31 LogError("DownloadVCodeImage failed");
32 return false;
33 }
34 //写入文件
35 time_t now = time(NULL);
36 struct tm* tblock = localtime(&now);
37 memset(m_szCurrVCodeName, 0, sizeof(m_szCurrVCodeName));
38#ifdef _DEBUG
39 sprintf(m_szCurrVCodeName, "vcode%04d%02d%02d%02d%02d%02d.jpg",
40 1900 + tblock->tm_year, 1 + tblock->tm_mon, tblock->tm_mday,
41 tblock->tm_hour, tblock->tm_min, tblock->tm_sec);
42#else
43 sprintf(m_szCurrVCodeName, "vcode%04d%02d%02d%02d%02d%02d.v",
44 1900 + tblock->tm_year, 1 + tblock->tm_mon, tblock->tm_mday,
45 tblock->tm_hour, tblock->tm_min, tblock->tm_sec);
46#endif
47 FILE* fp = fopen(m_szCurrVCodeName, "wb");
48 if (fp == NULL)
49 {
50 LogError("open file %s error", m_szCurrVCodeName);
51 return false;
52 }
53 const char* p = strResponse.data();
54 size_t count = fwrite(p, strResponse.length(), 1, fp);
55 if (count != 1)
56 {
57 LogError("write file %s error", m_szCurrVCodeName);
58 fclose(fp);
59 return false;
60 }
61 fclose(fp);
62 return true;
63}
我们再看下验证码去服务器验证的接口
1https://kyfw.12306.cn/passport/captcha/captcha-check
请求头:
1POST /passport/captcha/captcha-check HTTP/1.1
2Host: kyfw.12306.cn
3Connection: keep-alive
4Content-Length: 50
5Accept: application/json, text/javascript, */*; q=0.01
6Origin: https://kyfw.12306.cn
7X-Requested-With: XMLHttpRequest
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
9Content-Type: application/x-www-form-urlencoded; charset=UTF-8
10Referer: https://kyfw.12306.cn/otn/login/init
11Accept-Encoding: gzip, deflate, br
12Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
13Cookie: _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值:
1answer: 175,58,30,51
2login_site: E
3rand: sjrand
这里我选择了两张图片,所以有两组坐标值,(175,58)是一组,(30,51)是另外一组,这个坐标系如下:
因为每个图片的尺寸都一样,所以,我可以给每个图片设置一个坐标范围,当选择了一个图片,给一个在其中的坐标即可,不一定是鼠标点击时的准确位置:
1//刷新验证码 登录状态下的验证码传入”randp“,非登录传入”sjrand“ 具体参看原otsweb中的传入参数
2struct VCodePosition
3{
4 int x;
5 int y;
6};
7const VCodePosition g_pos[] =
8{
9 { 39, 40 },
10 { 114, 43 },
11 { 186, 42 },
12 { 252, 47 },
13 { 36, 120 },
14 { 115, 125 },
15 { 194, 125 },
16 { 256, 120 }
17};
18//验证码图片八个区块的位置
19struct VCODE_SLICE_POS
20{
21 int xLeft;
22 int xRight;
23 int yTop;
24 int yBottom;
25};
26const VCODE_SLICE_POS g_VCodeSlicePos[] =
27{
28 {0, 70, 0, 70},
29 {71, 140, 0, 70 },
30 {141, 210, 0, 70 },
31 {211, 280, 0, 70 },
32 { 0, 70, 70, 140 },
33 {71, 140, 70, 140 },
34 {141, 210, 70, 140 },
35 {211, 280, 70, 140 }
36};
37//8个验证码区块的鼠标点击状态
38bool g_bVodeSlice1Pressed[8] = { false, false, false, false, false, false, false, false};
验证的图片验证码的接口代码是:
1int Client12306::checkRandCodeAnsyn(const char* vcode)
2{
3 string param;
4 param = "randCode=";
5 param += vcode;
6 param += "&rand=sjrand"; //passenger:randp
7 string strResponse;
8 string strCookie = "Cookie: ";
9 strCookie += m_strCookies;
10 if (!HttpRequest(URL_CHECKRANDCODEANSYN, strResponse, false, strCookie.c_str(), param.c_str(), false, 10))
11 {
12 LogError("checkRandCodeAnsyn failed");
13 return -1;
14 }
15 ///** 成功返回
16 //HTTP/1.1 200 OK
17 //Date: Thu, 05 Jan 2017 07:44:16 GMT
18 //Server: Apache-Coyote/1.1
19 //X-Powered-By: Servlet 2.5; JBoss-5.0/JBossWeb-2.1
20 //ct: c1_103
21 //Content-Type: application/json;charset=UTF-8
22 //Content-Length: 144
23 //X-Via: 1.1 jiandianxin29:6 (Cdn Cache Server V2.0)
24 //Connection: keep-alive
25 //X-Cdn-Src-Port: 19153
26 //参数无效
27 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"0","msg":""},"messages":[],"validateMessages":{}}
28 //验证码过期
29 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"0","msg":"EXPIRED"},"messages":[],"validateMessages":{}}
30 //验证码错误
31 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"1","msg":"FALSE"},"messages":[],"validateMessages":{}}
32 //验证码正确
33 //{"validateMessagesShowId":"_validatorMessage","status":true,"httpstatus":200,"data":{"result":"1","msg":"TRUE"},"messages":[],"validateMessages":{}}
34 Json::Reader JsonReader;
35 Json::Value JsonRoot;
36 if (!JsonReader.parse(strResponse, JsonRoot))
37 return -1;
38 //{"validateMessagesShowId":"_validatorMessage", "status" : true, "httpstatus" : 200, "data" : {"result":"1", "msg" : "TRUE"}, "messages" : [], "validateMessages" : {}}
39 if (JsonRoot["status"].isNull() || JsonRoot["status"].asBool() != true)
40 return -1;
41 if (JsonRoot["httpstatus"].isNull() || JsonRoot["httpstatus"].asInt() != 200)
42 return -1;
43 if (JsonRoot["data"].isNull() || !JsonRoot["data"].isObject())
44 return -1;
45 if (JsonRoot["data"]["result"].isNull())
46 return -1;
47 if (JsonRoot["data"]["result"].asString() != "1" && JsonRoot["data"]["result"].asString() != "0")
48 return -1;
49 if (JsonRoot["data"]["msg"].isNull())
50 return -1;
51 //if (JsonRoot["data"]["msg"].asString().empty())
52 // return -1;
53 if (JsonRoot["data"]["msg"].asString() == "")
54 return 0;
55 else if (JsonRoot["data"]["msg"].asString() == "FALSE")
56 return 1;
57 return 1;
58}
由于微信公众号文章字数限制,您可以继续阅读下一篇《从零实现一款12306刷票软件1.4》。