前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何定制一款12306抢票浏览器——处理预订页面和验证码自动识别功能

如何定制一款12306抢票浏览器——处理预订页面和验证码自动识别功能

作者头像
方亮
发布2019-01-16 10:35:32
9070
发布2019-01-16 10:35:32
举报
文章被收录于专栏:方亮方亮方亮

判断是否进入预订页面

        我们先看一下预订页面的结构(转载请指明出于breaksoftware的csdn博客)

        可以见得,这个页面也是嵌入了两个IFrame。关于IFrame的跨域问题,我已经在前一篇文章中讲述了解决办法。

        我判断是否是预订页面是通过两个依据:

        1 URL是否是http://www.12306.cn/mormhweb/kyfw/

        2 是否可以在最里层IFrame中找到class是“table_qr”的元素该元素对应于

        具体的查找过程我这儿就不再赘述,我们通过代码来解读

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;
}
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位乘车人”,再填写一个……我们用代码说明这个过程。

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位乘车人”。

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

        填写每个乘客信息的代码是

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

        其中填写姓名的操作很简单,只要找到相应控件,并向该控件中插入文字即可

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选项则稍微复杂点,其实原理是一致的

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。其内容也很简单

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不是指向的是一个图片,而是一个随机地址。

<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图片。

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

截屏、识别、输入验证码的逻辑

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

        如果识别的字符数不对,则会认为失败,这样我们会刷新验证码,并重新识别。

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接口,并修改该接口的一个方法:

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代码

        于是完整的预订流程是

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;
}
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;
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2013年01月29日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 判断是否进入预订页面
  • 插入用户信息,并设置相应的选项
  • 验证码的自动识别
  • 如何保存验证码图片
  • 截屏、识别、输入验证码的逻辑
  • 屏蔽Alert
相关产品与服务
文字识别
文字识别(Optical Character Recognition,OCR)基于腾讯优图实验室的深度学习技术,将图片上的文字内容,智能识别成为可编辑的文本。OCR 支持身份证、名片等卡证类和票据类的印刷体识别,也支持运单等手写体识别,支持提供定制化服务,可以有效地代替人工录入信息。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档