如何使用Nodejs爬虫看漫画

栏目: Node.js · 发布时间: 5年前

内容简介:追完动画,刚见到波波,战车这是咋了,啥是镇魂曲啊,怎么就完了,要等周六啊啊啊啊啊啊啊,act3附体,小嘴就像抹了蜜......ヽ(。>д<)p于是想到看漫画版,但网页体验较差,一次只能看一页,一页只有一张图,还不能存缓。
如何使用Nodejs爬虫看漫画

追完动画,刚见到波波,战车这是咋了,啥是镇魂曲啊,怎么就完了,要等周六啊啊啊啊啊啊啊,act3附体,小嘴就像抹了蜜......

ヽ(。>д<)p

于是想到看漫画版,但网页体验较差,一次只能看一页,一页只有一张图,还不能存缓。

╮(╯_╰)╭

行吧,没缓存,我自己做。

大致看了下该页面的结构,做的不错,结构清晰、代码整洁、可读性好,那爬取就很方便了。

初步想法

明确下目标,需要的是任意一本漫画的全部内容,也就是图片src列表,然后批量下载到本地。没想过要爬整站(我是良民),入口就设为漫画的详情页好了,目测需要如下几个步骤:

编号 => 基本信息 => 章节列表 => 页列表 => 图片地址

个人比较熟悉node,插件应有尽有,写起来比较顺手,实现如下:

// 根据编号获取url地址
export async function getIndexUrl(number) {
  let url = `${baseUrl}/manhua/`;
  if (number) url += number;
  return url;
}

// 获取漫画基本信息和章节目录
export async function getData(url) {
  const html = await fetch(url).then(res => res.text());
  const $ = cheerio.load(html, { decodeEntities: false });

  //   获取title
  const title = await $('h1.comic-title').text();

  //   获取简介
  const description = await $('p.comic_story').text();

  //   获取creators
  const creators = [];
  function getCreators() {
    creators.push($(this).find('a').text());
  }
  await $('.creators').find('li').map(getCreators);

  //   获取卷
  const list = [];
  function getVlue() {
    const href = `${baseUrl}${$(this).find('a').attr('href')}`;
    list.push({
      href,
    });
  }
  await $('.active .links-of-books.num_div').find('li.sort_div').map(getVlue);

  //  获取数据
  const data = [];
  function getComicData() {
    const key = $(this).find('td').attr('class') || $(this).find('td a').attr('class');
    const value = key === 'comic-cover' ? $(this).find('td img').attr('src') : $(this).find('td').text();
    const label = $(this).find('th').text();
    data.push({
      key,
      label,
      value,
    });
  }
  await $('.table.table-striped.comic-meta-data-table').find('tr').map(getComicData);

  const detail = await $('article.comic-detail-section').html();

  return {
    title,
    creators,
    description,
    list,
    data,
    detail,
  };
}

// 获取章节内全部页
export async function getPageList(url) {
  const html = await fetch(url).then(res => res.text());
  const $ = cheerio.load(html, { decodeEntities: false });
  const list = [];
  function getPage2() {
    list.push(`${baseUrl}${$(this).attr('value')}`);
  }
  await $('select.form-control').eq(0).find('option').map(getPage2);
  return list;
}

// 获取页内图片地址
export async function getPage(url) {
  const html = await fetch(url).then(res => res.text());
  const $ = cheerio.load(html, { decodeEntities: false });
  const src = await $('img.img-fluid').attr('src');
  return `${baseUrl}${src}`;
}
复制代码

先将写好的步骤组合起来:

async function test(number) {
  const indexUrl = await getIndexUrl(number);
  const data = await getData(indexUrl);

  await Promise.all(data.list.map(async (i, idx) => {
    let pageList = await getPageList(i.href);
    const imgs = [];
    await Promise.all(pageList.map(async (pages, pdx) => {
      await Promise.all(pages.map(async (j, jdx) => {
        let src = await getPage(j);
        imgs.push(src);
      }));
    }));
  }));

  fs.writeFileSync(`../jojo/${number}.json`, JSON.stringify(data));
}
复制代码

嗯,很简单嘛。就是看起来太暴力了,这样不好,别给人服务器太大压力。

串行promise

实现类似Promise.all,但不同点在于,如果是数组直接返回promise,众所周知promise在创建时就已经执行了,不可能串行。于是将数组改写,返回一个可执行函数的队列,在串行方法里去执行,这样就可以实现了。

export function sequence(promises) {
  return new Promise((resolve, reject) => {
    let i = 0;
    const result = [];

    function callBack() {
      return promises[i]().then((res) => {
        i += 1;
        result.push(res);
        if (i === promises.length) {
          resolve(result);
        }
        callBack();
      }).catch(reject);
    }

    return callBack();
  });
}

// 使用方法
sequence([1, 2, 3, 4, 5].map(number => () => new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log(`$${number}`);
    resolve(number);
  }, 1000);
}))).then((data) => {
  console.log('成功');
  console.log(data);
}).catch((err) => {
  console.log('失败');
  console.log(err);
});

