async/await是什么?
js一个重大的特点就是异步,但是人类的思维模式天然是同步的。所以要把异步和同步的流程化统一起来,就会有回调中再回调的情况出现。当业务逻辑复杂的时候,回调的嵌套过多,代码复杂度增加,可读性降低,维护起来也复杂,调试也复杂,这就是回调地狱。前端界一直在不遗余力的解决这个问题,于是衍生出了非常多的解决方案:jquery的deferred、es6中的promise/generator、rx系列的rxjs、以及es7的async/await。
爬虫中异步场景非常的多,所以必须选择一些解决方案。由于我使用的node版本为7.7,天生支持async/await,所以就采用了async/await,顺便学习下验证下async/await。
下面借用mdn上的例子来简单介绍一下async/await的用法:
function resolveAfter2Seconds(x) { return new Promise(resolve => { setTimeout(() => { resolve(x); }, 2000); });};var add = async function(x) { var a = await resolveAfter2Seconds(20); var b = await resolveAfter2Seconds(30); return x + a + b;};add(10).then(v => { console.log(v); // prints 60 after 4 seconds.});
简单来说就是,在申明的一个async函数中可以进行对异步函数(返回一个Promise对象)使用await操作符,在异步函数resolve之后再执行下一步。
爬虫开始
首先爬虫采用的第三方模块有request、cheerio,爬取的网站为
先引入相关模块,定义爬取的总页数,创建存放图片的文件夹:
const fs = require('fs');const path = require('path');const request = require('request');const cheerio = require('cheerio');let sumPage = 141; // 定义总页数let baseFolder = path.join(__dirname, '/pics');if(!fs.existsSync(baseFolder)) { fs.mkdirSync(baseFolder);}
然后写一个函数来获取当前页中的所有封面的链接:
// 获取当前页的所有的封面urlfunction getCoverUrlsByPage(currentPage) { let url = 'http://www.mzitu.com/page/' + currentPage; let options = { url: url, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36' } }; return new Promise((resolve, reject) => { request(options, (err, res, body) => { if(err) { reject(err); } let $ = cheerio.load(body, {decodeEntities: false}); let coverUrls = []; $('#pins li > a').each((i, item) => { coverUrls.push({ url: $(item).attr('href'), title: $(item).next().find('a').text() }); }); resolve(coverUrls); }) }) }
由于每个封面下有很多张图片,而每一张图片展示在了一个单独的页面中,所以得获取到所有图片的链接,然后在去链接中爬去到对应图片的地址
function getPicSrcs(href) { let options = { url: href, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36' } }; return new Promise((resolve, reject) => { request(options, async (err, res, body) => { if(err) { reject(err); } let $ = cheerio.load(body, {decodeEntities: false}); let pages = $('.pagenavi a').eq(-2).text(); let srcs = []; for(let i = 1; i <= pages; i++) { let src = await getPicSrc(href + '/' + i); srcs.push(src) } resolve(srcs); }) });}function getPicSrc(href) { let options = { url: href, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36' } }; return new Promise((resolve, reject) => { request(options, (err, res, body) => { if(err) { reject(err); } let $ = cheerio.load(body, {decodeEntities: false}); let src = $('.main-image img').attr('src'); resolve(src); }) })}
这样我们就获取到了所有的图片的地址,下面就是下载图片:
// 下载整个封面中的图片async function downloadPics(srcs, folderName) { for(let i = 0; i < srcs.length; i++) { let filename = './pics/' + folderName + '/' + (i + 1) + '.jpg'; await downloadPic(srcs[i], filename); }}// 下载单张图片function downloadPic(src, filename) { return new Promise((resolve, reject) => { var stream = fs.createWriteStream(filename); request(src).pipe(stream).on('close', () => { resolve(); }) })}
在下载图片之前,我们希望把每个封面的图片单独放在一个以封面名命名的文件夹中,所以还要创建文件夹
function createFolder(name) { let new_name = name.replace(/[\\/:*?"<>|]/g, ''); let filepath = path.join(baseFolder, new_name); if(!fs.existsSync(filepath)) { fs.mkdirSync(filepath); } return new_name;}
最后我们需要把这些函数全部组织在一起:
async function run(currentPage=1) { console.log('开始下载第' + currentPage + '页'); let coverUrls = await getCoverUrlsByPage(currentPage); // 获取到当前页的所有链接 for(let i = 0; i < coverUrls.length; i++) { let item = coverUrls[i]; let folderName = createFolder(item.title); // 创建当前文件夹 // 获取当前封面下的所有图片 let picUrls = await getPicSrcs(item.url); downloadPics(picUrls, folderName); console.log('【' + item.title + '】下载完毕!') } console.log('第' + currentPage + '页下载完毕'); console.log('-------------------------------------'); if(currentPage < sumPage) { run(++currentPage); }}
最后只要执行run函数即可:
run();
问题
爬虫爬去到第二页时就会报错,猜测是网站限制爬虫的一些策略导致的(这种网站爬的人肯定特别多),由于只是随便写写,目的是验证下async/await,就没有纠结这个问题,以后如果有兴趣的话再进行改进。
更新:程序运行过程中经常会报两个异常,一个是连接超时,一个是cheerio解析错误,导致程序中断。因此在涉及这两个问题的地方给程序加上异常处理之后,程序就能够完美运行了~
完整代码可见