方法1:通過IE控制項提供的COM介面實現
1、我的程式是基於對話方塊的,不是用的HtmlView,所以要先在對話方塊上放一個IE控制項(Insert ActiveX Control,裡面有一個Microsoft Web瀏覽器),給這個IE控制項起個名字,比如m_ctrlWeb。另外要記著加上<mshtml.h>標頭檔,IE COM介面的東西都在裡面放著。<comdef.h>和<atlbase.h>如果沒有的話也要加上。
2、用ClassWizard加入DownloadComplete事件的響應,這樣網頁下載完了你就可以做填表單之類的工作了。當然你也可以在ProgressChange之類的事件中作這些操作了,不過這樣你就得判斷網頁是不是差不多下載到合適的位置了,要圖省事,直接用DownloadComplete算了。
3、下一步就是用這個控制項開啟目標網頁了,什麼時候開啟你自己看著辦,我是在InitDialog裡面開啟的,代碼如下:
COleVariant vaUrl="http://www.onlytest.net";
m_ctrlWeb.Navigate2(&vaUrl, &vtMissing, &vtMissing, &vtMissing, &vtMissing);
其中那個vtMissing是用作預設參數的。
4、然後就是主要的操作了。這些操作都放在OnDownloadCompleteExplorer裡。為了方便,我寫了幾個函數用來完成特定的功能,在具體說明OnDownloadCompleteExplorer中進行的操作之前,先把這幾個函數解釋一下。
//功能:判斷網頁裡是不是有strName指定的元素
//參數: pobjAllElement:網頁中所有元素的集合
// strName:網頁中元素的id或name
bool HasItem(IHTMLElementCollection *pobjAllElement,CString strName)
{
CComPtr<IDispatch>pDisp;
pobjAllElement->item(COleVariant(strName),COleVariant((long)0),&pDisp);
if(pDisp==NULL)
return false;
else
return true;
}
//功能:在網頁的文字框中輸入字串
//參數: pobjAllElement:網頁中所有元素的集合
// strName:要編輯的文字框的id或name
// strText:要在文字框中寫入的內容
void PutIEText(IHTMLElementCollection *pobjAllElement,CString strName,CString strText)
{
CComPtr pDisp;
pobjAllElement->item(COleVariant(strName),COleVariant((long)0),&pDisp);
CComQIPtr pElement;
if(pDisp==NULL)
{
AfxMessageBox(strName + "沒有找到!");
}
else
{
pElement=pDisp;
pElement->put_value(strText.AllocSysString());
}
}
//功能:提交一個網頁的Form
//參數: pobjAllElement:網頁中所有元素的集合
// strName:可以提交Form的按鈕的id或name(也可以直接Form的submit提交)
void SubmitPage(IHTMLElementCollection *pobjAllElement,CString strName)
{
CComPtrpDisp;
pobjAllElement->item(COleVariant(strName),COleVariant((long)0),&pDisp);
CComQIPtrpElement;
if(pDisp==NULL)
{
AfxMessageBox(strName + "沒有找到!");
}
else
{
pElement=pDisp;
pElement->click();
}
}
//功能:選中網頁中的一個CheckBox (其實就是點擊)
//參數: pobjAllElement:網頁中所有元素的集合
// strName:要選中的CheckBox的id或name
void CheckItem(IHTMLElementCollection *pobjAllElement,CString strName)
{
CComPtr pDisp;
pobjAllElement->item(COleVariant(strName),COleVariant((long)0),&pDisp);
CComQIPtr<IHTMLElement, &IID_IHTMLElement>pElement;
if(pDisp==NULL)
{
AfxMessageBox(strName + "沒有找到!");
}
else
{
pElement=pDisp;
pElement->click();
}
}
使用這幾個函數可以很輕鬆地完成投票操作。下面列出OnDownloadCompleteExplorer中的代碼。
另假設投票頁面為http://www.onlytest.com/vote.htm,資料提交到http://www.onlytest.com /vote2.asp
void CVoteDlg::OnDownloadCompleteExplorer()
{
// TODO: Add your control notification handler code here
IHTMLElementCollection *objAllElement=NULL;
IHTMLDocument2 *objDocument=NULL;
CString strUrl,strTemp;
strUrl=m_ctrlWeb.GetLocationURL();//得到當前網頁的URL
if(strUrl.IsEmpty())
return;
objDocument=(IHTMLDocument2 *)m_ctrlWeb.GetDocument(); //由控制項得到IHTMLDocument2介面指標
objDocument->get_all(&objAllElement); //得到網頁所有元素的集合
//由於所有頁面下載完後都會執行這個函數,所以必鬚根據URL判斷訊息來源網頁
if(strUrl=="http://www.onlytest.com/vote.htm")
{
CComPtr<IDispatch>pDisp;
if(HasItem(objAllElement,"voteform")==true) //voteform為投票選項所在的Form
{
objAllElement->item(COleVariant("voteform"),COleVariant((long)0),&pDisp);
CComQIPtr<IHTMLFormElement , &IID_IHTMLFormElement >pElement;
if(pDisp==NULL)
{
//介面指標擷取失敗,結束程式,不另外作處理,原因見後
EndDialog(IDOK);
return;
}
else
{
//如果投票結果在新視窗開啟,則應該修改網頁代碼,讓結果在本控制項中顯示
pElement=pDisp;
pElement->put_target(CComBSTR("_self")); //等效於target="_self"
pElement->put_action(CComBSTR("vote2.asp"));//等效於action="vote2.asp"
}
CheckItem(objAllElement,"chk2"); //將form中id為chk2的CheckBox選中
SubmitPage(objAllElement,"vote"); //提交網頁,vote為submit按鈕的id或name
}
}
else if(strUrl=="http://www.onlytest.com/vote2.asp")
{
EndDialog(IDOK); //如果投票處理頁面已經下載完畢,則結束程式,原因見後。
}
}
現在票已經投出去了,但看過程式你可能會奇怪,為什麼投完票或中間出了錯要用EndDialog結束程式,而不是繼續投票呢?事情是這樣的,有些網站一個session只能投一票,而一個IE控制項建立好,與伺服器串連後(有了Session),那個Session Key就定好了(一家之言),所以如果繼續用這個IE控制項投票,伺服器會告訴你你已經投過票了(當然如果投票程式寫的笨,沒管這個,那就簡單多了)。這個問題本來我想通過分析WinInet API的運行過程對付的,可是好像很麻煩,所以用了一個很笨的但簡單的方法:投票程式作為一個程式,然後另一個程式調用這個投票程式,投完票後投票程式結束,主程式再次運行投票程式,如此反覆。至於投票程式數量的限制之類的東西,用共用記憶體段是最簡單的(一家之言),具體就不在這裡討論了。
方法2:通過WinInet API來實現表單提交的工作
這種方法實現代碼量很少,而且由於不需要下載太多的無用資料(片等),form所在的頁面也不需要下載,所以效率要高得多,另外實現代碼是一個函數,很適合用線上程中。
用這種方法關鍵是要知道應該給伺服器提交些什麼資料如果自己去看網頁檔案,然後分析應該向伺服器提交什麼資料,網頁很簡單時還差不多,如果網頁很複雜,那就屬於費力不討好的事。現在不是考試,那種事情我們就不做了,現在有一個更簡單的辦法,就是用Win2000下的網路監視器,手工投一票看看向伺服器提交了些什麼資料。這樣我們就可以把那資料中屬於HTTP協議的部分Copy下來。直接從監視器裡拷出來的資料是沒法用的,因為監視器顯示文本的部分把斷行符號換行之類的字元都用小數點代替了,這些部分要先改回原來的斷行符號、換行(HTTP頭部分的就可以不用管了,只要你能分清邊界就可以了)。另外注意,提交資訊中可能會有Content-Length這個資訊,如果你修改了提交資料的內容,而且資料長度發生了變化,Content-Length項的值一定要跟著改。比如Content-Length原來的值是100,資料中有一條資料“1”,你現在改成了“12”,則Content-Length一定要改成101,否則伺服器會返回錯誤。
下面列出的就是投票函數:
UINT Vote(LPVOID)
{
CInternetSession session;
theApp.m_nThreads++; //用來記錄投票線程數的
try
{
CHttpConnection* pConnection =session.GetHttpConnection("www.onlytest.net"); //網站伺服器
CHttpFile* pFile = pConnection->OpenRequest(CHttpConnection::HTTP_VERB_POST,"vote2.asp"); //直接向投票處理頁面提交資料
//下面向提交資料中添加HTTP頭,這些可以由網路監視器得到
pFile->AddRequestHeaders("Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-powerpoint, application/vnd.ms-excel, application/msword, */*");
pFile->AddRequestHeaders("Referer: http://www.onlytest.net/vote.htm");
pFile->AddRequestHeaders("Accept-Language: zh-cn");
pFile->AddRequestHeaders("Content-Type: multipart/form-data; boundary=---------------------------7d11dc24268052c");
pFile->AddRequestHeaders("Accept-Encoding: gzip, deflate");
pFile->AddRequestHeaders("User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)");
pFile->AddRequestHeaders("Content-Length: 1351");
pFile->AddRequestHeaders("Connection: Keep-Alive");
pFile->AddRequestHeaders("Cache-Control: no-cache");
//HTTP頭後面就應該是真正的資料了,下面theApp.m_strFormData中就是要提交的資料,伺服器處理返回的資訊在pFile中
pFile->SendRequest(NULL,0,theApp.m_strFormData.GetBuffer(0),theApp.m_strFormData.GetLength()); //提交所有資料
//其實到這裡投票已經可以結束了,不過你如果想看看成果,完全可以把返回的頁面分析分析,得到些資料
char szBuffer[11001]; //用來存放返回的處理頁面,要多大看實際情況。當然也可以動態分配,不嫌累的話
int nLen=pFile->Read(szBuffer,11000);//讀取返回的內容,其實是投票結果頁面的HTML代碼
szBuffer[nLen]=0;
CString strTemp=szBuffer; //CString雖然濫了些,但用著就是方便,嘿嘿~
pFile->Close(); //資料讀出來後把該關閉的東西都關掉
pConnection->Close();
delete pFile;
delete pConnection;
session.Close();
//下面的代碼就是用來分析HTML代碼來得到你感興趣的資料了,和投票沒什麼關係,就不詳細解釋了
int nPos=strTemp.Find("選項A");
int nTempPos=nPos;
if(nPos==-1)
{
theApp.m_nThreads--;
return 0;
}
nPos=strTemp.Find("table width=100><tr><td align=right>",nPos)+36;
int nEndPos=strTemp.Find("票",nPos);
m_nOurNum=atoi(strTemp.Mid(nPos,nEndPos-nPos));
nPos=strTemp.Find("<tr bgcolor=#DEE6EB><td align=center width=50>1</td>");
nPos=strTemp.Find("table width=100><tr><td align=right>",nPos)+36;
nEndPos=strTemp.Find("票",nPos);
m_nDiff=atoi(strTemp.Mid(nPos,nEndPos-nPos))-m_nOurNum;
m_nVote++;
}
catch(...)
{
}
theApp.m_nThreads--;
return 0;
}
可以看到,關鍵代碼就那麼幾行,如果不分析投票結果,比方法1少多了,而且看著也沒方法1那麼亂。不過這種方法同樣存在方法1說的那個Session重複問題。而且據我嘗試,新開啟線程Session也是重複。所以我估計那個Session Key是根據Process ID決定的(一家之言,歡迎大家討論)。不過如果你同時啟動N個線程,著N個線程都可以成功的把票投進去,而不會說“您已經投過票了”。估計是因為這些資訊是同時提交上去的,伺服器在處理一條資訊時還不知道這個Session其實已經投過票了。是不是這個原因我也不清楚,大家可以討論一下。