有些网页内容显示不全,通常会采取两种方式提供浏览。
分页是通过一个请求,里面带有页面号,类似于http://xxx/doc?page=1
或http://xxx/doc/1/
,根据页面号会显示对应的页面(参考百度搜索)。这种方式的页面只需要通过拼接URL就可以爬取到。
但特殊情况,也有些页面是用ajax请求,拿到了json数据然后动态生成新的页面。这种情况我们也可以抓取json数据并解析,问题不大。
瀑布流一般是指在同一个页面里,滚动到最下方之后,或点击“加载更多”按钮之后,请求新的数据,并追加显示在页面下方(参考朋友圈,或百度图片)。
请求方式可能也是带有页面号的,但也有可能是直接带上将要显示的条目ID。比如说https://securingtomorrow.mcafee.com/。点击”Load more”按钮之后,请求的参数是这样的:
action:filter
visible[]:64695
visible[]:65271
visible[]:65236
visible[]:65209
visible[]:64205
visible[]:64367
visible[]:64405
visible[]:64399
visible[]:64392
visible[]:64345
visible[]:64128
visible[]:64304
visible[]:64297
visible[]:64291
visible[]:64285
visible[]:64279
visible[]:64266
visible[]:64253
visible[]:64165
visible[]:64242
visible[]:64220
visible[]:64077
visible[]:64190
visible[]:64175
visible[]:64168
search:
在猜不到它生成这些ID的逻辑的情况下,我们拼接不出下一页的URL,于是只能采用模拟点击的方式了。
phantomjs是一个不能浏览的浏览器|||-_-。。。它没有界面,但可以通过js代码来控制这个浏览器的行为,模拟拥护的点击、滚动鼠标、输入等操作。
注:phantomjs的浏览器内核是Webkit
不,我们不用phantomjs。我们用更方便的casperjs。
casperjs是一个框架,它在phantomjs的基础上封装了很多好用的操作。其test模块可以很方便地用来做网页的自动化测试。而casper模块本身提供的操作,可供我们方便地操作网页,配合文件系统fs模块,就可以很容易实现爬虫了。
casperjs的安装很容易,把它编译好的二进制文件下下来用就行了。在github https://github.com/casperjs/casperjs 里的bin目录里。但好像还没完……
casperjs依赖于phantomjs,所以还需要安装phantomjs。这个的安装也很容易,下载zip包解压出来就可以用了。下载链接:http://phantomjs.org/download.html
另外要让casperjs找到phantomjs,可以把它们放在一个目录里。。。然后配置好PATH环境变量,就可以在任何地方调用它们了。
为了避免设置环境变量这么高难度的动作,我们直接在casperjs的目录里新建一个baidu.js,然后用编辑器打开。记得把phantomjs的可执行文件也放进来! 然后写下如下代码:
// 创建casper对象
var casper = require('casper').create();
// 打开百度网站
casper.start('http://www.baidu.com/')
// (casper开始运行之后)等半秒钟然后截图存到baidupage.png里
casper.wait(500,function() {
this.capture('baidupage.png');
})
// casper开始运行
casper.run();
然后我们在命令行里执行
casperjs baidu.js
啊?你不知道命令行是啥?那我默认你是windows用户,所以可以再在刚才到目录里新建一个baidu.bat,用编辑器打开,把上面的
casperjs baidu.js
粘贴进去,保存关闭,然后双击baidu.bat。
然后我们就看到出现了一个baidupage.png,打开一看就赫然看到百度到首页赤裸裸地展现在面前啦!
此小节仅代表个人理解,并非真实的实现,如有误欢迎指出!
最基本的casper采用了类似于“命令模式“的设计。在casper.run()之前,casper提供的函数都是(这个”都是”用词不妥)用来添加命令的,并没有真正执行。在run()被调用时,才去一个一个的执行命令。
所以,上面的casper.wait(500, ...)
被调用时,并没有开始等待,而是添加了一个命令,命令的具体行为是等待。一直到casper.run(),执行到这条命令的时候,才开始真正的等待。
我们的目的是加载“点击加载更多”型的网页。当你真正开始想怎么去写的时候,才发现好像不知道怎么让它重复点击了。
拿https://securingtomorrow.mcafee.com/ 来说,它有个Load more按钮,点击之后它的文字会变成Loading。更多内容加载完成之后,它又变回了Load more。
我们可以用casper.click('a.btn.btn-primary')
点击Load more按钮。然后等待它变回Load more,然后再一次点击,然后再等待:
//...
// 用灵活性更高的xpath来拿到"包含Load more字样的按钮"
var xLoadMoreBtn = {type:'xpath',path:'//a[contains(text(), "Load more")]'};
// 点击并等待
casper.then(function() {
this.click(xLoadMoreBtn);
});
casper.waitForSelector(xLoadMoreBtn);
// 点击并等待
casper.then(function() {
this.click(xLoadMoreBtn);
});
casper.waitForSelector(xLoadMoreBtn);
casper.then() {
this.capture('clicked-twice.png');
}
casper.run();
可是我们怎么实现循环呢?结束条件是什么呢?怎么让循环结束呢?这样吗(千万不要运行下面的代码,它会导致死循环):
//...
var xLoadMoreBtn = {type:'xpath',path:'//a[contains(text(), "Load more")]'};
var shouldRun = true;
while (shouldRun) {
casper.then(function() {
this.click(xLoadMoreBtn);
});
casper.waitForSelector(xLoadMoreBtn, null, function onTimeout(){
// 等待超时之后Load more按钮仍然不出现,就退出循环
shouldRun = false;
});
}
casper.then() {
this.capture('clicked-many-times.png');
}
casper.run();
在不知道命令模式的前提下,你可能觉得这样写这是没毛病的。但是它确实会死循环!跟网页无关!
因为调用then的时候,函数里的内容并没有真正开始执行(并没有去点击按钮);而且waitForSelector被调用时,并没有开始去等待元素。也不会出现等待超时!
then和waitForSelector只是在添加命令,并没有去执行命令。
在casper中,添加的动作被称为step(步骤)而非命令。
这个循环会一直不停地添加命令,一直到内存溢出,程序崩溃。
其实在casper.run()
内部已经有一个循环了,只要有命令在,它会一直去执行命令。我们只需要在命令执行完之前,再给它添加新的命令就可以了。
//...
var xLoadMoreBtn = {type:'xpath',path:'//a[contains(text(), "Load more")]'};
function addStep() {
casper.waitForSelector(xLoadMoreBtn, function() {
casper.capture('ok.png');
casper.click(xLoadMoreBtn);
addStep();
})
}
addStep();
casper.run();
因为casper中称为step(步骤),而不是命令,我们从这里开始改口为步骤。
上面的代码中,addStep会添加一个步骤,此时casper队列中只有一个步骤Step1。
casper.run()被调用时,该步骤Step1会执行,具体的动作是等待Load more按钮出现,出现之后对整个网页截图留念,然后点击按钮,并调用addStep()继续添加步骤Step2。
然后Step1执行完毕,casper.run()内部一看,我〇居然还有步骤,然后拿到步骤Step2继续执行。过程跟上述相同不再赘述。
我们是不是忘了结束循环的事了?
并没有。如果等待不到带有”Load more”字样的按钮,等待会超时,于是不会进入我写的回调函数里,也就不会再新增步骤。
casper.run()执行完当前步骤发现不再有待执行的步骤之后,自然会结束程序。
casper 访问https的内容很有可能会出现各种错误。比如说卡在那不动了。比如说莫名其妙就结束了。比如说截图一看发现Forbidden了。
对于前两种错误,一般在运行的时候增加一个参数--ignore-ssl-errors=yes
就可以解决了:
casperjs --ignore-ssl-errors=yes https.js
对于Forbidden的错误,我们需要在casper.create的时候指定一个UserAgent(这个UseAgent的内容,每个浏览器不一样,可以随便百度一个都行,我这个是从我的chrome里复制出来的):
var casper = require('casper').create({
pageSettings: { // 没有userAgent不让访问,真是大坑!
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'
}
});
访问很多网站的时候,经常会有加载google流量分析,google广告等等(被墙)的资源。这些资源有时候对我们来说是不必要的,而加载它们会卡住网页的部分内容,一直到等待超时。这大大降低了我们的抓取速度。
解决这种问题,可以用类似下面的代码,让casper不加载它们:
// 避免加载被墙的资源
casper.on('resource.requested', function(reqData, req) {
if (/facebook|google|twitter|linkedin/.test(reqData.url)) {
req.abort();
}
})
贴上最后的代码:
// 封装一下xpath对象方便调用
function x(xpath) { return { type: 'xpath', path: xpath }; }
var fs = require('fs'); // 文件系统,用来保存最后的HTML
var casper = require('casper').create({
pageSettings: { // 没有userAgent不让访问,真是大坑!
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'
},
logLevel: 'debug',
verbose: true
});
// 避免加载被墙的资源
casper.on('resource.requested', function(reqData, req) {
if (/facebook|google|twitter|linkedin/.test(reqData.url)) {
req.abort();
}
})
casper.start('https://securingtomorrow.mcafee.com/');
function addLoadMoreStep() {
casper.waitForSelector(x('//a[contains(text(), "Load more")]'), function() {
console.log('button shown');
casper.capture('ok.png');
casper.click(x('//a[contains(text(), "Load more")]'));
addLoadMoreStep();
}, null, 20*1000);
}
addLoadMoreStep();
casper.run(function() {
// 执行结束后保存HTML
fs.write("temp.html", this.getHTML(), 'w');
});
我们不仅学会了用casperjs来模拟点击加载瀑布流页面,还简单地复习了解了命令模式。