判斷是否進入預定頁面
我們先看一下預定頁面的結構
可以見得,這個頁面也是嵌入了兩個IFrame。關於IFrame的跨域問題,我已經在前一篇文章中講述了解決辦法。
我判斷是否是預定頁面是通過兩個依據:
1 URL是否是
2 是否可以在最裡層IFrame中找到class是“table_qr”的元素該元素對應於
具體的查找過程我這兒就不再贅述,我們通過代碼來解讀
[cpp]
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;
}
[cpp]
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位乘車人”,再填寫一個……我們用代碼說明這個過程。
[cpp]
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位乘車人”。
[cpp]
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;
}
填寫每個乘客信息的代碼是
[cpp]
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;
}
其中填寫姓名的操作很簡單,只要找到相應控件,並向該控件中插入文字即可
[cpp]
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選項則稍微復雜點,其實原理是一致的
[cpp]
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的識別准確率。我列一些以前的處理結果對比圖