ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

小说下载工具全攻略:运行方式、导出格式与本地书库归档

小说下载工具全攻略:运行方式、导出格式与本地书库归档

把一本书完整地留在自己硬盘里,这件事听着简单,真动手坑不少。前阵子帮朋友整理他追了三年的书单,才发现很多人对小说下载工具的理解还停留在“点一下按钮,出来一个 txt”。真到自己上手,问题就来了:有人导出来的文件满屏乱码,转成 UTF-8 之后章节标题反而没了;有人做出的 epub 在阅读器里目录全是空白,点进去只能一页页往下翻;还有人挂了两小时只下回来三百章,剩下七百章打开全是空壳。这篇就把这套活儿从头到尾捋一遍——导出格式怎么选、运行方式怎么挑、本地保存要注意哪些细节。标题里那个平台名字大家都懂,下面我统一用“目标平台”来指代,避免不必要的麻烦。内容面向三类人:完全不懂代码只想双击即用的、会点 Python 想自己改参数的、以及想把几百本书长期归档管理的。不管你在哪一档,后面都能找到能直接抄的部分。

1. 先把需求拆清楚:本地书库到底要解决什么

1.1 四个真实痛点,决定你该选哪种方案

很多人一上来就问“哪个工具最好用”,这问法本身就有问题。工具好不好用,取决于你要解决的是哪一类问题。我把这几年经手的场景归了归类,基本跑不出下面四种。

第一种是离线阅读。通勤地铁、飞机、老家没网的房间里,你不可能指望随时连得上。这类需求的核心是“内容完整 + 文件能打开”,对排版要求不高,TXT 就够。第二种是跨设备同步。手机看完换平板,平板看完换电纸书,如果每次都要重新找进度,体验会崩。这时候格式就重要了,EPUB 能记住阅读位置,TXT 在很多阅读器里只能靠百分比估。第三种是长期归档。有些书你追了几年,中间断更、下架、改名、章节被删改,等你想回头找某一章,原平台已经找不到了。这类需求的关键是“可检索 + 可校验”,得有稳定的命名规则和目录结构。第四种是二次加工,比如你想把书导进笔记软件做摘录,或者用脚本统计某本书的词频、人物出场次数,那就要 HTML 或 Markdown 这种结构化的中间产物。

这四类需求对应完全不同的技术取舍。只做离线阅读却硬上 EPUB 打包,等于给自己找麻烦;反过来,想做归档却只存 TXT,三年后你连哪本书是哪个版本都分不清。所以我建议先花五分钟想清楚自己属于哪一类,再往下看。

1.2 技术路线三选一,先判断你要“重”还是“轻”

抛开班门弄斧的细节,这件事的技术路线其实只有三条。

第一条是现成的图形化工具,下载解压双击,填个链接选个格式就完事。优点是零门槛,缺点是黑盒——参数改不了,出问题只能等作者更新,而且这类工具良莠不齐,有些会捆绑一堆你不需要的东西。第二条是开源脚本 + 自己配环境,通常是 Python 写的,能改并发数、能改输出格式、能加自定义清洗规则。缺点是要装依赖,第一次跑起来可能要折腾半小时。第三条是容器化或常驻脚本,把整个流程封进一个镜像里,定时跑、批量跑、跑完自动归档。这台机器可以是你家里的旧笔记本、软路由、或者是台常年开着的迷你主机。

我的经验是:一次性需求走第一条,长期追更走第二条,书库规模超过五十本就走第三条。原因很朴素——手工操作的成本是线性增长的,而脚本化的成本是一次性的。你下第一本书的时候觉得手动挺快,下到第三十本就会开始烦躁,因为重复劳动最消耗人。而一旦上了脚本,后面每加一本书的边际成本几乎是零。

2. 三种运行方式:从双击即用到长期挂机

2.1 方式一:打包好的可执行程序,适合完全不想碰命令行的人

这是最省心的一条路。作者通常会把 Python 脚本用打包工具封成单个 exe 或 app,运行时不需要你装任何东西。你要做的只有三件事:解压到一个纯英文路径下、双击运行、按提示粘贴书籍地址。

