用node写个爬虫真的肥肠煎蛋,今天就来玩一下。
物料
首先准备物料。
cnpm i axios
cnpm i cheerio
我们需要准备两个第三方包,一个是axios,用来发送请求的,当然request包也行,看个人喜好了;另一个是cheerio,这货是用来解析dom的,跟jquery的用法一样一样的。
爬虫的实现思路
也就是说,我们通过axios请求过来的html的标签数据,然后用cheerio包来提取我们需要的内容,紧接着我们可以用fs包的流来读取数据,然后写入我们的磁盘,这就是一个完整的爬虫需要做的事情。
预热
先预热一下。
const axios = require('axios');
const cheerio = require('cheerio');
let httpUrl = 'http://www.adoutu.com/picture/list/1';
axios.get(httpUrl).then(res=>{
// console.log(res.data); //获取数据
let $ = cheerio.load(res.data); //将dom节点导入cheerio解析
$('.list-group .list-group-item a').each((i,ele)=>{ // 遍历dom节点
let url = $(ele).attr('href'); //获取a标签的href属性信息
console.log(url);
})
})
实现
预热完毕,开始干活。
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
let httpUrl = 'http://www.adoutu.com/picture/list/1';
axios.get(httpUrl).then(res=>{
// console.log(res.data); //获取数据
let $ = cheerio.load(res.data);
$('.list-group .list-group-item a').each((i,ele)=>{ // 遍历dom节点
let aUrl = $(ele).attr('href'); //获取a标签的href属性信息
parsePage('http://www.adoutu.com' + aUrl);
})
})
async function parsePage(url) {
let res = await axios.get(url);
let $ = cheerio.load(res.data);
let imgUrl = $('.detail-picture img').attr('src');
let urlObj = path.parse(imgUrl);
let ws = fs.createWriteStream(`./img/${urlObj['name']}${urlObj['ext']}`);
axios.get(imgUrl,{responseType:'stream'}).then(res=>{ //指定获取二进制流
res.data.pipe(ws);
res.data.on('close',()=>{
ws.close();
})
})
}
分页
如果有分页,我们可以先获取分页总数,然后循环发请求即可。
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
let httpUrl = 'http://www.adoutu.com/picture/list/1';
spider();
//循环发起请求
async function spider() {
let page = await getNum();
for (let i = 1; i <= page; i++) {
if(i < 5) // 如果page太大,会存在崩溃问题
getData(i)
else
break;
}
}
//获取每页数据
async function getData(page) {
let url = 'http://www.adoutu.com/picture/list/' + page;
console.log(url);
let res = await axios.get(url);
let $ = cheerio.load(res.data);
$('.list-group .list-group-item a').each((i, ele) => { // 遍历dom节点
let aUrl = $(ele).attr('href'); //获取a标签的href属性信息
// console.log(aUrl);
parsePage('http://www.adoutu.com' + aUrl);
})
}
// 获取分页数
async function getNum() {
let res = await axios.get(httpUrl);
let $ = cheerio.load(res.data);
let count = $('.pagination li').length;
// console.log(count);
let pageNum = $('.pagination li').eq(count - 2).find('a').text();
// console.log(pageNum);
return pageNum;
}
//解析并存储数据
async function parsePage(url) {
let res = await axios.get(url);
let $ = cheerio.load(res.data);
let imgUrl = $('.detail-picture img').attr('src');
let urlObj = path.parse(imgUrl);
let ws = fs.createWriteStream(`./img/${urlObj['name']}${urlObj['ext']}`);
axios.get(imgUrl, { responseType: 'stream' }).then(res => {
res.data.pipe(ws);
res.data.on('close', () => {
ws.close();
})
})
}
延迟执行
上面我们发现了一个问题,就是分页过多的情况下,程序就会报错,为什么呢?
因为我们一次性发送的请求太多了,for循环是同步执行的,刷的一下发那么多请求,不挂才怪。怎么办呢?
这问题能难倒你吗?让他延迟执行呗。
不过请注意,这个延迟执行也没那么简单,比如这么写:
async function spider() {
let page = await getNum();
for (let i = 1; i <= page; i++) {
setTimeout(()=>{
getData(i)
},2000);
}
}
...
这么写是不行的,因为这就等于你在事件队列中创建了n个getData()函数,然后等待2s后执行这n个getData()函数,其实还是同时执行的。
那怎么搞?
我们可以让请求拉开距离执行:
async function spider() {
let page = await getNum();
for (let i = 1; i <= page; i++) {
setTimeout(()=>{
getData(i)
},2000*i);
}
}
这样写还是在事件队列建立了n个getData(),不过他们的执行时间不一致了,分别是等待2s、4s…依次类推的执行,这样就拉开了请求之间的距离。
这个等待场景还是蛮常用的哈,写那么多定时器还是很烦人的哈,我们可以将这个逻辑用promise封装起来。
//等待
function sleep(time){
var timer;
return new Promise((resolve,reject)=>{
timer = setTimeout(()=>{
clearTimeout(timer);
resolve('请求延迟'+time)
},time)
})
}
然后我们利用这个等待函数就可以写出比较优雅的代码了:
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
let httpUrl = 'http://www.adoutu.com/picture/list/1';
spider();
//循环发起请求
async function spider() {
let page = await getNum();
for (let i = 1; i <= page; i++) {
await sleep(2000*i) //每个请求等待2s、4s....后执行
getData(i)
}
}
//等待
function sleep(time){
var timer;
return new Promise((resolve,reject)=>{
timer = setTimeout(()=>{
clearTimeout(timer);
resolve('请求延迟'+time)
},time)
})
}
//获取每页数据
async function getData(page) {
let url = 'http://www.adoutu.com/picture/list/' + page;
console.log(url);
let res = await axios.get(url);
let $ = cheerio.load(res.data);
$('.list-group .list-group-item a').each(async (i, ele) => { // 遍历dom节点
let aUrl = $(ele).attr('href'); //获取a标签的href属性信息
// console.log(aUrl);
await parsePage('http://www.adoutu.com' + aUrl);
})
}
// 获取分页数
async function getNum() {
let res = await axios.get(httpUrl);
let $ = cheerio.load(res.data);
let count = $('.pagination li').length;
// console.log(count);
let pageNum = $('.pagination li').eq(count - 2).find('a').text();
// console.log(pageNum);
return pageNum;
}
//解析并存储数据
async function parsePage(url) {
let res = await axios.get(url);
let $ = cheerio.load(res.data);
let imgUrl = $('.detail-picture img').attr('src');
let urlObj = path.parse(imgUrl);
let ws = fs.createWriteStream(`./img/${urlObj['name']}${urlObj['ext']}`);
axios.get(imgUrl, { responseType: 'stream' }).then(res => {
res.data.pipe(ws);
res.data.on('close', () => {
ws.close();
})
})
}