DEV Community

龚旭东
龚旭东

Posted on

Parsing and Rebuilding EPUB Files in Python: Lessons Learned from Building an AI Translation Service

How we handle complex EPUB structures, preserve metadata, and rebuild valid files after AI translation.

At LectuLibre, we translate books using LLMs. Users upload an EPUB, our backend extracts the text, sends it to Claude or DeepSeek, and then we need to put the translated text back into the same EPUB—preserving formatting, images, styles, and metadata. That last part turned out to be the hardest.

In this post, I'll walk you through the challenges we faced with parsing and rebuilding EPUB files in Python, the libraries we chose, and the trade-offs we made. If you're building any kind of ebook processing pipeline, these lessons should save you time.

The Problem: EPUB Is More Than Just HTML

EPUB files are ZIP archives containing XHTML/HTML documents, CSS, images, fonts, and metadata files like container.xml and content.opf. To translate a book, we need to:

  1. Extract the textual content from each chapter.
  2. Keep track of where that text came from so we can put translations back in the same place.
  3. Send the text to an LLM (in chunks, because books are long).
  4. Rebuild the EPUB with translated text while preserving all other resources.

Initially, we thought we could just unzip, parse HTML with BeautifulSoup, translate, and zip it back. But we quickly ran into issues:

  • EPUB structure varies: Some books use EPUB2 with NCX navigation, others EPUB3 with nav documents. Metadata is in different places.
  • Namespaces matter: XHTML files often use xmlns="http://www.w3.org/1999/xhtml", and if you don't handle namespaces correctly, BeautifulSoup can mangle the markup.
  • Internal references: The OPF manifest lists all files with IDs and properties. If you rename or add files, you must update the manifest.
  • Encoding and special characters: Books in different languages use various encodings, and LLM outputs may contain smart quotes or special characters that need to be handled correctly.

Our Approach: ebooklib + BeautifulSoup

We evaluated several Python libraries:

  • zipfile + manual XML parsing: too low-level, easy to break things.
  • lxml directly: powerful but requires deep understanding of EPUB spec.
  • ebooklib: a dedicated EPUB library that handles reading/writing, metadata, and manifest management.
  • epub-parser and others: less maintained or incomplete.

We ended up using ebooklib for the EPUB container operations (reading, writing, navigating items) and BeautifulSoup for fine-grained HTML manipulation inside chapters. This combination gave us the best balance of simplicity and control.

Implementation Details

Reading an EPUB and Extracting Text

Here's a simplified version of how we load an EPUB and iterate over its document items:

from ebooklib import epub
from bs4 import BeautifulSoup

def extract_chapters(epub_path):
    book = epub.read_epub(epub_path)
    chapters = []

    for item in book.get_items():
        if item.get_type() == ebooklib.ITEM_DOCUMENT:
            # item is an XHTML/HTML document
            soup = BeautifulSoup(item.get_content(), 'html.parser')
            # Extract text from all paragraphs
            text = '\n\n'.join(p.get_text() for p in soup.find_all('p'))
            chapters.append({
                'id': item.get_id(),
                'file_name': item.get_name(),
                'content': text,
                'soup': soup  # keep soup for later modification
            })
    return book, chapters
Enter fullscreen mode Exit fullscreen mode

We store the soup object because we'll need to modify it during rebuilding.

Translating and Rebuilding

After sending the extracted text to the LLM and receiving translations, we need to put the translated text back into the original HTML structure. Our approach splits the original text into paragraphs and replaces each paragraph's text with the translated version, preserving tags and attributes.

from bs4 import BeautifulSoup

def replace_text_in_soup(soup, original_paragraphs, translated_paragraphs):
    # Find all <p> tags
    p_tags = soup.find_all('p')
    if len(p_tags) != len(translated_paragraphs):
        raise ValueError("Mismatch in paragraph count")

    for p_tag, translated_text in zip(p_tags, translated_paragraphs):
        # Clear existing content and set new text
        p_tag.clear()
        p_tag.append(translated_text)

    return soup
Enter fullscreen mode Exit fullscreen mode

Then we write the modified soup back to the ebooklib item and save the EPUB:

import ebooklib
from ebooklib import epub

def rebuild_epub(book, chapters_with_new_text):
    for chapter in chapters_with_new_text:
        item = book.get_item_with_id(chapter['id'])
        # Convert soup back to bytes
        new_content = str(chapter['soup']).encode('utf-8')
        item.set_content(new_content)

    # Write to a new EPUB file
    epub.write_epub('translated_book.epub', book)
Enter fullscreen mode Exit fullscreen mode

