Golang开源EPUB3电子书生成工具EPUBGen

Golang开源EPUB3电子书生成工具EPUBGen 亲爱的 Go 开发者们,

EPUBGen 是我学习 Go 编程语言的第一个项目。我决定构建一个命令行程序,以帮助生成和打包符合 EPUB3 标准的电子书。与其构建一个花哨的视觉编辑器来创建电子书,为什么不使用单个 HTML 文件作为电子书的源文件呢?作为一名开发者,我可以使用许多功能强大的免费开源 IDE 和文本编辑器(例如 Eclipse、ItelliJ IDEA Community 和 VSCodium/VSCode)来创建 HTML 文件。如果您更喜欢花哨的视觉编辑器,可以看看 Sigil

EPUBGen 是一个文本处理器,它读取并处理 HTML 源文件,并在 EPUB3 预期的目录结构中生成所有文件。然后,可以通过运行工具 EPUBCheck 来检查生成的目录及其子目录和内容,并将其打包成 .epub 文件。

完整的源代码和示例电子书可以在 GitHub 上找到。

请随时将这篇帖子转发给您的朋友和同事。如果您遇到任何问题,请发送电子邮件至 roslan@meridiand.com


更多关于Golang开源EPUB3电子书生成工具EPUBGen的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang开源EPUB3电子书生成工具EPUBGen的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这是一个非常棒的Go语言项目!EPUBGen作为学习Go的第一个项目,展示了很好的工程实践和实用性。让我从技术角度分析一下这个工具的实现要点。

核心实现分析

EPUB3电子书生成的关键在于正确的目录结构和文件格式。以下是核心的Go实现示例:

// EPUB包结构生成示例
package main

import (
    "archive/zip"
    "encoding/xml"
    "io"
    "os"
    "path/filepath"
    "strings"
)

// MIME类型文件
const containerXML = `<?xml version="1.0" encoding="UTF-8"?>
<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>`

// EPUB生成器结构
type EPUBGenerator struct {
    Title       string
    Author      string
    HTMLContent string
    OutputPath  string
}

// 生成EPUB文件
func (g *EPUBGenerator) Generate() error {
    // 创建临时目录结构
    tempDir, err := os.MkdirTemp("", "epubgen-*")
    if err != nil {
        return err
    }
    defer os.RemoveAll(tempDir)

    // 创建必需目录
    dirs := []string{
        "META-INF",
        "OEBPS",
        "OEBPS/images",
        "OEBPS/styles",
    }
    
    for _, dir := range dirs {
        if err := os.MkdirAll(filepath.Join(tempDir, dir), 0755); err != nil {
            return err
        }
    }

    // 写入容器文件
    if err := os.WriteFile(
        filepath.Join(tempDir, "META-INF/container.xml"),
        []byte(containerXML),
        0644,
    ); err != nil {
        return err
    }

    // 生成content.opf文件
    opfContent := g.generateOPF()
    if err := os.WriteFile(
        filepath.Join(tempDir, "OEBPS/content.opf"),
        []byte(opfContent),
        0644,
    ); err != nil {
        return err
    }

    // 写入HTML内容
    if err := os.WriteFile(
        filepath.Join(tempDir, "OEBPS/chapter1.xhtml"),
        []byte(g.HTMLContent),
        0644,
    ); err != nil {
        return err
    }

    // 打包为ZIP文件(EPUB格式)
    return g.createEPUB(tempDir)
}

// 生成OPF包文档
func (g *EPUBGenerator) generateOPF() string {
    return `<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
    <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
        <dc:identifier id="uid">urn:uuid:` + generateUUID() + `</dc:identifier>
        <dc:title>` + g.Title + `</dc:title>
        <dc:creator>` + g.Author + `</dc:creator>
        <dc:language>en</dc:language>
        <meta property="dcterms:modified">` + getCurrentTime() + `</meta>
    </metadata>
    <manifest>
        <item id="toc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav"/>
        <item id="chapter1" href="chapter1.xhtml" media-type="application/xhtml+xml"/>
        <item id="css" href="styles/style.css" media-type="text/css"/>
    </manifest>
    <spine>
        <itemref idref="chapter1"/>
    </spine>
</package>`
}

// 创建EPUB文件(ZIP格式)
func (g *EPUBGenerator) createEPUB(sourceDir string) error {
    zipFile, err := os.Create(g.OutputPath)
    if err != nil {
        return err
    }
    defer zipFile.Close()

    zipWriter := zip.NewWriter(zipFile)
    defer zipWriter.Close()

    // 遍历目录并添加文件到ZIP
    return filepath.Walk(sourceDir, func(filePath string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        
        if info.IsDir() {
            return nil
        }

        relPath, err := filepath.Rel(sourceDir, filePath)
        if err != nil {
            return err
        }

        // EPUB要求mimetype必须是ZIP中的第一个文件且不压缩
        if relPath == "mimetype" {
            return g.addMimetype(zipWriter, filePath)
        }

        return g.addFileToZip(zipWriter, filePath, relPath)
    })
}

