跳到正文
DEV Community· 龚旭东·· 3 小時前AI 評分45

Python 解析與重建 EPUB:實作經驗

Parsing and Rebuilding EPUB Files in Python: Lessons Learned

AI 導讀

LectuLibre 團隊分享如何用 Python 解析及重建 EPUB,為 AI 翻譯替換書中文字,同時保留版面、圖片和樣式。團隊以 ebooklib 讀取 EPUB,並用 lxml 處理 HTML;翻譯時須按段落等區塊合併文字節點,避免逐片翻譯造成句子破碎。直接修改 ZIP 內的 XHTML 再重新封裝,能保留原有中繼資料及自訂檔案。

正文

How we extract and replace text in EPUBs for AI translation without breaking the book

When we first built the translation pipeline for LectuLibre, we thought handling EPUBs would be straightforward: it’s just a ZIP file with HTML inside, right? We quickly learned that preserving a book’s structure while swapping its language is a lot harder than it sounds.

Our platform translates books using LLMs. A user uploads an EPUB, we parse it, extract text, translate chunks with Claude or DeepSeek, then rebuild the EPUB with translated text while keeping layout, images, and styles intact. This article distills the lessons we learned about the parsing and rebuilding steps, which are surprisingly tricky and rarely covered in tutorials.

We’re a Python shop, so we looked for a library to handle EPUB reading/writing. The most popular option is ebooklib. It’s pure Python, well-documented, and abstracts away the EPUB container structure. It seemed perfect. We started with a simple script:

import ebooklib
from ebooklib import epub

book = epub.read_epub('input.epub')
for item in book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
    html_content = item.get_content().decode('utf-8')
    # ... translate html_content ...

This worked for reading. But when we tried to write the translated content back into the EPUB, we hit a wall. ebooklib’s high-level API is designed for creating new EPUBs from scratch, not for surgical modification. If you try to set the content of an existing item, you often lose metadata or break the spine order unless you manually reconstruct everything. We needed a way to modify the HTML in place without disturbing the rest of the book.

Our first approach was to use ebooklib to read the EPUB, convert each document to plain text, translate it, and then rebuild a completely new EPUB from scratch. That meant re-creating the table of contents, the cover page, the CSS links, and hoping we didn’t miss anything. The result was a Frankenstein book: sometimes it looked fine, but often images went missing, fonts broke, and navigation was gone.

We realized we needed to treat the EPUB as a ZIP archive of XHTML files and operate directly on those files. ebooklib is still useful for reading the EPUB and iterating items, but for the actual content transformation, we needed more control. Enter lxml — it’s fast, battle-tested, and can parse HTML (even malformed HTML) with an HTML parser, which is crucial because many real-world EPUBs contain invalid XHTML.

Here’s the core of our content replacement strategy. We parse the HTML with lxml using its HTML parser (which is lenient but preserves structure), then traverse the tree to find text nodes and replace their text with translated content, skipping elements like script, style, and img alt attributes if we don’t want to translate those (we actually do translate alt text later, but that’s a separate step).

from lxml import etree

def replace_text_in_html(html_bytes, translation_map):
    parser = etree.HTMLParser()
    tree = etree.fromstring(html_bytes, parser)
    # Find all elements with text content
    for elem in tree.iter():
        if elem.tag in ('script', 'style'):
            continue  # skip non-translatable content
        if elem.text and elem.text.strip():
            # translation_map maps original text -> translated text
            # In practice, we use a chunking and alignment system
            translated = translation_map.get(elem.text.strip(), elem.text)
            elem.text = translated
        if elem.tail and elem.tail.strip():
            translated = translation_map.get(elem.tail.strip(), elem.tail)
            elem.tail = translated
    return etree.tostring(tree, method='html', encoding='utf-8')

