扒一扒node爬虫框架node-crawler
node-crawler目标打造成Node社区最强大和流行的爬虫/内容抽取工具库,且支持生产环境。
特性:
服务端DOM和自动jQuery注入,使用Cheerio(默认)或JSDOM
可配置的连接池大小和重试次数
Control rate limit 控制请求间隔时间
支持设置请求队列优先级
forceUTF8模式可让爬虫处理字符集编码探测和转换
兼容Node 4.x及以上版本
1 基本使用
# npm install crawler
# 01.movie.js
const Crawler = require('crawler');
const c = new Crawler({
//每次请求爬取的时间间隔
rateLimit: 2000,
//最大连接数量
maxConnections: 10,
//爬取成功的回调函数(通用)
callback: (error, res, done) => {
if (error) {
console.log(error);
} else {
const $ = res.$;
const $searchList = $('.search-list>div');
$searchList.map((index,item)=>{
console.log($(item).find('.pic-pack-outer>h3').text())
})
}
done();
}
});
//大片首页
c.queue('https://www.1905.com/vod/list/n_1/o4p1.html');
//微电影 系列电影
c.queue(['https://www.1905.com/vod/list/n_1_c_922/o4p1.html','https://www.1905.com/vod/list/n_2/o3p1.html']);
//纪录片
c.queue([{
uri: 'https://www.1905.com/vod/list/c_927/o3p1.html',
jQuery: true,
//自定义参数:可以在callback中的res.options.parameter中获取
parameter: '参数',
//自定义爬取的回调函数
callback: (error, res, done) => {
if (error) {
console.log(error);
} else {
console.log(res.options.parameter)
const $ = res.$;
const $searchList = $('.search-list>div');
$searchList.map((index,item)=>{
console.log($(item).find('.pic-pack-outer>img').attr('src'))
})
}
done();
}
}]);
2 下载文件
//下载文件
const c2 = new Crawler({
encoding: null,
jQuery: false,
callback: (err, res, done) => {
if (err) {
console.error(err.stack);
} else {
fs.createWriteStream("data\\"+res.options.filename).write(res.body);
}
done();
}
});
c2.queue({
uri: 'https://image11.m1905.cn/uploadfile/2019/0401/thumb_1_150_203_20190401021843670441.jpg',
filename: 'thumb_1_150_203_20190401021843670441.jpg'
});
3 请求预处理
在每次发送请求之前要做的事情可以在preRequest方法中设置(直接发送请求不会触发该方法)
const c = new Crawler({
preRequest: (options, done) => {
//这里的options将会传递给request模块
console.log(options);
//当done方法调用的时候,请求将被发送
done();
},
callback: (err, res, done) => {
if (err) {
console.log(err);
} else {
console.log(res.statusCode);
}
}
});
c.queue({
uri: 'http://www.google.com',
//这里的preRequest将会覆盖new Crawler中定义的preRequest
preRequest: (options, done) => {
setTimeout(() => {
console.log(options);
done();
}, 1000);
}
});
4 直接发送请求
如果想要直接发送请求而不经过Crawler的任务队列,可以使用direct方法(其配置和queue一样)。
crawler.direct({
uri: 'http://www.google.com',
// default to true, direct requests won't trigger Event:'request'
skipEventRequest: false,
callback: (error, response) => {
if (error) {
console.log(error)
} else {
console.log(response.statusCode);
}
}
});
5 常用事件
a) Event: 'schedule'
options
当一个任务被加到计划时触发.
crawler.on('schedule',function(options){
options.proxy = "http://proxy:port";
});
b) Event: 'request'
options
当爬虫准备好发送请求时触发.
如果你想在发出请求之前的最后阶段改变配置,可以监听这个事件。
crawler.on('request',function(options){
options.qs.timestamp = new Date().getTime();
});
c) Event: 'drain'
当队列为空时触发。
crawler.on('drain',function(){
// 执行一些操作,如,释放数据库连接。
db.end(); // 关闭MySQL连接。
});
d) crawler.queue(uri|options)
uri
String
options
将任务加入队列并等待执行。
e) crawler.queueSize
Number
队列数量,该属性为只读。
6 配置指南
你可以将这些配置发给 Crawler()
构造器,让它们成为全局配置,或者自定义 queue()
的请求已覆盖全局配置。
这个配置列表在mikeal的request项目配置的基础上做了扩展,并且会直接发送给 request()
方法。
a) 基本请求配置项
uri
:String
你想爬取的网页链接.timeout
:Number
单位是毫秒 (默认 15000).
支持mikea的request所有配置
b) 回调
callback(error, res, done)
: 请求完成后会被调用res.statusCode
:Number
HTTP status code. E.G.200
res.body
:Buffer
|String
HTTP返回内容,可能是HTML页面、纯文本或XML文档。res.headers
:Object
HTTP请求的返回头res.request
:Request
Mikeal的 Request 的实例,以取代 http.ClientRequestres.request.uri
: urlObject解析后的URL实体res.request.method
:String
HTTP request method. E.G.GET
res.request.headers
:Object
HTTP request headersres.options
: Options配置项$
: HTML和XML的选择器error
: Errorres
: 请求的回应,包括$
和options
done
:Function
回调中要做的事情做完后需要调用这个
c) 计划任务选项
maxConnections
:Number
连接池大小 (默认 10).rateLimit
:Number
每条请求之间的间隔时间,单位毫秒 (默认 0).priorityRange
:Number
可接受的优先级数值,最小为0 (默认 10).priority
:Number
当前请求的优先级 (默认 5).
d) 重试选项
retries
:Number
请求失败后的重试次数 (默认 3),retryTimeout
:Number
重试前等待的时间,单位毫秒 (默认 10000),
e) 服务端DOM配置
jQuery
:Boolean
|String
|Object
如果设置为true,使用cheerio
和默认配置来注入爬取内容。或使用解析选项自定义cheerio
. 当返回false时停止注入jQuery选择器。如果在你的项目中存在内存泄漏,使用一个替代的解析器"whacko"来避免。(默认 true)
d) 字符集编码
forceUTF8
:Boolean
。如果设置为true,爬虫将从HTTP请求头中获取字符集或从HTML中获取meta tag,并且将它转换到UTF8,不用再担心编码问题了(默认 true)incomingEncoding
:String
。当设置forceUTF8: true
时可自行设置接收内容的编码 (默认 null),爬虫就不用自己检测编码了。如incomingEncoding : 'windows-1255'
。
e) 缓存
skipDuplicates
:Boolean
设置为true时,跳过已爬取过的URI,甚至不触发callback()
(默认 false)。不推荐改动,更好的做法是在爬虫之外使用seenreq进行处理。
f) http头
rotateUA
:Boolean
当为true时,userAgent
应该是数组,并进行轮换 (默认 false)userAgent
:String
|Array
, 如果rotateUA
为 false, 但userAgent
是一个数组, 爬虫将使用第一个值。referer
:String
当为真时设置HTTP的 referer headerheaders
: Object Raw key-value of http headers
7.爬取电影(后端渲染)
//1.通过起始地址获取所有分类地址
//2.通过分类地址获取分类地址下的所有影片地址
//3.通过每一个影片地址获取影片的详情数据
const Crawler = require('crawler');
const fs = require('fs')
const c = new Crawler({
//每次请求爬取的时间间隔
rateLimit: 100,
//最大连接数量
maxConnections: 10,
//爬取成功的回调函数(通用)
callback: (error, res, done) => {
if (error) {
console.log(error);
} else {
}
done();
}
});
//获取url下所有的分类数据
function getAllCategories(url) {
return new Promise((resolve, reject) => {
c.queue([{
uri: url,
jQuery: true,
callback: (error, res, done) => {
if (error) {
reject(error)
} else {
const $ = res.$;
const $indexSearchR = $(".search-index-R");
const categoryArr = []
$indexSearchR.map((index, item) => {
const $item = $(item);
const $categories = $item.find("a");
$categories.map((index, category) => {
let href = $(category).attr('href');
if (href == "javascript:void(0);") {
href = $(category).attr('onclick').split("'")[1]
}
const categoryobj = {
name: $(category).text(),
href: href
}
categoryArr.push(categoryobj)
})
})
resolve(categoryArr)
}
done();
}
}]);
})
}
//获取某一个分类下的影片信息
function getMovieByCategoriy(category) {
return new Promise((resolve, reject) => {
c.queue({
uri: category.href,
jQuery: true,
callback: (error, res, done) => {
if (error) {
reject(error)
return;
}
const $ = res.$;
const $searchList = $(".search-list>div");
const movies = []
$searchList.map((index, item) => {
const name = $(item).find('.pic-pack-outer>h3').text()
const href = $(item).find('.pic-pack-outer').attr('href')
movies.push({
name,
href
})
})
resolve(movies)
done();
}
})
})
}
//获取所有分类下的影片信息 async修饰的方法会返回一个Promise
async function getAllMovieByCategories(categoryArr) {
const allMovies = [];
for (let i = 0; i < categoryArr.length; i++) {
let category = categoryArr[i];
let movies = await getMovieByCategoriy(category);
console.log(movies)
allMovies.push(...movies)
}
return allMovies;
}
//获取指定movie(一个)的影片信息
function getMovieMessage(movie){
return new Promise((resolve, reject) => {
c.queue({
uri: movie.href,
jQuery: true,
callback: (error, res, done) => {
if (error) {
reject(error)
return;
}
const $ = res.$;
const $movieInfo = $('.playerBox-info-leftPart');
const name = $movieInfo.find('.playerBox-info-title>.playerBox-info-cnName').text();
const ename = $movieInfo.find('.playerBox-info-title>.playerBox-info-enName').text();
const year = $movieInfo.find('.playerBox-info-title>.playerBox-info-year').text()
const grade = $movieInfo.find('.playerBox-info-title .playerBox-info-grade').html()
const intro = $movieInfo.find('.playerBox-info-intro>#playerBoxIntroCon').text()
const movieInfo = {
name,
ename,
year,
grade,
intro
}
resolve(movieInfo)
done();
}
})
})
}
//获取所有电影的影片信息
async function getAllMovieMessageByMovies(movies){
const allMovieMessages = [];
for (let i = 0; i < movies.length; i++) {
let movie = movies[i];
let movieMsg = await getMovieMessage(movie);
allMovieMessages.push(movieMsg)
console.log(movieMsg)
}
return allMovieMessages;
}
//执行爬取网站请求
getAllCategories("https://www.1905.com/vod/list/n_1/o4p1.html")
.then((res) => {
return getAllMovieByCategories(res)
})
.then((res)=>{
console.log(res,"8888888888888")
return getAllMovieMessageByMovies(res)
})
.then((res)=>{
console.log(res)
})
//在实际爬取过程中,可以先将分类数据爬取入库,然后下次爬取的时候直接读取分类数据,然后进行下一步的爬取
8.反爬虫策略
爬虫面临的最大挑战就反爬虫。在爬网站的内容的时候并不是一爬就可以了,有时候就会遇到一些网站的反爬虫,会经常遇到返回一些404,403或者500的状态码,或者直接返回的是混淆后的密文,根本无法提取出其中的有效信息。下面列举常见的反爬虫方式以及解决方案。
8.1 请求头HTTP-Header屏蔽
Headers反爬虫是最常见的反爬虫策略。由于正常用户访问网站时是通过浏览器访问的,所以目标网站通常会在收到请求时校验Headers中的User-Agent字段,如果不是携带正常的User-Agent信息的请求便无法通过请求。还有一部分网站为了防盗链,还会校验请求Headers中的Referer、COOKIE信息。
遇到了这类反爬虫机制,需要根据实际爬取的网站进行定制化的模拟来符合正常访问,需要工程师去分析判断网站Http-header的验证方式,并针对性地进行调整。
8.2 验证码屏蔽方式
验证码几乎是现在所有站点对付爬虫的手段,验证码包括图像验证码、短信动态密码、语音密码、USB加密狗等方式。简单的验证码可以通过OCR技术实现验证码识别,而遇到复杂的验证机制,依托现有技术很难解决,往往需要人工来进行辅助验证。只有通过平台的验证码后,才能访问到所需要的信息。
8.3 封IP、限制IP访问频率
如果一个固定的IP在短暂的时间内快速大量的访问一个网站,应用后台往往会自动判断是机器爬虫。带来的结果就是直接将这个IP给封了,爬虫程序自然也就做不了什么了。
8.4 动态加载技术实现反爬虫
对于一些动态网页,利用JS动态填充技术,实现内容的动态填充。如果简单解析Http会发现返回的信息为空,而真正有用的信息则影藏在JS文件中,或者通过ajax请求得到。这种情况需要进行完全模拟终端用户的正常访问请求,以及浏览动作,在浏览器端重现用户行为,才能够获取到所需要的信息。
8.5 内容的加密与混淆
数据加密混淆的方式有很多种,国内主流的网站(淘宝、天猫、微博、新浪等)基本都采用了特殊的数据混淆加密的收到,本地数据均为混淆后的数据无法直接采集使用,需要开发人员进行分析,找出混淆的规律,再利用解析器来提取出有效信息。
还有一种加密是服务器端加密,在客户端或者前端解密,比如想抓app端的一些数据,发现接口返回的结果都是加密后的密文,需要反编译apk文件获得源码读源码,找到加密算法、密匙进行反向工程。
8.6 增量与存量内容对比与整合
针对一些长期的爬取需求,会定时进行爬取,而如何有效的爬取出其中新增和更新的内容,以及如何将爬取下来的增量内容与存量的内容进行对比分析、整合形成一个完整数据集。
8.7 不定期的应用变更、上线反爬虫措施
爬虫和反爬虫作为相生相克的死对头,无论爬虫多厉害,都是能被复杂的反爬虫机制发现,从而采用更为先进、难以破解的反爬虫措施。国内主流的网站(淘宝、天猫、微博、京东等)为了防止被爬虫,会不定期地对反爬虫措施进行变更,使得原有的爬虫程序完全失效,开发者也将不得不重新开发一套新的爬虫程序,以适应爬取数据需求。
9.使用代理
使用limiter控制爬取频率。所有提交到limiter的任务都需要遵守rateLimit 和maxConnections 的限制。rateLimit是两个任务之间的最小间隔,maxConnections是最大的并发数。limiters之间是互相独立的。一个通常的用例是为不同的代理设置不同的limiter。另外值得一提的是,当rateLimit设置为非0的值时,maxConnections 的值将被强制为1
const Crawler = require('crawler');
const fs = require('fs')
const c = new Crawler({
//每次请求爬取的时间间隔
rateLimit: 2000,
//最大连接数量
maxConnections: 10
});
c.queue({
uri: 'https://www.1905.com/vod/list/n_1/o4p1.html',
limiter: 'proxy_1',
proxy: 'http://1.181.48.68:3128',
//爬取成功的回调函数(通用)
callback: (error, res, done) => {
if (error) {
console.log(error);
} else {
const $ = res.$;
const $searchList = $('.search-list>div');
$searchList.map((index, item) => {
console.log($(item).find('.pic-pack-outer>h3').text(),"proxy_1")
})
}
done();
}
})
c.queue({
uri: 'https://www.1905.com/vod/list/n_1_c_922/o4p1.html',
limiter: 'proxy_2',
proxy: 'http://129.204.182.65:9999',
//爬取成功的回调函数(通用)
callback: (error, res, done) => {
if (error) {
console.log(error);
} else {
const $ = res.$;
const $searchList = $('.search-list>div');
$searchList.map((index, item) => {
console.log($(item).find('.pic-pack-outer>h3').text(),"proxy_2")
})
}
done();
}
})
c.queue({
uri: 'https://www.1905.com/vod/list/n_2/o3p1.html',
limiter: 'proxy_3',
proxy: 'http://113.195.202.183:9999',
//爬取成功的回调函数(通用)
callback: (error, res, done) => {
if (error) {
console.log(error);
} else {
const $ = res.$;
const $searchList = $('.search-list>div');
$searchList.map((index, item) => {
console.log($(item).find('.pic-pack-outer>h3').text(),"proxy_3")
})
}
done();
}
})
c.queue({
uri: 'https://www.1905.com/vod/list/c_927/o3p1.html',
limiter: 'proxy_4',
proxy: 'http://120.222.17.151:3128',
//爬取成功的回调函数(通用)
callback: (error, res, done) => {
if (error) {
console.log(error);
} else {
const $ = res.$;
const $searchList = $('.search-list>div');
$searchList.map((index, item) => {
console.log($(item).find('.pic-pack-outer>h3').text(),"proxy_4")
})
}
done();
}
})
# 网上的免费代理,绝不大部分都是不能用的,如有需求,可以购买代理
10.爬取音乐(前后端分离)
https://www.vfinemusic.com/music-library
10.1 爬取下载音乐
const Crawler = require('crawler');
const path = require('path')
const fs = require('fs')
const c = new Crawler({
//每次请求爬取的时间间隔
rateLimit: 2000,
//最大连接数量
maxConnections: 10,
});
function getMusicList() {
const musics = []
return new Promise((resolve, reject) => {
c.queue({
jQuery: false,
uri: 'https://www.vfinemusic.com/v1/works',
qs: {
format: 'json',
page: 1,
page_size: 24,
status: "ONLINE"
},
//自定义爬取的回调函数
callback: (error, res) => {
if (error) {
reject(error)
} else {
const results = JSON.parse(res.body).results;
results.map((item) => {
musics.push({
name: item.name,
producer: item.producer.name,
url: item.preview
})
})
resolve(musics)
}
}
})
})
}
const c2 = new Crawler({
//设置encoding为null,不会将服务器返回内容用字符串进行编码
encoding: null,
jQuery: false,
});
function downLoadOneMusic(url, name) {
const extname = path.extname(url);
return new Promise((resolve, reject) => {
c2.queue({
uri: url,
filename: name + extname,
callback: (err, res, done) => {
if (err) {
reject(err);
} else {
fs.createWriteStream("data\\musics\\" + res.options.filename).write(res.body);
resolve()
}
done();
}
});
})
}
async function downLoadAllMusic(musics) {
for (let i = 0; i < musics.length; i++) {
let result = await downLoadOneMusic(musics[i].url, musics[i].name);
console.log("下载" + musics[i].name + "完毕")
}
return "下载完毕"
}
getMusicList()
.then((musics) => {
console.log(musics)
return downLoadAllMusic(musics)
})
10.2 爬取我的订单
const Crawler = require('crawler');
const c = new Crawler({
//每次请求爬取的时间间隔
rateLimit: 2000,
//最大连接数量
maxConnections: 10,
});
/*
注意点:
1.需要使用fiddler抓包工具分析接口
2.借助postman来测试请求
3.借助浏览器来登录注销查看接口地址
主要流程:
1.调用登录接口(post请求),获取用户登录的cookie信息
2.根据登录后的cookie,调用user/profile接口,获取用户信息(包含uuid)
3.在获取订单数据的时候,调用auth接口,根据uuid获取访问的tocken凭证
4.根据tocken凭证和uuid,调用order接口,获取用户对应的订单信息
*/
//模拟登录的方法:返回登录之后的cookies信息
function doLogin() {
return new Promise((resolve, reject) => {
c.direct({
uri: 'https://www.vfinemusic.com/v1/users/login',
method: "POST",
headers: {
"Content-Type": "application/json; charset=UTF-8"
},
body: JSON.stringify({
"email": "[email protected]",
"phone": "",
"password": "abcd1234",
"device_id": "vf1621510105960-39532"
}),
//自定义爬取的回调函数
callback: (error, res) => {
if (error) {
console.log(error);
reject(error)
} else {
resolve(res.headers['set-cookie'])
}
}
})
})
}
let userInfo = {}
//根据cookies获取用户信息(里面包含uuid,后面在获取订单数据的时候需要根据uuid来生成tocken,再根据tocken获取订单数据)
function getUserId(cookies) {
console.log(cookies);
return new Promise((resolve, reject) => {
c.direct({
jQuery: false,
uri: 'https://www.vfinemusic.com/v1/users/profile',
headers: {
"Cookie": `${cookies[0]};${cookies[1]}`,
},
//自定义爬取的回调函数
callback: (error, res) => {
if (error) {
reject(error)
} else {
console.log(JSON.parse(res.body))
userInfo = JSON.parse(res.body)
resolve(JSON.parse(res.body))
}
}
})
})
}
//根据用户信息获取access_tocken
function getAcceccTocken(uuid) {
return new Promise((resolve, reject) => {
c.direct({
jQuery: false,
uri: `https://www.vfinemusic.com/php/v1/auth/?user_id=${uuid}`,
//自定义爬取的回调函数
callback: (error, res) => {
if (error) {
reject(error)
} else {
console.log(JSON.parse(res.body))
resolve(JSON.parse(res.body).access_token)
}
}
})
})
}
//根据access_tocken获取用户的订单数据
function getOrderList(tocken) {
return new Promise((resolve, reject) => {
c.direct({
jQuery: false,
uri: 'https://www.vfinemusic.com/php/v1/get_charge_orders/',
qs: {
user_id: userInfo.uuid,
page: 1,
access_token: tocken
},
//自定义爬取的回调函数
callback: (error, res) => {
if (error) {
reject(error)
} else {
console.log(JSON.parse(res.body))
resolve(JSON.parse(res.body))
}
}
})
})
}
doLogin()
.then((cookies) => {
return getUserId(cookies)
})
.then((userInfo) => {
return getAcceccTocken(userInfo.uuid)
})
.then((tocken)=>{
return getOrderList(tocken)
})