听起来简单,但这里有三个高频翻车点,我几乎每次帮人看问题都能碰到。第一个是路径里带中文或空格。很多打包工具生成的程序在读写临时文件时对非 ASCII 路径处理得很糟糕,表现就是程序一闪而过、或者卡在“正在解析”不动。解决办法很土但有效:直接扔到D:\books这种路径下。第二个是杀毒软件误报。打包后的可执行文件体积大、行为模式像“批量网络请求”,很容易被启发式引擎盯上。这不是让你去关杀软,而是建议你把程序目录加进白名单,并且只从你能确认来源的地方拿文件。第三个是缺少运行库。部分打包方案依赖系统里的 VC++ 运行库或 .NET 组件,老系统上会提示缺 dll,装一下对应的运行库即可。

这个方式还有个隐藏限制:参数基本锁死。作者给你什么就是什么,你想改并发数、想加个自定义的章节过滤规则,做不到。所以它适合“我就下这一本,下完拉倒”的场景。

2.2 方式二:源码 + Python 环境,灵活度最高

会一点点命令行的,强烈建议走这条。流程是:装 Python(建议 3.9 以上)、建虚拟环境、装依赖、改配置、运行。

为什么一定要建虚拟环境?因为这类脚本的依赖往往比较“挑版本”,比如某个解析库要求 4.x,另一个转换库又锁死了 3.x,直接装在全局环境里,早晚会和你其他项目打架。虚拟环境把这堆东西隔离在一个文件夹里,删掉就干净了,不留后患。命令很简单:

python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install -r requirements.txt

装完依赖之后,第一件事不是急着跑,而是打开配置文件看一眼。绝大多数脚本都会有个config.yaml或者config.py,里面有几个关键项值得你亲手调:并发线程数、请求间隔、超时时间、重试次数、输出目录、导出格式。这几个参数的默认值往往是作者按“最保守”设的,跑起来慢得让人着急,但贸然拉满又容易触发目标站点的频控。后面第 4 节我会给一组实测比较稳的数值。

这条路的额外好处是能改代码。比如你想在正文清洗阶段加一条规则,把每章末尾那段“本章未完,点击下一页”自动删掉,改两行正则就行。这类小定制,图形化工具是永远给不了你的。

2.3 方式三:容器化或脚本常驻,为长期书库准备

到了这一步,思路就变了——你不再是“下一本书”,而是在维护一个持续运转的小系统。典型做法是把整个流程封进一个容器镜像,或者写一个 shell 脚本挂到定时任务里,每天凌晨跑一次,把有更新的书增量拉下来。

容器化的好处是环境完全隔离,换台机器把镜像一拷就能跑,不会出现“在我这儿好好的,到你那儿就报错”的经典问题。关键点有三个:目录挂载要把输出目录映射到宿主机,否则容器一删书就没了;时区要设对,不然日志时间全是 UTC,排查问题时对不上;日志要落盘,跑批任务的失败必须能事后回溯,不能只在终端刷一下就没了。

下面是一份可以直接改的编排配置,用来示意结构:

services: bookgrab: image: local/bookgrab:latest volumes: - ./output:/app/output - ./config:/app/config environment: - TZ=Asia/Shanghai - LOG_LEVEL=INFO restart: unless-stopped

定时任务这边,cron表达式写成0 3 * * *就行,凌晨三点跑,避开人多的时段。有个小坑提醒一下:增量更新必须依赖一个稳定的书籍标识,通常是书籍 ID 加章节 ID 的组合。如果你只按书名判断,遇到改名的书就会重复下载;如果只按章节序号判断,遇到插章、并章、倒序的书就会错位。我在第 4 节会讲怎么做这个去重。

2.4 三种方式的取舍对照

维度可执行程序源码运行容器/常驻
上手门槛极低中等较高
参数可调性几乎为零高高
环境隔离无中(虚拟环境)强
适合规模1 到 3 本10 到 50 本50 本以上
主要风险来源不可控、误报依赖版本冲突配置写错导致静默失败
排错难度高(黑盒)中低(有日志)

这张表里的“排错难度”是最容易被忽略的一栏。很多人只看上手门槛,选了个双击即用的,结果出了问题完全无从下手,最后还是要回来学第二条路。

3. 五种导出格式:别急着全都要

3.1 TXT:最丑但最不会出错的那个

