1. 项目背景与核心需求
去年接手一个电子书管理工具的开发需求时,我发现市面上现成的书源接口要么收费昂贵,要么稳定性堪忧。作为一个常年混迹技术社区的老码农,我决定自己动手实现一个集成搜索和下载功能的本地化解决方案。这个项目前后迭代了三个版本,从最初的so-novel爬虫到现在的51mazi多源聚合,踩过的坑比读过的书还多。
核心要解决两个痛点:
- 跨平台运行能力(Windows/macOS/Linux)
- 多书源聚合与智能择优下载
Electron作为跨平台桌面应用框架天然契合第一个需求,而Node.js强大的网络爬虫生态则完美支持第二个目标。整个方案的技术栈非常"前端友好":用Electron构建界面,Cheerio做HTML解析,Puppeteer处理动态渲染,再配合简单的文件系统操作就组成了完整工具链。
2. 技术架构设计
2.1 整体架构分层
mermaid复制graph TD
A[Electron主进程] --> B[IPC通信]
B --> C[渲染进程UI]
A --> D[Node.js爬虫服务]
D --> E[书源适配层]
E --> F[Cheerio/Puppeteer]
D --> G[下载队列管理]
(注:实际开发中移除了mermaid图表,改用文字说明)
系统分为三个主要模块:
- 前端交互层:基于React+Ant Design的Electron渲染进程
- 核心服务层:在主进程运行的Node.js服务,包含:
- 书源管理(当前集成7个主流书站)
- 请求调度(自动切换UA/IP避免封禁)
- 下载引擎(支持断点续传)
- 数据持久层:使用lowdb实现本地JSON存储,记录搜索历史/下载进度
2.2 关键设计决策
为什么选择混合爬虫方案?
- 静态站点用Cheerio:速度快(比Puppeteer快3-5倍)
- 动态渲染用Puppeteer:兼容SPA类书站
- 示例:51mazi需要执行JS生成目录,而so-novel可直接解析HTML
下载调度算法演进:
javascript复制// 第一版:简单顺序下载
async function naiveDownload() {...}
// 现版:基于网络质量的智能调度
class DownloadScheduler {
constructor() {
this.speedTestResults = new Map();
}
async selectBestSource() {
// 综合评分 = 速度权重×0.6 + 稳定性权重×0.4
}
}
3. 核心实现细节
3.1 书源适配器模式
定义统一接口规范:
typescript复制interface BookSourceAdapter {
search(keyword: string): Promise<BookItem[]>;
download(bookId: string): Promise<DownloadTask>;
getDetail(bookId: string): Promise<BookDetail>;
}
具体实现示例(so-novel):
javascript复制class SoNovelAdapter {
async search(keyword) {
const html = await axios.get(`https://so-novel.com/search?q=${encodeURIComponent(keyword)}`);
const $ = cheerio.load(html);
return $('.book-list li').map((i, el) => ({
id: $(el).attr('data-bid'),
title: $('.title', el).text().trim(),
author: $('.author', el).text().trim(),
source: 'so-novel'
})).get();
}
}
3.2 反爬对抗实践
常见防御手段处理方案:
| 防御类型 | 解决方案 | 实现示例 |
|---|---|---|
| UserAgent检测 | 轮换UA池 | headers: {'User-Agent': getRandomUA()} |
| IP频率限制 | 代理IP池 + 请求间隔控制 | axios.interceptors 添加延迟 |
| 验证码 | 人工打码+本地缓存 | 调用第三方打码平台API |
| 数据混淆 | 动态解析JS生成的内容 | Puppeteer执行完整页面环境 |
实测有效的延迟策略:
javascript复制// 在axios拦截器中实现
axios.interceptors.request.use(async (config) => {
if (config.isBookRequest) {
await new Promise(resolve =>
setTimeout(resolve, 1000 + Math.random() * 2000)
);
}
return config;
});
4. 性能优化实战
4.1 内存泄漏排查
使用Electron时最容易忽略主进程与渲染进程的通信泄漏。通过以下方式定位问题:
- 在DevTools的Memory面板创建堆快照
- 对比操作前后的内存差异
- 发现滞留的IPC监听器未被销毁
解决方案:
javascript复制// 错误示例
ipcMain.on('search', (event, keyword) => {...});
// 正确做法
function createSearchHandler() {
const controller = new AbortController();
ipcMain.on('search', (event, keyword) => {
if (controller.signal.aborted) return;
// ...
});
return {
dispose: () => controller.abort()
};
}
4.2 下载加速技巧
通过并行下载分卷提升速度:
- 解析章节列表时先获取所有分卷URL
- 使用p-limit控制并发数(通常3-5个)
- 合并下载后的分段文件
javascript复制const limit = pLimit(3);
const downloads = chapterUrls.map(url =>
limit(() => downloadChapter(url))
);
await Promise.all(downloads);
fs.writeFileSync('full.txt',
downloads.map(d => fs.readFileSync(d.path)).join('\n')
);
5. 成品效果与扩展方向
最终实现的功能矩阵:
- 多关键词联合搜索(支持作者+书名组合)
- 下载质量排序(按文件大小/格式优先)
- 本地书架管理(自动去重)
实测数据:
- 平均搜索响应时间:1.2s(冷启动)/0.4s(缓存命中)
- 下载速度:2-5MB/s(依赖网络环境)
- 内存占用:主进程常驻约120MB
未来可扩展:
- 接入Calibre实现格式转换
- 添加WebDAV同步支持
- 开发浏览器插件版
重要提示:所有书源适配器应实现自动禁用机制,当连续请求失败超过阈值时暂时禁用该源,避免影响整体性能。