
在 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.jsfetchHtml、Cheerio 解析、cleanContent │ │ index.jsipcMain.handle(novel:*)下载时 send(novel-download-progress) │ └───────────────────────────────────────────────────────────────────┘这样做的结果爬虫逻辑和书源规则集中在一个服务文件里便于维护和扩展书源前端只关心「调接口、展示、进度」。⚙️ 书源配置把「规则」搬进 JavaScriptso-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 / POSTsearchMethod为get时用 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-Agent、Accept、Accept-Language、Cache-Control、Pragma减少被站点当脚本直接掐的概率。constcontrollernewAbortController()consttimeoutIdsetTimeout(()controller.abort(),FETCH_TIMEOUT_MS)constresawaitfetch(url,{signal:controller.signal,redirect:follow,headers})// ...clearTimeout(timeoutId)超时或连接失败时把错误转成用户可读的「请求超时 / 无法连接书源请检查网络或更换书源」再抛给前端。 2. 编码UTF-8 与 GBKres.arrayBuffer()拿到的是原始字节。若书源声明encoding: gbk就用iconv-lite解码constbufawaitres.arrayBuffer()constbytesnewUint8Array(buf)if(encodingencoding.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收集text和href。正文页用contentSelector取一块 HTML交给清洗函数。 正文清洗filterTxt 与广告说再见很多站会在正文里插「一秒记住本站」「手机用户请…」「本章完」等。我们在 so-novel 里看到用filterTxt存一串正则用|分隔在正文里逐个替换掉。实现方式先把 HTML 变成纯文本去script/stylebr换行其他标签去壳HTML 实体解码。若配置了filterTxt按|拆成多条正则对文本做replace(re, )无效正则静默跳过。最后把连续多个换行压成双换行trim。这样导出的 TXT 和加入书架后的正文会干净很多。 踩坑实录超时、编码、以及「对象无法克隆」⏱️ 超时与连接错误一开始用较短超时部分书源响应慢容易报错。把超时调到 25 秒并对AbortError、ECONNREFUSED、UND_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-litefetch 用 AbortController 超时错误转成用户可读提示✅正文去广告filterTxt 多段正则导出 TXT 与加入书架后的正文更干净✅Vue Proxy 与 IPC章节列表先转成纯对象再传主进程避免「对象无法克隆」核心文件一览文件职责src/main/services/novelDownloader.js书源配置、fetchHtml、搜索/目录/正文解析、cleanContentsrc/main/index.jsnovel:get-sources/novel:search/novel:get-chapter-list/novel:download-chapters进度事件src/preload/index.js暴露novel*API转发novel-download-progresssrc/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 开源项目「下载小说」功能仅供学习与合规使用。