如何定制一款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很好检测,那么我们如何判断是否存在查询按钮呢?我们先看一下订票页面的页面特征。

解决跨域问题

        可以见得订票页面内部嵌入了两个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元素。

          获取这个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中的位置是

        于是通过该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;
}

插入开始和停止自动查询按钮         为了在该页面中提供给用于控制开启和关闭自动查询功能的按钮,我插入了两个按钮。如下图

        我们看下”单程“和”返程“按钮的页面结构

        我会在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);
}

        点击停止按钮原理同点击开始按钮原理一致。此处不再赘述。         当用户选择好出发地和目的地及时间后,用户点击查询按钮。并点击“开始”按钮。我们的“人”线程就开始了自动查询操作。 查询是否存在票,有票则预订,无票则再次查询

        当我们执行完一次查询后,我们要查看下搜索结果列表信息中用户选择的车次是否存在票。我们先看一下页面结构

        其查找该节点的方法如下

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;
}

        如此,我们便实现了自动查询和自动订票的功能。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券