TXT 是所有格式里兼容性最强的,几乎没有之一。十几年前的 MP3、老式电子词典、现在的任何一台设备,打开 TXT 都没问题。它的结构简单到极致——就是纯字节流,没有任何元数据、目录、样式的概念。

正因为简单,它的问题也很明确。编码是第一道坎。Windows 上很多老工具默认写 GBK,你拿到 Mac 或者手机上打开就是一片问号。解决办法是统一输出 UTF-8,且不带 BOM——带 BOM 的 UTF-8 在部分阅读器里开头会多出一个看不见的字符,导致首行标题错位。第二道坎是章节定位。TXT 没有目录结构,所谓“目录”只是靠“第X章”这样的文本约定,阅读器能不能识别全看它自己的正则写得怎么样。所以我导出 TXT 的时候会做一个约定:章节标题单独占一行,前后各留一个空行,格式统一成“第X章 标题”,这样绝大多数阅读器都能正确切分。

还有一个实操细节:换行符。Windows 是\r\n,类 Unix 是\n。跨平台使用时统一成\n更稳妥,Windows 上的现代编辑器也都能正确显示。句子之间的空行也别省,一段一行读起来太挤,段间加空行观感会好很多。

注意:TXT 里千万别把广告段落一起带进来。很多平台的正文页会插入“本章完,请投推荐票”之类的尾巴,一本书几百章,攒下来能有好几万字垃圾。

3.2 EPUB:标准电子书,排版和目录都靠它

如果你只打算认真学一种格式,学 EPUB。它是目前电子书领域事实上的标准,本质是一个符合特定结构的 ZIP 压缩包,里面装着 XHTML 页面、样式表、图片和一堆描述文件。

结构上必须包含这几样东西:根目录一个叫mimetype的文件,内容固定是application/epub+zip,而且必须是压缩包里的第一个文件、且不能被压缩(存储方式,compression method 为 0)。这一条是规范硬要求,很多自己写打包脚本的人栽在这里——阅读器打开直接报“文件损坏”,排查半天发现是 mimetype 被 deflate 了。接着是META-INF/container.xml,它告诉阅读器去哪里找主描述文件。再往后是OEBPS/content.opf,里面写书名、作者、语言、封面、章节清单和阅读顺序。目录方面,EPUB 2 用toc.ncx,EPUB 3 用nav.xhtml,想兼容老设备就两个都放。

我做 EPUB 有几个固定习惯。元数据一定要填全:书名、作者、语言(zh-CN)、出版社(可以留空)、唯一标识(UUID)。语言这一项很多人不填,结果在某些阅读器里中文排版会按日文规则处理,标点位置全歪。每个章节单独一个 XHTML 文件,别把整本书塞进一个文件里,否则打开大文件时阅读器会卡死。样式表写得越简单越好,就三条:行高 1.6 到 1.8、段间距 0.5em、首行缩进 2em,别搞花里胡哨的字体,阅读器不一定支持。

3.3 MOBI 与 AZW3:给电纸书准备的

这两个格式主要服务于电纸书生态。MOBI 是老格式,兼容性广,但对中文排版的支持比较粗糙,字体嵌入也受限。AZW3 是它的继任者,支持更丰富的 CSS 和字体,同一个文件在设备上显示效果明显更好。如果你的设备两种都认,优先选 AZW3,别犹豫。

这条路的现实做法是:先做出 EPUB,再用转换工具转过去。转换时最常遇到的问题有两个。一个是目录丢失,原因是源 EPUB 的目录文件不规范,转换器没识别出来,所以要保证第 3.2 节说的toc.ncx和nav.xhtml都得有。另一个是中文断行错乱,表现是一行字被莫名切成两截,或者标点跑到行首。这通常是转换时的段落识别规则问题,把源文件里段与段之间的空行补齐,基本能缓解。

顺便说一句,转换这一步完全可以命令行批量做,一条命令处理整个文件夹,比手动一个个拖进图形界面快得多。

3.4 PDF:只在你需要“固定样子”的时候用

PDF 的优势是所见即所得,你在这台机器上看到的排版,换十台设备打开都一样。适合的场景其实很窄:打印装订、做长图分享、或者给完全不熟悉电子书格式的人看。

