nodejs|nodejs爬取漫画

起因 同学希望可以下载这个网站的连载漫画,之前爬过小说了,那么这次就来漫画吧,下载下来有快一个G的大小,162部,每部大约有二十几张图片。。
未解决的问题 【nodejs|nodejs爬取漫画】下载一定时间后服务器端不响应,虽然可以通过setTimeout来控制每次发送请求的间隔,但觉得依然指标不治本。基本上下完一部后间隔3s的话下载五十部左右后会挂,需要手动重启。我总共重启了三次
需要解决并解决的问题 异步与同步问题

  • 问题
    因为为了方便按顺序观看,以及防止一次请求过多导致服务器端断开连接,需要选择使用同步方式设计逻辑,但是Nodejs的一大特色就是异步,第一次我没有处理直接异步发送请求,下载了几张图片之后服务器主动断开连接。。
  • 解决
    将最新es6的async-await与Promise结合使用,效果超级棒,完全实现同步,async-await的入门教程请移步这里,至于Promise嘛,我之前也总结过一篇 :-)
  • 注意事项
    使用async-await的文件在用node启动时需要加上 --harmony-async-await 后缀,而且node版本要是 7.0+的才行
无法根据页面获取图片链接
  • 问题
    这个网站似乎是等页面加载完全时在动用脚本来载入图片的,所以使用简单的request模块获取的页面中没有图片链接,而且翻页也是通过脚本进行的,亦无法获取下一张图片的链接
  • 解决
    就事论事,从浏览器界面中获取多个图片链接,寻找链接的命名规律,这样以一个图片链接为入口,就可获取全部图片的链接
项目结构 nodejs|nodejs爬取漫画
文章图片

  • src文件夹用于存放下载后的漫画,具体是这样
nodejs|nodejs爬取漫画
文章图片

  • download.js 脚本主入口
  • download_comic.js 用于下载一册漫画的模块
  • download_img.js 用于下载一页漫画(一张图片),并保存本地的脚本
  • get_list_uri.js 用于获取目录信息(这个模块可有可无,因为下载下的目录对应的书的页面无法获取img的uri的)
使用到的第三方模块
  • request 获取页面
  • cheerio 分析页面
项目源代码与注解
//get_list_uri.js const request = require('request'); const cheerio = require('cheerio'); const fs = require('fs'); const path = require('path'); // 主入口 const main_uri = 'http://www.manhuatai.com/baozoulinjia/'; let getListUri = function (uri) { return new Promise((resolve, reject) => { request(uri, (err, response, body) => { if (err) { reject(err); } console.log(response && response.statusCode); // 解析页面,获取每一部的序号与入口链接 $ = cheerio.load(body); let lis = $('#topic1').children(); let keys = Object.getOwnPropertyNames(lis); // 用于存储漫画各个章节信息 let comics = []; for (let i = 0, length = keys.length; i < length; i++) { let comic = {}; let context = $(lis[i]).text(); let first = ''; if (context.includes('回')) { first = context.split('回')[0]; } else { first = context.split('话')[0]; } comic.id = first.substr(1); if (comic.id === '')continue; comic.uri = main_uri + $(lis[i]).find('a').attr('href'); comics.push(comic); // 根据获取的序号新建文件夹 if (!fs.existsSync(path.join(__dirname, 'src', comic.id))) { fs.mkdir(path.join(__dirname, 'src', comic.id), (err) => { if (err) console.log(err); }) } } // 将章节信息写入一个文件中,存档 fs.writeFile('list.txt', JSON.stringify(comics, null, 2), 'utf8', (err) => { if (err) { console.log(err); } }) resolve(comics); }) }) }; module.exports = getListUri;

// download_img.jsconst fs = require('fs'); const request = require('request'); const download = function(uri, filename){ //新建Promise对象,使其成为可回调函数,方便后面async-await调用 return new Promise((resolve, reject) => { request(uri, (err, response, body) => { if (err || response.statusCode === 404) { reject(err); } else { console.log('reaching to ' + uri); // 将下载内容写入本地文件,参(zhan)考(tie)stackoverflow解答写法 request(uri).pipe(fs.createWriteStream(filename)).on('close', ()=>{ resolve(); }); } }) }) }; module.exports = download;

// download_comic.js // 引入下载图片模块 const downloadImg = require('./download_img'); const reuqest = require('request'); const cheerio = require('cheerio'); const fs = require('fs'); const path = require('path'); /** * @param comicId漫画章节序号 */ let downloadComic = function (comicId) { // 添加async前缀,说明函数内部存在同步写法 return new Promise(async function (resolve, reject){ // 漫画一章的序号,从1开始 let startPoint = 1; // 这是图片链接,嵌入变量的地方为规律所在 let start_url = `http://mhpic.zymkcdn.com/comic/B%2F%E6%9A%B4%E8%B5%B0%E9%82%BB%E5%AE%B6%2F${comicId}%E8%AF%9D%2F${startPoint}.jpg-mht.middle`; let filename = path.join(__dirname, 'src', comicId, '1.jpg'); // 这里的1000其实代表这个是死循环,因为我并不知道一章有多少页,我不断地发出请求,直到请求404,代表到头,跳出循环 for (let i = 0; i < 1000; i++) { try { await downloadImg(start_url, filename); console.log('finish comic ' + comicId + ':' + startPoint); } catch (e) { console.log('****comic: ' + comicId +' finish******'); resolve(); break; } startPoint++; // 重置图片链接与文件名 start_url = `http://mhpic.zymkcdn.com/comic/B%2F%E6%9A%B4%E8%B5%B0%E9%82%BB%E5%AE%B6%2F${comicId}%E8%AF%9D%2F${startPoint}.jpg-mht.middle`; filename = path.join(__dirname, 'src', comicId, startPoint + '.jpg'); } }) }; module.exports = downloadComic;

// download.js // 引入之前写的几个子模块,第三方模块,以及node核心模块 const getListUri = require('./get_list_uri'); const downloadImg = require('./download_img'); const request = require('request'); // 网站主入口 const main_uri = 'http://www.manhuatai.com/baozoulinjia/'; const downloadComic = require('./download_comic'); let download = function () { getListUri(main_uri) .then(async function (comics) { console.log('get list'); //这里是几次下载失败后从最近的没下载的地方开始。。。 // comics = comics.slice(44); // comics = comics.slice(69); // comics = comics.slice(120); let keys = Object.getOwnPropertyNames(comics); let length = keys.length; //遍历获取的目录对象 for (let i = 0; i < length - 1; i++) { let urlKey = ''; let key = keys[i]; //如果id为个位数,那么需要在前面加'0',根据对图片uri分析得出 if (comics[key].id.length === 1) { urlKey = '0' + comics[key].id; } else { urlKey = comics[key].id; } await downloadComic(urlKey); //下载间隙进行短暂停顿,防止服务器不响应(虽然效果不完美 :-( ) await timePause(); } }) .catch((err) => { console.log(err); }) }; // 启动程序! download(); // 对 setTimeout 函数进行Promise封装,使其可以同步调用,跳出事件循环 let timePause = function () { return new Promise((resolve, reject) => { setTimeout(() => { console.log('next begin'); resolve(); }, 3000) }) };

启动
node --harmony-async-await download.js

写在最后
  • 箭头函数不支持async-await!!
  • 箭头函数不支持async-await!!
  • 箭头函数不支持async-await!!
  • Array的变量方法里的函数(高阶函数)不支持async-await!!
  • Array的变量方法里的函数(高阶函数)不支持async-await!!
  • Array的变量方法里的函数(高阶函数)不支持async-await!!

    推荐阅读