©️ OverlookArt

EPUB

阅读 EPUB 电子书的三板斧 解包器, 解析器, 阅读器
iOS EPUB 电子书 通过RPS(Reader Parser Server)实现在线阅读,并通过RSA,AES 双重加密保证数据的安全性
阅读器采用复用页面机制(Page Reuse Mechanism)用使程序开销降到最低并保证阅读体验
使用 WebKit 框架 作为swift代码与电子书html页面交互的桥梁,并编写辅助js脚本实现翻页模式,字号,主题的切换

在线阅读离线阅读ServerParserReader解包器解析器阅读器ServerParserReader解包器解析器阅读器基础数据包括 Content, Toc, CSS, 密钥 等文件parlooppar[加载与渲染页面数据]Decryptor让解包器解压缩 EPUB 文件得到解压缩后的 EPUB 文件夹得到解析后的 EPUB 数据模型请求 EPUB 基础数据解析网络数据配置基础数据请求最后一次阅读页面数据获得要渲染页面标识请求页面数据解密页面内容得到页面的原始数据请求页面图片资源将图片资源配置到页面数据中请求页面的书签与笔记数据将书签笔记配置到页面数据中进行最终的页面渲染Decryptor

Epub 解包器

EPUB 文件其实是一个压缩包,使用压缩工具可将其解压,得到一个 EPUB 文件夹。

在 iOS 平台使用开源的解压缩工具库 SSZipArchive ,

 1// Unpacker.swift
 2import SSZipArchive
 3import Foundation
 4class Unpacker: NSObject {
 5    /// Epub 文件解包
 6    /// - Parameters:
 7    ///   - epubFileURL: 原文件路径
 8    ///   - unPackageURL: 解包文件路径
 9    func unPackage(epubFileURL: URL, unPackageURL: URL) -> Bool {
10        guard FileManager.default.fileExists(atPath: epubFileURL.path) else {
11            debugPrint("未找到 epub 文件", epubFileURL.path)
12            return false
13        }
14        if FileManager.default.fileExists(atPath: unPackageURL.path) {
15            debugPrint("解包路径已存在:",unPackageURL.path)
16            return true
17        }
18        return SSZipArchive.unzipFile(atPath: epubFileURL.path, toDestination: unPackageURL.path, delegate: self)
19    }
20}
21
22/// 解压文件代理方法
23extension Unpacker: SSZipArchiveDelegate {
24    /// 将要解包
25    func zipArchiveWillUnzipArchive(atPath path: String, zipInfo: unz_global_info) { }
26    
27    /// 是否解包
28    func zipArchiveShouldUnzipFile(at fileIndex: Int, totalFiles: Int, archivePath: String, fileInfo: unz_file_info) -> Bool { return true }
29    
30    /// 将要生成解包后的文件路径
31    func zipArchiveWillUnzipFile(at fileIndex: Int, totalFiles: Int, archivePath: String, fileInfo: unz_file_info) { }
32    
33    /// 解包完成生成解压后的文件夹
34    func zipArchiveDidUnzipFile(at fileIndex: Int, totalFiles: Int, archivePath: String, fileInfo: unz_file_info) { }
35    
36    /// 解包进度
37    func zipArchiveProgressEvent(_ loaded: UInt64, total: UInt64) { }
38    
39    /// 解包完成
40    func zipArchiveDidUnzipArchive(atPath path: String, zipInfo: unz_global_info, unzippedPath: String) { }
41
42    /// 解包完成
43    func zipArchiveDidUnzipFile(at fileIndex: Int, totalFiles: Int, archivePath: String, unzippedFilePath: String) { }
44}
1/-
2 |-META-INF
3  |-container.xml (该文件EPUB阅读器读取EPUB内容(content.opf)文件路径)

/META-INF/container.xml 该文件始终存在,否则就是非法的 EPUB 文件,解析器会通过该文件逐步将 EPUB 文件内容解析出来。

Epub 解析器

根据 EPUB 文件夹结构来确定解析流程

需要解析的 EPUB 文件类型都是 XML 类型, 虽然有些文件的后缀不是 xml,但里面的内容都是由 xml 标签组成。
这里使用到的 XML 解析工具是 SwiftSoup。

获取 资源文件根目录 RootPath
获取 content.opf 文件路径

解析 container.xml 文件

metadata (元数据,包含书籍名称,版权,作者,出版方等信息)
manifest (整本书的资源文件清单, 包含目录文件 toc.ncx 路径)
spine (书脊,所有xhtml文档的线性阅读顺序)

解析 content.opf 文件

label (目录标题)
contentSrc (内容资源文件路径)
navItems (子目录)

解析 toc.ncx 文件

EPUB解压后文件

解析器委托代理事件

 1// Parser.swift
 2protocol ParserDelegate {
 3    
 4    /// 开始解析 epub
 5    func beginParserEpub(url: URL)
 6    
 7    /// 已解析 container
 8    func didParserContainer()
 9    
10    /// 已解析 Content
11    func didParserContent()
12    
13    /// 已解析 TOC
14    func didParserToc()
15    
16    /// 解析 epub 完成
17    func endedParserEpub()
18    
19    /// 解析 Epub 出错
20    func errorParserEpub(error: ParserError)
21}

枚举解析失败错误

 1enum ParserError: Error {
 2    /// 无效的文件
 3    case FileInvalid(message: String, fileUrl: URL?)
 4    
 5    /// 转 String 失败
 6    case ToStrFailed(message: String, data: Data?)
 7    
 8    /// 转 XML 失败
 9    case ToXMLFailed(message: String, xmlStr: String)
10    
11    /// content 内容缺失
12    case ContentLack(message: String)
13}

解析器类实现

 1// Parser.swift
 2class Parser {
 3    /// 声明解析 EPUB 数据模型
 4    var parserData = ParserData()
 5    /// 声明代理
 6    var delegate: ParserDelegate?
 7
 8    /// 开始解析 Epub解压后的 文件内容
 9    private func beginParserEpub(url: URL) throws { ··· }  
10
11    /// 解析 container 文件
12    private func parseContainer() throws { ··· }  
13
14    /// 解析 content 文件
15    private func parseContent() throws { ··· }
16
17    /// 解析 TOC 目录
18    private func parseToc() throws { ··· }
19
20
21    /// 衍生方法
22
23    /// 解析外部的 container 文件
24    public func parseContainer(data: Data) throws  -> Container { ··· }
25    /// 解析外部的 Content 文件
26    public func parseContent(data: Data) throws -> Content { ··· }
27    /// 解析外部的 TOC 目录文件
28    public func parseToc(data: Data) throws -> Toc { ··· }  
29    
30}

解析后的数据

rootPath
contentPath
Container
title 书名
creators 作者
publisher 出版方
identifier 唯一标识
date 出版日期
rights 版权
description 描述
language 语言
cover 封面图
Metadata
id 唯一标识
href 文件相对路径
mediaType 文件类型
rootPath 文件根路径
[Resource]
Manifest
idref
Resource
content 页面内容
audio 音频地址
permissions 阅读权限 0可读 1试读 2付费
[SpineItem]
Spine
Content
id 唯一标识
label 章节标题
contentSrc 章节内容资源
navItems 子章节
[NavItem]
Toc
ParserData

Epub 阅读器