它的劣势在阅读体验上非常明显。PDF 是固定页面尺寸,屏幕小的时候要么字小得看不清,要么得左右拖动。更麻烦的是中文字体嵌入——如果不嵌入字体,换台没装对应字体的机器打开,中文会变成方框或者宋体替代,观感直接崩掉。转换时务必勾选字体嵌入,文件会大一圈,但这是必须付的代价。

还有一个坑:图片章节。有些书的某些章节是图片形式(比如插图页、或者是纯图片排版的番外),转 PDF 时如果处理不当,这些页面会丢。我一般会在转换前先把图片全部下载到本地,确认数量对得上,再开始转。

3.5 HTML 与 Markdown:为二次加工准备的中间态

如果你的目的不是“读”,而是“用”——导入笔记软件、做摘录整理、跑文本分析——那这两种才是正解。

HTML 的好处是保留了完整的结构,标题是<h2>,段落是<p>,你后续想用任何工具解析都方便。Markdown 更轻,纯文本、可读性极强,扔进任何支持 MD 的笔记工具都能识别。我自己的习惯是:每本书生成一个总的 Markdown,章节用二级标题,正文用普通段落。这样导入之后,笔记软件会自动生成一个大纲,跳转非常快。

代价是格式信息会丢失。加粗、斜体、特殊的排版效果,转换过程中多少会掉一些。如果你在意这些,还是用 EPUB;如果你只在意文字本身,Markdown 就是最舒服的选择。

3.6 五种格式横向对比

格式排版能力目录支持阅读体验二次加工推荐场景
TXT无靠文本约定一般弱快速备份、老设备
EPUB强完整好中日常阅读首选
AZW3强完整电纸书上最好弱电纸书用户
PDF固定有书签屏幕适配差弱打印、分享
Markdown弱标题层级编辑友好强笔记、分析

看这张表你会发现,没有一种格式是全能的。我自己的做法是“一次抓取,多格式输出”:原始章节数据存一份 JSON,然后按需生成 EPUB 和 Markdown 两个版本。EPUB 用来读,Markdown 用来搜。这样既不会重复抓取浪费流量,也兼顾了两种用途。

4. 完整实操:从零把一本书落到本地

4.1 环境准备与依赖安装

先把环境搭起来。Python 建议装 3.9 到 3.11 之间,太新的版本某些库还没跟上。虚拟环境按 2.2 节的方式建好之后,装这几类依赖:网络请求、HTML 解析、编码识别、电子书打包。

pip install requests lxml chardet ebooklib beautifulsoup4

为什么是这几个?requests负责发请求,比标准库的 urllib 好用太多,会话保持、超时、重试都现成;lxml解析 HTML 比正则靠谱,正则处理嵌套标签迟早出事;chardet用来猜编码,碰上非 UTF-8 的页面能救急;ebooklib专门处理 EPUB 打包,能省掉手动拼 ZIP 的一堆细节;beautifulsoup4在结构不规范的页面上更宽容。

装完之后先跑个最简单的连通性测试,别急着上主程序。这一步很多人跳过,结果主程序报错时分不清是网络问题还是代码问题。测法就是请求一个页面,打印状态码和长度:

import requests headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept-Language": "zh-CN,zh;q=0.9", "Referer": "https://example.com/" } r = requests.get("https://example.com/book/123", headers=headers, timeout=15) print(r.status_code, len(r.text), r.encoding)

三个请求头的作用得说清楚。User-Agent是告诉对方“我是什么客户端”,很多站点对空 UA 或明显的脚本 UA 会直接拒绝;Accept-Language影响返回内容的语言版本;Referer是来源页,有些站点会校验它,从站内跳过去才给内容。这三个不是玄学,是实打实影响成功率的。

4.2 请求层:并发、限速、重试三件套

这是整个流程里最容易出问题的地方,也是决定你“是两小时下完还是两天下完”的关键。

先说并发。并发数不是越大越好,我实测下来的经验值是3 到 5。低于 3,速度上不去;高于 8,被限流的概率急剧上升,反而更慢。做个简单估算:假设一本书 600 章,单章平均响应 0.6 秒,串行需要 360 秒;并发 4 的情况下理论上是 90 秒,但考虑到实际网络抖动和重试,真实耗时大概在 150 到 200 秒之间。这个数字比盲目开到 20 并发然后被限流卡死要靠谱得多。

