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.
An EPUB is essentially a ZIP archive containing HTML, CSS, images, and a manifest file (content.opf
). To translate a book, we needed to:
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.
We evaluated several options:
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.
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
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')
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)
for elem in tree.iter():
if elem.text:
translatable_segments.append({
'id': item.get_id(),
'tag': elem.tag,
'text': elem.text.strip(),
'path': tree.getpath(elem)
})
For LLM translation, we concatenate segments into manageable chunks, preserving paragraph boundaries to give the model context.
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:
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):
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')
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:
zout.writestr(item, zin.read(item.filename))
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)
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")
We also check that the OPF file references all necessary resources. This catches many upload errors before they hit the expensive translation step.
lxml
and zipfile
.chardet
or a similar library to decode text safely.write_epub
might suffice. For complex rebuilds, consider manual ZIP manipulation.mimetype
file correctly.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!