news 2026/9/12 1:56:37

在 Electron 里造一个「搜书 + 下载」:从 so-novel 到 51mazi 的爬虫实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 Electron 里造一个「搜书 + 下载」:从 so-novel 到 51mazi 的爬虫实践

🔍 在 Electron 里造一个「搜书 + 下载」:从 so-novel 到 51mazi 的爬虫实践

一句话推荐:在 Electron + Vue 3 里实现「搜书名 → 选书源 → 一键下载到本地」的完整方案,含多书源配置、Cheerio 解析、GBK 编码、正文去广告与 IPC 踩坑实录,附源码与 GitHub 链接。

💡适合谁读:做 Electron / Vue 桌面应用、想接「网页抓取」或「多数据源聚合」的开发者;对 so-novel、小说下载、Cheerio 爬虫感兴趣的读者。本文从产品界面到主进程实现、从书源配置到踩坑实录一条龙讲清,文末附完整代码地图与 GitHub 链接,欢迎 Star 与 Fork,仅供学习与合规使用。

📌 阅读收获:Electron 主进程里用 fetch + Cheerio 做多书源抓取的完整思路;书源配置驱动、GET/POST/编码/选择器的落地写法;Vue 响应式对象经 IPC 传递时的「对象无法克隆」问题与解决方案;正文去广告的 filterTxt 设计。

📋 目录

  • 📸 功能长什么样:一图看懂
  • 💡 需求从哪来:so-novel 的启发
  • 🏗️ 整体架构:谁在干活、谁在展示
  • ⚙️ 书源配置:把「规则」搬进 JavaScript
  • 🛠️ 抓取三板斧:请求、编码、解析
  • 🧹 正文清洗:filterTxt 与广告说再见
  • 🐛 踩坑实录:超时、编码、以及「对象无法克隆」
  • 🎨 前端体验:进度、取消与免责
  • 🎉 小结与 GitHub

📸 功能长什么样:一图看懂

在 51mazi 里,从首页侧栏进入「下载小说」,你会看到:顶部悬浮栏(返回首页 + 页面说明)、书源选择与搜索框、搜索结果列表,以及选中某本书后的目录信息与操作按钮。

搜索「火影」后的结果:多书源列表、选中书籍的章节数与「下载并加入书架 / 导出为 TXT / 取消」操作 — 全部在写作软件内完成,无需切浏览器

流程很简单:选书源 → 输入书名或作者 → 搜索 → 点某本书的「下载」→ 拉取目录后选择「下载并加入书架」或「导出为 TXT」。下面从实现角度拆解:规则从哪来、谁在请求、谁在解析、以及我们踩了哪些坑。文末有完整代码地图与 GitHub 链接,方便你直接对照源码或给项目点个 Star。


💡 需求从哪来:so-novel 的启发

so-novel 是一个用 Java 写的小说搜索与下载工具,核心思路是:用 JSON 配置描述每个书源的搜索方式、列表/目录/正文的 CSS 选择器,再写一套通用引擎去「按规则抓」。我们不想重写 Java,但可以复用同一套「规则思维」:把 main.json 里的书源抽象成「搜索接口 + 选择器 + 过滤正则」,在 Node 里用fetch+ Cheerio 实现一版,塞进 Electron 主进程,渲染进程只负责搜索框、结果列表和「下载 / 导出 TXT」。

目标很明确:

  • 用户输入书名或作者 → 当前书源搜索 → 展示结果列表
  • 用户点某本书 → 拉取目录 → 可选「下载并加入书架」或「导出为 TXT」
  • 正文要能去广告、去站内推广文案,尽量干净

下面就从架构到实现、从配置到坑,捋一遍。


🏗️ 整体架构:谁在干活、谁在展示

51mazi 是 Electron + Vue 3。所有对外请求和 HTML 解析都放在主进程,避免渲染进程直接请求第三方站点带来的跨域和安全性问题;渲染进程只通过 IPC 发「搜索 / 拉目录 / 下载章节」的指令,并接收进度事件。

┌─────────────────────────────────────────────────────────────────┐ │ 渲染进程 (Vue 3) │ │ NovelDownload.vue:搜索框、书源选择、结果表、下载区、进度条 │ │ novel.js:封装 window.electron.novel*,序列化章节再发 IPC │ └───────────────────────────┬─────────────────────────────────────┘ │ IPC: novel:search / get-chapter-list / download-chapters │ 事件: novel-download-progress ▼───────────────────────────┴─────────────────────────────────────┐ │ 主进程 (Node.js) │ │ novelDownloader.js:fetchHtml、Cheerio 解析、cleanContent │ │ index.js:ipcMain.handle('novel:*'),下载时 send('novel-download-progress') │ └───────────────────────────────────────────────────────────────────┘