再说限速。光限制并发还不够,因为并发 4 意味着瞬时可能有 4 个请求同时打过去,某些站点对“短时间窗口内的请求数”很敏感。稳妥的做法是加一个固定间隔,每个请求发出后至少间隔 0.3 到 0.5 秒。这会让总耗时上升,但成功率会明显提高。我一般把它做成可配置的,先跑 50 章看看成功率,再决定要不要调。

重试策略这边,建议用指数退避。第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒,最多重试 3 次。这样做的逻辑是:如果对方只是瞬时抖动,短暂等待就能恢复;如果是持续限流,等待时间逐次拉长,能给你留出“冷却”的窗口。常见的错误是固定间隔重试,一秒一次连打十次,等于主动告诉对方“我在刷”。

import time, random def fetch(session, url, retries=3): for i in range(retries): try: r = session.get(url, timeout=15) if r.status_code == 200: return r.text if r.status_code in (429, 503): time.sleep((2 ** i) + random.uniform(0, 0.5)) continue except Exception: time.sleep((2 ** i) + random.uniform(0, 0.5)) return None

这段里那个random.uniform(0, 0.5)是刻意的,加一点随机抖动,避免多个线程在同一时刻集体重试,形成新的尖峰。

4.3 解析层:章节列表与正文清洗

拿到页面之后,要做两件事:解析出章节列表、从每个章节页里抽出正文。

章节列表这一关,坑主要集中在分页和倒序上。有的书目录分了好几页,只解析第一页会漏掉大半;有的平台把最新章排在前面,直接按顺序拼会得到一本倒着写的书。处理办法是先解析出总数,再循环翻页,最后按章节序号重新排序——注意是按序号排,不是按页面上的出现顺序排。序号本身也要处理,因为可能混着“第12章”“第十二章”“Chapter 12”等各种写法,我的做法是统一转成阿拉伯数字,转不了的保留原样但标记出来,方便事后人工看一眼。

正文抽取这边,先用选择器定位到正文容器,再逐段取文本。这里有个非常实用的原则:宁可多留,不要错杀。因为清洗规则写得太激进,很容易把正文里的对话段落也删掉。我的流程是三段式:

先做白名单式提取,只从明确的内容容器里取,不碰页面其他区域,这一步就把导航栏、推荐位、评论区的绝大部分噪音挡住了。然后做行级过滤,删掉完全匹配某些固定模式的短行,比如“本章未完”“点击下一页”“请记住本站”“手机用户请浏览”。这些模式要写得足够具体,别用宽泛的关键词匹配,否则正文里出现“下一页”这个词也会被误删。最后做去重,有些站点会在章节末尾重复一遍开头的段落,用正文的前后几十个字符做哈希比对,重复的直接砍掉。

提示:清洗规则一定要用至少 20 章做回归测试。我见过一个案例,规则里写了“删除包含‘推荐’的行”,结果那本书里有个人物叫“推荐长老”,一整段剧情被删得干干净净。

4.4 合并与元数据:目录、封面、编码

章节数据都拿到之后,先落一份原始 JSON,再生成各种格式。这一步容易被跳过,但它的价值在出问题的时候会体现出来——如果 EPUB 打包出错,你不用重新抓一遍,直接从 JSON 重新生成就行。

JSON 的结构建议是这样:一个对象,包含书籍级的元信息(书名、作者、简介、封面地址、来源标识、抓取时间),加上一个章节数组,每个章节有id、index、title、content(纯文本)、html(可选)。

元信息里封面值得单独说。封面图要下载到本地并与书一起打包,不要只存个远程链接——链接会失效,而且很多阅读器在离线状态下不会去加载远程图片。下载之后检查一下文件头,确认确实是图片格式,有些站点在图片地址失效时会返回一张很小的占位图,你不检查的话,书库里就会多出一堆灰色方块封面。

编码这一步,统一转 UTF-8 是必须的。转换过程中顺手做两件事:把全角空格替换成半角(某些平台的缩进是用全角空格堆的,转成 EPUB 后会被当成正文内容),把连续三个以上的换行压成两个(保持段落感又不至于空一大片)。