But this naive approach has a problem: text is often split across multiple elements. For example, a paragraph might contain <em>italic</em> words. The text node before the <em> is "This is ", the <em> element’s text is "italic", and its tail is " words". Translating each fragment separately results in broken sentences and grammatical errors. We need to translate whole paragraphs at once, not fragments.

So we changed strategy: we group text nodes at the block level (paragraphs, headings, list items) into a single string, translate that whole string, then split the translated result back into the original fragments using a mapping technique. This is the real challenge, and we’ll cover it in a future post. For now, we focused on the mechanics of updating the EPUB without breaking it.

After transforming all the HTML documents, we need to write the modified EPUB. We could use ebooklib again to write the book, but because we modified the underlying items, we can simply update the content of each item and then write. However, as we learned, ebooklib sometimes reorders items or drops ones it doesn’t understand. A more reliable approach is to directly modify the ZIP archive: unzip the EPUB, edit the XHTML files on disk, then zip it back up. This preserves everything exactly as it was, including unknown metadata and custom files. The downside is that you have to manage the ZIP structure yourself, but it’s not hard.

Here’s a snippet to rebuild the EPUB from a directory of modified files:

import zipfile
import os

def create_epub_from_dir(source_epub_path, temp_dir, output_epub_path):
    # Extract original epub to temp_dir
    with zipfile.ZipFile(source_epub_path, 'r') as z:
        z.extractall(temp_dir)
    # Modify files in temp_dir (we already replaced HTML content)
    # Re-zip the directory into a new epub
    # Important: the first file in the archive must be 'mimetype' (uncompressed)
    with zipfile.ZipFile(output_epub_path, 'w') as z:
        z.write(os.path.join(temp_dir, 'mimetype'), 'mimetype', compress_type=zipfile.ZIP_STORED)
        for root, dirs, files in os.walk(temp_dir):
            for file in files:
                if file == 'mimetype':
                    continue
                full_path = os.path.join(root, file)
                arcname = os.path.relpath(full_path, temp_dir)
                z.write(full_path, arcname, compress_type=zipfile.ZIP_DEFLATED)

This works, but we also needed to handle the EPUB’s metadata, like the content.opf manifest and spine. In our case, we didn’t need to change those because we were only replacing text within existing documents, not adding or removing files. So the manifest stayed valid.

Performance: Using lxml to parse and modify a 300-page novel’s worth of XHTML (around 1.5 MB of text) takes about 2.5 seconds on our VPS. The ZIP extraction and re-compression adds another 0.5 seconds. The translation step (calling the LLM) is the real bottleneck, so the EPUB handling is well within acceptable limits. We also compared lxml with BeautifulSoup for parsing: lxml was about 5 times faster for our use case, which matters when you’re processing thousands of books.

Lessons learned:

  1. EPUB is a ZIP, treat it like one. High-level libraries can hide too much; when you need fine-grained control, go low-level. We still use ebooklib for reading metadata and iterating items, but for modification we manipulate the archive directly.
  2. Use an HTML parser, not an XML parser. Real-world EPUBs are messy. lxml with HTMLParser recovers from errors, while the strict XML parser would crash on half the books we tested.
  3. Preserve the mimetype file. The EPUB specification requires the first file in the ZIP to be the uncompressed mimetype file. If you zip with default settings, you might break the file. The snippet above handles it.
  4. Don’t translate fragment by fragment. You must translate at the block level and then redistribute the translated text back into the original element structure. This is the hardest part, and if you get it wrong, your book reads like a word salad.
  5. Test on a wide variety of EPUBs. We downloaded free books from Project Gutenberg, Standard Ebooks, and some publisher samples. The variety of markup and quirks is staggering.

We’re still improving our fragment-to-block mapping, especially for complex layouts like poetry, code blocks, and tables. If you’ve solved this problem, we’d love to hear your approach. Ultimately, the key takeaway is: don’t fight the format. EPUB is just a ZIP of XHTML files; the simpler you keep your tooling, the fewer surprises you’ll have.

來源:DEV Community · dev.to