0%

小说章节合并插件-第五篇

小说合并插件第五篇

本来想着这一篇完成第一个可用的版本, 可惜这个脚本编写耗时超过我的预料,

这次我们主要要解决的问题是第一步;第二部第三步和第四步。

当初我们分析脚本的四大步骤, 这里重新提一下, 如果看不懂不要紧, 看代码就明白了, 当然你已经理解promise和事件的前提下。

  • 获取第一页章节列表, 发出EVT_CHAPTER_MORE事件

  • 获取更多章节列表, 继续发出EVT_CHAPTER_MORE事件, 没有更多发出evt_chapter_done事件

  • 获取小说某个章节第一页内容 发出EVT_CONTENT_MORE事件

  • 获取本章节更多内容 发出evt_content_more事件, 没有更多发出evt_content_done事件

    这里我们要解决chapter_more; content_more和content_done事件, chapter_done留到下次在说, 此外还有一个novel_done事件所有工作完成后发出, 这个也是留到下次分解。

    到这里我们可以发现整个脚本围绕着这五个事件来正常运行的, 五者缺一不可, 这一篇说三个。

    这次基本上整个脚本的主体股价给实现出来, 从此脚本不会有什么大的变化。

    此外这里解决了数据过滤的问题……

    首先定义几个事件, 然后把下载按钮放到对象里, init函数没有变化。

function Novel()
{
// 定义几个有用的事件
const EVT_CHAPTER_MORE = "chapterMore";
const EVT_CONTENT_MORE = "contentMore";
const EVT_CONTENT_DONE = "contentDone";
// 几个有用的属性
this.chapters = new Map(); // 存放小说所有数据, key为章节序号整数类型, value里包含了本章节的所有有用信息, 对象
this.escapeChapters = new Array(); // 保存没有URL的章节标题和序号
this.main = document.createElement("div"); // 作为我们插件的主要节点插入到当前页面的DOM里, 而且所有的事件都绑定在了这个元素上
this.table = document.createElement("table"); // 显示小说章节标题和URL
this.downloadButton = document.createElement("button");
// init函数里
// 触发EVT_CHAPTER_MORE事件, 而且传递小说章节列表的第一页的URL
this.main.dispatchEvent(new CustomEvent(EVT_CHAPTER_MORE, {detail: getTextURL(document, "ablum_read", "正序")}));
}

我们在escapeChapters里存放那些没有URL的章节序号和标题, 等到合适的时候通过其他途径获取这些章节的URL从而获取他们的内容。

第一步第二部获取小说所有章节标题序号和URL

我们知道Novel对象的init函数最后触发了EVT_CHAPTER_MORE事件, 那么如下就是它的事件处理函数,

  // 处理EVT_CHAPTER_MORE事件
this.main.addEventListener(EVT_CHAPTER_MORE, (e) =>
{
download(e.detail)
.then((htmlDoc) =>
{
let dom = document.createElement("div");
dom.innerHTML = htmlDoc;
// 迭代所有的章节
for (let el of dom.getElementsByClassName("chapter")[0].getElementsByTagName("li"))
{
let title = el.innerText;
let url = el.getElementsByTagName("a")[0]?.getAttribute("href") ?? "/"

let chapterNum = parseInt(new RegExp("\\d+").exec(title)[0]);// 获取章节序号, 这是整个脚本一个关键标记
// 如果没有URL加入到escapeChapters里
if (url === "/")
{
this.escapeChapters.push({title, chapterNum});
}
else
{
// 这里触发了CONTENT_MORE事件, 请记住
this.main.dispatchEvent(new CustomEvent(EVT_CONTENT_MORE, {detail: {url, chapterNum}}));
// 在测试阶段应该让循环退出, 否则人家网站看你访问特别频繁, 封锁你的IP就傻眼了, 真正运行的时候需要删掉, 要不然获取的小说都是章节列表的第一个章节, 第1章; 第101章 第201章如此等等。
break; // 以后正文里提到去掉chapter_more循环的break的时候删除这行代码
}
}
let url = getTextURL(dom, "page", "下一页");
// 如果有下一页继续触发EVT_CHAPTER_MORE事件, 相当于自己调用自己, 否则触发EVT_CHAPTER_DONE事件
if (url)
{
this.main.dispatchEvent(new CustomEvent(EVT_CHAPTER_MORE, {detail: url}));
}
// 以后这里加入else语句, 在 里面触发chapter_done事件
})
.catch(alert); // 错误处理需要完善
});

《?.和??》编写跟优雅的代码

首先看双问号

let name = getName() ?? getDefaultName();

如果getName返回无效值就用getDefaultName的值, 这个特性在下面的代码里实际使用过。

?.那更是非常重要, 如果没有.?写这样的代码

let a = document.getElementsByTagName("a")[0];
if (a)
{
result = a.innerHTML;
}

如果用?.的话可以这样写:

result = document.getElementsByTagName("a")[0]?.innerHTML;

好像C#也有了这个特性, Java还没有。

一个?.不知道节约了多少if语句。

