最近一直在用Selenium这个开源项目写一些web 自动化的小玩意。本来一直运行的挺好,直到有一天突然发现资源抓取失败了,翻看日志才发现,原来本该正常打开的页面返回了504错误所以自然失败了。如何避免这种情况呢?事实上对于Selenium提供的RemoteWebDriver 来说,一般都是采用下面两种方式来打开网页:
1 using (var _driver = new PhantomJSDriver()) 2 { 3 _driver.Navigate().GoToUrl("http://www.cnblogs.com/"); 4 //或 5 _driver.Url = "http://www.cnblogs.com/"; 6 }
然而这两种方式都没有提供相应的API或返回值来判断网页是否打开, 所以通常还需要加上一个判断看看是否出现了指定的页面元素才能最终确定页面是否打开:
1 try 2 { 4 _driver.FindElementById("main-content"); 5 Console.WriteLine("Page opened."); 6 } 7 catch (NoSuchElementException) 8 { 9 Console.WriteLine("Page opened failed."); 10 }
如果需要对所有Open的页面进行判断的话,整个代码结构就会显得很乱,所以我只是简单的使用了URL进行判断,只要_driver返回的URL和我要前往的URL一致的话,就认为页面打开成功了。但实际上,很多情况下,http出现错误的时候页面并不会redirect
因此URL也不会发生改变。既然不能通过URL进行判断,那么久通过http状态码来判断吧?简单又方便。
然而,等我翻遍RemoteWebDriver的文档才发现,RemoteWebDriver本身并不提供获取Http状态码的API,并没有。。。。。没有。。。。
这简直就是。。。。。。。。。悲剧呀~~~~~~~~~~~~~~
不过好在大路走不通,咱还可以走小路,经过一番耐心细致的google,还是被我找到了一条曲线救国的方法,就是下面这条:
注:本方法只适用于 PhantomJSDriver, 其他的driver, 如:ChromeDriver, FireFoxDriver并不能用(当然也许也可以,但是我确实没有试成功)
PhantomJSDriver本身是对 PhantomJS的封装,而PhantomJS是一个基于 WebKit 的服务器端 JavaScript API。它全面支持web而不需浏览器支持,没有UI界面,使用起来也比其他带UI的WebDriver快速。在它的官网上可以看到PhantomJS提供的一系列API, 而我们要使用的就是其中一个:onResourceReceived, 这个API的详细介绍可以戳这里, 可以看到在这个回调中,返回的参数是一个response结构体,而其中的response.status 就是我们需要的状态码啦。
var webPage = require('webpage'); var page = webPage.create(); page.onResourceReceived = function(response) { console.log('Response (#' + response.status + ', stage "' + response.stage + '"): ' + JSON.stringify(response)); };
好了,思路已经有了,下来就是怎么实现。要在PhantomJSDriver里使用上面这段JavaScript就需要用到ExecutePhantomJS()这个方法啦,这也是为什么我说这个方法只适用于PhantomJSDriver, 因为这个方法是PhantomJSDriver单独提供的。。。。
怎么用呢,请看下面这段代码, 我们用博客园的首页来试试,看看都加载了那些资源吧。
using (var _driver = new PhantomJSDriver()) { const string phantomScript = "var page = this; page.onResourceReceived = function(response) {console.log('***** ' + response.id + ' ***** ' + response.url + ' ***** ' + response.stage + ' ***** ' + response.status + ' ***** ' + response.statusText + ' *****');};"; _driver.ExecutePhantomJS(phantomScript); _driver.Navigate().GoToUrl("http://www.cnblogs.com/"); }
我们把每一个获取到的response都打印出来看看,还真不少。注意看,有一些response出现了两遍, 对应的response.stage分别是start和end,这是指什么呢?官网上也给出了解释:stage : "start", "end" (FIXME: other value for intermediate chunk?)
当然对于我们来说,不需要这么多,只需要知道http://www.cnblogs.com/这个URL本身的状态,因此我们简化一下上面的代码, 过滤到状态不是end和URL不是http://www.cnblogs.com/的内容。
using (var _driver = new PhantomJSDriver()) { const string phantomScript = "var url = 'http://www.cnblogs.com/';" + "var page = this; page.onResourceReceived = function(response) {if (response.stage !== \"end\" || response.url != url) return; console.log('##### ' + response.id + ' ##### ' + response.url + ' ##### ' + response.stage + ' ##### ' + response.status + ' ##### ' + response.statusText + ' #####'); };"; _driver.ExecutePhantomJS(phantomScript); _driver.Navigate().GoToUrl("http://www.cnblogs.com/"); }
这下看上去就简洁多了吧。
走到这里,我们已经完成了需求的一大半,成功的获取到了Http的状态码,但是只是打印到console里面可不行,怎么能在代码中取到这个值呢?重定向console输出然后解析文本?No,No,No,我们还需要更优雅的方法。
实际上,对于ExecutePhantomJS()这个方法本身来说是可以有返回值的,例如下面的代码执行之后script的值是2。
string phantomScript = "return 1+1;"; var script = _driver.ExecutePhantomJS(phantomScript); _driver.Navigate().GoToUrl("http://www.lepan.cc/down2-932746.html");
因此我最开始的想法很简单,直接把response返回出来不就好了?就像下面这段代码:
using (var _driver = new PhantomJSDriver()) { const string phantomScript = "var url = 'http://www.cnblogs.com/';" + "var page = this; page.onResourceReceived = function(response) {if (response.stage !== \"end\" || response.url != url) return; return response; };"; var result= _driver.ExecutePhantomJS(phantomScript); _driver.Navigate().GoToUrl("http://www.cnblogs.com/"); }
然而运行之后才发现,result的值始终是null,为啥?原来我们忘记了onResourceReceived 是个回调函数,代码运行到page.onResourceReceived = function(response) {if (response.stage !== \"end\" || response.url != url) return; return response; };的时候就直接返回了。所以,此路不通,为之奈何?看来又只能曲线救国了。看看下面这段代码:
using (var _driver = new PhantomJSDriver()) { const string phantomScript = "var url = 'http://www.cnblogs.com/';" + "var page = this; page.onResourceReceived = function(response) {if (response.stage !== \"end\" || response.url != url) return; page.tag = response;};"; _driver.ExecutePhantomJS(phantomScript); _driver.Navigate().GoToUrl("http://www.cnblogs.com/"); var result = (Dictionary<string, object>) _driver.ExecutePhantomJS("var page = this; return page.tag;"); Console.WriteLine(result["status"]); }
JavaScript是动态语言,我们就可以利用这一点,给page动态的创建一个tag属性,然后把response赋值过去,在页面加载完成后把tag取回来,这样就可以获取到Http的状态码啦。接下来只需要判断result["status"]的值是不是等于200, 就知道页面是否正确打开了。
最后,除了onResourceReceived之外, PhantomJS还提供了很多其他很有用处的API,下一回我们聊聊利用onResourceRequested这个API来进行简单的资源过滤来实现一个基本的AD Block功能。谢谢大家。