天天看點

如何定制一款12306搶票浏覽器——實作自動查詢和預訂功能

檢查是否進入訂票頁面

        判斷是否進入訂票頁面,我是确定了兩個标準:(轉載請指明出于breaksoftware的csdn部落格)

        1 網址是否為http://www.12306.cn/mormhweb/kyfw/

        2 該頁面否有查詢按鈕

BOOL CDeal12306WebPage::IsQueryPage( CComPtr<IHTMLDocument2> & spDoc, CComBSTR & bstrUrl )
{
    HRESULT hr = E_FAIL;
    do  {
        CString cstrUrl = CString((LPWSTR)bstrUrl);
        if ( 0 == cstrUrl.CompareNoCase(LOGIN12306URL) ) {
            CComPtr<IHTMLElement> spQueryButton;
            hr = GetQueryButtonInQueryPage( spDoc, spQueryButton);
            CHECKHRPOINTER(hr, spQueryButton);
        }
    } while (0);
    return FAILED(hr) ? FALSE : TRUE;
}           

複制

         URL很好檢測,那麼我們如何判斷是否存在查詢按鈕呢?我們先看一下訂票頁面的頁面特征。

如何定制一款12306搶票浏覽器——實作自動查詢和預訂功能

解決跨域問題

        可以見得訂票頁面内部嵌入了兩個Iframe,而我們關心的那塊頁面恰恰就是最裡面一層IFrame。那我們直接通過最外層的Doc擷取到最裡面的Doc,然後在最裡面的Doc執行有關的查詢操作即可。然而熟悉javascript的同學可能馬上就會想到“跨域”問題。其實在浏覽器層面,跨域問題是很好解決的。

HRESULT CDeal12306WebPage::GetIFrameDoc( CComPtr<IHTMLDocument2>& spDoc, 
    const CString& cstrIFrameName, CComPtr<IHTMLDocument2>& spInnerDoc )
{
    HRESULT hr = E_FAIL;
    do {
        CComQIPtr<IHTMLFramesCollection2> spFrameCollection;
        hr = spDoc->get_frames(&spFrameCollection);
        CHECKHRPOINTER(hr, spFrameCollection);

        CComVariant IframeNameReq = CComBSTR(cstrIFrameName.GetString());
        CComVariant FramePage;
        hr = spFrameCollection->item(&IframeNameReq, &FramePage);
        CHECKHRPOINTER(hr,FramePage.pdispVal);

        CComPtr<IHTMLWindow2> spIFramePage;
        hr = FramePage.pdispVal->QueryInterface(IID_IHTMLWindow2, (LPVOID*)&spIFramePage);
        CHECKHRPOINTER(hr, spIFramePage);

        hr = spIFramePage->get_document(&spInnerDoc);
        if ( E_ACCESSDENIED == hr ) {
            CComQIPtr<IServiceProvider> spServiceProvider = spIFramePage;
            CHECKPOINT(spServiceProvider);

            CComQIPtr<IWebBrowser2> spInnerWebBrowser;
            hr = spServiceProvider->QueryService(IID_IWebBrowserApp, IID_IWebBrowser2, (LPVOID*)&spInnerWebBrowser);
            CHECKHRPOINTER(hr, spInnerWebBrowser);

            CComPtr<IDispatch> spDisp;
            hr = spInnerWebBrowser->get_Document(&spDisp);
            CHECKHRPOINTER(hr, spDisp);

            hr = spDisp->QueryInterface(IID_IHTMLDocument2, (LPVOID*)&spInnerDoc);
            CHECKHRPOINTER(hr, spInnerDoc);
        }
    } while (0);
    return hr;
}           

複制

        上面這個函數試圖在spDoc頁面中擷取其内嵌的名字是cstrIFrameName的IFrame的Doc。于是我們要擷取其中最裡面一層Iframe的Doc可以如下調用

