作为一名长期从事Electron应用开发的工程师,最近我完成了一个小说搜索与下载工具的开发。这个项目源于我在使用写作软件时的一个痛点:经常需要在多个小说网站之间切换查找资源,下载过程繁琐且格式不统一。于是,我决定在Electron+Vue3的框架下,打造一个集搜索、下载、格式化为一体的小说工具。
这个工具的核心功能包括:
提示:本项目仅供学习Electron开发和网络爬虫技术使用,请遵守相关法律法规,尊重版权。
我采用了Electron经典的主进程+渲染进程架构:
code复制主进程(Node.js)
├── 网络请求(fetch)
├── HTML解析(Cheerio)
├── 编码转换(iconv-lite)
└── 文件操作(fs)
渲染进程(Vue3)
├── 用户界面
├── 状态管理
└── IPC通信
这种设计有三大优势:
在选择技术栈时,我主要考虑了以下因素:
HTML解析:对比了Cheerio、JSDOM和Puppeteer后,选择了Cheerio。因为它:
编码处理:针对中文网站常见的GBK编码问题,使用iconv-lite:
javascript复制const iconv = require('iconv-lite');
const gbkBuffer = await response.arrayBuffer();
const utf8Text = iconv.decode(Buffer.from(gbkBuffer), 'gbk');
网络请求:使用现代fetch API替代传统的request库:
书源配置是整个项目的核心,我设计了一个灵活的JSON配置方案:
javascript复制{
"id": "xbiqugu",
"name": "香书小说",
"encoding": "gbk",
"search": {
"method": "POST",
"url": "http://www.xbiqugu.la/modules/article/waps.php",
"params": {"searchkey": "{keyword}"},
"list": "#checkform table tbody tr",
"title": "td.even > a",
"author": "td:nth-of-type(3)",
"link": "href"
},
"chapter": {
"list": "#list dl dd a"
},
"content": {
"selector": "#content",
"filters": [
"一秒记住【文学巴士...】",
"www.xbiquge.la...",
"请记住本书首发域名..."
]
}
}
配置项说明:
encoding:处理不同网站的编码问题search:定义搜索页的请求方式和内容提取规则chapter:章节列表页的解析规则content:正文内容的选择器和过滤规则针对小说网站的特点,我封装了一个健壮的请求模块:
javascript复制async function fetchHtml(url, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 25000);
try {
const response = await fetch(url, {
signal: controller.signal,
method: options.method || 'GET',
headers: {
'User-Agent': 'Mozilla/5.0...',
...options.headers
},
body: options.body
});
clearTimeout(timeout);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const buffer = await response.arrayBuffer();
return decodeBuffer(buffer, options.encoding);
} catch (error) {
clearTimeout(timeout);
throw enhanceFetchError(error);
}
}
function decodeBuffer(buffer, encoding) {
if (encoding && encoding.toLowerCase() !== 'utf-8') {
return iconv.decode(Buffer.from(buffer), encoding);
}
return new TextDecoder('utf-8').decode(buffer);
}
关键点:
内容解析分为三个步骤:
列表页解析:
javascript复制function parseBookList(html, config) {
const $ = cheerio.load(html);
return $(config.search.list).map((i, el) => ({
title: $(el).find(config.search.title).text().trim(),
author: $(el).find(config.search.author).text().trim(),
url: resolveUrl(config.baseUrl, $(el).find(config.search.link).attr('href'))
})).get();
}
章节列表解析:
javascript复制function parseChapterList(html, config) {
const $ = cheerio.load(html);
return $(config.chapter.list).map((i, el) => ({
title: $(el).text().trim(),
url: resolveUrl(config.baseUrl, $(el).attr('href'))
})).get();
}
正文清洗:
javascript复制function cleanContent(html, filters) {
let text = html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, '');
filters.forEach(filter => {
text = text.replace(new RegExp(filter, 'g'), '');
});
return text
.replace(/\n{3,}/g, '\n\n')
.trim();
}
主进程与渲染进程的通信采用Electron的IPC机制:
javascript复制// 主进程 (index.js)
ipcMain.handle('novel:search', async (event, { keyword, sourceId }) => {
const source = getSourceConfig(sourceId);
const html = await fetchHtml(buildSearchUrl(source, keyword));
return parseBookList(html, source);
});
// 渲染进程 (NovelDownload.vue)
const searchBooks = async () => {
loading.value = true;
try {
books.value = await window.electron.ipcRenderer.invoke(
'novel:search',
{ keyword: searchText.value, sourceId: selectedSource.value }
);
} finally {
loading.value = false;
}
};
为了提升用户体验,实现了下载进度反馈:
javascript复制// 主进程
async function downloadChapters(chapters, source) {
const total = chapters.length;
for (let i = 0; i < chapters.length; i++) {
const content = await fetchChapter(chapters[i], source);
event.sender.send('novel-download-progress', { current: i + 1, total });
await saveChapter(content);
}
}
// 渲染进程
window.electron.ipcRenderer.on('novel-download-progress', (_, { current, total }) => {
progress.value = Math.round((current / total) * 100);
});
编码问题:
请求被拒绝:
内容结构变化:
并发控制:
javascript复制async function fetchAllChapters(chapters, source, concurrency = 5) {
const results = [];
const queue = [...chapters];
async function worker() {
while (queue.length) {
const chapter = queue.shift();
results.push(await fetchChapter(chapter, source));
}
}
await Promise.all(Array(concurrency).fill().map(worker));
return results;
}
缓存机制:
延迟加载:
智能书源维护:
增强下载功能:
阅读体验优化:
用户提示:
访问控制:
数据管理:
这个项目让我深入理解了Electron的进程通信、网络请求优化和内容解析技术。最大的收获是学会了如何设计灵活可扩展的配置系统,以及如何处理各种边缘情况。对于想要学习Electron和网络爬虫技术的开发者,这是一个很好的实践项目。