// 添加mimetype文件(必须不压缩)
func (g *EPUBGenerator) addMimetype(zipWriter *zip.Writer, filePath string) error {
    header := &zip.FileHeader{
        Name:   "mimetype",
        Method: zip.Store, // 不压缩
    }
    
    writer, err := zipWriter.CreateHeader(header)
    if err != nil {
        return err
    }
    
    data, err := os.ReadFile(filePath)
    if err != nil {
        return err
    }
    
    _, err = writer.Write(data)
    return err
}

// 添加普通文件到ZIP
func (g *EPUBGenerator) addFileToZip(zipWriter *zip.Writer, filePath, relPath string) error {
    file, err := os.Open(filePath)
    if err != nil {
        return err
    }
    defer file.Close()

    writer, err := zipWriter.Create(relPath)
    if err != nil {
        return err
    }

    _, err = io.Copy(writer, file)
    return err
}

HTML处理示例

// HTML解析和处理
package processor

import (
    "golang.org/x/net/html"
    "strings"
)

type HTMLProcessor struct {
    SourceHTML string
}

// 提取标题
func (p *HTMLProcessor) ExtractTitle() string {
    doc, err := html.Parse(strings.NewReader(p.SourceHTML))
    if err != nil {
        return ""
    }
    
    var title string
    var traverse func(*html.Node)
    traverse = func(n *html.Node) {
        if n.Type == html.ElementNode && n.Data == "title" {
            if n.FirstChild != nil {
                title = n.FirstChild.Data
            }
            return
        }
        for c := n.FirstChild; c != nil; c = c.NextSibling {
            traverse(c)
        }
    }
    traverse(doc)
    
    return title
}

// 清理HTML并转换为XHTML
func (p *HTMLProcessor) ToXHTML() string {
    // 实现HTML到XHTML的转换逻辑
    // 包括:闭合所有标签、添加XML声明、确保属性引号等
    return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
    <title>Converted Document</title>
</head>
<body>
    ` + p.cleanHTML() + `
</body>
</html>`
}

// 清理HTML内容
func (p *HTMLProcessor) cleanHTML() string {
    // 移除不需要的标签,确保XHTML兼容性
    // 这里可以添加具体的清理逻辑
    return p.SourceHTML
}

命令行接口示例

// 命令行工具实现
package main

import (
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {
    // 定义命令行参数
    inputFile := flag.String("input", "", "输入HTML文件路径")
    outputFile := flag.String("output", "output.epub", "输出EPUB文件路径")
    title := flag.String("title", "", "电子书标题")
    author := flag.String("author", "Unknown", "作者")
    
    flag.Parse()

    // 验证输入
    if *inputFile == "" {
        log.Fatal("必须指定输入HTML文件")
    }

    // 读取HTML内容
    htmlContent, err := os.ReadFile(*inputFile)
    if err != nil {
        log.Fatalf("读取文件失败: %v", err)
    }

    // 创建生成器
    generator := &EPUBGenerator{
        Title:       *title,
        Author:      *author,
        HTMLContent: string(htmlContent),
        OutputPath:  *outputFile,
    }

    // 生成EPUB
    if err := generator.Generate(); err != nil {
        log.Fatalf("生成EPUB失败: %v", err)
    }

    fmt.Printf("EPUB文件已生成: %s\n", *outputFile)
}

测试示例

// 单元测试
package main

import (
    "testing"
    "os"
)

func TestEPUBGeneration(t *testing.T) {
    tempFile := "test_output.epub"
    defer os.Remove(tempFile)
    
    generator := &EPUBGenerator{
        Title:       "测试电子书",
        Author:      "测试作者",
        HTMLContent: "<h1>测试内容</h1><p>这是一个测试段落。</p>",
        OutputPath:  tempFile,
    }
    
    if err := generator.Generate(); err != nil {
        t.Fatalf("生成失败: %v", err)
    }
    
    // 验证文件存在
    if _, err := os.Stat(tempFile); os.IsNotExist(err) {
        t.Fatal("EPUB文件未生成")
    }
    
    // 可以添加更多验证,如ZIP结构、XML有效性等
}

这个项目很好地展示了Go在文件处理、XML生成、ZIP打包和命令行工具开发方面的能力。通过将HTML转换为符合EPUB3标准的结构,EPUBGen为开发者提供了一个简单而有效的电子书生成方案。

回到顶部