这样做的结果:爬虫逻辑和书源规则集中在一个服务文件里,便于维护和扩展书源;前端只关心「调接口、展示、进度」。


⚙️ 书源配置:把「规则」搬进 JavaScript

so-novel 的规则是 JSON:搜索 URL、GET/POST、搜索参数模板、列表/书名/作者/目录/正文的选择器、正文过滤正则等。我们在novelDownloader.js里用了一个BOOK_SOURCES数组,每个元素对应一个书源,例如:

{id:'xbiqugu',name:'香书小说',encoding:'utf-8',searchMethod:'post',searchUrl:'http://www.xbiqugu.la/modules/article/waps.php',searchData:'{"searchkey": "%s"}',searchListSelector:'#checkform > table > tbody > tr',searchTitleSelector:'td.even > a',searchAuthorSelector:'td:nth-of-type(3)',searchBookUrlAttr:'href',chapterListSelector:'#list > dl > dd > a',contentSelector:'#content',filterTxt:'一秒记住【文学巴士...】|(www.xbiquge.la...)|...'}
  • GET / POSTsearchMethodget时用 URL 拼%s(关键词);为post时用searchData模板,%s替换为 JSON 转义后的关键词,再fetch(..., { method: 'POST', body })
  • 目录 URL 不一定等于书籍页:有的书源目录是单独路径,用tocUrlPattern: 'https://xxx/read/%s/',从书籍 URL 里抽出 bookId 再拼成目录页。
  • encoding:不少站是 GBK,用iconv-lite在拿到ArrayBuffer后解码成字符串再交给 Cheerio。

这样,新增书源就是往BOOK_SOURCES里推一个对象,不用改业务逻辑。


🛠️ 抓取三板斧:请求、编码、解析

📡 1. 请求:fetch + 超时 + 像浏览器

主进程里用原生fetch,配合AbortController做超时(例如 25 秒),避免某个书源卡死整条链路。请求头尽量像浏览器:User-AgentAcceptAccept-LanguageCache-ControlPragma,减少被站点当脚本直接掐的概率。

constcontroller=newAbortController()consttimeoutId=setTimeout(()=>controller.abort(),FETCH_TIMEOUT_MS)constres=awaitfetch(url,{signal:controller.signal,redirect:'follow',headers})// ...clearTimeout(timeoutId)

超时或连接失败时,把错误转成用户可读的「请求超时 / 无法连接书源,请检查网络或更换书源」再抛给前端。

🔤 2. 编码:UTF-8 与 GBK

res.arrayBuffer()拿到的是原始字节。若书源声明encoding: 'gbk',就用iconv-lite解码:

constbuf=awaitres.arrayBuffer()constbytes=newUint8Array(buf)if(encoding&&encoding.toLowerCase()!=='utf-8'){returniconv.decode(Buffer.from(bytes),encoding)}returnnewTextDecoder('utf-8').decode(bytes)

否则 Cheerio 解析时会得到乱码,书名、作者、正文全挂。

🔍 3. 解析:Cheerio 按选择器抽

搜索页:用searchListSelector找到每一行,再在每行里用searchTitleSelector/searchAuthorSelector取书名、作者,用searchBookUrlAttr取链接,并resolveUrl(base, href)转成绝对 URL。
目录页:用chapterListSelector遍历<a>,收集texthref
正文页:用contentSelector取一块 HTML,交给清洗函数。


🧹 正文清洗:filterTxt 与广告说再见

很多站会在正文里插「一秒记住本站」「手机用户请…」「本章完」等。我们在 so-novel 里看到用filterTxt存一串正则,用|分隔,在正文里逐个替换掉。实现方式:

  • 先把 HTML 变成纯文本:去<script>/<style><br>换行,其他标签去壳,HTML 实体解码。
  • 若配置了filterTxt,按|拆成多条正则,对文本做replace(re, ''),无效正则静默跳过。
  • 最后把连续多个换行压成双换行,trim。

这样导出的 TXT 和加入书架后的正文会干净很多。


🐛 踩坑实录:超时、编码、以及「对象无法克隆」

⏱️ 超时与连接错误

一开始用较短超时,部分书源响应慢,容易报错。把超时调到 25 秒,并对AbortErrorECONNREFUSEDUND_ERR_CONNECT_TIMEOUT做专门处理,统一成前端可展示的文案,避免直接把 Node 的报错扔给用户。

🔤 编码

某书源列表乱码,检查后发现该站是 GBK 而代码里按 UTF-8 解。给该书源加上encoding: 'gbk',并用iconv-lite解码后恢复正常。

🔗 「An object could not be cloned」

下载章节时要把「章节列表」从渲染进程传到主进程。章节列表来自 Vue 的响应式数据,本质是 Proxy。Electron IPC 使用结构化克隆,无法序列化 Proxy,会报 “An object could not be cloned”。