HRESULT CDeal12306WebPage::GetIFrameNamedIFramePageDoc( CComPtr<IHTMLDocument2> & spDoc, 
    CComPtr<IHTMLDocument2> & spInnerDoc )
{
    HRESULT hr = E_FAIL;
    do {
        hr =  GetIFrameDoc(spDoc, L"iframepage", spInnerDoc);
        CHECKHRPOINTER(hr, spInnerDoc);
    } while (0);
    return hr;
}

HRESULT CDeal12306WebPage::GetIFrameNamedMainDoc( CComPtr<IHTMLDocument2> & spIFramPageDoc,
    CComPtr<IHTMLDocument2> & spMainDoc )
{
    HRESULT hr = E_FAIL;
    do {
        hr =  GetIFrameDoc(spIFramPageDoc, L"main", spMainDoc);
        CHECKHRPOINTER(hr, spMainDoc);
    } while (0);
    return hr;
}

HRESULT CDeal12306WebPage::GetMainDoc( CComPtr<IHTMLDocument2> & spDoc,
    CComPtr<IHTMLDocument2> & spMainDoc )
{
    HRESULT hr = E_FAIL;
    do {
        CComPtr<IHTMLDocument2> spIFramePageDoc;
        hr = GetIFrameNamedIFramePageDoc(spDoc, spIFramePageDoc);
        CHECKHRPOINTER(hr, spIFramePageDoc);

        hr = GetIFrameNamedMainDoc(spIFramePageDoc, spMainDoc);
        CHECKHRPOINTER(hr, spMainDoc);
    } while (0);
    return hr;
}           

複制

        當我們獲得最裡層的Doc後,我們将根據頁面結構擷取Class為cx_from的Table元素。

如何定制一款12306搶票浏覽器——實作自動查詢和預訂功能

          擷取這個Table的原因是,之後我們會以該Table為節點,執行“查詢按鈕”查找的操作。

HRESULT CDeal12306WebPage::GetQueryButtonInQueryPage( CComPtr<IHTMLDocument2> & spDoc, CComPtr<IHTMLElement> & spQueryButtonElem )
{
    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> spQueryTable;
        hr = GetQueryTable(spEnter_wElem, spQueryTable);
        CHECKHRPOINTER(hr, spQueryTable);

        CComPtr<IHTMLButtonElement> spQueryButton;
        hr = GetQueryButtonInQueryPage(spQueryTable, spQueryButton);
        CHECKHRPOINTER(hr, spQueryButton);

        hr = spQueryButton->QueryInterface(IID_IHTMLElement, (LPVOID*)& spQueryButtonElem);
        CHECKHRPOINTER(hr, spQueryButtonElem);
    } while (0);
    return hr;
}           

複制

        查詢按鈕在這個table中的位置是

如何定制一款12306搶票浏覽器——實作自動查詢和預訂功能

        于是通過該Table查詢”查詢“按鈕的代碼是

HRESULT CDeal12306WebPage::GetQueryButtonInQueryPage( CComPtr<IHTMLElement>& spQueryTable, 
    CComPtr<IHTMLButtonElement> & spQueryButton )
{
    HRESULT hr = E_FAIL;
    do {
        CComPtr<IHTMLElement> spTBody;
        hr = GetElementByIndex(spQueryTable, 0, spTBody);
        CHECKHRPOINTER(hr, spTBody);

        CComPtr<IHTMLElement> spFirstTR;
        hr = GetElementByIndex(spTBody, 0, spFirstTR);
        CHECKHRPOINTER(hr, spFirstTR);

        CComPtr<IHTMLElement> spEighthTR;
        hr = GetElementByIndex(spFirstTR, 8, spEighthTR);
        CHECKHRPOINTER(hr, spEighthTR);

        CComPtr<IHTMLElement> spButtonTemp;
        hr = GetElementByIndex(spEighthTR, 0, spButtonTemp);
        CHECKHRPOINTER(hr, spButtonTemp);

        hr = spButtonTemp->QueryInterface(IID_IHTMLButtonElement, (LPVOID*)&spQueryButton);
        CHECKHRPOINTER(hr, spQueryButton);
    } while (0);
    return hr;
}           

