判断是否进入预订页面
我们先看一下预订页面的结构(转载请指明出于breaksoftware的csdn博客)
可以见得,这个页面也是嵌入了两个IFrame。关于IFrame的跨域问题,我已经在前一篇文章中讲述了解决办法。
我判断是否是预订页面是通过两个依据:
1 URL是否是/mormhweb/kyfw/
2 是否可以在最里层IFrame中找到class是“table_qr”的元素该元素对应于
具体的查找过程我这儿就不再赘述,我们通过代码来解读
BOOL CDeal12306WebPage::IsBookingPage( CComPtr<IHTMLDocument2> & spDoc, CComBSTR & bstrUrl ){HRESULT hr = E_FAIL;do {CString cstrUrl = CString((LPWSTR)bstrUrl);if ( 0 == pareNoCase(LOGIN12306URL) ) {CComPtr<IHTMLElement> spTableQrTbody;hr = GetTableQrTbody( spDoc, spTableQrTbody);CHECKHRPOINTER(hr, spTableQrTbody);}} while (0);return FAILED(hr) ? FALSE : TRUE;}
HRESULT CDeal12306WebPage::GetTableQrTbody( CComPtr<IHTMLDocument2> & spDoc,CComPtr<IHTMLElement> & spElem ){HRESULT hr = E_FAIL;do {CComPtr<IHTMLDocument2> spMainDoc;hr = GetMainDoc( spDoc, spMainDoc);CHECKHRPOINTER(hr, spMainDoc);CComPtr<IHTMLElement> spEnter_wElem;hr = GetEnter_wElement(spMainDoc, spEnter_wElem );CHECKHRPOINTER(hr, spEnter_wElem);CComPtr<IHTMLElement> spForm;hr = GetElementByID( spEnter_wElem, L"confirmPassenger", spForm);CHECKHRPOINTER(hr, spForm);CComPtr<IHTMLElement> spTable;hr = GetElementByClassName( spForm, L"table_qr", spTable);CHECKHRPOINTER(hr, spTable);hr = GetElementByIndex( spTable, 0, spElem);CHECKHRPOINTER(hr, spElem);} while (0);return hr;}
插入用户信息,并设置相应的选项
我们看下用户填写信息的位置的HTML代码结构
我们可以看到5个passenger可填写区域。目前只有第一个显示出来,而其他四个还没有显示。在上图的最下面是个超链接,其对应于“添加1位乘车人”按钮。可以想象,该按钮的一个操作就是将不能显示的tr显示出来。我们“人”线程填写用户信息的过程和人的行为是一致的:填写一个人信息后 ,点击“添加1位乘车人”,再填写一个……我们用代码说明这个过程。
HRESULT CDeal12306WebPage::AddPassengerInfo( CComPtr<IHTMLElement>& spTableQrTbody,const VecStSinglePassengerInfo& vecStSingleinfo ){HRESULT hr = E_FAIL;do {// 下标没有从0开始!int i = 1;for ( VecStSinglePassengerInfoCIter it = vecStSingleinfo.begin(); it != vecStSingleinfo.end();i++ ) {CString cstrPassengerId;cstrPassengerId.Format(PASSENGERID, i);hr = BookSinglePassenger( spTableQrTbody, cstrPassengerId, it);CHECKHR(hr);it++;if ( it != vecStSingleinfo.end() ) {AddPassenger(spTableQrTbody);}}} while (0);return hr;}
上面代码我们将枚举用户设置的乘客信息。第12行,我们将在table中填写一个乘客信息。第16行,我们将判断最新加入的用户是否是最后一个,如果不是最后一个,则点击“添加1位乘车人”。
HRESULT CDeal12306WebPage::AddPassenger( CComPtr<IHTMLElement> & spTableQrTbody ){HRESULT hr = E_FAIL;do {CComPtr<IHTMLElement> spTr;hr = GetElementByIndex(spTableQrTbody, 6, spTr);CHECKHRPOINTER(hr, spTr);CComPtr<IHTMLElement> spTd;hr = GetElementByIndex(spTr, 1, spTd);CHECKHRPOINTER(hr, spTd);CComPtr<IHTMLElement> spA;hr = GetElementByIndex(spTd, 0, spA);CHECKHRPOINTER(hr, spA);hr = spA->click();} while (0);return hr;}
填写每个乘客信息的代码是
HRESULT CDeal12306WebPage::BookSinglePassenger( CComPtr<IHTMLElement> & spElem, const CString& cstrPassengerID, VecStSinglePassengerInfoCIter iter ){HRESULT hr = E_FAIL;do {CComPtr<IHTMLElement> spTr;hr = GetElementByID( spElem, cstrPassengerID, spTr );CHECKHRPOINTER(hr, spTr);hr = SetName(spTr, iter->cstrName);CHECKHR(hr);hr = SetCardNo(spTr, iter->cstrCardNo);CHECKHR(hr);hr = SetMobileNo(spTr, iter->cstrMobileNo);CHECKHR(hr);hr = SetTicket(spTr, iter->cstrTicket);CHECKHR(hr);hr = SetCardtype(spTr, iter->cstrCardtype);CHECKHR(hr);hr = SetSeat(spTr, iter->ListSeat);} while (0);return hr;}
其中填写姓名的操作很简单,只要找到相应控件,并向该控件中插入文字即可
HRESULT CDeal12306WebPage::SetName( CComPtr<IHTMLElement> & spElem, const CString& cstrName ){return SetInputHelper(spElem, cstrName, 4);}HRESULT CDeal12306WebPage::SetInputHelper( CComPtr<IHTMLElement> & spElem, const CString& cstrValue, long lIndex ){HRESULT hr = E_FAIL;do {CComPtr<IHTMLElement> spTd;hr = GetElementByIndex( spElem, lIndex, spTd );CHECKHRPOINTER(hr, spTd);CComPtr<IHTMLElement> spInputElem;hr = GetElementByIndex(spTd, 0, spInputElem);CHECKHRPOINTER(hr, spInputElem);CComPtr<IHTMLInputElement> spInput;hr = spInputElem->QueryInterface(IID_IHTMLInputElement, (LPVOID*)&spInput);CHECKHRPOINTER(hr, spInput);hr = spInput->put_value( CComBSTR(cstrValue.GetString()) );CHECKHR(hr);} while (0);return hr;}
设置席别这类Select选项则稍微复杂点,其实原理是一致的
HRESULT CDeal12306WebPage::SetSeat( CComPtr<IHTMLElement> & spElem, const CString& cstrSeat ){return SetOptionHelper( spElem, cstrSeat, 2);}HRESULT CDeal12306WebPage::SetOptionHelper( CComPtr<IHTMLElement> & spElem, const CString& cstrValue, long lIndex ){HRESULT hr = E_FAIL;do {CComPtr<IHTMLElement> spTd;hr = GetElementByIndex( spElem, lIndex, spTd );CHECKHRPOINTER(hr, spTd);CComPtr<IHTMLElement> spSelectElem;hr = GetElementByIndex(spTd, 0, spSelectElem);CHECKHRPOINTER(hr, spSelectElem);hr = SetOptionSelect( spSelectElem, cstrValue);CHECKHR(hr);} while (0);return hr;}HRESULT CDeal12306WebPage::SetOptionSelect( CComPtr<IHTMLElement> & spElem, const CString& cstrValue ){HRESULT hRes = E_FAIL;HRESULT hr = E_FAIL;do {CComPtr<IHTMLElementCollection> spElemCollection;hr = GetElementCollection(spElem, spElemCollection );CHECKHRPOINTER(hr, spElemCollection);long lCount = 0;hr = spElemCollection->get_length(&lCount);CHECKHR(hr);for ( long lindex = 0; lindex < lCount; lindex++ ) {CComVariant VarIndex = lindex;CComPtr<IDispatch> spDispatchElem;hr = spElemCollection->item( VarIndex, VarIndex, &spDispatchElem );CHECKHRPOINTER(hr,spDispatchElem);CComPtr<IHTMLOptionElement> spOption;hr = spDispatchElem->QueryInterface(IID_IHTMLOptionElement, (LPVOID*)& spOption);if ( FAILED(hr) || NULL == spOption ) {continue;}CComBSTR bstrValue;hr = spOption->get_value(&bstrValue);if ( FAILED(hr) ) {continue;}CString cstrReadValue(bstrValue);if ( 0 == pare(cstrValue) ) {hRes = spOption->put_selected(VARIANT_TRUE);break;}}} while (0);return hRes;}
如此自动填写乘客信息的操作就完成了。
验证码的自动识别
说来惭愧,这个模块本来是我这个软件的一个亮点。可是随着12306将验证码生成方法改变,导致我原来的逻辑产生了很大的误差。其实图像识别这块,我使用的是第三方库tesseract-ocr。之前12306的验证码相对比较简单,但是仍然加入了噪点和干扰线,使得tesseract-ocr识别率非常不准。于是我写了一个bmp文件格式分析和图片转换类去处理原始验证码图片,使得验证码变得清晰,同时提高了tesseract-ocr的识别准确率。我列一些以前的处理结果对比图
网上有使用编译tesseract-ocr的介绍。我做了点改动:在tesseract-ocr的init函数中,提供了一个指定相关目录的参数,但是代码底层却优先读取了系统环境变量TESSDATA_PREFIX的值作为相关目录。我修改了源代码中的这部分:即只使用我指明的程序路径,而不是使用系统环境变量TESSDATA_PREFIX的值。
我封装了一个文字识别的类COcr。其内容也很简单
BOOL COcr::Init(const CString& cstrSetupFloder){std::string sSetupFloder = CW2A(cstrSetupFloder.GetString());int nstatus = m_Tesseract.Init(sSetupFloder.c_str(), "eng", tesseract::OEM_TESSERACT_ONLY);if ( nstatus < 0 ) {return FALSE;}m_Tesseract.SetPageSegMode(tesseract::PSM_SINGLE_BLOCK);nstatus = m_Tesseract.SetVariable( "tessedit_char_whitelist", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwsyz" );return nstatus > 0 ? TRUE : FALSE;}BOOL COcr::GetText( const CString& cstrImgPath, CString & cstrText ){std::string sImgPath = CW2A(cstrImgPath.GetString());STRING text_out;if (!m_Tesseract.ProcessPages(sImgPath.c_str(), NULL, 0, &text_out)) {return FALSE;}std::string sText = text_out.string();cstrText = CA2W(sText.c_str());return TRUE;}
简单说明下上述代码。代码第4行,我们设置了语言是eng,即英语体系。因为目前12306的验证码还只是数字和字母。代码第9行,告诉tesseract-ocr验证码中只是包含0~9A~Za~z字符。之前12306的验证码只有数字和大写字母,所以那个时候设置这个参数为0~9A~Z是非常必要的。
代码识别模块ok后,就是如何保存验证码图片的问题了。
如何保存验证码图片
仔细看过12306验证码区域的HTML代码的朋友,应该知道,该处的IMG的src不是指向的是一个图片,而是一个随机地址。
<img title="单击刷新验证码" id="img_rrand_code" style="vertical-align: text-bottom; cursor: hand;" οnclick="this.src=this.src+'&'+Math.random();" src="/otsweb/passCodeAction.do?rand=randp" border="0"/>
我之前想通过Src下载图片的方法明显是行不通的。那么就得使用截屏技术了。下面的代码,将验证码区域复制到剪贴板中,然后再将剪贴板中的图片保存为一个32位真彩色的bmp图片。
HRESULT CDeal12306WebPage::SaveImg( CComPtr<IHTMLElement> spElement, const CString& cstrFilePath ){HRESULT hr = E_FAIL;do {CComPtr<IDispatch> spDispDoc;hr = spElement->get_document(&spDispDoc);CHECKHRPOINTER(hr, spDispDoc);CComPtr<IHTMLDocument2> spMainDoc;hr = spDispDoc->QueryInterface(IID_IHTMLDocument2, (LPVOID*)&spMainDoc);CHECKHRPOINTER(hr, spMainDoc);CComPtr<IHTMLElement> spBody;hr = spMainDoc->get_body(&spBody);CHECKHRPOINTER(hr, spBody);CComPtr<IHTMLElement2> spBody2;hr = spBody->QueryInterface(IID_IHTMLElement2, (LPVOID*)&spBody2);CHECKHRPOINTER(hr, spBody2);CComPtr<IDispatch> spDisp;hr = spBody2->createControlRange(&spDisp);CHECKHRPOINTER(hr, spDisp);CComPtr<IHTMLControlRange> spControlRange;hr = spDisp->QueryInterface(IID_IHTMLControlRange, (LPVOID*)&spControlRange);CHECKHRPOINTER(hr, spControlRange);CComPtr<IHTMLControlElement> spControlElem;hr = spElement->QueryInterface(IID_IHTMLControlElement, (LPVOID*)&spControlElem);CHECKHRPOINTER(hr, spControlElem);hr = spControlRange->add(spControlElem);CHECKHR(hr);VARIANT_BOOL vbReturn = VARIANT_FALSE;CComVariant vEmpty;CComBSTR bstrCmd(L"Copy");hr = spControlRange->execCommand(bstrCmd, VARIANT_FALSE, vEmpty, &vbReturn );CHECKHR(hr);if ( VARIANT_FALSE == vbReturn ) {hr = E_FAIL;break;}if(OpenClipboard(NULL)){//获得剪贴板数据HBITMAP handle = (HBITMAP)GetClipboardData(CF_BITMAP);if ( NULL != handle ) {CImage Img;Img.Attach(handle);hr = Img.Save(cstrFilePath);}else {hr = E_FAIL;}CloseClipboard();}} while (0);return hr;}
截屏、识别、输入验证码的逻辑
HRESULT CDeal12306WebPage::SetCaptcha( CComPtr<IHTMLElement> & spTableQrTbody ){HRESULT hr = E_FAIL;do {CComPtr<IHTMLElement> spImg;hr = GetCaptchaImgElem( spTableQrTbody, spImg);CHECKHRPOINTER(hr, spImg);CComPtr<IHTMLElement> spInput;hr = GetCaptchaInputElem( spTableQrTbody, spInput );CHECKHRPOINTER(hr, spInput);CString cstrImgPath;cstrImgPath.Format(L"%s%d.bmp", m_cstrFloder, GetTickCount());hr = SaveImg( spImg, cstrImgPath);CHECKHR(hr);CString cstrNewImgPath = cstrImgPath + ".bmp";CBmp bmp;bmp.SetFilePath( cstrImgPath, cstrNewImgPath );if ( FALSE == bmp.DealBmp() ) {hr = E_FAIL;break;}CString cstrTxet;if ( FALSE == m_ocr.GetText( cstrNewImgPath, cstrTxet) ) {hr = E_FAIL;break;}if ( CAPTCHACOUNT > cstrTxet.GetLength() ) {hr = E_FAIL;break;}cstrTxet = cstrTxet.Left(CAPTCHACOUNT);CComPtr<IHTMLInputElement> spInputElem;hr = spInput->QueryInterface(IID_IHTMLInputElement, (LPVOID*)&spInputElem);CHECKHRPOINTER(hr, spInputElem);hr = spInputElem->put_value( CComBSTR(cstrTxet.GetString()) );CHECKHR(hr);} while (0);return hr;}
如果识别的字符数不对,则会认为失败,这样我们会刷新验证码,并重新识别。
HRESULT CDeal12306WebPage::SetCaptchaEx( CComPtr<IHTMLElement>& spTableQrTbody ){HRESULT hr = E_FAIL;do {for ( int n = 0; n < CAPTCHARETRYCOUNT; n++ ) {hr = SetCaptcha( spTableQrTbody );if ( FAILED(hr) ) {// 如果失败刷新验证码再来一次CComPtr<IHTMLElement> spImg;hr = GetCaptchaImgElem( spTableQrTbody, spImg);CHECKHRPOINTER(hr, spImg);spImg->click();Sleep(CAPTCHAWAITTIME);}else {break;}}} while (0);return hr;}
验证码输入完毕后,我们将点击“提交订单”按钮。现在有个问题冒出来了:如果我们验证码输入错误,那么网页会alert一下提示“验证码错误”,这个迫使我们得去点击这个按钮。如何去点击这个按钮呢?这个问题困扰了我一下,最后我决定还是绕过这个问题——彻底屏蔽Alert弹框,并记录Alert准备弹出的内容。在点击完按钮后,我将根据保存的Alert准备弹出的内容判断是否成功和失败。
屏蔽Alert
我们的窗口要继承IDocHostShowUI接口,并修改该接口的一个方法:STDMETHODIMP CBrowserHost::ShowMessage( /* [in] */ HWND hwnd, /* [annotation][in] */ __in __nullterminated LPOLESTR lpstrText, /* [annotation][in] */ __in __nullterminated LPOLESTR lpstrCaption, /* [in] */ DWORD dwType, /* [annotation][in] */ __in __nullterminated LPOLESTR lpstrHelpFile, /* [in] */ DWORD dwHelpContext, /* [out] */ LRESULT *plResult ){*plResult = 0;return S_OK;}
从上面代码看,我并没有记录alert的内容。因为我发现了一个更为有效和简单的办法去判断是否成功了。我们看下提交没有成功时HTML网页结构
我们再看下提交成功的页面的网页结构
可以见得,提交成功的页面中新增了两个Div。其中最下面那个Div就是确认信息的HTML代码
于是完整的预订流程是
HRESULT CDeal12306WebPage::BookTickets( CComPtr<IHTMLDocument2> & spDoc ){HRESULT hr = E_FAIL;do {CComPtr<IHTMLElement> spTableQrTbody;hr = GetTableQrTbody( spDoc, spTableQrTbody);CHECKHRPOINTER(hr, spTableQrTbody);if ( m_stTrainNoPassenger.vecPassengerInfo.size() > MAXPASSENGERCOUNT) {ATLASSERT(FALSE);}hr = AddPassengerInfo( spTableQrTbody, m_stTrainNoPassenger.vecPassengerInfo );CHECKHR(hr);DWORD dwCount = 0;Sleep(6*1000);do {hr = SetCaptchaEx( spTableQrTbody );CHECKHR(hr);hr = ClickSubmitButton(spTableQrTbody);CHECKHR(hr);dwCount++;} while ( FAILED(ConfirmOrd(spDoc)));} while (0);return hr;}
HRESULT CDeal12306WebPage::ConfirmOrd( CComPtr<IHTMLDocument2> & spDoc ){HRESULT hr = E_FAIL;do {CComPtr<IHTMLElement> spDiv;hr = GetOrderConfirm( spDoc, spDiv);CHECKHRPOINTER(hr, spDiv);CComPtr<IHTMLElement> spOkButton;hr = GetConfirmOKElem(spDiv, spOkButton);CHECKHRPOINTER(hr, spOkButton);hr = spOkButton->click();CHECKHR(hr);} while (0);return hr;}
如果觉得《如何定制一款12306抢票浏览器——处理预订页面和验证码自动识别功能》对你有帮助,请点赞、收藏,并留下你的观点哦!