程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 如何定制一款12306搶票浏覽器——處理預定頁面和驗證碼自動識別功能

如何定制一款12306搶票浏覽器——處理預定頁面和驗證碼自動識別功能

編輯:C++入門知識

     判斷是否進入預定頁面           我們先看一下預定頁面的結構            可以見得,這個頁面也是嵌入了兩個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;   }  

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved