目标:
解决页面加载更多问题。笔记三中,我们只爬取到网页的部分信息,而点击加载更多后的页面内容是没有提取到的。开始我的想法是找到加载更多的数据接口(可参照:http://www.jianshu.com/p/3fdb6ab47aef),但是我又发现一个问题,当我打开一个订阅号页面时,找到数据接口如下图,点击response会发现里面有相应的内容,对其进行解析时得到的内容却是空的,也就是说我得不到页面的信息。而且我发现有些网页的数据接口是加密的,根本访问不到。因此,我又只能换种思路,看能不能模仿浏览器进行点击,等加载更多没有的时候我再去解析页面,是不是就能够获取到所有页面信息呢。我开始找到了PhantomJS这个模块,但是运行的时候速度有点慢,后面和同学一起又找到了一个更好的模块---nightmare。
一、nightmare
1、简单介绍
nightmare是PhantomJS的高级封装,让你能够实现浏览器自动化任务。PhantomJS 是一个基于WebKit的服务器端 JavaScript API。它全面支持web而不需浏览器支持,其快速,原生支持各种Web标准: DOM 处理, CSS 选择器, JSON, Canvas, 和 SVG。PhantomJS可以用于页面自动化,网络监测,网页截屏,以及无界面测试等。下面再贴一段官网介绍。我们可以看到速度是phantomJs的两倍,这正是我们想要的。
2、应用时需要注意的地方
首先肯定是要安装啦,安装介绍我就不废话了,可以参考nightmare官方文档,还有一些常用的api,如type输入操作,click点击操作等等。具体可以参考»。下面我只提一下几个需要注意的地方:
(1)evaluate方法,方法的参数函数的执行环境是浏览器环境,而不是脚本的运行环境,因此,其内部无法访问到脚本内定义的全局变量,但是可以通过evaluate的后续参数,来作为第一参数函数的参数传入,虽说如此,但是!!!这种方法不能传一些特殊对象,比如原型链上的内容,是不会带上的,而且即使说node环境有Date对象,浏览器也有Date对象,也不能直接传入一个Date的实例,传入之后,也用不了Date的内置方法。例如:下面的例子中我将定义的数组arr传入给了evaluate方法。
var arr = [];
nightmare //加载页面 .goto('http://www.baidu.com') //等待选择器加载完毕,可以用数值(表示等待时间,单位毫秒) .wait('body') //浏览器内页面执行 .evaluate(function(arr){ //这里外部的变量hello以参数的形式传了进来,可以用content获得。 var p = document.querySelector('#cp').innerText; arr.push(p); return arr; }, arr) //结束操作 .end() //前面都是操作队列,需要有then方法才会触发执行上述队列的操作 .then(function(res){//函数参数为evaluate的返回值 console.log(res); }) //处理异常情况 .catch(function (error) { console.error('failed:', error); });
(2)在使用 nightmare 的时候,如果在忘记了在代码的最后调用 then 方法,会发现 nightmare 不会执行任何操作。例如执行下面这段代码,在打开浏览窗口之后不会有任何操作结果。
nightmare
.goto('https://github.com')
.wait(1000)
.evaluate(function(){})
.end()
(3)循环利用nightmare ,可以参照:nightmare async operations loops。
(4)如果想利用JQuery解析页面,得在evaluate方法前面利用inject()方法插入本地jquery.js文件,如:
.inject('js', 'jquery.min.js')
二、以音乐为例,模拟加载更多。
打开页面,我们可以发现页面底部有一个加载更多的按钮,而当我们等页面加载完成,加载更多这个按钮便会消失,因而我们可以利用nightmare的wait()方法,等待页面加载完毕后再去进行解析。
通过nightmare的API可以看到wait()方法如下,这说明wait会一直等待,直到返回ture,才会执行下面的操作(默认等待时间30s),因而结合YouTube页面加载的特性,可以考虑如果页面正在加载,我们返回false,如果页面底部还有加载更多,我们模拟点击按钮,同样返回false,直到检查到页面底部没有加载更多按钮时,我们才返回true,此时我们的页面就全部加载完成了,再去对页面进行解析就可以获取到加载更多以后的内容。
新建crawler.js文件,编辑如下代码:
var Nightmare = require('nightmare');
var nightmare = Nightmare({
show: true,
pollInterval: 50
});
nightmare
.goto('https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ')
//.inject('js', 'jquery-2.1.1.min.js')
.wait(function() {
var loadMoreText = document.querySelector('.load-more-text');
if (loadMoreText == null) return true;
if (/hid/.test(loadMoreText.classList.value)) return false;
document.querySelector('.browse-items-load-more-button').click();
return false;
})
.evaluate(function() {
//获取订阅号Id
var $channelName = $('#c4-primary-header-contents .branded-page-header-title a').attr('href');
var $channelId = $channelName.match(/channel\/([a-zA-Z0-9_-]+)/);
var category = [];
$('#browse-items-primary .branded-page-module-title').each(function(){
var $category = $(this).find('a').first();
var item = {
categoryName : $category.text().trim(),
url : 'https://www.youtube.com' + $category.attr('href')
};
//根据URL判断为订阅号还是视频分类
if(item.url.indexOf('list')!==-1){
if(Array.isArray($channelId)){
item.channelId = $channelId[1];
}
}else{
var s = item.url.match(/channel\/([a-zA-Z0-9_-]+)/);
if(Array.isArray(s)){
item.id = s[1];
}
}
//获取youtube某个订阅号下的视频分类
if(item.categoryName!==''&&item.hasOwnProperty('channelId')){
category.push(item);
}
});
return category;
})
.end()
.then(function(result) {
console.log(result);
console.log(result.length);
})
.catch(function(err) {
console.log(err);
})
运行代码后会发现这个订阅号下的所有视频分类都会显示出来,到这里我们的任务就完成了。
三、总结
通过这个简单的例子,我发现了nightmare的强悍之处,但是不适合用于处理大量页面,因为这样运行时间会很长,可以将其用于获取相关列表的url等信息,获取下来后到具体的页面可以采取其他模块,如node-crawler。采用笔记三的思路,我爬取YouTube时,先通过订阅号获取到每个订阅号下面的分类列表,再通过分类列表获取到视频列表,然后再到具体的视频。我通过测试5个订阅号,获取到视频列表大概花去5分钟的时间,视频数量大约为15000个,这个速度还可以。但是当我进入具体视频时,页面就变成了一万多个,解析一个差不多会花掉两秒的样子,这样的话速度太慢了。因而到第三层可以考虑采用其他模块解析,源码请点击。