4.5 生成与校验:EPUB 打包和入库命名

用ebooklib打包的基本流程是:创建 Book 对象、设置元数据、逐章添加 XHTML 项、设置目录、写文件。这里有两个必须注意的点。

第一,每个章节的 XHTML 必须是完整文档,要有<?xml version="1.0" encoding="utf-8"?>声明、有<html>根元素、有正确的命名空间。只写一个<div>片段塞进去,部分严格的阅读器会直接报错。第二,文件名的排序。阅读器有时候会按文件名排序,如果你的章节文件名是chap1.xhtml、chap10.xhtml、chap2.xhtml这种,顺序就乱了。解决办法是补零,统一成chap0001.xhtml这种四位数格式。

打包完之后三件事做校验。一验大小:一本 600 章的中文小说,纯文字 EPUB 一般在 1.5 到 3MB 之间,如果只有 200KB,大概率是章节没进去。二验目录:用阅读器打开,点目录,看能不能跳到对应章节、跳过去内容对不对。三验乱码:随机翻三五章,确认没有问号方块。这三步加起来不到两分钟,能挡住八成的问题。

入库命名我用的是一套固定模板:

书库/ 作者名/ 书名 - 作者名.epub 书名 - 作者名.md meta.json

用“作者/书名”两层目录,是因为书多了之后按作者找比按书名找快得多。文件名里带作者名,是为了防止同名书覆盖。meta.json里存抓取时间、章节数、来源标识,以后做增量更新时直接读它,不用再解析文件名。

5. 常见问题与排查速查表

5.1 抓取阶段的高频故障

返回 403 或直接空内容。九成是请求头的问题。按可能性排序:UA 缺失或被识别、Referer 不对、缺少必要的 Cookie。排查方法是先用浏览器正常打开同一页面,把请求头逐个对照,缺哪个补哪个。如果浏览器能开、脚本不能,基本可以确定是请求头差异。

章节列表只拿到一部分。先确认目录是不是分页的,再确认是不是懒加载——有些页面的目录在初始 HTML 里只有前 20 章,剩下的靠滚动触发接口加载。这种情况要去网络面板里找那个真实的数据接口,直接请求它。

部分章节正文是空的。可能是选择器写错了(页面结构改版),也可能是这一章本身是图片形式。判断方法是把原始 HTML 存下来看一眼,如果是<img>标签而不是文字,那就是图片章节,得走下载图片再嵌入的流程。

跑着跑着突然全部失败。典型的限流。表现是成功率高一阵子之后断崖式下跌。这时候别硬刚,停十分钟再跑,同时把并发降到 2、间隔加到 1 秒。

5.2 转换与阅读阶段的坑

EPUB 打开提示文件损坏。按可能性排查:mimetype是不是第一个文件、是不是用了存储而非压缩、container.xml路径对不对。这三条占了绝大多数的损坏原因。

目录能显示但点了跳不过去。检查content.opf里的 manifest 和 spine 是否一致——manifest 里声明了但 spine 里没列的文件,阅读器不会加载;反过来 spine 里列了但 manifest 里没声明的,直接报错。

中文显示成方块或问号。源文件编码没统一,或者 XHTML 里声明的是iso-8859-1。统一在 XHTML 头部写<meta charset="utf-8"/>,并且确保写入文件时用的确实是 UTF-8。

图片全部丢失。检查图片是不是只存了远程链接。EPUB 是离线格式,远程图片在多数阅读器里不会加载,必须下载到本地并加入 manifest。

电纸书上翻页很卡。单章节文件太大,或者 CSS 太复杂。把每章拆成独立文件、精简样式表,能明显改善。

5.3 稳定性与性能问题

跑批量任务最容易遇到的是内存增长。原因是把整本书的所有章节内容都攒在内存里再统一写盘。600 章纯文本大概几十 MB,倒也不至于爆,但如果同时跑多本、或者正文里带大量图片,就会出问题。解决办法是分阶段落盘:抓完一章就追加写到一个临时文件,最后再统一读回来组装。

另一个是长任务中断。跑了两小时,网络一抖,脚本挂了,前面的都白干。这个必须做断点续传——每完成一章就往一个进度文件里记一行,重启时先读进度文件,跳过已完成的。实现成本很低,收益极高。

