ePub 在线阅读器
上传 .epub 浏览器内阅读 · 目录 / 翻页 / 字号 / 暗色 · 基于 epub.js
ePub→PDF/Mobi/TXT
上传 .epub 浏览器内阅读 · 目录 / 翻页 / 字号 / 暗色 · 基于 epub.js
ePub(Electronic Publication)是国际数字图书馆联盟(IDPF)2007 年制定的电子书标准,基于 HTML + XML + CSS,本质是 ZIP 压缩包。
支持设备:iBooks / Kindle (转 MOBI) / Calibre / Adobe Digital Editions / 多看阅读 / 微信读书 / 几乎所有电子书阅读器。
本地阅读:基于 epub.js 浏览器解析,文件永不上传,隐私安全。适合临时查看 / 试读。
DRM 加密:本工具不支持 Adobe DRM 加密的 ePub(亚马逊 / Google Play Books 等付费书需破解)。
了解工具定位 · 使用场景 · 对比优势
Kindle 用户购买了大量 ePub 电子书,但 Kindle 原生只支持 Mobi/AZW3。将 ePub 批量转换为 Mobi 格式后,可直接通过邮件推送至 Kindle 设备或 App,保留原书目录、字体样式和图片排版,无需在电脑上安装 Calibre 等重型软件。
研究生阅读大量学术 ePub 文献,但 iPad 上做笔记时 PDF 的批注工具更成熟。将 ePub 转为 PDF 后,可在 GoodNotes / Notability 中直接划线、写批注、导出高亮摘要,同时保留原书页码和脚注链接,避免手动翻页定位。
运营人员需要将公司内部的 ePub 培训手册转为纯文本 TXT 格式,以便在老旧工控机、Linux 终端或仅支持纯文本的阅读器上查看。转换后文件体积缩小 90% 以上,且无需安装任何 ePub 阅读软件,直接 cat 或 notepad 即可浏览。
微信读书支持导入 ePub 但部分自制书排版异常(字体过小、章节错乱)。先将 ePub 转为标准 PDF,再导入微信读书,可强制锁定字体大小和行间距,避免 App 端重新排版导致的格式崩坏,尤其适合带代码块或表格的技术类电子书。
设计师需要将 ePub 格式的电子书打印为纸质样书,但直接打印 ePub 会丢失页眉页码、边距过窄。转为 PDF 后在打印前可自定义纸张尺寸(A5/B5)、设置对称页边距、添加裁剪标记,输出可直接送印厂的标准印刷文件。
| 维度 | 本工具 | Calibre | Sigil |
|---|---|---|---|
| 操作方式 | 在线转换,上传即转 | 需下载安装桌面软件 | 需下载安装桌面软件 |
| 处理速度 | 秒级完成 | 秒级完成 | 分钟级(需手动编辑) |
| 转换格式范围 | ePub → PDF / Mobi / TXT | ePub ↔ 几乎所有格式 | 仅编辑 ePub,不直接转换 |
| 学习成本 | 零学习,即开即用 | 中等,功能复杂需学习 | 较高,需了解 HTML/CSS |
| 适用场景 | 快速转换单一文件 | 批量转换、管理电子书库 | 深度编辑 ePub 源码 |
上手步骤 · 输入输出 · 避坑提示
| 输入 | 输出 | 说明 |
|---|---|---|
| example.epub | example.pdf | 典型场景:将 ePub 转换为 PDF 格式 |
| example.epub | example.mobi | 典型场景:将 ePub 转换为 Kindle 支持的 Mobi 格式 |
| example.epub | example.txt | 典型场景:提取纯文本内容,去除排版和图片 |
| large_book.epub (50MB) | large_book.pdf (处理时间约 10 秒) | 边界 case:大文件转换,测试性能上限 |
| no_cover.epub | no_cover.pdf (无封面页) | 边界 case:ePub 缺少封面元数据时的输出 |
| corrupted.epub | 错误:文件格式损坏,无法解析 | 易错 case:损坏的 ePub 文件导致转换失败 |
| empty.epub | 错误:文件中无有效内容 | 易错 case:空文件或仅有空章节的 ePub |
将 .epub 文件手动改为 .zip 扩展名后上传到工具直接上传原始的 .epub 文件,无需任何重命名操作EPUB 本质是 ZIP 压缩包,但工具已内置解压逻辑;重命名会破坏文件签名,导致工具无法识别为有效 EPUB 文件。
上传从 Amazon/Kobo 购买的受 DRM 保护的 .epub 文件仅上传无 DRM 的 EPUB 文件(如自出版、古登堡计划、开源电子书)DRM 加密的 EPUB 内部文件被加密锁住,工具无法解密读取;需先通过合法手段(如 Calibre + DeDRM 插件)去除 DRM 后再转换。
选择输出格式为 TXT,抱怨转换后丢失了加粗、斜体、图片需要保留排版样式时,选择输出格式为 PDF 或 Mobi;TXT 仅保留纯文本内容TXT 格式不支持任何富文本(字体、颜色、图片、表格),这是纯文本格式的固有约束,非工具缺陷。
上传包含特殊艺术字体的 EPUB,转换后 PDF 中字体显示为默认宋体/黑体若需保留特定字体,确保 EPUB 内嵌了字体文件(.otf/.ttf),且转换时选择 PDF 格式EPUB 中引用系统字体在 PDF 转换时可能找不到;只有内嵌字体才能被 PDF 渲染引擎正确使用。
将复杂排版的 EPUB(多栏、浮动图片)转为 Mobi 后发送到 Kindle对于复杂排版的 EPUB,优先选择 PDF 输出;或先用工具转为 Mobi 再在 Kindle Previewer 中预览Mobi 格式对 CSS 支持有限(尤其是浮动、多栏、SVG),Kindle 渲染引擎会降级处理,导致布局变形。
上传包含大量高清图片的 EPUB(如漫画、扫描本),文件大小超过 100MB将 EPUB 中的图片压缩至 1-2MB/张后再上传;或分章节转换浏览器端/服务端对上传文件大小有默认限制(通常 50MB),且大文件转换耗时过长可能触发超时。
将 EPUB 转为 PDF 后,用 PDF 阅读器选中文字却发现无法复制/搜索转换后检查 PDF 是否为「扫描版」;若原 EPUB 是纯文本排版,PDF 应保留可选中文字;若原 EPUB 是图片扫描本,PDF 也是图片PDF 的「可选中文字」取决于源文件是否包含文本层;若 EPUB 本身是图片扫描件(如 PDF 转 EPUB),转换后 PDF 仍为图片。
上传中文 EPUB 文件,选择 TXT 输出,下载后打开显示乱码确认 EPUB 文件的编码为 UTF-8(大多数现代 EPUB 默认),且文本阅读器也设置为 UTF-8 编码部分老旧 EPUB 使用 GBK/GB2312 编码,TXT 输出默认 UTF-8 会导致乱码;可先用工具查看 EPUB 内部文件编码再转换。
公式推导 · 流程图解 · 依据出处
转换过程无单一数学公式,核心为 EPUB 内部 XHTML/CSS 到目标格式(PDF/Mobi/TXT)的解析与重排。
示例:一个 EPUB 文件包含 10 个 XHTML 章节,每个章节有 2000 字中文和 5 张图片。转换为 PDF 时,工具解析每个 XHTML 的 DOM 树,提取文本流和图片引用,按 A4 页面尺寸(210mm × 297mm)重新排版,生成 30 页 PDF。转换为 TXT 时,仅提取纯文本,丢弃所有样式和图片,输出约 20000 字的纯文本文件。转换为 Mobi 时,将 XHTML 转换为 Kindle 支持的 KF8 格式,保留基本排版(标题、段落、斜体)。
适用于标准 EPUB 2/3 格式文件,不适用于加密(DRM)EPUB 或包含复杂交互脚本(JavaScript)的 EPUB。转换质量取决于 EPUB 内部标记的规范性。
3 种主流语言 · 复制即用
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
# 解析 ePub(本质是 ZIP 包),提取纯文本内容
epub_path = "example.epub"
with zipfile.ZipFile(epub_path, "r") as z:
# 读取容器文件获取 OPF 路径
container = ET.fromstring(z.read("META-INF/container.xml"))
ns = {"c": "urn:oasis:names:tc:opendocument:xmlns:container"}
opf_path = container.find(".//c:rootfile", ns).get("full-path")
# 解析 OPF 获取所有 HTML 文件列表
opf = ET.fromstring(z.read(opf_path))
pkg_ns = {"p": "http://www.idpf.org/2007/opf"}
manifest = opf.find("p:manifest", pkg_ns)
spine = opf.find("p:spine", pkg_ns)
id_to_href = {item.get("id"): item.get("href") for item in manifest.findall("p:item", pkg_ns)}
base_dir = Path(opf_path).parent
# 按 spine 顺序提取文本
texts = []
for itemref in spine.findall("p:itemref", pkg_ns):
href = id_to_href[itemref.get("idref")]
content = z.read(str(base_dir / href)).decode("utf-8")
# 简单去除 HTML 标签
import re
text = re.sub(r"<[^>]+>", "", content)
texts.append(text.strip())
print("\n".join(texts)[:500]) # 输出前 500 字符package main
import (
"archive/zip"
"encoding/xml"
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
)
// 解析 ePub 容器文件
func main() {
r, _ := zip.OpenReader("example.epub")
defer r.Close()
// 读取 META-INF/container.xml 获取 OPF 路径
var container struct {
Rootfiles []struct {
FullPath string `xml:"full-path,attr"`
} `xml:"rootfiles>rootfile"`
}
data, _ := readFileInZip(r, "META-INF/container.xml")
xml.Unmarshal(data, &container)
opfPath := container.Rootfiles[0].FullPath
// 读取 OPF 获取 spine 顺序
opfData, _ := readFileInZip(r, opfPath)
type Opf struct {
Manifest []struct {
ID string `xml:"id,attr"`
Href string `xml:"href,attr"`
} `xml:"manifest>item"`
Spine []struct {
IDref string `xml:"idref,attr"`
} `xml:"spine>itemref"`
}
var opf Opf
xml.Unmarshal(opfData, &opf)
idToHref := make(map[string]string)
for _, item := range opf.Manifest {
idToHref[item.ID] = item.Href
}
baseDir := filepath.Dir(opfPath)
re := regexp.MustCompile(`<[^>]+>`)
for _, ref := range opf.Spine {
href := idToHref[ref.IDref]
content, _ := readFileInZip(r, filepath.Join(baseDir, href))
text := re.ReplaceAllString(string(content), "")
fmt.Println(strings.TrimSpace(text)[:200])
}
}
func readFileInZip(r *zip.ReadCloser, name string) ([]byte, error) {
for _, f := range r.File {
if f.Name == name {
rc, _ := f.Open()
defer rc.Close()
return io.ReadAll(rc)
}
}
return nil, fmt.Errorf("file not found: %s", name)
}// 浏览器端用 JSZip 解析 ePub 并提取纯文本
const JSZip = require('jszip');
const fs = require('fs');
async function extractEpubText(filePath) {
const data = fs.readFileSync(filePath);
const zip = await JSZip.loadAsync(data);
// 读取容器文件
const containerXml = await zip.file('META-INF/container.xml').async('string');
const opfPath = containerXml.match(/full-path="([^"]+)"/)[1];
// 读取 OPF 获取 spine 顺序
const opfXml = await zip.file(opfPath).async('string');
const idRefs = [...opfXml.matchAll(/<itemref[^>]+idref="([^"]+)"/g)].map(m => m[1]);
const items = [...opfXml.matchAll(/<item[^>]+id="([^"]+)"[^>]+href="([^"]+)"/g)];
const idToHref = Object.fromEntries(items.map(m => [m[1], m[2]]));
const baseDir = opfPath.substring(0, opfPath.lastIndexOf('/') + 1);
const texts = [];
for (const id of idRefs) {
const href = idToHref[id];
const html = await zip.file(baseDir + href).async('string');
const text = html.replace(/<[^>]+>/g, '').trim();
texts.push(text);
}
console.log(texts.join('\n').slice(0, 500));
}
extractEpubText('example.epub').catch(console.error);8 个高频疑问