nodejs爬取在线轻小说

    xiaoxiao2021-03-25  114

    起因

    刚在B站看完《约会大作战》,觉得不错,听说还有轻小说,但是苦于找不到资源,后来发现一个网站有,但是提供下载的是一个TXT文件,就是说它把二十多册的内容合在一个TXT中了,看它网站上的目录还是比较清晰的,那我何不自己写一个脚本来按书名下载呢

    网页截图:

    脚本下载效果截图:


    所用到的第三方module

    cheerio 用于分析下载下来的HTML页面源代码

    request 用于下载相应的页面

    iconv-lite 用于对文本进行转码操作(这里用来将gbk转为utf8)


    本地所建的module及项目目录

    getContent.js

    用于获取小说每一章节的内容

    getPage.js

    用于获取(下载)所需求的HTML页面

    getUrlList.js

    用于获取所有章节对应的url,以便于后面分类有序下载,获取相应内容

    writeFile.js (主入口)

    用于将获取的内容写入文件中(保存在src文件夹中)


    所遇到的大问题

    文本编码不同

    那个网站使用的是gbk编码,但是我需要的是utf8编码,这个问题可以使用第三方模块 iconv-lite 来进行处理

    网站服务器无法响应过多请求

    估计那个网站访问的人不算多,所以服务器的性能可能不太高,我计算过,由于nodejs为异步的,所以会同时一次性发送192个请求,如果不进行处理,那么那个服务器最多只能响应170多个,这个可以通过setTimeout来进行分批请求解决

    ps:如果直接从网站下载合在一个txt文件的小说的话,也是比较慢的


    代码解析

    // getPage.js const request = require('request'); const iconv = require('iconv-lite'); module.exports.getPage = function (url, callback) { // 第二个参数很重要,因为默认的为utf8 request(url, {encoding: null}, (err, response, body) => { if (err) throw err; // 转码 **非常重要,否则乱码** const result = iconv.decode(Buffer.concat([body]), 'gbk'); // callback 用于返回获取HTML页面 callback(result); }); }; // getContent.js const cheerio = require('cheerio'); const fs = require('fs'); const getPage = require('./getPage'); module.exports.getContent = function (contentUrl, callback) { // 使用上一个自定义模块的方法,根据指定的url获取HTML文件 getPage.getPage(contentUrl, (content) => { // 对页面进行一些操作, // 内容在id为content的div中 $ = cheerio.load(content); $('#contentdp').remove(); callback($('#content').text()); }); };

    // getUrlList.js const getpage = require('./getPage'); const cheerio = require('cheerio'); // 小说章节的url地址 const enterUrl = 'http://www.wenku8.com/novel/1/1143/'; module.exports.getUrlList = function (callback) { getpage.getPage(enterUrl, (mainPage) => { $ = cheerio.load(mainPage); // 目录为table式布局,这里先找到所有<tr>标签 // 包含的内容有以下三种 // 1. 书的标题 // 2. 章节标题,包含在<a>标签中,含有其url地址 // 3. 插图, 和上相同 let result = $('table').find('tr'); // 用于存储各个书所有的章节url let bookList = {}; // 用于存储书名 let key = ''; result.each((i, elem) => { let $hrefTag = $(elem).find('a'); // 如果不是第一种情况,也就是不是标题 if ($hrefTag.length !== 0) { $hrefTag.each((i, elem) => { // 一个章节的信息,包含章节名与url let bookCharInfo = {}; // 如果不是第三种情况,也就是不是插图 if ($(elem).text().trim() !== '插图') { // 存储章节名 bookCharInfo.charName = $(elem).text().trim(); // 存储章节对应的url bookCharInfo.charUrl = enterUrl + $(elem).attr('href'); // 存入对应的书的数组中(见 else 语句) bookList[key].push(bookCharInfo); } }); } else { // 如果是书的标题 if ($(elem).text().trim() !== ' ') { key = $(elem).text().trim(); bookList[key] = []; } } }); // 返回以json格式存储的书的信息 callback(JSON.stringify(bookList, null, 2)); }); };

    返回的json数据如下

    // writeFile.js const fs = require('fs'); const getUrlList = require('./getUrlList'); const path = require('path'); const getContent = require('./getContent'); getUrlList.getUrlList((data) => { // 解析json格式的数据 const bookList = JSON.parse(data); // 获取所有的书名,用于后面的遍历与创建文件夹 let bookListKeys = Object.keys(bookList); // 由于前面提到的第二个请求数问题,这里分为三波进行下载 let s1 = bookListKeys.slice(0, bookListKeys.length / 3); let s2 = bookListKeys.slice(bookListKeys.length / 3, (bookListKeys.length/3)*2); let s3 = bookListKeys.slice((bookListKeys.length/3)*2, bookListKeys.length); s1.forEach((bookName, key) => { // 根据书名创建相应的文件夹 fs.mkdir('./src/' + bookName, (err) => { // 根据章节名获取内容写入文件 bookList[bookName].forEach((chapter, key) => { getContent.getContent(chapter.charUrl, (data) => { // 这里写的原因是:书名中会包含'/'对路径的生成有影响,所以对其进行转义 fs.writeFile(path.join(__dirname, 'src', bookName, (chapter.charName).replace(/\//g, ':') + '.txt'), data, (err) => { if (err) throw err; // 输出完成的章节名 console.log("finish: " + chapter.charName ); }); }); }); }); }); // 后面分别在30s后与60s后再次发起请求,不过好像可以再缩减短一点 s2.forEach((bookName, key) => { fs.mkdir('./src/' + bookName, (err) => { if (err) throw err; setTimeout(() => { bookList[bookName].forEach((chapter, key) => { getContent.getContent(chapter.charUrl, (data) => { fs.writeFile(path.join(__dirname, 'src', bookName, (chapter.charName).replace(/\//g, ':') + '.txt'), data, (err) => { if (err) throw err; console.log("finish: " + chapter.charName ); }); }); }); }, 30000); }); }); s3.forEach((bookName, key) => { fs.mkdir('./src/' + bookName, (err) => { if (err) throw err; setTimeout(() => { bookList[bookName].forEach((chapter, key) => { getContent.getContent(chapter.charUrl, (data) => { fs.writeFile(path.join(__dirname, 'src', bookName, (chapter.charName).replace(/\//g, ':') + '.txt'), data, (err) => { if (err) throw err; console.log("finish: " + chapter.charName ); }); }); }); }, 60000); }); }) });

    总结

    正好清明节没事干就折腾了一下,碰到很多坑,不过也是蛮有意思的,代码美中不足的就是出现了恐怖的 ‘回调金字塔’ ,后面会用Promise进行修改

    转载请注明原文地址: https://ju.6miu.com/read-8209.html

    最新回复(0)