如何定制一款12306抢票浏览器——实现自动查询和预订功能

来源:互联网 发布:微信点餐软件 编辑:程序博客网 时间:2024/04/27 20:00

        检查是否进入订票页面

        判断是否进入订票页面,我是确定了两个标准:(转载请指明出于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;}

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