That's the core loop. But real-world books threw many curveballs.

Lessons Learned: Handling Edge Cases

Namespaces

EPUB XHTML files often declare the XHTML namespace. If you use BeautifulSoup's html.parser, it may treat tags as HTML and ignore namespaces, which can lead to issues when serializing. We switched to lxml parser with BeautifulSoup:

soup = BeautifulSoup(content, 'lxml')
Enter fullscreen mode Exit fullscreen mode

This parser is namespace-aware and better preserves the original markup. However, it's slightly slower. For a 500-page book, parsing all chapters with lxml took about 1.2 seconds vs 0.8 seconds with html.parser—a negligible difference compared to the LLM translation time.

Handling Non-Paragraph Content

Not all text is in <p> tags. Headings, list items, tables, and footnotes need translation too. Our initial paragraph-only extraction missed a lot. We expanded extraction to include all text-bearing elements:

def extract_text_elements(soup):
    elements = []
    for tag in soup.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'td', 'th']):
        if tag.get_text(strip=True):
            elements.append(tag)
    return elements
Enter fullscreen mode Exit fullscreen mode

Then we translate each element's text and replace it individually. This works but can disrupt inline formatting like bold or italic within paragraphs. To handle that, we need to translate the entire HTML subtree, not just the text—but that's a much harder problem. For our MVP, we translate per element and accept that some inline formatting may be lost.

CSS and Layout

EPUBs contain CSS files that control layout. We don't modify these, so the translated book looks similar to the original, assuming the structure is intact. However, some languages expand or contract text significantly. For example, German translations are often 30% longer than English. This can cause pagination issues, but that's beyond our current scope.

Cover and Metadata

We preserve the cover image and metadata by using ebooklib's built-in methods. We read the original metadata and write it back unchanged:

# Preserve metadata
original_metadata = book.get_metadata('DC', 'title')
# ... after rebuild, set same metadata
book.set_metadata('DC', 'title', original_metadata[0][0])
Enter fullscreen mode Exit fullscreen mode

Ebooklib handles the OPF manifest automatically when we use its methods, which saved us from manually editing XML.

Malformed EPUBs

Some user uploads are not valid EPUBs; they might be mislabeled or corrupted. We added validation:

import ebooklib
from ebooklib import epub

def validate_epub(path):
    try:
        book = epub.read_epub(path)
        if not book.get_items():
            return False
        # Check for required files
        if not book.get_item_with_id('nav') and not book.get_item_with_id('ncx'):
            # EPUB2 uses NCX, EPUB3 uses nav
            return False
        return True
    except Exception:
        return False
Enter fullscreen mode Exit fullscreen mode

If validation fails, we reject the file with a clear error message.

Performance Numbers

On our VPS (2 vCPU, 4GB RAM), parsing a typical 300-page EPUB with 20 chapters takes about 0.5 seconds using lxml. Extracting all text elements and sending them to the LLM (in chunks) dominates the total processing time—often minutes. Rebuilding the EPUB is fast: under 1 second.

Memory usage is modest because we process chapter by chapter, not the whole book at once. For very large books (1000+ pages), we still keep the entire soup tree in memory per chapter, which can spike to ~50MB per chapter, but it's manageable.

What We'd Do Differently

  1. Use lxml directly for HTML parsing: BeautifulSoup's overhead isn't huge, but if we need finer control over inline elements, we might switch to direct lxml.etree with custom translation logic that preserves markup.
  2. Translate at the block level with inline markup awareness: We'd like to translate each block element while keeping inline tags like <em>, <strong>, and <a> intact. This requires more sophisticated text extraction and reinsertion—possibly using placeholders.
  3. Handle EPUB3 fixed-layout books: These are common for children's books and comics, and they use absolute positioning. Our current approach does not support them.

Conclusion

Parsing and rebuilding EPUBs in Python is doable with the right libraries, but the devil is in the details. ebooklib handles the EPUB container well, and BeautifulSoup with lxml gives you the HTML manipulation power you need. The main challenges are preserving structure, handling namespaces, and dealing with the variety of real-world EPUBs.

If you're building ebook processing tools, my advice is to start with a simple pipeline like the one above, then iterate as you encounter edge cases. Validate early and often.

Open question for the community: How do you handle inline formatting (bold/italic/links) when translating HTML content? We've tried placeholder approaches but are curious about other solutions.


LectuLibre is an AI-powered book translation service. We're constantly improving our EPUB handling to support more formats and languages. If you have experience with ebook pipelines, we'd love to hear from you in the comments.

Top comments (0)