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

咱们接着上一篇《从零实现一款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请求,HostRefererCookie这三个字段是必须的,且Cookie字段必须带上上文说的JSESSIONID,下载图片验证码和下文中各个步骤也必须在Cookie字段中带上这个JSESSIONID值,否则无法从12306服务器得到正确的应答。后面会介绍如何拿到这个这。这个拉取图片验证码的http GET请求需要三个参数,如上面的代码段所示,即login_sitemodule、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》。

原文发布于微信公众号 - 高性能服务器开发(easyserverdev)

原文发表时间:2018-05-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏DannyHoo的专栏

清理Xcode

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

1283
来自专栏ytkah

Bootstrap幻灯轮播如何支持触屏左右滑动手势?

  最近ytkah在学习用bootstrap搭建网站,Bootstrap能自适应pc端和手机端,并且移动设备优先,适合现如今移动营销。bootstrap是封装好...

4095
来自专栏CRPER折腾记

Vue 折腾记 - (10) 给axios做个挺靠谱的封装(报错,鉴权,跳转,拦截,提示)

不推荐完全copy过去,可以看看我是如何针对我这边业务; 做的一个axios的封装及实现的思路

3362
来自专栏腾讯移动品质中心TMQ的专栏

十分钟学会 Fiddler

Fiddler是一个http抓包改包工具,fiddle英文中有“欺骗、伪造”之意,与wireshark相比它更轻量级,上手简单,因为只能抓http和https数...

2.4K1
来自专栏从零开始学自动化测试

Fiddler抓包1-抓firefox上https请求

前言 fiddler是一个很好的抓包工具,默认是抓http请求的,对于pc上的https请求,会提示网页不安全,这时候需要在浏览器上安装证书。 一、网页不安全 ...

3635
来自专栏Java修行之道

java图片验证码乱码问题

在本地电脑上查询"Times New Roman”这种字体(路径:C:/Windows/Fonts):

4882
来自专栏谈补锅

<转>关于Certificate、Provisioning Profile、App ID的介绍及其之间的关系

刚接触iOS开发的人难免会对苹果的各种证书、配置文件等不甚了解,可能你按照网上的教程一步一步的成功申请了真机调试,但是还是对其中的缘由一知半解。这篇文章就对Ce...

1232
来自专栏电光石火

美化ThinkPHP

首先我们来看看Thinkphp的模板, 成功界面: ? 错误页面 ? 说到美化的话,我们需要先找到这个模板在那里 ThinkPHP/Tp...

2207
来自专栏前端布道

Angular开发实践(六):服务端渲染

Angular Universal Angular在服务端渲染方面提供一套前后端同构解决方案,它就是 Angular Universal(统一平台),一项在服务...

62610
来自专栏Create Sun

python 爬虫入门案例----爬取某站上海租房图片

前言   对于一个net开发这爬虫真真的以前没有写过。这段时间开始学习python爬虫,今天周末无聊写了一段代码爬取上海租房图片,其实很简短就是利用爬虫的第三方...

3997

扫码关注云+社区

领取腾讯云代金券