还有一个隐性问题:系统休眠导致任务假死。笔记本合盖之后网络断了,脚本卡在某个请求上,超时设得又长,看起来就是“不动了”。把超时设成 15 秒、并且关掉休眠,或者在计划任务里设置“唤醒执行”,都能规避。

现象最可能的原因处理方式
403 / 空响应请求头不全对照浏览器补 UA、Referer、Cookie
目录缺章分页或懒加载找真实数据接口,循环翻页
正文为空选择器失效或图片章存原始 HTML 检查结构
中途全部失败触发限流降并发、加间隔、冷却十分钟
EPUB 报损坏mimetype 不合规首个文件、存储不压缩
目录跳转失效manifest/spine 不一致核对两处清单
中文乱码编码未统一全链路 UTF-8,去掉 BOM
图片丢失只存了远程链接下载本地并注册进 manifest
内存飙升全量攒内存分阶段落盘
任务中断白跑无断点续传逐章记进度,重启跳过

6. 几条实操心法与边界

6.1 先把边界想清楚,再谈技术

这部分我得说在前面,因为它比任何代码都重要。这类工具的使用场景,本质上是把你有权阅读的内容做一份本地副本,用于自己的离线阅读和长期保存。它不应该被用来绕过付费、批量分发、或者搭建任何形式的对外分享服务。

具体到操作上,我的自我约束有这么几条。只处理自己已经能正常阅读的内容,不去碰需要额外权限才能看的章节;导出的文件只存本地,不上传到任何公开位置;书库里不放超出个人使用范围的数量;如果某个内容明确标注了禁止下载,那就不做。这些不是技术限制,是使用者的自觉。

还有一点值得提:不要为了“完整”去抓取整站。技术上做得到,但没有意义,而且会给对方服务器带来不必要的负担。我的原则是“要哪本抓哪本”,抓完就停,不做全站扫描。

6.2 书库的长期维护,比抓取本身更费心

书抓下来只是开始,真正麻烦的是三年后你还能不能找到、能不能打开。

我的做法是三层防护。第一层是命名规范,前面说的“作者/书名 - 作者名.epub”,这套规则一旦定下来就别改,改一次全库都要重命名,很痛苦。第二层是校验记录,每本书生成时算一个文件哈希,写进meta.json。以后怀疑文件损坏,重新算一次对比就知道。第三层是定期抽查,每隔几个月随机打开几本书,翻两章,确认还能正常读。硬盘损坏、同步盘冲突、误删,这些问题都是悄无声息发生的,不抽查你根本不知道。

另外还有一个我踩过的坑:增量更新时的章节错位。有本书中途改版,原来的第 300 章被拆成了两章,序号全往后错了一位。如果你的增量逻辑是“比对最大序号”,就会漏掉内容。稳妥的比对方式是章节 ID 加标题加正文哈希三者结合,任何一个变了就当作新章节重新抓。这样做成本高一点,但不会漏。

6.3 几个我用了很久的小技巧

先跑 20 章再全量。这个习惯帮我省了大量时间。20 章能暴露九成的问题:选择器对不对、编码有没有问题、清洗规则会不会误伤、限流会不会触发。确认没问题再放开跑,比跑了一半发现全错要划算得多。

保留原始 HTML。调试阶段把每个章节的原始页面存一份,出了问题不用重新请求。跑通之后可以关掉这个开关,节省空间。

多格式输出但只抓一次。原始数据存 JSON,EPUB 和 Markdown 都从 JSON 生成。这样你想换格式、想改排版、想重新打包,都不用再走一遍网络。

配置文件里加个 dry-run 开关。打开时只打印将要做什么,不实际发请求、不写文件。改完参数先 dry-run 看一眼,能避免很多低级错误,比如把输出目录写成了根目录。

给输出目录加个时间戳子目录,比如2024-06-01/。这样每次全量重跑都是一个独立的快照,出问题能回滚,也不会覆盖掉之前的版本。

这套流程我用了挺久,从最早的手动一章章复制,到现在的脚本化批处理,中间踩的坑基本都写在上面的表里了。真正有价值的从来不是某个工具,而是对“请求怎么发、内容怎么清洗、文件怎么组织”这几件事的理解——工具会过时,这套思路不会。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进