用Fiddler和JScript捕获网页
因为要写篇分析报告,需要反复从网页里提取数据,因此作了些http和网页捕获方面的研究。下面把过程回顾一下,做个总结、以利于下次工作的提高。
1、开始的时候准备用VB编一个网页自动循环下载软件,所以去下载了VS2008和MSDN。选VB是考虑到能方便地过渡到excel的宏VBA,写数据分析论文不可能不用到excel,没道理画个图、作个统计都要自己编程吧。而且excel本身也具备从网页导入数据的功能,如果可能的话,让办公室里的小妹帮个忙,能省不少功夫!
2、然后又搜索http捕获、分析教程和工具,准备照版煮碗,仿照开发一个。
其中Fidder比较有吸引力,许多人提到它可扩展性很强。
打开Fiddler的网站一看,几个大大的"Building Extensions with C#, VB.NET, Managed C++",用C、VB扩展?!
顿时眼睛一亮,窃喜、相见恨晚。
3、不过还是犯了先入为主的毛病,走了弯路,一根筋的考虑VB,不停的在VS的IDE界面和Fiddler之间来回编译、切换。
后来发现Fiddler是直接支持脚本的,可以用JScript直接写脚本。而且在它的《FiddlerScript CookBook》(大概可以翻译成《随身手册》吧)里有各种情况下的功能扩展举例,很容易上手。
这下方便多了,完全抛开VS、VB,效率大大提高,写完脚本马上运行,哪里用得着编译那么麻烦。
倒是MSDN起了很大作用,我之前完全没学过JScript,好在现在的开发工具之间差异很小,MSDN里每个例子都有不同开发语言的版本,对照看,触类旁通,很容易理解。
Fiddler的功能和使用方法我不介绍了,网上很容易找到,我提供一个链接,一个CSDN网友贡献的《Fiddler工具使用说明》。
我用的是Fiddler2.2.2,下面说说我做的功能扩展。
另外我只说说开发时的思路和落脚点,编码就不放上来了。
前者是因为Fiddler的网站和论坛提供的资料太详尽了,大家多半只是不知道该怎么下手,把思路放上来给个启发、避免弯路我想足够了。
后者是因为每个人的需求千差万别,根本不可能简单复制。当然必要的编码我还是会放的。
一、准备工作
MSDN之类的开发指引不用说了。
Fiddler如果要调用外部动态链接库,例如.NET,应先通过菜单Tools——Fiddler Options——Extensions——References指示路径。
Fiddler功能扩展脚本文件是CustomRules.js,默认路径“我的文档”——Fiddler2——Scripts,可以用文本工具直接编辑。如果通过菜单Rules——Customize Rules(或者Ctrl-R)直接调用Fidder的脚本编辑工具Fiddler ScriptEditor更好,编辑完了保存时会自动重载,立马生效,很方便。
脚本分两大块。
第一块是import,不用多说,申明准备调用的动态链接库。
第二块是class Handlers,处理方法。自定义变量、常数、类型、函数、方法都放在这里,是下一步的主要工作空间。
其中有两个函数:
static function OnBeforeRequest(oSession: Session);
static function OnBeforeResponse(oSession: Session);
刚开始的时候我理解有问题,以为是在用户请求之前、响应给用户之前。我就奇怪了,请求之前?我还没下指示,你知道干什么?等着不就得了。响应之前?既然准备回应了,就是已经收到请求了,那还不赶快照指示做、还要干别的?
想想不对劲,回过头仔细研究《FiddlerScript CookBook》,转念一想会不会是Fiddler请求之前、Fiddler响应之前,用户提交请求、Fiddler截获、Fiddler把请求提交给服务器之前,或者Fiddler收到服务器响应、提交给用户之前。
去Fiddler论坛找版主讨教,果然如此。估计刚接触Fiddler无从下手的朋友多半也会卡在这里。
这下所有关节全部打通,所有要求软件自动完成的功能都可以插在这两个函数里。
于是整体设想这样:
1、巡航。
人工操作浏览器在目标网页范围内巡航。
2、标识。
客户/服务器对答增加自定义标识,Fiddler实时监视客户端和服务器之间的对话,把疑似符合既定模式的对话标识出来。
3、触发。
人工精确判断、刷选对话,设置参数、循环变量、转存路径,触发Fiddler提取对话中的请求体,改造、重新编码,再重发服务器。这个功能由鼠标右键触发。
4、循环。
改造Fiddler接收模块,放开浏览器,Fiddler直接分析服务器应答,符合要求的,下载存盘、调用压缩工具,进入下一个循环;或者,超时则重新请求;
上面1、3是用户操作,由用户触发。2、4是软件操作,自动触发。
二、自定义对话标识
Fiddler已经给对话定义了很多标识,用户还可以增加自己的标识,详细说明见Fiddler对话标识。
这里又有个难点,Fiddler截获的原始请求和应答都是HTTP编码字符串,用户无法直接识别,必须反向解码、还原成用户可识别字符,方便于其它处理。反过来,如果要把用户请求发给服务器,就要按HTTP规范编回码去。
关于各种编码的理论渊源我也很糊涂,但大体意思应该如下,不对之处请高手指教。
用户编码,用户在浏览器里看到的字符,有不同的编码,例如GB、UTF-8等。
应用协议,软件之间有自己的沟通方式,它们也不能直接处理用户编码,要把用户数据编码成软件之间的数据,才能被它们利用。例如HTTP、FTP协议,有各自编码规则。
然后是网络协议,例如Winsock、TCP/IP。再往下还有网卡驱动程序、机器码等。这些协议我们不用去管它,Fiddler已经把它们解码回HTTP编码了。
编码不光数据转换,还包括压缩、打包,添加必要的标识和指令等。解码则相反。
Fiddler夹在浏览器和网络之间,已经自带了一些编码、解码函数,再结合微软.NET提供的大量编码转换函数,完全可以实现不同协议、编码之间自由、任意转换。
例如我的一段代码,oSession表示Fiddler捕获的一个对话实例:
oSession.utilDecodeRequest(); //请求体解除chunked编码,我推测是转成了二进制流。
var oRBB = Encoding.UTF8.GetString( oSession.requestBodyBytes ); //把解码后的字符串按utf8编码返回。
var sRequestBody = HttpUtility.UrlDecode( oRBB, Encoding.UTF8 ); //按url标准返回字符串。
HTTP传输消息时都要chunked-encoding,什么意思大家自己查吧,我解释不了,我只会盯着变量,跟踪、监控,然后对着样版来。
Encoding.UTF8.GetString是.NET的System.Text里的函数,HttpUtility.UrlDecode是.NET的System.Web里的函数,所以调用前先要在脚本文件CustomRules.js的最前面加上:
import System.Text;
import System.Web;
请求体(sRequestBody)包含目标资源路径和参数,据此能判断对话“主题类型”。我做了些正则表达式模版,每个请求和这个模版比较,相符合的,加标识;否则,略过。(JScript的正则表达式有它自己的特点,我准备另外撰文写点体会。)
判断标识是否存在和增加一个标识的函数分别是:
oSession.oFlags.ContainsKey( "用户标识");
oSession.oFlags.Add( "用户标识");
增加了用户标识后,用oSession.RefreshUI()刷新一下,标识就显示出来了。
注意:
1、可以只给符合要求的对话加标识。不符合要求的对话,不光没有标识值,甚至连这个标识都可以没有。
2、增加自定义标识前应判断该标识是否已经存在,否则会引发脚本异常终止。如果存在只需重新赋值。
我把“主题类型”识别放在了onBeforeRequest函数里,请求一提交就标识主题。又在onBeforeResponse函数里放置了长度、日期等应答特征的的识别,收到应答就标识出来。
三、用户自定义界面
定义了用户标识,形成对话的内在属性,还要把它绑定到屏幕介面、显示出来给用户看到,方法见Fiddler配置列。我比较喜欢使用其中的这种方法:
public static BindUIColumn("HTTPMethod")
function CalcMethodCol(oS: Session){
if (null != oS.oRequest){
return oS.oRequest.headers.HTTPMethod;
}else{
return String.Empty;
}
}
简单明了,Fiddler刷新的时候就有显示了。
上面例子里if块的{}括号是我加的,Fiddler自己的例子没有用{}括号。我试过不加,但Fiddler不是总能正确识别,不知道为什么。于是干脆都加了。读代码时也省点脑精。
另外鬼子的习惯跟咱们中国人可能真的很不同,Fiddler作判断时都把运算量(常量)放逻辑运算符前面、被运算量(变量)放后面,例如上面if ( null != oS.oRequest ),我全部按自己习惯颠倒过来,没有任何问题。
这段代码和下面鼠标右键菜单都可以放在class Handlers块内任何地方。
四、自定义定义鼠标右键菜单
在设想3中,计划用鼠标右键菜单实现人工选择对话,触发Fiddler重新请求网页。《FiddlerScript CookBook》里context-menu样版如下:
public static ContextAction("Open in Firefox")
function DoOpenInIE(oSessions: Fiddler.Session[]){
if (null == oSessions){
MessageBox.Show("Please choose at least 1 session."); return;
}
for (var x = 0; x < oSessions.Length; x++){
System.Diagnostics.Process.Start("firefox.exe", oSessions[x].url);
}
}
我下载的时候还是“Open in IE”,现在已经是火狐狸了,Fiddler没闲着啊!题外话。
右键菜单被激发时,会自动提交当前选择对话作为参数。注意这里还作了参数校验,以防还没有选择对话就调用该函数。
我因为只提取网页里的数据,不关心网页的显示,除了刚开始巡航到目的网页,后面都没必要打开浏览器,所以从这里开始由Fiddler直接请求网页。《FiddlerScript CookBook》里提交请求的方法是这样的:
FiddlerObject.utilIssueRequest(sRequest);
采用这个方法用户要自己组合sRequest串,添加HTTP头,我搞不懂,我从Fiddler论坛找到另一个方法:
FiddlerApplication.oPorxy.InjectCustomRequest(oSession.oRquest.headers, oSession.requestBodyBytes, true, false );
我记得Fiddler版主说过,InjectCustomRequest后两个参数现在没有任何意义,但让我照写。
oSession就用当前选择的对话,headers不变,什么cookie、session之类的,照搬原样。其实这也就是我标识、选择对话的原因,费事去搞headers了。
重发请求可能要调整参数。和前面定义用户标识一样,先解码被选择对话的请求体,随便采用什么方法置换sRequestBody里的参数,然后再重新编码、注入oSession,一鼓作气方法是:
oSession.utilSetRequestBody( HttpUtility.UrlEncode( sRequestBody, Encoding.UTF8));
HTTP请求体中用符号&和=分隔、赋值参数,有特殊含义。但HttpUtility.UrlEncode编码的时候并不知道自己是在给参数编码,一视同仁,结果&和=也被编了码。因此还要把请求体中的&和=置换回来。
with ( oSession ){
utilReplaceInRequest( '%3d','=');
utilReplaceInRequest( '%26','&');
}。
当然这样做存在着把本该作为参数值的&和=也置换回来的风险。好在到目前为止我的工作中还没有出现这种情况,等以后有必要时再考虑吧。
五、应答下载
应答存盘之前也要解码,方法是:
oSession.utilDecodeResponse();
然后就可以直接存盘了:
oSession.SaveResponseBody( sFileName );
如果要再调用外部软件对下载文件进行处理,例如压缩,方法是:
Utilities.RunExecutableAndWait( sExecute, sParams);
注意sExecute可以包含路径,但某些软件可能不支持带空格的路径,建议强制给路径加上引号。这个方法和上面那个InjectCustomRequest都是Fiddler自带,用VS的对象浏览器打开Fiddler,可以找到这两个方法。
好了,终于写完了,长长舒口气吧!
还有些东西没写清楚吧,不管了,先放上来接受一下检验,等有了更多的思路、理解再补充吧。