Parsing and Rebuilding EPUB Files in Python: Lessons Learned from LectuLibre

작성자

카테고리:

← 피드로
DEV Community · 龚旭东 · 2026-08-12 개발(SW)

龚旭东

How we tackled EPUB parsing and rebuilding for AI book translation, with code examples and hard-won lessons.

At LectuLibre (https://lectulibre.com), we translate books using AI. Users upload EPUB or PDF files, and our system translates the text inside while preserving the original formatting. Sounds straightforward, right? Well, we quickly learned that handling EPUB files in production is anything but trivial. In this article, I’ll share the real-world challenges we faced parsing and rebuilding EPUBs with Python, the libraries we chose, the code we wrote, and the lessons we learned along the way.

The Problem: Translate Books, Not Just Text

An EPUB is essentially a ZIP archive containing HTML, CSS, images, and a manifest file (content.opf). To translate a book, we needed to:

  1. Parse the EPUB’s structure and extract translatable content.
  2. Send that content to our LLM pipeline (Claude, DeepSeek) while preserving context.
  3. Rebuild a new EPUB with the translated content, keeping all formatting, images, and metadata intact.

Simple in theory, but real-world EPUBs are messy. Some have broken manifests, missing required files (mimetype, META-INF/container.xml), or text encoded in obscure charsets. Others are huge, with hundreds of images that we don’t want to load into memory. Our translation pipeline needed to be fast, memory-efficient, and resilient to broken input.

Our Approach: ebooklib + lxml + Careful Validation

We evaluated several options:

  • EbookLib: A widely-used Python library for reading and writing EPUBs. It abstracts the OPF structure and provides convenient methods.
  • manual zipfile processing: Full control but requires reimplementing all the EPUB logic.
  • Calibre: A powerful suite, but its CLI and heavy dependencies didn’t fit our lightweight VPS deployment.

We settled on EbookLib for most reading tasks, lxml for high-performance HTML parsing, and Python’s built-in zipfile for the final rebuilding step where we needed fine-grained control. This combination gave us the right balance of development speed and runtime performance.

Implementation Details (with Real Code)

1. Parsing and Extracting Translatable Text

First, we read the EPUB using EbookLib and iterate over the spine (the linear reading order). We only process items of type ITEM_DOCUMENT (HTML files).

import ebooklib
from ebooklib import epub

book = epub.read_epub('path/to/book.epub')

translatable_segments = []

for item in book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
    content = item.get_body_content().decode('utf-8')  # assuming utf-8
    # But wait – not all EPUBs use utf-8!

Enter fullscreen mode Exit fullscreen mode

Lesson 1: Encoding Hell

Many EPUBs (especially older ones) encode their HTML in Windows-1252 or ISO-8859-1. EbookLib’s get_body_content() often returns bytes, and decoding as utf-8 raises UnicodeDecodeError. We added charset detection using chardet:

import chardet

raw_bytes = item.get_content()
encoding = chardet.detect(raw_bytes)['encoding'] or 'utf-8'
text = raw_bytes.decode(encoding, errors='replace')

Enter fullscreen mode Exit fullscreen mode

Once decoded, we parse the HTML with lxml to extract plain text while preserving the structure for later reassembly.

from lxml import etree

parser = etree.HTMLParser()
tree = etree.fromstring(text.encode('utf-8'), parser=parser)

# Extract text with XPath, but keep track of element boundaries
for elem in tree.iter():
    if elem.text:
        translatable_segments.append({
            'id': item.get_id(),
            'tag': elem.tag,
            'text': elem.text.strip(),
            # storing path for later replacement
            'path': tree.getpath(elem)
        })

Enter fullscreen mode Exit fullscreen mode

For LLM translation, we concatenate segments into manageable chunks, preserving paragraph boundaries to give the model context.

2. Rebuilding the EPUB with Translated Text

After translation, we need to replace the original text in the HTML documents and reassemble the EPUB. EbookLib provides epub.write_epub() but we found it had a few quirks:

  • It sometimes reorders the manifest, which can break navigation.
  • It doesn’t handle custom namespaces or non-standard OPF entries well.
  • It might omit files that are referenced but not in the spine (like cover images).

We opted to manipulate the EPUB as a ZIP file directly for the rebuild step, while still using EbookLib’s data structures to understand the original manifest.

import zipfile
import shutil
import os

def rebuild_epub(original_path, output_path, translated_segments):
    # Read original EPUB as ZIP to get all files
    with zipfile.ZipFile(original_path, 'r') as zin:
        with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zout:
            for item in zin.infolist():
                if item.filename.endswith('.html') or item.filename.endswith('.xhtml'):
                    html_content = zin.read(item.filename).decode('utf-8')
                    # Replace translated segments using lxml
                    tree = etree.fromstring(html_content.encode('utf-8'), parser=parser)
                    for seg in translated_segments:
                        if seg['id'] == item_to_id[item.filename]:
                            elem = tree.xpath(seg['path'])[0] if seg['path'] else None
                            if elem is not None and elem.text:
                                elem.text = seg['translated_text']
                    new_content = etree.tostring(tree, encoding='unicode')
                    zout.writestr(item, new_content.encode('utf-8'))
                else:
                    # Copy other files (images, css, etc.) as-is
                    zout.writestr(item, zin.read(item.filename))

Enter fullscreen mode Exit fullscreen mode

Lesson 2: Preserve the Mimetype File

The EPUB specification requires that the first file in the ZIP be the mimetype file, stored without compression. EbookLib does this automatically when writing, but when using zipfile manually, you must add it first with stored compression. We forgot this initially and our books were rejected by strict readers. Fix:

zout.writestr('mimetype', 'application/epub+zip', zipfile.ZIP_STORED)

Enter fullscreen mode Exit fullscreen mode

3. Validation and Error Handling

Users upload terribly broken EPUBs. We learned to validate early:

def validate_epub(file_path):
    with zipfile.ZipFile(file_path, 'r') as zf:
        if 'META-INF/container.xml' not in zf.namelist():
            raise ValueError("Missing container.xml")
        if 'mimetype' not in zf.namelist():
            raise ValueError("Missing mimetype file")
    # Also check content.opf exists and parses correctly

Enter fullscreen mode Exit fullscreen mode

We also check that the OPF file references all necessary resources. This catches many upload errors before they hit the expensive translation step.

Performance and Trade-offs

  • Memory: EbookLib loads the entire EPUB into memory. For a 50MB book with many images, this was problematic. We modified our pipeline to stream the ZIP and skip binary items during parsing, only extracting document content. This reduced peak memory from ~150MB to ~30MB per book.
  • Speed: Parsing a typical 500-page novel with EbookLib and lxml takes ~1.5 seconds. Translation is the bottleneck (LLM API calls), so this is acceptable. We process books sequentially on a single VPS worker; concurrency comes from multiple workers.
  • Failure rate: Thanks to validation and robust encoding handling, we now successfully process ~97% of user-uploaded EPUBs. The remaining 3% are often DRM-protected or hopelessly malformed.

Honest Reflection: What We’d Do Differently

  • EbookLib is fine, not great. Its development has slowed, and some advanced EPUB3 features aren’t supported (e.g., media overlays). For a future rewrite, we might use a lightweight custom parser on top of lxml and zipfile.
  • HTML manipulation is fragile. We originally tried using regular expressions for text replacement – never do that. lxml saved us countless hours.
  • Always test output with EPUBCheck. We integrated https://github.com/w3c/epubcheck into our CI to validate every translated EPUB before delivery.

Practical Takeaways for Your EPUB Processing Project

  1. Validate early, validate often. Check required files and OPF integrity before processing.
  2. Never trust the encoding. Use chardet or a similar library to decode text safely.
  3. Choose the right rewrite strategy. If your translation needs are simple, EbookLib’s write_epub might suffice. For complex rebuilds, consider manual ZIP manipulation.
  4. Handle the mimetype file correctly. It must be the first entry, stored without compression.
  5. Test with broken books. Grab a sample from Project Gutenberg or feed your parser a corrupted file to see where it fails.

At LectuLibre, this EPUB processing pipeline now handles hundreds of translations per week, allowing readers around the world to enjoy books in their native language. The journey was bumpy, but the lessons we learned have made our service robust and our engineering team wiser.

What libraries or techniques have you used for EPUB processing in Python? We’d love to hear about alternatives to EbookLib or other war stories in the comments!

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다