内容简介:追完动画,刚见到波波,战车这是咋了,啥是镇魂曲啊,怎么就完了,要等周六啊啊啊啊啊啊啊,act3附体,小嘴就像抹了蜜......ヽ(。>д<)p于是想到看漫画版,但网页体验较差,一次只能看一页,一页只有一张图,还不能存缓。
追完动画,刚见到波波,战车这是咋了,啥是镇魂曲啊,怎么就完了,要等周六啊啊啊啊啊啊啊,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('获取成功'); } 复制代码
尝试一下,问题不大,图片会按章节下载到对应目录,速度比自己翻快多了,目标达成!
折腾了几个小时,终于又可以愉快地在ipad上看漫画咯,我真是嗨到不行了~
[]~( ̄▽ ̄)~* (~ ̄▽ ̄)~
完整代码: github.com/liumin1128/… 。
PS1:仅供学习,请在24小时内删除。
PS2:有机会请支持正版。
PS3:整天看动漫,感觉自己不务正业啊。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。