複制

插入開始和停止自動查詢按鈕

        為了在該頁面中提供給用于控制開啟和關閉自動查詢功能的按鈕,我插入了兩個按鈕。如下圖

如何定制一款12306搶票浏覽器——實作自動查詢和預訂功能

        我們看下”單程“和”返程“按鈕的頁面結構

如何定制一款12306搶票浏覽器——實作自動查詢和預訂功能

        我會在Name為querySingleForm的form下的class為cx_tab的Div下插入“開始”和“停止”按鈕。

HRESULT CDeal12306WebPage::InsertButtonInQueryPage( CComPtr<IHTMLDocument2> & spDoc )
{
    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 = GetQuerySingleForm(spEnter_wElem, spForm);
        CHECKHRPOINTER(hr, spForm);

        hr = InsertButtons( spForm );
    } while (0);
    return hr;
}           

複制

HRESULT CDeal12306WebPage::InsertButtons(CComPtr<IHTMLElement> & spEnter_wElem )
{
    HRESULT hr = E_FAIL;
    do {
        CComPtr<IHTMLElement> spDiv;
        hr  = GetInsertButtonElem(spEnter_wElem, spDiv);
        if (  FALSE == IsStartButtonExist(spDiv) ) {
            hr = InsertStartButton(spDiv);
            CHECKHR(hr);
#ifdef DEBUG
            if ( FALSE == IsStartButtonExist(spDiv) ) {
                DebugBreak();
            }
#endif
        }
        
        if ( FALSE == IsStopButtonExist(spDiv) ) {
            hr = InsertStopButton(spDiv);
            CHECKHR(hr);
#ifdef DEBUG
            if ( FALSE == IsStopButtonExist(spDiv) ) {
                DebugBreak();
            }
#endif
        }
        
    } while (0);
    return hr ;
}           

複制

HRESULT CDeal12306WebPage::GetInsertButtonElem( CComPtr<IHTMLElement> & spForm, 
    CComPtr<IHTMLElement> & spDiv )
{
    HRESULT hr = E_FAIL;
    do {
        CComPtr<IHTMLElement> spCx_TabDiv;
        hr = GetElementByClassName(spForm, L"cx_tab", spCx_TabDiv);
        CHECKHRPOINTER(hr, spCx_TabDiv);

        hr = GetElementByIndex(spCx_TabDiv, 0, spDiv);
        CHECKHRPOINTER(hr, spDiv);
    } while (0);
    return hr;
}

HRESULT CDeal12306WebPage::InsertStartButton( CComPtr<IHTMLElement> & spElem )
{
    HRESULT hr = E_FAIL;
    do {
        CComBSTR bstrWhere(L"beforeEnd");
        CString cstrHTML;
        cstrHTML.Format( BUTTONFORMAT, STARTBUTTONID, STARTCOMD, L"開始" );
        CComBSTR bstrHTML(cstrHTML.GetString());
        hr = spElem->insertAdjacentHTML( bstrWhere, bstrHTML );
        CHECKHR(hr);
    } while (0);
    return hr ;
}

HRESULT CDeal12306WebPage::InsertStopButton( CComPtr<IHTMLElement> & spElem )
{
    HRESULT hr = E_FAIL;
    do {
        CComBSTR bstrWhere(L"beforeEnd");
        CString cstrHTML;
        cstrHTML.Format( BUTTONFORMAT, STOPBUTTONID, STOPCMD, L"停止" );
        CComBSTR bstrHTML(cstrHTML.GetString());
        hr = spElem->insertAdjacentHTML( bstrWhere, bstrHTML );
        CHECKHR(hr);
    } while (0);
    return hr ;
}           

複制

