判断是否进入预订页面
我们先看一下预订页面的结构(转载请指明出于breaksoftware的csdn博客)
可以见得,这个页面也是嵌入了两个IFrame。关于IFrame的跨域问题,我已经在前一篇文章中讲述了解决办法。
我判断是否是预订页面是通过两个依据:
1 URL是否是http://www.12306.cn/mormhweb/kyfw/
2 是否可以在最里层IFrame中找到class是“table_qr”的元素该元素对应于
具体的查找过程我这儿就不再赘述,我们通过代码来解读
代码语言:javascript复制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;
}
代码语言:javascript复制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位乘车人”,再填写一个……我们用代码说明这个过程。
代码语言:javascript复制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位乘车人”。
代码语言:javascript复制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;
}
填写每个乘客信息的代码是
代码语言:javascript复制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;
}
其中填写姓名的操作很简单,只要找到相应控件,并向该控件中插入文字即可
代码语言:javascript复制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选项则稍微复杂点,其实原理是一致的
代码语言:javascript复制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。其内容也很简单
代码语言:javascript复制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不是指向的是一个图片,而是一个随机地址。
代码语言:javascript复制<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图片。
代码语言:javascript复制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;
}
截屏、识别、输入验证码的逻辑
代码语言:javascript复制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;
}
如果识别的字符数不对,则会认为失败,这样我们会刷新验证码,并重新识别。
代码语言:javascript复制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接口,并修改该接口的一个方法:
代码语言:javascript复制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代码
于是完整的预订流程是
代码语言:javascript复制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;
}
代码语言:javascript复制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;
}