把 EPUB 解压之后:mimetype 为什么必须排第一,OPF 又管着什么

· 约 5 分钟 📗 EPUB 编辑器

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 里的存放方式有三条规范约束:

  1. 必须是压缩包里的第一个条目
  2. 必须用 store(不压缩)方式存储
  3. 不能带 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 在哪

因为除了 mimetypeMETA-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 的区别值得说清楚

manifestspine
管什么包里有哪些文件正文按什么顺序翻
谁要进所有文件(图片、CSS、字体都要)只有正文页面
不在里面会怎样文件形同不存在翻页翻不到,但可以被链接跳过去

所以一张图片只进 manifest,不进 spine;一个封面页两个都要进。

<spine toc="ncx"> 上那个属性指向 EPUB 2 的目录文件,这是两代格式共存留下的痕迹。

四、两套目录

一本典型的 EPUB 会同时带两份目录:

文件属于格式怎么被找到
toc.ncxEPUB 2XMLspine 的 toc 属性
nav.xhtmlEPUB 3XHTMLmanifest 的 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                      ✗  裸的 & 要写成 &amp;

为什么规范要这么严:让所有阅读器对同一本书的解析结果完全一致,避免「在这个阅读器上正常、换一个就错位」。代价就是容错为零。

六、路径的基准换了三次

这一条让很多人在调整目录结构之后图全裂了:

位置路径相对于
container.xmlfull-path包根
OPF 里的 hrefOPF 所在目录
正文 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 有两种完全不同的用途:

  1. 字体混淆 —— 一种轻度的字体保护措施,规范里有定义,正常的阅读器都支持
  2. DRM —— 商业电子书的数字版权保护

看到这个文件时先确认是哪一种。带 DRM 的书不能编辑EPUB 编辑器 检测到会直接拒绝打开,本站也不提供 DRM 解除方法。

相关

❓ 常见问题

我把文件夹重新压成 zip 改名 epub,为什么阅读器打不开?

几乎一定是 mimetype 那一条没满足OCF 规范对它有三条硬约束:(1) 必须是 zip 里的第一个条目;(2) 必须用 store(不压缩)方式存储;(3) 不能带 extra field,内容就是 application/epub+zip 这一行,不加 BOM、不加换行。为什么这么苛刻:这样一来,任何程序只要读文件开头固定偏移的几十个字节就能确认「这是 EPUB」,不必先解析整个 zip 目录。正确的打包方式是分两步:先用 store 方式只加 mimetype,再用正常压缩追加其余文件。顺带:直接右键「压缩」整个文件夹通常会多出一层目录,那样 container.xml 的路径也对不上了。用 EPUB 编辑器 改完直接导出可以完全绕开这一环。

为什么很多书里同时有 toc.ncx 和 nav.xhtml?

为了同时兼容 EPUB 2 和 EPUB 3 的阅读器两者的分工:(1) toc.ncx 是 EPUB 2 的导航文档,XML 格式,由 spine 上的 toc 属性指向;(2) nav.xhtml 是 EPUB 3 的,本身就是一个 XHTML 页面,在 manifest 里带 properties="nav" 标记。EPUB 3 规范里 ncx 已经是可选的,但老阅读器只认它,所以转换工具普遍两个都生成。维护上的代价:增删章节时两份目录都要改,漏掉一份的表现是——书能打开、正文正常,但在某些阅读器上目录缺一项或者点了跳到空白页,而且不报任何错。这也是手工改 EPUB 最容易留下的隐患,改完最好用 EPUBCheck 跑一遍。

正文少一个闭合标签,为什么整本书就打不开?

因为 EPUB 的正文要求是良构(well-formed)XHTML,不是宽松的 HTML区别:(1) 浏览器解析 HTML 时会容错 —— 标签没闭合、属性没引号,它会自己猜着补上;(2) XML 解析器不容错 —— 一处不良构就整个文档解析失败,规范要求阅读器此时报错而不是猜。最常见的四种:标签没闭合、<br><img> 没写成自闭合形式、属性值少引号、正文里裸写了 &(必须写成 &amp;)。为什么规范要这么严:让所有阅读器对同一本书的解析结果一致,避免出现「在这个阅读器上正常、换一个就错位」。实用做法:手改 EPUB 之后一定要过一遍语法校验,EPUB 编辑器 在保存前会挡住不良构的文件,不会产出一本打不开的书。

包里的路径是相对于哪里的?我调整了文件夹结构,图全裂了。

相对于 OPF 文件所在的目录,不是包的根目录举例:OPF 在 OEBPS/content.opf,manifest 里写 href="images/cover.jpg",实际指的是 OEBPS/images/cover.jpg正文 XHTML 里的路径则相对于它自己——Text/chapter1.xhtml 里写 ../Images/a.jpg 指的是 OEBPS/Images/a.jpg所以移动文件会同时断掉三类引用:(1) OPF manifest 里的 href;(2) 正文里的 <img src><a href>;(3) CSS 里的 url()唯一不受影响的是 container.xml —— 它里面的 full-path 固定相对于包根。做法:改名或移动文件要把全书所有文本文件扫一遍同步改引用,工具做这件事比手改可靠得多。

📗 打开 EPUB 编辑器 改正文/样式/目录·增删章节·调阅读顺序·全书查找替换·代码排版·EPUBCheck 校验并跳到出错行·只重写改动过的文件·本地处理

🔗 相关阅读

全部教程 →