#define BUTTONFORMAT    L"<li id=\"%s\"><a href=\"%s\" style=\"width:50px;height:30px;\">%s</a></li>"
#define STARTBUTTONID   L"StartButton"
#define STOPBUTTONID    L"StopButton"           

複制

#define STARTCOMD       L"http://www.12306.cn/mormhweb/kyfw/StartQuery.fl"
#define STOPCMD         L"http://www.12306.cn/mormhweb/kyfw/StopQuery.fl"           

複制

        當我們點選開始按鈕是,頁面将試圖跳轉到http://www.12306.cn/mormhweb/kyfw/StartQuery.fl,此時,我将終止該跳轉,同時将“開啟查詢”标志設定為TRUE。

void CBrowserHost::BeforeNavigate2(IDispatch *pDisp, VARIANT *url,
        VARIANT *Flags, VARIANT *TargetFrameName, VARIANT *PostData,
        VARIANT *Headers, VARIANT_BOOL *Cancel)
{
    do  {
        if ( NULL != url ) {
            CString cstrUrl((LPWSTR)(url->bstrVal));
            if ( 0 == cstrUrl.CompareNoCase(SETTINGOK) ) {
               ……
            }
            else if ( 0 == cstrUrl.CompareNoCase(STARTCOMD) ) {
                *Cancel = VARIANT_TRUE;
                m_AutoMan.SetStart(TRUE);
                break;
            }
            else if (  0 == cstrUrl.CompareNoCase(STOPCMD) ) {
                *Cancel = VARIANT_TRUE;
                m_AutoMan.SetStart(FALSE);
                break;
            }
        }
        *Cancel = VARIANT_FALSE;
    } while (0);
}           

複制

        點選停止按鈕原理同點選開始按鈕原理一緻。此處不再贅述。

        當使用者選擇好出發地和目的地及時間後,使用者點選查詢按鈕。并點選“開始”按鈕。我們的“人”線程就開始了自動查詢操作。

查詢是否存在票,有票則預訂,無票則再次查詢

        當我們執行完一次查詢後,我們要檢視下搜尋結果清單資訊中使用者選擇的車次是否存在票。我們先看一下頁面結構

如何定制一款12306搶票浏覽器——實作自動查詢和預訂功能

        其查找該節點的方法如下

HRESULT CDeal12306WebPage::QueryTicketsInfo( CComPtr<IHTMLDocument2> & spDoc )
{
    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> spIDGridbox;
        hr = GetElementByID( spEnter_wElem, L"gridbox", spIDGridbox);
        CHECKHRPOINTER(hr, spIDGridbox);

        CComPtr<IHTMLElement> spTable;
        hr = GetElementByIndex( spIDGridbox, 0, spTable);
        CHECKHRPOINTER(hr, spTable);

        CComPtr<IHTMLElement> spTbody;
        hr = GetElementByIndex( spTable, 0, spTbody);
        CHECKHRPOINTER(hr, spTbody);

        CComPtr<IHTMLElement> spTr;
        hr = GetElementByIndex( spTbody, 1, spTr);
        CHECKHRPOINTER(hr, spTr);

        CComPtr<IHTMLElement> spTd;
        hr = GetElementByIndex(spTr, 0, spTd);
        CHECKHRPOINTER(hr, spTd);

        CComPtr<IHTMLElement> spDiv;
        hr = GetElementByIndex(spTd, 0, spDiv);
        CHECKHRPOINTER(hr, spDiv);

        CComPtr<IHTMLElement> spDiv2;
        hr = GetElementByIndex(spDiv, 0, spDiv2);
        CHECKHRPOINTER(hr, spDiv2);

        CComPtr<IHTMLElement> spTable2;
        hr = GetElementByIndex(spDiv2, 0, spTable2);
        CHECKHRPOINTER(hr, spTable2);

        CComPtr<IHTMLElement> spTbody2;
        hr = GetElementByIndex(spTable2, 0, spTbody2);
        CHECKHRPOINTER(hr, spTbody2);

        CComPtr<IHTMLElementCollection> spElemCollection;
        hr = GetElementCollection(spTbody2, spElemCollection );
        CHECKHRPOINTER(hr, spElemCollection);

        long lCount = 0;
        hr = spElemCollection->get_length(&lCount);
        CHECKHR(hr);

        for ( long lindex = 0; lindex < lCount; lindex++ ) {
            if ( 0 == lindex ) {
                continue;
            }
            CComVariant VarIndex = lindex;
            CComPtr<IDispatch> spDispatchElem;
            hr = spElemCollection->item( VarIndex, VarIndex, &spDispatchElem );
            CHECKHRPOINTER(hr,spDispatchElem);
            
            CComPtr<IHTMLElement> spChildTr;
            hr = spDispatchElem->QueryInterface(IID_IHTMLElement, (LPVOID*)& spChildTr);
            CHECKHRPOINTER(hr, spChildTr);

            hr = GetQueryInfoInTr( spChildTr );
            if ( SUCCEEDED(hr) ) {
                // 點選了訂購按鈕了
                break;
            }
        }
    } while (0);
    return hr;
}           

