报名机器人
昨天帮人报一个参加日语能力考试的朋友报名,无奈报名比较火暴,到处名额已满,需要不断重试碰运气。想到本科的时候,老师组织我们去上机,结果是帮人报TOFEL的名,那场景,如打仗一般,大家先对表,精确到秒。先把表单的数据填好,等时间一到,马上按提交按钮。早了不行,要重定向回来充填好几项数据耽误时间,晚了也不行,要不就是报满,要不就是服务器负荷太重处理不过来。记得当时前后花了半个小时,每人发了30元的报酬,看了报名还是一种待遇不错的劳动,也许以后会出现不少职业的代报名公司或个人。
手工操作几次,像一般的注册或报名程序一样,没报成功要重定向回来,有几项填写的结果(如密码框)会被清掉,重试还得重填,很麻烦,就决定做一个机器人试试,于是翻出了去年做的一个投票机器人(见《一个简单的投票机器人》)。
看了一下,原来的投票页面做得很业余,就一个ASP页面,没有用户注册和IP限制,完全的匿名投票。而这个报名程序看来是花了一定心思的,有5个(根据页面的代码猜的,因为没有成功,只看到前面的三个)步骤(JSP页面),更麻烦的是几个步骤后会弹出对话框(这大大增加了难度,后面会描述解决办法)。因此不能想投票机器人那样简单的构造页面模拟提交,要想别的办法。
还是轻车熟路,用Delphi的Webbrowser控件开发。
首先就是决定不再在本地生成临时页面,而是直接打开它的页面。因为试过想跳过前面两个无关紧要的步骤而直接提交关键的第三步,但发现它检查Cookie,本地无法生成匹配的Cookie,也无法跳过,只好从头开始。
然后自动填写表单。
为了保证灵活性,表单要填的数据保存在一个XML文件中。
表格 1
读如要填数据的XML文件:
procedure TfrmMain.readInfo;
var
// xmlDom1: TXMLDocument;
Nroot,Nentry,NsubEntry:IXMLNode;
filePath : string;
i:integer;
begin
filePath := ExtractFilePath(Application.ExeName) + 'infor.xml';
try
xmlDom1.LoadFromFile(filePath);
except
on e:exception do
showmessage(e.Message);
end;
Nroot:=xmldom1.DocumentElement;
Nentry:= Nroot.ChildNodes['level'];
level := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['ID'];
id := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['pwd'];
pwd := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['sex'];
sex := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['sr'];
sr := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['cFirstName'];
cFirstName := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['cLastName'];
cLastName := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['eFirstName'];
eFirstName := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['eLastName'];
eLastName := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['question'];
question := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['answer'];
answer := Nentry.NodeValue;
Nentry:= Nroot.ChildNodes['places'];
placeNum := Nentry.ChildNodes.Count;
for i := 1 to placeNum do
begin
places[i] := Nentry.ChildNodes[i-1].NodeValue;
placesID[i] := strtoint(Nentry.ChildNodes[i-1].NodeValue);
end;
end;
用读入的数据填写第一步的表单。
表格 2
procedure TfrmMain.FillForm1;
var
idInput:IHTMLInputElement;
pinInput:IHTMLInputElement;
doc: ihtmldocument2;
begin
doc:=(WebBrowser1.Document as IHTMLDocument2);
idInput := (doc.all.item('user_name',0) as IHTMLInputElement) ;
idInput.value := id;
pinInput := (doc.all.item('pin',0) as IHTMLInputElement) ;
pinInput.value := pwd;
end;
注意填表单的时机,是在页面下载完毕以后,由webbrowser的DocumentComplete事件触发。填完表单后马上模拟点击Submit按钮,进入第二步。
表格 3
procedure TfrmMain.WebBrowser1DocumentComplete(Sender: TObject;
const pDisp: IDispatch; var URL: OleVariant);
var
kk:hwnd;
obj:OleVariant;
doc: ihtmldocument2;
begin
if (stage = 0) then //第一步
begin
FillForm1; //填表单1
stage := stage +1;
doc:=(WebBrowser1.Document as IHTMLDocument2);
obj:=doc.all.item('Submit',0);
obj.click; //点“下一步”按钮
statusBar1.Panels[0].Text := '正在打开报名协议页面。。。';
end
else if (stage = 1) then //第二步
begin
//showmessage(inttostr(stage) + ' WebBrowser1DocumentComplete');
doc:=(WebBrowser1.Document as IHTMLDocument2);
obj:=doc.all.item('Agree',0);
obj.click;
stage := stage +1;
end
else if (stage = 2) then //第三步
begin
FillForm2; //填表单2
TryAgain; //报名一次
timer1.Enabled := true;
timer1.Interval := strtoint(edtInterval.Text) * 1000;
stage := stage +1;
statusBar1.Panels[0].Text := '正在报名。。。';
end
else if (stage >= 3) then
begin
if (isRun) then
begin
TryAgain;
stage := stage +1;
end;
end
end;
第一步后,会弹出一个对话框,确定后才进入第二步,因为只有一次,这里没有处理,由手工点击确定。
第二步比较简单,一个协议条款,直接模拟点击“同意”按钮,进入第三步。
第三步是关键步骤,因为报名信息都在这里填,并提交。首先用读入的XML中的数据填第二个表单,然后提交表单。并启动一个定时器,定时提交表单重复尝试报名。
表格 4
定时器重复报名:
procedure TfrmMain.Timer1Timer(Sender: TObject);
var
doc: ihtmldocument2;
placeSelect:IHTMLSelectElement;
pwd1Input:IHTMLInputElement;
begin
TryAgain;
Application.ProcessMessages;
end;
每次报名希望重试不同的考点,以扩大选择的范围,因此重复时轮流用选择的几个考点报名:
表格 5
procedure TfrmMain.TryAgain;
var
doc: ihtmldocument2;
placeSelect:IHTMLSelectElement;
pwd1Input:IHTMLInputElement;
kk:hwnd;
begin
if (webbrowser1.LocationURL = url3) then
begin
doc:=(WebBrowser1.Document as IHTMLDocument2);
placeSelect := (doc.all.item('tp_cname',0) as IHTMLSelectElement) ;
placeSelect.selectedIndex := placesID[(times mod placeNum) + 1]; //轮流换考点
pwd1Input := (doc.all.item('pwd1',0) as IHTMLInputElement) ;
pwd1Input.value := pwd;
submitForm;
times := times + 1;
statusBar1.Panels[0].Text := '正在第' + inttostr(times) + '次报名。。。';
Application.ProcessMessages;
end
else //报名成功,报名成功,我也不知道成功是什么样子,只好根据页面地址判断
begin
timer1.Enabled := false;
btnReg.Caption := '开始';
isRun := false;
statusBar1.Panels[0].Text := '报名已成功。。。';
showmessage('报名成功!!!!!!!!!!!!');
end;
end;
在重复报名中遇到一个问题:报名不成功会弹出一个对话框,然后页面阻塞在那里,无法进行下去。没有办法,只有再起一个进程,找到IE弹出的对话框,强行关掉。(其实关掉并不是最好的办法,如果能发送一个消息点击确定按钮最好,可惜没找到相关资料)。
表格 6
procedure TfrmMain.NewThread;
var
hThread:Thandle;//定义一个句柄
ThreadID:DWord;
begin
//创建线程,同时线程函数被调用
//messagebox(Handle,'创建线程',nil,MB_OK);
hthread:=CreateThread(nil,0,@ClosePopDialog,nil,0,ThreadID);
if hThread=0 then
messagebox(Handle,'创建线程失败',nil,MB_OK);
end;
function ClosePopDialog(P:pointer):Longint;stdcall;
var
i:longint;
DC:HDC;
kk:hwnd;
begin
while (true) do
begin
if ((frmMain.stage = 3) ) then
begin
kk := findwindow(nil,'Microsoft Internet Explorer');
if kk<>0 then //如果找到IE弹出对话框,就干掉它
begin
setActiveWindow(kk);
sendmessage(kk,wm_close,0,0);
end
end;
sleep(5000);
end;
end;
图表 1 主界面