复制代码

分组并发

完全串行的话,和正常逐步浏览网页没什么区别,性能是没问题了,但那有什么意义,我们得加快速度。于是想到将200页分组,每10个一组,每组串行执行,组内并行执行,相当于10个用户同时访问网站,这总不可能扛不住吧,如此应该能兼顾效率和性能。

分组就没必要自己写了,使用lodash/chunk和Promise.all即可。

异常处理

考虑到爬取一个章节,至少都是200页,还是并行的,不敢保证中途不出任何意外,基本的异常处理还是要做的。

赶时间,直接将利用race来实现,只要没有结果,一律重试,实践起来还是比较稳的。

完整的爬取方法

async function test(number) {
    const indexUrl = await getIndexUrl(number);
    const data = await getData(indexUrl);

    await sequence(data.list.map((i, idx) => async () => {
    await sleep(Math.random() * 1000);
    let pageList = await getPageList(i.href);

    pageList = pageList
        .map(src => ({ src, index: parseInt(src.substr(42, 3).replace('_', ''), 0) }))
        .sort((x, y) => x.index - y.index)
        .map(({ src }) => src);

    const imgs = [];
    const len = 5;

    // 分len一组,串行访问
    await sequence(chunk(pageList, len).map((pages, pdx) => async () => {
        await sleep(Math.random() * 5000);
        // 以len一组,并行访问
        await Promise.all(pages.map(async (j, jdx) => {
        let src;
        async function race() {
            await sleep(Math.random() * 1000);
            const res = await Promise.race([ getPage(j), sleep(5000) ]);
            if (res) {
            src = res;
            console.log(`拿到src: ${src}`);
            } else {
            console.log('重新加入队列');
            await race();
            }
        }
        await race();
        imgs.push(src);
        }));
    }));

    data.list[idx].list = imgs
        .map(src => ({ src, index: parseInt(src.substr(42, 3).replace('_', ''), 0) }))
        .sort((x, y) => x.index - y.index)
        .map(({ src }) => src);
    }));

    console.log('爬取成功!!!');

    fs.writeFileSync(`../jojo/${number}.json`, JSON.stringify(data));
}
复制代码

批量下载

以上,已经拿到了所需的全部信息。

接下来只需要批量下载即可,这好办,先整一个promise化的下载方法:

function downloadFile(url, filepath) {
  return new Promise((resolve, reject) => {
    // 块方式写入文件
    const ws = fs.createWriteStream(filepath);
    ws.on('open', () => {
      console.log('下载:', url, filepath);
    });
    ws.on('error', (err) => {
      console.log('出错:', url, filepath);
      reject(err);
    });
    ws.on('finish', () => {
      console.log('完成:', url, filepath);
      resolve(true);
    });
    request(url).pipe(ws);
  });
}
复制代码

再如法炮制一套串行并行混合的下载规则:

async function readJson() {
//   await checkPath('../jojo');
  const data = await readFile('../jojo/128.json');
  const json = JSON.parse(data);
  console.log(json.list[0].list.length);

  const cur = 10;
  await sequence(json.list.map((list, idx) => async () => {
    await sequence(chunk(list.list, cur).map((pages, pdx) => async () => {
      await sleep(Math.random() * 1000);
      console.log('正在获取:', `/jojo/${idx + 1}/`, ` 第${pdx + 1}批`);
      await Promise.all(pages.map(async (j, jdx) => {
        const dirpath = `../jojo/${idx + 1}`;
        const filepath = `../jojo/${idx + 1}/${cur * pdx + jdx + 1}.jpg`;
        await checkPath(dirpath);
        async function race() {
          await sleep(Math.random() * 1000);
          const res = await Promise.race([
            downloadFile(j, filepath),
            sleep(20000),
          ]);
          if (res) {
            console.log('下载成功');
          } else {
            console.log('重新加入队列');
            await race();
          }
        }
        await race();
      }));
    }));
  }));
  console.log('获取成功');
}
复制代码

尝试一下,问题不大,图片会按章节下载到对应目录,速度比自己翻快多了,目标达成!

如何使用Nodejs爬虫看漫画

折腾了几个小时,终于又可以愉快地在ipad上看漫画咯,我真是嗨到不行了~

[]~( ̄▽ ̄)~* (~ ̄▽ ̄)~

完整代码: github.com/liumin1128/…

PS1:仅供学习,请在24小时内删除。

PS2:有机会请支持正版。

PS3:整天看动漫,感觉自己不务正业啊。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First HTML and CSS

Head First HTML and CSS

Elisabeth Robson、Eric Freeman / O'Reilly Media / 2012-9-8 / USD 39.99

Tired of reading HTML books that only make sense after you're an expert? Then it's about time you picked up Head First HTML and really learned HTML. You want to learn HTML so you can finally create th......一起来看看 《Head First HTML and CSS》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具