判斷是否進入預定頁面 我們先看一下預定頁面的結構 可以見得,這個頁面也是嵌入了兩個IFrame。關於IFrame的跨域問題,我已經在前一篇文章中講述了解決辦法。 我判斷是否是預定頁面是通過兩個依據: 1 URL是否是 2 是否可以在最裡層IFrame中找到class是“table_qr”的元素該元素對應於 具體的查找過程我這兒就不再贅述,我們通過代碼來解讀 [cpp] 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; } [cpp] 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位乘車人”,再填寫一個……我們用代碼說明這個過程。 [cpp] 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位乘車人”。 [cpp] 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; } 填寫每個乘客信息的代碼是 [cpp] 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; } 其中填寫姓名的操作很簡單,只要找到相應控件,並向該控件中插入文字即可 [cpp] 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選項則稍微復雜點,其實原理是一致的 [cpp] 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。其內容也很簡單 [cpp] 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不是指向的是一個圖片,而是一個隨機地址。 [html] <img title="單擊刷新驗證碼" id="img_rrand_code" style="vertical-align: text-bottom; cursor: hand;" onclick="this.src=this.src+'&'+Math.random();" src="/otsweb/passCodeAction.do?rand=randp" border="0"/> 我之前想通過Src下載圖片的方法明顯是行不通的。那麼就得使用截屏技術了。下面的代碼,將驗證碼區域復制到剪貼板中,然後再將剪貼板中的圖片保存為一個32位真彩色的bmp圖片。 [cpp] 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; } 截屏、識別、輸入驗證碼的邏輯 [cpp] 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; } 如果識別的字符數不對,則會認為失敗,這樣我們會刷新驗證碼,並重新識別。 [cpp] 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接口,並修改該接口的一個方法: [cpp] 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代碼 於是完整的預定流程是 [cpp] 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; } [cpp] 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; }