解决办法:在调用novelDownloadChapters前,先把章节列表转成纯对象数组再传:

constplainList=(chapterList||[]).map((c)=>({title:String(c.title),url:String(c.url)}))returnwindow.electron.novelDownloadChapters({chapterList:plainList,sourceId})

这样主进程拿到的是普通数组,不再碰 Vue 的 Proxy。


🎨 前端体验:进度、取消与免责

  • 进度:主进程在每章抓取完成后event.sender.send('novel-download-progress', { current, total }),渲染进程监听novel-download-progress,更新进度条并禁用按钮,避免重复点击。
  • 取消 / 换书:用户取消或切换书籍时,除了清空当前选中书和目录,还要把「下载中」状态和进度重置,否则会一直卡在「下载中」。
  • 导出 TXT:和「下载并加入书架」共用同一套章节拉取逻辑,只是最后一步改为写单个 TXT 文件到用户选择目录。
  • 免责声明:页面顶部保留醒目免责说明:个人非商业使用、用户对内容合法性自负、禁止传播侵权内容等,既合规又降低法律风险。

🎉 小结与 GitHub

在 Electron 里做「搜书 + 下载」,本质是:主进程负责请求与解析,书源用配置驱动,渲染进程只发指令和展示。借鉴 so-novel 的规则设计,用 Node + Cheerio + iconv-lite 实现多书源、GET/POST、编码与正文过滤,再把 IPC 和 Vue 响应式之间的「可序列化」问题处理掉,就能在 51mazi 里实现一版可用的下载小说功能。

本文要点回顾:

  • 多书源、可配置:书源用 JS 对象描述,新增书源只需加配置,无需改引擎
  • 主进程抓取、渲染进程展示:请求与解析在主进程,避免跨域与安全风险
  • 编码与超时:GBK 用 iconv-lite,fetch 用 AbortController 超时,错误转成用户可读提示
  • 正文去广告:filterTxt 多段正则,导出 TXT 与加入书架后的正文更干净
  • Vue Proxy 与 IPC:章节列表先转成纯对象再传主进程,避免「对象无法克隆」

核心文件一览:

文件职责
src/main/services/novelDownloader.js书源配置、fetchHtml、搜索/目录/正文解析、cleanContent
src/main/index.jsnovel:get-sources/novel:search/novel:get-chapter-list/novel:download-chapters,进度事件
src/preload/index.js暴露novel*API,转发novel-download-progress
src/renderer/src/service/novel.js封装 IPC,章节列表序列化后再传主进程
src/renderer/src/views/NovelDownload.vue搜索 UI、结果表、下载区、进度条、免责声明

如果你也在给桌面应用加「从网页抓内容」的能力,希望这篇从 so-novel 到 51mazi 的实践能帮你少踩一点坑。51mazi 完全开源,欢迎直接拿代码、提 Issue、提 PR;使用本功能时请务必遵守当地法律法规与版权要求,仅限个人学习与合规使用。

  • GitHub 仓库:51mazi — ⭐给个 Star 支持一下,让更多写作者发现这款工具!
  • 同系列:书架管理、AI 封面、地图设计等可参考仓库内其他博客与源码。

🏷️ 标签

#Electron#Vue3#爬虫#Cheerio#小说下载#so-novel#IPC#多书源#51mazi#写作软件


💡如果这篇文章对你有帮助,欢迎在 GitHub 给 51mazi 点个 ⭐️ Star,让更多写作者和开发者发现这个项目!

本文基于 51mazi 开源项目「下载小说」功能,仅供学习与合规使用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 1:56:20

51单片机外挂MCP2515实现CAN通信的驱动开发指南

简介&#xff1a;面向51单片机的MCP2515完整驱动工程包&#xff0c;配套《51单片机驱动MCP2515与SPI及CAN总线协议详解》一文&#xff0c;适合嵌入式初学者、电子竞赛参赛者及需要快速接入CAN总线的开发者。包内提供Keil工程源码&#xff0c;涵盖MCP2515初始化、SPI读写时序、报…

作者头像 李华
网站建设 2026/9/12 1:55:03

在线教育数据治理与实时分析技术实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 1:54:08

BUUCTF WEB赛题解析:命令注入漏洞实战

1. BUUCTF WEB赛题深度解析 作为一名长期活跃在CTF赛场的选手&#xff0c;我最近集中刷完了BUUCTF平台上的WEB类题目。这个系列在CTF圈内以贴近实战、题型丰富著称&#xff0c;特别适合用来锻炼WEB安全攻防思维。今天我就以第7题为例&#xff0c;拆解这类题目的通用解题思路和技…

作者头像 李华
网站建设 2026/9/12 1:53:30

sward文档评审工具:提升企业协作效率的技术实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华