EPUB 的文件后缀改成 .zip,双击就能解压。里面是一组普通的 XHTML、CSS、图片,外加几个描述文件。
但它对 zip 本身有一条不讲理的规定,也是自己重新打包最容易失败的地方。先从这一条说起。
解压之后的样子
book.epub
├── mimetype ← 必须是第一个,且不压缩
├── META-INF/
│ └── container.xml ← 指路:OPF 在哪
└── OEBPS/ ← 目录名随意,习惯叫 OEBPS
├── content.opf ← 全书的总账
├── toc.ncx ← EPUB 2 的目录
├── nav.xhtml ← EPUB 3 的目录
├── Text/
│ ├── chapter1.xhtml
│ └── chapter2.xhtml
├── Styles/
│ └── style.css
└── Images/
└── cover.jpg
一、mimetype 的三条硬约束
这个文件只有一行内容:
application/epub+zip
没有换行、没有 BOM。但它在 zip 里的存放方式有三条规范约束:
- 必须是压缩包里的第一个条目
- 必须用 store(不压缩)方式存储
- 不能带 extra field
为什么这么苛刻:满足这三条之后,任何程序只要读文件开头固定偏移的那几十个字节,就能确认「这是一个 EPUB」,不需要先解析整个 zip 的中央目录。这是一种嵌在容器格式里的魔数。
后果:用系统的「右键 → 压缩」把文件夹打成 zip,几乎一定不满足——文件顺序不可控,而且通常会多出一层目录。正确的打包是两步:
zip -X0 book.epub mimetype # -0 不压缩,先加它
zip -Xr9D book.epub META-INF OEBPS # 再追加其余文件
这也是「我明明只改了一个错别字,重新压完就打不开了」的头号原因。
二、container.xml:唯一固定的路径
<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="OEBPS/content.opf"
media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
它存在的意义只有一个:告诉阅读器 OPF 在哪。
因为除了 mimetype 和 META-INF/container.xml 这两个位置,EPUB 里其余所有文件的路径和目录名都是自由的——OEBPS 只是习惯,叫什么都行。
full-path 是相对于包根的,这是全包唯一一个以包根为基准的路径,后面所有路径都换了基准。
三、OPF:全书的总账
OPF(Open Packaging Format)是整本书的中枢,分三段:
metadata —— 书的身份
书名、作者、语言、标识符、出版日期、封面指向。改书名、改作者动的就是这里。
manifest —— 包里有哪些文件
每一个文件都必须在 manifest 里登记,没登记的文件等于不存在,阅读器不会去读它。
<item id="ch1" href="Text/chapter1.xhtml" media-type="application/xhtml+xml"/>
<item id="css" href="Styles/style.css" media-type="text/css"/>
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
注意最后那个 properties="nav"——EPUB 3 就是靠它认出哪个文件是导航文档。
spine —— 按什么顺序读
<spine toc="ncx">
<itemref idref="ch1"/>
<itemref idref="ch2"/>
</spine>
spine 引用的是 manifest 里的 id,它决定翻页顺序。
manifest 和 spine 的区别值得说清楚:
| manifest | spine | |
|---|---|---|
| 管什么 | 包里有哪些文件 | 正文按什么顺序翻 |
| 谁要进 | 所有文件(图片、CSS、字体都要) | 只有正文页面 |
| 不在里面会怎样 | 文件形同不存在 | 翻页翻不到,但可以被链接跳过去 |
所以一张图片只进 manifest,不进 spine;一个封面页两个都要进。
<spine toc="ncx"> 上那个属性指向 EPUB 2 的目录文件,这是两代格式共存留下的痕迹。
四、两套目录
一本典型的 EPUB 会同时带两份目录:
| 文件 | 属于 | 格式 | 怎么被找到 |
|---|---|---|---|
toc.ncx | EPUB 2 | XML | spine 的 toc 属性 |
nav.xhtml | EPUB 3 | XHTML | manifest 的 properties="nav" |
EPUB 3 规范里 ncx 已经是可选的,但老阅读器只认它,所以转换工具普遍两个都生成。
维护代价很实在:增删一章要改四处——manifest、spine、nav.xhtml、toc.ncx。漏掉任何一处的表现都很隐蔽:
- 漏 manifest → 文件形同不存在
- 漏 spine → 那一章在阅读器里直接消失,不报错
- 漏某一份目录 → 在某些阅读器上目录缺一项,或者点了跳到空白页
这也是手工改 EPUB 最容易留下的隐患。EPUB 编辑器 做章节增删时会把四处一起同步,改完还能用内置的 EPUBCheck 跑一遍。
五、正文必须是良构 XHTML
这是「少个闭合标签整本书就打不开」的原因。
浏览器解析 HTML 时是容错的——标签没闭合、属性没引号,它会自己猜着补。而 EPUB 的正文走的是 XML 解析器,一处不良构就整个文档解析失败,规范要求阅读器此时报错而不是猜。
四种最常见的不良构:
<p>没有闭合的段落 ✗ 少了 </p>
<br> ✗ 要写成 <br/>
<img src=cover.jpg> ✗ 属性值要有引号,且要自闭合
AT&T ✗ 裸的 & 要写成 &
为什么规范要这么严:让所有阅读器对同一本书的解析结果完全一致,避免「在这个阅读器上正常、换一个就错位」。代价就是容错为零。
六、路径的基准换了三次
这一条让很多人在调整目录结构之后图全裂了:
| 位置 | 路径相对于 |
|---|---|
container.xml 的 full-path | 包根 |
OPF 里的 href | OPF 所在目录 |
正文 XHTML 里的 src / href | 该 XHTML 自己 |
CSS 里的 url() | 该 CSS 自己 |
所以 OPF 在 OEBPS/content.opf 时,manifest 里的 href="Images/a.jpg" 指的是 OEBPS/Images/a.jpg;而 OEBPS/Text/ch1.xhtml 里要引用同一张图,得写 ../Images/a.jpg。
移动或重命名一个文件,会同时断掉三类引用:OPF 的 href、正文的 src/href、CSS 的 url()。手工改必须把所有文本文件扫一遍。
七、顺带说一下 encryption.xml
META-INF/encryption.xml 有两种完全不同的用途:
- 字体混淆 —— 一种轻度的字体保护措施,规范里有定义,正常的阅读器都支持
- DRM —— 商业电子书的数字版权保护
看到这个文件时先确认是哪一种。带 DRM 的书不能编辑,EPUB 编辑器 检测到会直接拒绝打开,本站也不提供 DRM 解除方法。
相关
- 书打不开、报错不说原因 → EPUB 打不开怎么排查
- 只想改书名、作者、封面 → EPUB 元数据编辑,以及 元数据与阅读器兼容性
- 从 TXT 生成一本结构正确的书 → TXT 转 EPUB,章节识别与目录生成见 章节识别与目录
- 各格式之间怎么选 → 电子书格式对比