複制

        上述代碼執行到第57行時,for循環将逐個讀取每列車的資訊。為了最快速達到點選“預訂”按鈕,我将判斷的操作放在GetQueryInfoInTr中。

HRESULT CDeal12306WebPage::GetQueryInfoInTr( CComPtr<IHTMLElement> & spElem)
{
    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);

        StTrainInfo stTraininfoItem;
        for ( long lindex = 0; lindex < lCount; lindex++ ) {
            CComVariant VarIndex = lindex;
            CComPtr<IDispatch> spDispatchElem;
            hr = spElemCollection->item( VarIndex, VarIndex, &spDispatchElem );
            CHECKHRPOINTER(hr,spDispatchElem);

            CComPtr<IHTMLElement> spChildTd;
            hr = spDispatchElem->QueryInterface(IID_IHTMLElement, (LPVOID*)& spChildTd);
            CHECKHRPOINTER(hr, spChildTd);

            hr = GetQueryInfoSubItem( spChildTd, stTraininfoItem, lindex );   
            CHECKHR(hr);
        }
        
        CHECKHR(hr);

        CComPtr<IHTMLElement> spTd;
        hr = GetElementByIndex( spElem, lCount - 1, spTd);
        CHECKHRPOINTER(hr, spTd);

        CComPtr<IHTMLElement> spButton;
        hr = GetElementByIndex( spTd, 0, spButton );
        CHECKHRPOINTER(hr, spButton);

        CComBSTR bstrClassName;
        hr = spButton->get_className(&bstrClassName);
        CHECKHR(hr);
       
        CString cstrClassName = bstrClassName;
        if ( 0 == cstrClassName.CompareNoCase(HAVETICKETSACLASS) ) {
            hr = spButton->click();
        }
        else {
            // 還沒有票
        }

        m_VecTrainInfo.push_back(stTraininfoItem);
    } while (0);
    return hr;
}           

複制

        我這兒做了簡化:隻要“預訂”按鈕變成可點選,即點選之。其實這兒應該做更多的判斷,比如使用者的席别是否有票。上述代碼第44行,即是點選“預訂”按鈕的操作。

        如果沒有票,則我們點選“查詢”按鈕。

HRESULT CDeal12306WebPage::StartQueryInQueryPage( CComPtr<IHTMLDocument2> & spDoc )
{
    HRESULT hr = S_FALSE;
    do  {
        CComPtr<IHTMLElement> spQueryButton;
        hr = GetQueryButtonInQueryPage( spDoc, spQueryButton);
        CHECKHRPOINTER(hr, spQueryButton);

        hr = spQueryButton->click();
    } while (0);
    return hr;
}           

複制

        如此,我們便實作了自動查詢和自動訂票的功能。