文章中所讨论的技术应用范围其实还是比较广的,除了投票这种比较阴的应用,还可以完成比如自动填写表单、邮箱自动申请、网站注册等功能。
这里提供的方法有两种:
方法1: 通过ie控件提供的com接口实现。这种方法是通过ihtmldocument2接口编辑网页表单(有时得修改网页代码),然后触发一个事件提交网页。 这种方法由于使用了ie控件,打开网页时会下载很多与应用无关的数据,如图片等(当然你可以在ie中把图片之类的选项关掉),效率很低,而且每投一票就得开一个ie控件,如果你的机子差些,等着按reset吧~~。另外,这种方法编码比较多,挺烦。
方法2: 通过wininet api来实现表单提交的工作。这种方法要比前面那种方法高明多了,不但执行效率高,耗资源少,而且具体实现起来也比上一种简单。
下面分别把两种方法介绍一下:
方法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,否则服务器会返回错误。
下面列出的就是投票函数:
{
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其实已经投过票了。是不是这个原因我也不清楚,大家可以讨论一下。
两种程序投票的方法就写到这里了。本人的水平实在一般,所以文章中有错误的话大家千万不要不屑一说啊,把错误指出来大家一起讨论一下。另外文中说到的那个session的问题也希望大家讨论一下。
另外打个小广告:http://www.baizhuang.net,嘿嘿~~