把杂乱的 TXT 小说转成规范的 EPUB 3 —— 真目录、真元数据、真排版。
English: README.en.md
一个命令,把这样的文件:
仙逆.txt 13 MB · GB18030 编码 · 夹杂广告水印 · 没有元数据
变成这样的电子书:
仙逆 - 耳根.epub 1198 章两级目录 · 完整元数据 · 官方封面 · 通过 EPUB 规范校验
到 Releases 下载对应平台的压缩包。解压即用,没有任何运行时依赖,不需要装 Python、Java 或别的东西。
下面的命令里
v0.1.0是版本号,请替换成 Releases 页面 上的最新版本。
# Apple Silicon(M1/M2/M3/M4)
curl -LO https://github.com/shinycheng/epubforge/releases/download/v0.1.0/epubforge-v0.1.0-aarch64-apple-darwin.tar.gz
tar xzf epubforge-v0.1.0-aarch64-apple-darwin.tar.gz
# Intel Mac 把上面的 aarch64 换成 x86_64
# 首次运行会被 Gatekeeper 拦下,解除隔离标记:
xattr -d com.apple.quarantine ./epubforge
# 装到 PATH 里,以后任何目录都能直接用
sudo mv epubforge /usr/local/bin/
epubforge --version- 下载
epubforge-v0.1.0-x86_64-pc-windows-msvc.zip(ARM 版 Windows 用aarch64-...zip) - 解压,得到
epubforge.exe - 把
epubforge.exe和你的 txt 放同一个文件夹 - 在该文件夹地址栏输入
powershell回车,然后:
.\epubforge.exe "我的小说.txt"想在任意目录使用,把 exe 所在文件夹加进系统环境变量 Path。
# x86_64,静态链接版,任何发行版都能跑
curl -LO https://github.com/shinycheng/epubforge/releases/download/v0.1.0/epubforge-v0.1.0-x86_64-unknown-linux-musl.tar.gz
tar xzf epubforge-v0.1.0-x86_64-unknown-linux-musl.tar.gz
chmod +x epubforge
sudo mv epubforge /usr/local/bin/ARM64 机器(树莓派 64 位、AWS Graviton)用 aarch64-unknown-linux-musl。
不想装东西就用这个:
docker run --rm -v "$PWD:/work" ghcr.io/shinycheng/epubforge 我的小说.txt-v "$PWD:/work" 把当前目录挂进容器,转换结果会出现在当前目录。镜像里预装了中文字体,封面能正常渲染。
cargo install --git https://github.com/shinycheng/epubforge每个压缩包都配了 .sha256 校验文件:
shasum -a 256 -c epubforge-v0.1.0-aarch64-apple-darwin.sha256把 txt 拖到某个文件夹,然后:
epubforge 我的小说.txt就这样。EPUB 会生成在和 txt 相同的目录,文件名是 书名 - 作者.epub。
想指定输出目录:
epubforge 我的小说.txt -o ~/Books转换是有损的 —— 会删广告、合并换行、切分章节。在正式转换前,先用 inspect 预览它打算怎么处理,这一步不产生任何文件:
epubforge inspect 我的小说.txt想看更详细的(全部被删除的行、更多章节预览、编码候选打分):
epubforge inspect 我的小说.txt -v如果预览结果不对(章节数明显不对、正文被删了),先按下面 常见任务 里的方法调整参数,别急着转。
TXT 里通常只有书名,作者都未必有。加 --scrape 联网补齐简介、标签、完结状态和官方封面:
epubforge 我的小说.txt --scrape它会搜索并列出候选让你选。不想被打断就加 --scrape-auto 自动采用最佳匹配。
这就是全部日常用法了。 下面是各种具体情况的处理方法。
每次转换都会打印一份报告。逐行解释:
📖 《仙逆》 — 耳根
编码 gb18030 置信度 100%
清洗 清除广告 101 行 / 64 类 · 折叠空行 107
结构 1198 章 [cn-chapter] · 6,449,219 字
刮削 qidian · 仙逆 / 耳根 匹配度 72
补全 作者·日期·简介·标签 · 封面已下载
输出 仙逆 - 耳根.epub (8.8 MB, 1207 个条目)
校验 通过(检查了 1203 个文档)
提示 章节序号有 30 处跳号(52→62, 64→71...),源文件可能缺章
耗时 4.64s
| 行 | 含义 | 什么时候该警觉 |
|---|---|---|
| 编码 | 识别出的原文编码和把握程度 | 置信度低于 70% 会标黄;出现「N 个字符无法解码」说明有乱码,试试 --encoding 手动指定 |
| 清洗 | 删了多少广告、合并了多少硬换行 | 广告「类别数」远大于实际广告种类(比如 200 多类但每类只命中 1 次)→ 可能误删了正文,用 -v 查看被删的行 |
| 结构 | 切出多少卷/章,用的哪个识别模式,总字数 | 章节数明显不对,或出现「未识别到标题,已按字数切分」→ 需要自定义正则 |
| 刮削 | 匹配到的书、补全了哪些字段 | 「未应用」说明没找到足够可信的匹配,可以用 --scrape-url 直接指定 |
| 输出 | 文件路径和大小 | —— |
| 校验 | 产物是否符合 EPUB 规范 | 出现「错误」说明产物有问题,请提 issue |
| 提示 | 各类警告 | 跳号通常是源文件本身缺章,不是工具的问题 |
inspect -v 还会额外显示:
章节模式打分
✔ cn-chapter 1198 处 得分 0.822 序号连续度 98% 平均 5,386 字
num-only 312 处 得分 0.000 已排除:匹配过于密集,疑似匹配到正文
这是章节识别的决策过程 —— 打勾的是选中的模式,被排除的会说明原因。识别错了就看这里,能直接看出是哪个模式赢了、为什么。
# 转换目录下所有 txt(不含子目录)
epubforge ./小说 -o ./epub输出
# 递归处理子目录
epubforge ./小说 -r -o ./epub输出
# 限制并行数(默认按 CPU 核数)
epubforge ./小说 -r -o ./epub输出 -j 4已存在的输出文件会被跳过,不会覆盖。要重新生成加 -f:
epubforge ./小说 -r -o ./epub输出 -f批量质检,只挑出有问题的:
epubforge ./小说 -r --json | jq '.[] | select(.outputs[].errors > 0)'# 用推断出的书名+作者搜索
epubforge 我的小说.txt --scrape
# txt 里没有书名作者,自己指定(同时作为搜索词)
epubforge 未知.txt --scrape --title "仙逆" --author "耳根"
# 搜索匹配不准,直接给书籍页链接
epubforge 我的小说.txt --scrape-url "https://www.qidian.com/book/1209977/"
# 批量:不询问,自动采用最佳匹配
epubforge ./小说 -r --scrape --scrape-auto
# 只要元数据,不要下载封面(用自动生成的)
epubforge 我的小说.txt --scrape --no-cover-download补全的字段:作者、简介、分类标签、完结状态、更新日期、封面。
几个要点:
- 刮到的值只填空缺。你在命令行或配置文件里指定的任何字段都不会被覆盖。
- 单本转换且在终端里会列出候选让你选;批量或加
--scrape-auto则自动。 --scrape-url支持起点的各种链接形式,也支持任何实现了og:novel标准的小说站(大部分中文小说站都实现了)。- 默认两次请求间隔 1.5 秒,
--scrape-delay 3000可以调慢。 - 失败不影响转换 —— 网络不通、搜不到,照常出 EPUB,只在报告里写一行。
先用 inspect -v 看模式打分,判断是哪种情况。
情况一:识别出的章节数明显偏少或为 0
你的 txt 用了非常规的章节标题格式。用 --chapter-pattern 给一条正则:
# 假设你的章节标题长这样:◆◆◆ 12 ◆◆◆
epubforge book.txt --chapter-pattern '^\s*◆◆◆\s*(?P<num>\d+)\s*◆◆◆\s*$'
# 长这样:☆、第一章 相遇
epubforge book.txt --chapter-pattern '^\s*☆、第(?P<num>[一二三四五六七八九十百千]+)章\s*(?P<title>.*)$'命名捕获组 num(序号)和 title(标题)会被使用,num 用于检测跳号。
想完全弃用内置模式库、只用你的:加 --only-custom-patterns。
情况二:正文中的句子被误当成章节标题
正常情况下打分机制会排除它们。如果还是发生了,收紧标题宽度限制(配置文件里的 max_heading_width),或者用 --chapter-pattern 给一条更严格的正则。
情况三:完全识别不出章节
默认会按字数切分成若干「节」,保证阅读器还能正常翻页。想改变这个行为:
epubforge book.txt --fallback single # 整本作为一章
epubforge book.txt --fallback fail # 直接报错,不生成文件情况四:不想要卷结构
epubforge book.txt --no-volumes # 输出单级目录工具会自动检测编码,覆盖 UTF-8 / UTF-16 / GB18030 / Big5 / Shift_JIS / EUC-KR 等。如果报告里置信度很低或出现乱码:
epubforge book.txt --encoding gbk # 简体中文常见
epubforge book.txt --encoding big5 # 繁体中文常见
epubforge book.txt --encoding utf-16leinspect -v 会列出所有编码候选和它们的得分,能看出第二名是谁:
编码候选
gb18030 得分 1.040 替换字符 0
Big5 得分 0.780 替换字符 2926
EUC-KR 得分 0.432 替换字符 3979
UTF-8 得分 0.000 替换字符 152311
「替换字符」是解码失败留下的 � 的个数 —— 数量越大越说明这个编码是错的。
没清干净 —— 加自己的规则。在 txt 同目录建 epubforge.toml:
[clean]
extra_ad_patterns = [
'本文首发于.*论坛',
'^\s*订阅本章.*$',
]清过头了(正文被删) —— 先用 -v 确认删了什么:
epubforge inspect book.txt -v把误删的内容加进白名单:
[clean]
keep_patterns = ['未完待续']或者干脆关掉广告清除:
epubforge book.txt --no-ads内置规则刻意保守。曾经有一条规则会命中任何在结尾附近出现「制作/整理/校对」的行 —— 这些是普通动词,在修仙题材里满篇都是,结果删掉了上万字正文。那条规则已经整个移除。准则是:留下一条广告是瑕疵,删掉一段正文是数据丢失。
# 默认:按书名确定性生成一张(同一本书永远同一张)
epubforge book.txt
# 用自己的图
epubforge book.txt --cover 封面.jpg
# 联网下载官方封面
epubforge book.txt --scrape
# 不要封面
epubforge book.txt --no-cover内置样式表只定义结构,不定义外观 —— 不设字体、字号、颜色,全部交给阅读器,所以暗色模式自动就是对的。
想完全替换样式:
epubforge book.txt --css 我的样式.css想在内置样式基础上追加,用配置文件:
[output]
extra_css = "p { line-height: 1.9; }"其他排版相关开关:
epubforge book.txt --no-title-page # 不生成书名页
epubforge book.txt --no-epub2 # 不生成 toc.ncx(放弃老设备兼容)epubforge book.txt --convert-to hant # 转繁体
epubforge book.txt --convert-to hans # 转简体
epubforge book.txt --convert-to tw # 繁体 + 台湾用词
epubforge book.txt --convert-to hk # 繁体 + 香港用词
epubforge book.txt --convert-to cn # 简体 + 大陆用词是词汇级转换,不是简单的字符映射(「滑鼠」↔「鼠标」这类会正确处理)。
# 每卷输出一个独立 EPUB,自动关联成系列
epubforge book.txt --split-volumes
# 自定义文件名
epubforge book.txt --filename "{author} - {title}"
epubforge book.txt --filename "{series}{series_index} {title}"可用占位符:{title} {author} {series} {series_index} {stem}(原文件名)。
命令行参数适合临时调整。整个书库的固定设置放配置文件更方便。
生成一份带完整注释的模板:
epubforge init放在 txt 同目录,命名规则:
| 文件名 | 作用范围 |
|---|---|
epubforge.toml |
该目录下所有 txt |
我的小说.epubforge.toml |
只对 我的小说.txt 生效 |
优先级从低到高:预设 → 配置文件 → 命令行参数。
profile = "webnovel" # webnovel | published | lightnovel | raw
[meta]
series = "山海三部曲"
series_index = 1
tags = ["玄幻", "完结"]
[clean]
join_hard_wraps = "auto" # auto | always | never
extra_ad_patterns = ['本文首发于.*论坛']
keep_patterns = ['未完待续'] # 白名单,永不当作广告
convert = "none" # none | hans | hant | tw | hk | cn
[detect]
volumes = true
min_chapter_chars = 150
max_heading_width = 60
fallback = "split" # split | single | fail
[output]
filename = "{title} - {author}"
cover = "auto" # "auto" | "none" | { file = "cover.jpg" }
epub2_compat = true
split_volumes = false用 --profile 或配置文件里的 profile 选择:
| 预设 | 适用 | 行为 |
|---|---|---|
webnovel |
网络小说(默认) | 全部清洗开启,识别卷/章两级结构 |
published |
出版书 | 不合并换行(尊重原排版),广告阈值放宽 |
lightnovel |
轻小说 | 允许更短的章节 |
raw |
只要结构 | 不清广告、不改标点、不合并换行 |
输入输出
| 参数 | 说明 |
|---|---|
-o, --output-dir <DIR> |
输出目录,默认与输入同目录 |
--filename <模板> |
文件名模板 |
-r, --recursive |
递归处理子目录 |
-j, --jobs <N> |
并行任务数 |
-f, --force |
覆盖已存在的输出 |
元数据
| 参数 | 说明 |
|---|---|
--title --author |
书名、作者(也用作刮削搜索词) |
--description --publisher |
简介、出版方 |
--series --series-index <N> |
系列名、系列序号 |
--tags <标签,标签> |
标签,逗号分隔 |
--language <BCP47> |
语言标记,如 zh-Hans |
联网刮削
| 参数 | 说明 |
|---|---|
--scrape |
开启联网补全元数据 |
--scrape-url <URL> |
直接指定书籍链接,跳过搜索 |
--scrape-auto |
不询问,采用最佳匹配 |
--no-cover-download |
不下载官方封面 |
--scrape-delay <毫秒> |
请求间隔,默认 1500 |
文本处理
| 参数 | 说明 |
|---|---|
--encoding <标签> |
强制指定输入编码 |
--no-ads |
不清除广告行 |
--join-wraps <auto|always|never> |
硬换行合并策略 |
--no-punctuation |
不做标点规范化 |
--convert-to <变体> |
繁简转换 |
结构识别
| 参数 | 说明 |
|---|---|
--chapter-pattern <正则> |
自定义章节标题正则,可重复 |
--volume-pattern <正则> |
自定义卷标题正则,可重复 |
--only-custom-patterns |
忽略内置模式库 |
--no-volumes |
不识别卷结构 |
--fallback <split|single|fail> |
识别不到章节时的处理 |
产物
| 参数 | 说明 |
|---|---|
--cover <文件> / --no-cover |
指定封面 / 不要封面 |
--css <文件> |
替换内置样式表 |
--split-volumes |
每卷一个 EPUB |
--no-title-page --no-epub2 |
不生成书名页 / toc.ncx |
--no-validate --epubcheck |
跳过校验 / 额外调用 epubcheck |
其他
| 参数 | 说明 |
|---|---|
--config <文件> |
指定配置文件 |
--profile <预设> |
选择预设 |
-v -q --json |
详细 / 静默 / JSON 输出 |
退出码:0 成功 · 1 转换失败 · 2 转换成功但校验有错误(适合 CI 使用)
转出来全是乱码
→ 编码识别错了。epubforge inspect book.txt -v 看编码候选,然后 --encoding gbk 或 --encoding big5 手动指定。
目录里只有一条「第 1 节」
→ 没识别出章节标题,退回按字数切分了。用 inspect -v 看模式打分,然后用 --chapter-pattern 给一条自定义正则。
正文缺了一段
→ 被当成广告删了。inspect -v 会列出所有被删除的行,把误删的内容加进配置文件的 keep_patterns,或者 --no-ads 关掉。
目录里章节标题都是「第一章」没有标题内容 → 你的 txt 本身就是这样。工具不会凭空补标题。
报告说「章节序号有 N 处跳号」 → 源文件缺章。这是提示不是错误,通常是盗版打包本身不全。
每段之间没有空行,挤在一起
→ 这是中文电子书的正常排版(首行缩进两字、段间无空行)。想改用配置文件的 extra_css 加 p { margin-bottom: 0.6em }。
macOS 提示「无法打开,因为无法验证开发者」
→ xattr -d com.apple.quarantine ./epubforge
刮削一直失败
→ 检查网络能否访问目标站点。也可能是站点结构变了 —— 这种情况刮削会失败但转换照常完成。可以改用 --scrape-url 直接给链接。
封面上的中文是方块
→ 系统缺中文字体。工具会自动降级为 SVG 封面。装个中文字体,或用 --cover 指定图片。
一句话版本:每一步都能解释自己的决定,宁可少做也不猜。
读入 → 编码识别 → 元数据推断 → 清洗 → 结构识别 → [联网补全] → 生成 EPUB → 校验
- 编码识别:用每种候选编码解一遍,给解码结果打分(乱码有明显指纹),比从字节猜可靠。
- 清洗:统计检测硬折行并还原段落;广告用规则库 + 高频重复识别,每一条删除都写进报告。
- 结构识别:不是一条正则,而是一个模式库,按产出结果打分择优(序号是否连续、章节是否均匀、是否覆盖全文)。
- 元数据:文件名 → 正文头部 → 刮削 → 配置文件 → 命令行,逐层覆盖,报告标注每项来源。
- 排版:样式表只定义结构,字体字号颜色留给阅读器。
- 校验:产物重新打开逐项检查,包括每个文档的 XML 良构性。
想看更细的设计取舍,见 CHANGELOG 的 Design notes 一节。
- 只吃纯文本,不解析 HTML / Markdown。
- 不自动识别诗歌、书信块(误判率太高)。
- 不做脚注配对(
[1]与章末注释的关联)。 - 插图只支持指定或下载的封面,不扫描同目录图片。
- 不会用官方目录修正本地章节标题 —— 试过,真实书籍的序号跨卷重排、重复、缺号,不可靠,已移除。
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
cargo build --release端到端测试会在内存里构造一个真实脏度的样本 —— GB18030 编码、抓取器头部、36 列硬折行、每章三种广告水印、三卷结构外加楔子和番外 —— 转换后逐项断言产物的容器结构、目录嵌套、元数据和正文内容。
src/
encoding.rs 编码检测(候选打分)
clean.rs 清洗管线
detect.rs 章节与卷识别
meta.rs 元数据推断
book.rs 结构化数据模型
cover.rs 封面生成
epub.rs EPUB 3 打包
validate.rs 产物校验
report.rs 报告渲染
config.rs 配置与预设
scrape/ 联网刮削
MIT