先来熟悉一下几个框架...为我们的爬虫做一下准备,做成目录的形式,是为了方便阅读,无奈似乎不支持我这样写 。。。QAQ就当成目录形式把,别点了。点了也没用。无奈好像不支持
- 1. cheerio
- 1.1 cheerio 概念
- 1.2 cheerio API
- 1.3 cheerio demo
- 2. superAgent
- 2.1 superAgent 概念
- 2.2 superAgent API
- 2.3 superAgent demo
- 3. eventproxy
- 3.1 eventproxy 概念
- 3.2 eventproxy API
- 3.3 eventproxy demo
- 4. 通过cheerio ,superAgent,eventproxy实现爬虫
cheerio
cheerio 概念
github 地址
cheerio是快速、灵活和精益核心jQuery专门为服务器的实现。实现了jquery的核心子集,完全删除了DOM的操作,cheerio能够解析任何HTML,XML页面。从而真正的实现华丽的API。
特色
语法简介
高效快捷
非常灵活
cheerio API
Selectors
$( selector, [context], [root] )
Attributes
.attr( name, value )
.prop( name, value )
.data( name, value )
.val( [value] )
.removeAttr( name )
.hasClass( className )
.addClass( className )
.removeClass( [className] )
.toggleClass( className, [switch] )
.is( selector )
.is( element )
.is( selection )
.is( function(index) )
Forms
.serialize()
.serializeArray()
Traversing
.find(selector)
.find(selection)
.find(node)
.parent([selector])
.parents([selector])
.parentsUntil([selector][,filter])
.closest(selector)
.next([selector])
.nextAll([selector])
.nextUntil([selector], [filter])
.prev([selector])
.prevAll([selector])
.prevUntil([selector], [filter])
.slice( start, [end] )
.siblings([selector])
.children([selector])
.contents()
.each( function(index, element) )
.map( function(index, element) )
.filter( selector )
.filter( selection )
.filter( element )
.filter( function(index, element) ).not( selector )
.not( selection )
.not( element )
.not( function(index, elem) ).has( selector )
.has( element ).first()
.last()
.eq( i )
.get( [i] )
.index()
.index( selector )
.index( nodeOrSelection )
.end()
.add( selector [, context] )
.add( element )
.add( elements )
.add( html )
.add( selection )
.addBack( [filter] )
Manipulation
.append( content, [content, ...] )
.appendTo( target )
.prepend( content, [content, ...] )
.prependTo( target )
.after( content, [content, ...] )
.insertAfter( target )
.before( content, [content, ...] )
.insertBefore( target )
.remove( [selector] )
.replaceWith( content )
.empty()
.html( [htmlString] )
.text( [textString] )
.wrap( content )
.css( [propertName] )
.css( [ propertyNames] )
.css( [propertyName], [value] )
.css( [propertName], [function] )
.css( [properties] )
cheerio demo
const cheerio = require('cheerio')
const $ = cheerio.load('Hello world
')
$('h2.title').text('Hello there!')
$('h2').addClass('welcome')
$.html()
//=>Hello there!
方法获取和修改属性 .attr
.attr( name, value )
$('ul').attr('id')
$('.apple').attr('id', 'favorite').html()
//=> Apple
.prop(name,value)
获取和设置属性的方法。就只有第一个元素的属性值匹配集。
.serialize() ,返回是一个string类型
$('').serialize()
//=> foo=bar&foo=qux
.serializeArray(),返回是一个数组
$('').serializeArray()
//=> [ { name: 'foo', value: 'bar' } ]
Traversing 遍历
.find(selector)
.find(selection)
.find(node)
$('#fruits').find('li').length
//=> 3
$('#fruits').find($('.apple')).length
//=> 1
$('.pear').parent().attr('id')
//=> fruits
修改DOM结构的方法
.append( content, [content, ...] )
//old
//=>
// - Apple
// - Orange
// - Pear
//
//now we can use 'append'
$('ul').append('plum ');
$.html();
//new
//=>
// - Apple
// - Orange
// - Pear
// - Plum
//
.appendTo( target )
$('Plum ').appendTo('#fruits')
$.html()
//=>
// - Apple
// - Orange
// - Pear
// - Plum
//
.prepend( content, [content, ...] )
$('ul').prepend('Plum ')
$.html()
//=>
// - Plum
// - Apple
// - Orange
// - Pear
//
.prependTo( target )
Insert every element in the set of matched elements to the beginning of the target.
$('Plum ').prependTo('#fruits')
$.html()
//=>
// - Plum
// - Apple
// - Orange
// - Pear
//
.after( content, [content, ...] )
$('.apple').after('Plum ')
$.html()
//=>
// - Apple
// - Plum
// - Orange
// - Pear
//
.insertAfter( target )
$('Plum ').insertAfter('.apple')
$.html()
//=>
// - Apple
// - Plum
// - Orange
// - Pear
//
.before( content, [content, ...] )
$('.apple').before('Plum ')
$.html()
//=>
// - Plum
// - Apple
// - Orange
// - Pear
//
.insertBefore( target )
$('Plum ').insertBefore('.apple')
$.html()
//=>
// - Plum
// - Apple
// - Orange
// - Pear
//
.remove( [selector] )
$('.pear').remove()
$.html()
//=>
// - Apple
// - Orange
//
.html( [htmlString] )
Gets an html content string from the first selected element. If htmlString is specified, each selected element's content is replaced by the new content.
$('.orange').html()
//=> Orange
$('#fruits').html('Mango ').html()
//=> Mango
.text( [textString] )
$('.orange').text()
//=> Orange
$('ul').text()
//=> Apple
// Orange
// Pear
如果操作The "DOM Node" object还有很多API需要慢慢去学习体会
- tagName
- parentNode
- previousSibling
- nextSibling
- nodeValue
- firstChild
- childNodes
- lastChild
superAgent
superagent是nodejs里一个非常方便的客户端请求代理模块,当你想处理get,post,put,delete,head请求时,你就应该想起该用它了
superAgent 概念
superagent 是一个轻量的,渐进式的ajax api,可读性好,学习曲线低,内部依赖nodejs原生的请求api,适用于nodejs环境下.
superAgent API
这是链接,是用Mocha‘s文档自动输出的。下面提供了这个测试文档对应的源文件。
superAgent demo
一个简单的post请求,并设置请求头信息的例子
request
.post('/api/pet')
.send({ name: 'Manny', species: 'cat' })
.set('X-API-Key', 'foobar')
.set('Accept', 'application/json')
.end(function(res){
if (res.ok) {
alert('yay got ' + JSON.stringify(res.body));
} else {
alert('Oh no! error ' + res.text);
}
});
一个请求的初始化可以用请求对象里合适的方法来执行,然后调用end()来发送请求,下面是一个简单的get请求
request
.get('/search')
.end(function(res){
});
请求方式也可以通过参数传递:
request('GET', '/search').end(callback);
node客户端也允许提供绝对路径:
request
.get('http://example.com/search')
.end(function(res){
});
delete,head,post,put和别的http动作都可以使用,来换个方法看看:
request
.head('/favicon.ico')
.end(function(res){
});
delete是一个特列,因为它是系统保留的关键字,所以应该用.del()这个名字:
request
.del('/user/1')
.end(function(res){
});
http请求默认的方法为get,所以就像你看到的,下面的这个例子也是可用的:
request('/search', function(res){
});
设置头字段
设置头字段非常简单,只需调用.set()方法,传递一个名称和值就行:
request
.get('/search')
.set('API-Key', 'foobar')
.set('Accept', 'application/json')
.end(callback);
你也可以直接传递一个对象进去,这样一次就可以修改多个头字段:
request
.get('/search')
.set({ 'API-Key': 'foobar', Accept: 'application/json' })
.end(callback);
query
request
.get('/search')
.query({ query: 'Manny', range: '1..5', order: 'desc' })
.end(function(res){
});
同样支持传递字符串:
request
.get('/querystring')
.query('search=Manny&range=1..5')
.end(function(res){
});
或者字符串拼接:
request
.get('/querystring')
.query('search=Manny')
.query('range=1..5')
.end(function(res){
});
POST/PUT 请求
一个典型的json post请求看起来就像下面的那样,设置一个合适的Content-type头字段,然后写入一些数据,在这个例子里只是json字符串:
request.post('/user')
.set('Content-Type', 'application/json')
.send('{"name":"tj","pet":"tobi"}')
.end(callback)
因为json非常通用,所以就作为默认的Content-type,下面的例子跟上面的一样:
request.post('/user')
.send({ name: 'tj', pet: 'tobi' })
.end(callback)
默认发送字符串,将设置Content-type为application/x-www-form-urlencoded,多次调用将会通过&来连接,这里的结果为name=tj&pet=tobi:
request.post('/user')
.send('name=tj')
.send('pet=tobi')
.end(callback);
Response status
响应状态标识可以用来判断请求是否成功,除此之外,可以用superagent来构建理想的restful服务器,这些标识目前定义为:
var type = status / 100 | 0;
// status / class
res.status = status;
res.statusType = type;
// basics
res.info = 1 == type;
res.ok = 2 == type;
res.clientError = 4 == type;
res.serverError = 5 == type;
res.error = 4 == type || 5 == type;
// sugar
res.accepted = 202 == status;
res.noContent = 204 == status || 1223 == status;
res.badRequest = 400 == status;
res.unauthorized = 401 == status;
res.notAcceptable = 406 == status;
res.notFound = 404 == status;
res.forbidden = 403 == status;
同样可以通过req.abort()来中止请求.
跨域资源共享
.withCredentials()方法可以激活发送原始cookie的能力,不过只有在Access-Control-Allow-Origin不是一个通配符(*),并且Access-Control-Allow-Credentials为’true’的情况下才行.
request
.get('http://localhost:4001/')
.withCredentials()
.end(function(res){
assert(200 == res.status);
assert('tobi' == res.text);
next();
})
eventproxy
世界上本没有嵌套回调,写得人多了,也便有了}}}}}}}}}}}}—— fengmk2
eventproxy 概念
EventProxy 仅仅是一个很轻量的工具,但是能够带来一种事件式编程的思维变化。有几个特点:
- 利用事件机制解耦复杂业务逻辑
- 移除被广为诟病的深度callback嵌套问题
- 将串行等待变成并行等待,提升多异步协作场景下的执行效率
- 友好的Error handling
- 无平台依赖,适合前后端,能用于浏览器和Node.js
- 兼容CMD,AMD以及CommonJS模块环境
eventproxy API
api文档-链接
eventproxy demo
异步协作
此处以页面渲染为场景,渲染页面需要模板、数据。假设都需要异步读取。
var ep = new EventProxy();
ep.all('tpl', 'data', function (tpl, data) { // or ep.all(['tpl', 'data'], function (tpl, data) {})
// 在所有指定的事件触发后,将会被调用执行
// 参数对应各自的事件名
});
fs.readFile('template.tpl', 'utf-8', function (err, content) {
ep.emit('tpl', content);
});
db.get('some sql', function (err, result) {
ep.emit('data', result);
});
all方法将handler注册到事件组合上。当注册的多个事件都触发后,将会调用handler执行,每个事件传递的数据,将会依照事件名顺序,传入handler作为参数。
快速创建
EventProxy提供了create静态方法,可以快速完成注册all事件。
var ep = EventProxy.create('tpl', 'data', function (tpl, data) {
// TODO
});
等同于
var ep = new EventProxy();
ep.all('tpl', 'data', function (tpl, data) {
// TODO
});
重复异步协作
此处以读取目录下的所有文件为例,在异步操作中,我们需要在所有异步调用结束后,执行某些操作。
var ep = new EventProxy();
ep.after('got_file', files.length, function (list) {
// 在所有文件的异步执行结束后将被执行
// 所有文件的内容都存在list数组中
});
for (var i = 0; i < files.length; i++) {
fs.readFile(files[i], 'utf-8', function (err, content) {
// 触发结果事件
ep.emit('got_file', content);
});
}
after方法适合重复的操作,比如读取10个文件,调用5次数据库等。将handler注册到N次相同事件的触发上。达到指定的触发数,handler将会被调用执行,每次触发的数据,将会按触发顺序,存为数组作为参数传入。
持续型异步协作
此处以股票为例,数据和模板都是异步获取,但是数据会持续刷新,视图会需要重新刷新。
var ep = new EventProxy();
ep.tail('tpl', 'data', function (tpl, data) {
// 在所有指定的事件触发后,将会被调用执行
// 参数对应各自的事件名的最新数据
});
fs.readFile('template.tpl', 'utf-8', function (err, content) {
ep.emit('tpl', content);
});
setInterval(function () {
db.get('some sql', function (err, result) {
ep.emit('data', result);
});
}, 2000);
tail与all方法比较类似,都是注册到事件组合上。不同在于,指定事件都触发之后,如果事件依旧持续触发,将会在每次触发时调用handler,极像一条尾巴。
基本事件
通过事件实现异步协作是EventProxy的主要亮点。除此之外,它还是一个基本的事件库。携带如下基本API
- on/addListener,绑定事件监听器
- emit,触发事件
- once,绑定只执行一次的事件监听器
- removeListener,移除事件的监听器
- removeAllListeners,移除单个事件或者所有事件的监听器
异常处理
exports.getContent = function (callback) {
var ep = new EventProxy();
ep.all('tpl', 'data', function (tpl, data) {
// 成功回调
callback(null, {
template: tpl,
data: data
});
});
// 添加error handler
ep.fail(callback);
fs.readFile('template.tpl', 'utf-8', ep.done('tpl'));
db.get('some sql', ep.done('data'));
};
具体详细的内容请看github库中,作者的编写的内容
node中的 异步事件触发:
var ep = EventProxy.create();
db.check('key', function (err, permission) {
if (err) {
return ep.emitLater('error', err);
}
ep.emitLater('check', permission);
});
ep.once('check', function (permission) {
permission && db.get('key', function (err, data) {
if (err) {
return ep.emit('error');
}
ep.emit('get', data);
});
});
ep.once('get', function (err, data) {
if (err) {
return ep.emit('error', err);
}
render(data);
});
ep.on('error', errorHandler);
上面代码中,我们把db.check的回调函数中的事件通过emitLater触发,这样,就算db.check的回调函数被同步执行了,事件的触发也还是异步的,ep在当前事件循环中监听了所有的事件,之后的事件循环中才会去触发check事件。代码顺序将和逻辑顺序保持一致。 当然,这么复杂的代码,必须可以像ep.done()一样通过doneLater来解决:
var ep = EventProxy.create();
db.check('key', ep.doneLater('check'));
ep.once('check', function (permission) {
permission && db.get('key', ep.done('get'));
});
ep.once('get', function (data) {
render(data);
});
ep.fail(errorHandler);
注意事项
- 请勿使用all作为业务中的事件名。该事件名为保留事件。
- 异常处理部分,请遵循 Node 的最佳实践(回调函数首个参数为异常传递位)。
通过以上技术,node实现爬虫
代码示例
// app.js
//使用 eventproxy 控制并发
// 体会 Node.js 的 callback hell 之美
// 学习使用 eventproxy 这一利器控制并发
// 我们需要取出每个主题的第一条评论,这就要求我们对每个主题的链接发起请求,并用 cheerio 去取出其中的第一条评论。
var eventproxy = require('eventproxy');
var superagent = require('superagent');
var cheerio = require('cheerio');
var express = require('express');
var url = require('url');
var app = express();
var cnodeUrl = 'https://cnodejs.org/';
superagent.get(cnodeUrl)
.end(function (err, res) {
if (err) {
return console.error(err);
}
var topicUrls = [];
var $ = cheerio.load(res.text);
$('#topic_list .topic_title').each(function (idx, element) {
var $element = $(element);
var href = url.resolve(cnodeUrl, $element.attr('href'));
topicUrls.push(href);
});
var ep = new eventproxy();
ep.after('topic_html', topicUrls.length, function (topics) {
topics = topics.map(function (topicPair) {
var topicUrl = topicPair[0];
var topicHtml = topicPair[1];
var $ = cheerio.load(topicHtml);
return ({
title: $('.topic_full_title').text().trim(),
href: topicUrl,
comment1: $('.reply_content').eq(0).text().trim(),
});
});
console.log('final:');
console.log(topics);
});
topicUrls.forEach(function (topicUrl) {
superagent.get(topicUrl)
.end(function (err, res) {
console.log('fetch ' + topicUrl + ' successful');
ep.emit('topic_html', [topicUrl, res.text]);
});
});
});
参考资料
[译] SuperAgent中文使用文档 -作者 xuwenmin
github-node-lesson-作者 alsotang