如题,本篇简单分析如何使用go语言解析epub格式的电子书,获取其内部资源内容。
EPUB格式
首先我们需要了解epub格式具有哪些特点。
已知的是,epub是一种类似doc或者pdf,可以提供图文并茂电子书的格式。
那么我们首先使用二进制编辑器打开,看看能收集到哪些信息:
可以看到是 50 4B (PK) 开头的文件,也就是说,他本身根本就是个 ZIP 压缩文件。
经常上班的朋友都知道,zip格式是比较简单的压缩格式,其文件头部分包含了整篇文件的目录索引。那么我们直接通过压缩软件打开EPUB格式一探究竟。
非常顺利,我们发现epub就是个zip包,其中的内容大概包括配置文件、html或xml、图片资源等等。
那么我们先使用 golang 读取这个文件及其内容。
解包
首先,我使用 archive/zip 这个库来解包
// 创建ZIP读取器
info, _ := file.Stat()
zipReader, err := zip.NewReader(file, info.Size())
if err != nil {
fmt.Println("Error creating ZIP reader:", err)
return
}
// 在内存中存储文本文件内容
textFiles := make(map[string][]byte)
for _, zipFile := range zipReader.File {
var content bytes.Buffer
// 打开并读取ZIP条目的内容
rc, err := zipFile.Open()
if err != nil {
fmt.Println("Error opening ZIP entry for reading:", err)
continue
}
defer rc.Close()
_, err = io.Copy(&content, rc)
if err != nil {
fmt.Println("Error copying data from ZIP entry:", err)
continue
}
textFiles[zipFile.Name] = content.Bytes()
}
我将文件路径作为 key,文件内容作为 value 存储到 textFiles 这个 map 中。
定位资源
这里省略逐个文件探索的过程,直接给出解析思路。
在这部分我使用 github.com/PuerkitoBio/goquery 库来解析 xml、html 文件
首先,我们要解析 META-INF/container.xml 这个文件,这个文件应当是整个 epub 的入口。我们来看看这个文件的内容大概是什么样的。
<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">
<rootfiles>
<rootfile full-path="OPS/content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
可以见得,入口文件在 full-path 属性指定了一个后缀为 opf 的文件作为引导,除此之外没有多少有用的信息。其实这个 OPS/content.opf 据我的实践,在大部分情况下都固定是这位置。但我还是建议大家从头解析,因为你不确定你遇到的格式是否遵循主流约定。
以下三步是从头解析,或者你也可以跳过前两步,直接解析 OPS/content.opf 但我不推荐你这样做。
// 使用 go-query 解析 META-INF/container.xml
metaDoc, metaErr := goquery.NewDocumentFromReader(bytes.NewReader([]byte(textFiles["META-INF/container.xml"])))
if metaErr != nil {
log.Printf("读取 META-INF 出错: %v", metaErr)
//c.JSON(core.RetErr(metaErr.Error()))
return
}
// 获取opf文件路径
confPath, _ := metaDoc.Find("rootfile").Attr("full-path")
// 解析opf文件
confDoc, confErr := goquery.NewDocumentFromReader(bytes.NewReader([]byte(textFiles[confPath])))
if confErr != nil {
log.Printf("读取配置文件 %s 出错: %v", confPath, confErr)
}
这里我们已经解析了opf文件,实际上,我们通常会看看opf的文件结构是什么样的,这里补充一下吧。opf是纯文本格式,可以直接通过记事本打开。
红色标注的分别是封面资源、书籍内容资源。注意,这个xml的格式并不规范,这两部分标签应当是平级的。(下方红框处有多余的缩进,这是错误的)
解析内容
在头部的 metadata 标签中,还会提供一些额外的信息,感兴趣的小伙伴可以分析一下。简单看起来包括语言、作者等信息。
//计算 opf 文件的相对路径
opfPath := confPath[:strings.LastIndex(confPath, "/")+1]
// 解析opf文件
confDoc, confErr := goquery.NewDocumentFromReader(bytes.NewReader([]byte(textFiles[confPath])))
if confErr != nil {
log.Printf("读取配置文件 %s 出错: %v", confPath, confErr)
}
// 读取作者(如果有的话)
confDoc.Find("dc\\:creator").Eq(0).Text()
注意,我们核心关注的是,解析 manifest 标签中的内容,该文件会有其他标签容易混淆。例如 spine 标签,其中也有章节结构的,但我们要解析的是 manifest 标签。
// 解析 manifest 标签
confDoc.Find("manifest").Find("item").Each(func(i int, item *goquery.Selection) {
mediaType, _ := item.Attr("media-type")
//log.Printf("正在处理第 %d 个章节, href=%s, media-type=%s", i, item.AttrOr("href", ""), mediaType)
})
我的思路比较懒,因为只关注类型为 html / xml 等内容,所以我判断 media-type 是否以 “ml” 结尾。
小伙伴们可以做一些更复杂的逻辑,例如进一步解析图片、css、或其他内容。
以下demo用于放在上面那段代码的括号中,提供简易的文件解析。
对文件内容的解析,简易的使用 “ * ” 星号一把梭,也就是解析xml中所有文字信息。
if strings.HasSuffix(mediaType, "ml") {
//内容页
chapterDoc, chapterErr := goquery.NewDocumentFromReader(bytes.NewReader(textFiles[opfPath+item.AttrOr("href", "")]))
if chapterErr != nil {
log.Printf("读取章节文件 %s 出错: %v", item.AttrOr("href", ""), chapterErr)
}
//处理章节内容
var strBuilder strings.Builder
chapterDoc.Find("*").Each(func(i int, item *goquery.Selection) {
strBuilder.WriteString(item.Text())
strBuilder.WriteString("\n")
})
内容字串 := strBuilder.String()
} else if strings.HasPrefix(mediaType, "image/") {
imgCover, _ = item.Attr("href")
//提取图片
图片文件 := textFiles[opfPath+imgCover]
}
本篇完