如何定制一款12306抢票浏览器——处理预订页面和验证码自动识别功能
判斷是否進入預訂頁面
? ? ? ? 我們先看一下預訂頁面的結構(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 可以見得,這個頁面也是嵌入了兩個IFrame。關于IFrame的跨域問題,我已經在前一篇文章中講述了解決辦法。
? ? ? ? 我判斷是否是預訂頁面是通過兩個依據:
? ? ? ? 1 URL是否是http://www.12306.cn/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 == cstrUrl.CompareNoCase(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 == cstrReadValue.Compare(cstrValue) ) {hRes = spOption->put_selected(VARIANT_TRUE);break;}}} while (0);return hRes;
}
? ? ? ? 如此自動填寫乘客信息的操作就完成了。
驗證碼的自動識別
? ? ? ? 說來慚愧,這個模塊本來是我這個軟件的一個亮點。可是隨著12306將驗證碼生成方法改變,導致我原來的邏輯產生了很大的誤差。其實圖像識別這塊,我使用的是第三方庫tesseract-ocr。之前12306的驗證碼相對比較簡單,但是仍然加入了噪點和干擾線,使得tesseract-ocr識別率非常不準。于是我寫了一個bmp文件格式分析和圖片轉換類去處理原始驗證碼圖片,使得驗證碼變得清晰,同時提高了tesseract-ocr的識別準確率。我列一些以前的處理結果對比圖
? ? ? ? 網上有使用2012編譯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抢票浏览器——处理预订页面和验证码自动识别功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何定制一款12306抢票浏览器——实现
- 下一篇: 如何定制一款12306抢票浏览器——完结