第一步和第二部算是这样轻描淡写的搞定了, 接下来看第三步和第四步, 和上面的逻辑相同。

第三步第四步获取小说内容

我们知道CHAPTER_MORE的循环里触发了CONTENT_MORE事件, 测试阶段每一页只触发一次, 那么这就是它的事件处理函数。

  this.main.addEventListener(EVT_CONTENT_MORE, (e) => 
{
let url = e.detail.url;
download(url)
.then((htmlDoc) =>
{
let content = null;
// 如果已经在chapters里存在了就获取, 如果没有的话新增
if (this.chapters.has(e.detail.chapterNum))
{
content = this.chapters.get(e.detail.chapterNum);
}
else
{
content = {pages: []}; // 本章小说所有页内容
this.chapters.set(e.detail.chapterNum, content);
}

let dom = document.createElement("div");
dom.innerHTML = htmlDoc;
// 获取内容
let text = dom.getElementsByClassName("nr_nr")[0]?.innerText + "\n";
// 所谓爬虫和反爬虫下面稍微表现了一下, 具体情况参考正文
for (let el of dom.getElementsByTagName("script"))
{
if (el.innerText.includes(new RegExp("\\d+").exec(url)[0]))
{
text += el.innerText + "\n";
break;
}
}
// 这里过滤了乱码filter函数
content.pages.push(this.filter(text));

url = getTextURL(dom, "nr_title", "下一页");
if (url)
{
// 和上面的函数一样套路, 自己调用自己, 就像递归一样, 当然这个也许就叫事件递归
this.main.dispatchEvent(new CustomEvent(EVT_CONTENT_MORE, {detail: {url, chapterNum: e.detail.chapterNum}}));
}
else if ((url = getTextURL(dom, "nr_title", "下一章")))
{
// 保存下一张的URL, 因为我们知道章节列表当中某些URL是缺失的, 所以这里先保存好, 合适的时机在获取内容
content.nextChapterURL = url;
// 获取章节标题, 当然这句可有可无
content.title = dom.getElementsByTagName("title")[0]?.innerText.split("-")[0] ?? `第${e.detail.chapterNum}张`;
// 发出CONTENT_DONE事件
this.main.dispatchEvent(new CustomEvent(EVT_CONTENT_DONE, {detail: e.detail.chapterNum}));
}
})
.catch(alert);
});

如果你看懂CONTENT_MORE那么也能看懂CHAPTER_MORE, 反之亦然。

对方网站的反扒措施

实际上小说正文的部分内容不在nr_nr里, 而是在一个id为当前章节ID的div元素里, 更妙的是它是动态添加的, 也就是说通过js来添加的,

虽然动态添加的但问题真正的内容直接写在了某个script里了, 没有做更多的处理。 所以便宜了我们, 如果它做其他手段, 我们可能就傻眼了。

现在是铭文, 可以轻而易举的拿下, 那以后就不好说了, 对方网站哪天闲着没事升级一下, 估计我们的脚本玩玩了。

这算是一个小插曲。

后续步骤把小说章节和摘要添加到表格里

我们知道CONTENT_MORE发现没有更多页后, 触发了CONTENT_DONE事件, 如下就是它的事件处理函数, 不完整。

this.main.addEventListener(EVT_CONTENT_DONE, (e) => 
{
let chapterNum = e.detail;
let backChapter = this.chapters.get(chapterNum);
let title = backChapter.title;
let pageCount = `共${backChapter.pages.length}页`;
let summary = backChapter.pages[1].substring(0);
this.updateTable({title, pageCount, summary})
});

我们今天的任务算是差不多了, 下一次继续完善content_done事件, 还要处理CHAPTER_DONE和NOVEL_DONE事件, 如下是更新表格的函数。

// 更新表格
this.updateTable = (item, index = -1) =>
{
// 把章节标题和URL加入到表格里
let row = null;
if (index === -1)
{
row = this.table.insertRow();
}
else
{
row = this.table.insertRow(index);
}
row.insertCell().appendChild(document.createTextNode(item.title));
row.insertCell().appendChild(document.createTextNode(item.pageCount));
row.insertCell().appendChild(document.createTextNode(item.summary));
}

过滤函数, 用正则表达式替换那些乱码和多余的内容, 直接拷贝粘贴就可以了, 我亲手写下的这玩意太让人嫌弃了, 啥时候有空优化一下。

this.filter = (str) => 
{
let reg = new RegExp("<br/>|&nbsp;|www\\.dudu0\\.com|上一章|下一章|上一页|下一页|返回目录|最新网址|关闭+畅\\/读=,看完整内容。本章未完,请点击【|】继续阅读。|请关闭\\-畅\\*读\\/模式阅读。|关闭\\+畅\\/读=,看完整内容。本章未完,请点击【|document.getElementById.+=\\s", "gi");
return str.replace(reg, "\n").replace(/\n{2, }/g, "\n");
}

到此为止我们还生下了如下几个工作, 这个留到下次继续了。

  • 如何判断所有章节下载完成

  • 如何下载那些缺失的章节

  • 如何让用户方便的下载到本地