Procesamiento de ficheros EPUB con Python: parsear, modificar y reconstruir

Un EPUB no es un formato opaco ni propietario. Dentro hay ficheros HTML, CSS e imágenes empaquetados en un ZIP con una estructura concreta. Eso lo convierte en un objetivo perfecto para Python: con la biblioteca estándar ya puedes descomprimirlo, y con un par de dependencias externas puedes hacer casi cualquier cosa.

Qué hay dentro de un EPUB

Si renombras un .epub a .zip y lo abres, verás algo así:

mimetype
META-INF/
    container.xml
OEBPS/
    content.opf
    toc.ncx
    nav.xhtml
    capitulo_01.xhtml
    capitulo_02.xhtml
    images/
        portada.jpg
    styles/
        main.css

El fichero META-INF/container.xml indica dónde está el documento raíz, que casi siempre es content.opf. Ese OPF contiene los metadatos del libro (título, autor, ISBN...) y el spine, que es el orden en que se deben leer los documentos.

Los capítulos son XHTML normal, así que ya sabes cómo manejarlos. Las imágenes van referenciadas desde el OPF y desde el HTML de cada capítulo.

Leer un EPUB con ebooklib

Instala la dependencia:

pip install ebooklib

Abrir el libro es una línea:

import ebooklib
from ebooklib import epub

book = epub.read_epub('mi_libro.epub')

Para recorrer los capítulos, filtra por tipo de ítem:

for item in book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
    html_content = item.get_content().decode('utf-8')
    print(item.get_name(), '-', len(html_content), 'caracteres')

Los metadatos siguen el esquema Dublin Core:

titulo = book.get_metadata('DC', 'title')
autor  = book.get_metadata('DC', 'creator')
fecha  = book.get_metadata('DC', 'date')

print(titulo)   # [('El nombre del viento', {})]

Devuelve una lista de tuplas (valor, atributos), así que el texto del título está en titulo[0][0].

Parsear el HTML con BeautifulSoup

Cada capítulo es HTML, y BeautifulSoup lo trata sin problemas. Instálalo si no lo tienes:

pip install beautifulsoup4

Extraer el texto limpio de un capítulo:

from bs4 import BeautifulSoup

soup = BeautifulSoup(html_content, 'html.parser')
texto = soup.get_text(separator='n', strip=True)

Si necesitas elementos concretos:

# Todos los párrafos
parrafos = soup.find_all('p')

# Divs con clase específica
capitulo = soup.select('div.chapter')

# Eliminar un elemento del árbol
for nota in soup.find_all('aside', class_='footnote'):
    nota.decompose()

# Modificar el atributo src de una imagen
for img in soup.find_all('img'):
    img['src'] = img['src'].replace('../images/', 'images/')

El resultado de decompose() es inmediato: ese nodo desaparece del árbol y ya no aparece en str(soup).

Modificar un EPUB y reconstruirlo

Puedes cambiar los metadatos del libro antes de guardarlo:

book.set_identifier('nuevo-uuid-aqui')

# Reemplazar el título
book.metadata['http://purl.org/dc/elements/1.1/']['title'] = [('Nuevo título', {})]

Para añadir o reemplazar un capítulo completo:

nuevo_cap = epub.EpubHtml(
    title='Epílogo',
    file_name='epilogo.xhtml',
    lang='es'
)
nuevo_cap.content = '

Epílogo

Aquí el texto del epílogo.' book.add_item(nuevo_cap) # Actualizar el spine para incluirlo al final book.spine.append(nuevo_cap)

Guardar el resultado:

epub.write_epub('libro_modificado.epub', book)

Si quieres validar el EPUB resultante, puedes llamar a epubcheck desde Python:

import subprocess

resultado = subprocess.run(
    ['java', '-jar', 'epubcheck.jar', 'libro_modificado.epub'],
    capture_output=True,
    text=True
)
print(resultado.stdout)
print(resultado.stderr)

Necesitas tener Java instalado y el JAR de epubcheck descargado aparte.

Pipeline completo: EPUB a texto limpio

Un caso de uso frecuente es procesar un catálogo entero de libros para indexarlos o analizarlos. El flujo es siempre el mismo: abrir el EPUB, recorrer los capítulos, extraer el texto y limpiarlo.

import json
import ebooklib
from ebooklib import epub
from bs4 import BeautifulSoup

def epub_a_texto(ruta_epub):
    book = epub.read_epub(ruta_epub)

    meta = {
        'titulo':  book.get_metadata('DC', 'title')[0][0]   if book.get_metadata('DC', 'title')   else '',
        'autor':   book.get_metadata('DC', 'creator')[0][0] if book.get_metadata('DC', 'creator') else '',
        'capitulos': []
    }

    for item in book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
        soup = BeautifulSoup(item.get_content().decode('utf-8'), 'html.parser')

        # Quitar notas al pie y cabeceras repetidas
        for tag in soup.find_all(['aside', 'header', 'footer']):
            tag.decompose()

        texto = soup.get_text(separator='n', strip=True)

        if texto:
            meta['capitulos'].append({
                'fichero': item.get_name(),
                'texto':   texto
            })

    return meta


datos = epub_a_texto('novela.epub')
with open('novela.json', 'w', encoding='utf-8') as f:
    json.dump(datos, f, ensure_ascii=False, indent=2)

Con ese JSON ya puedes automatizar tareas de procesamiento de ficheros con Python sin tocar el EPUB original.

Extraer y reemplazar imágenes

Las imágenes también son ítems del libro, y puedes sacarlas igual que los capítulos:

import os

os.makedirs('imagenes_extraidas', exist_ok=True)

for item in book.get_items_of_type(ebooklib.ITEM_IMAGE):
    nombre = os.path.basename(item.get_name())
    ruta   = os.path.join('imagenes_extraidas', nombre)

    with open(ruta, 'wb') as f:
        f.write(item.get_content())

Si quieres reincluir las imágenes comprimidas, Pillow te lo pone fácil:

from PIL import Image
import io

for item in book.get_items_of_type(ebooklib.ITEM_IMAGE):
    img = Image.open(io.BytesIO(item.get_content()))

    # Reducir si es muy grande
    img.thumbnail((1200, 1200), Image.LANCZOS)

    buffer = io.BytesIO()
    img.save(buffer, format='JPEG', quality=75, optimize=True)

    # Reemplazar el contenido del ítem en el libro
    item.set_content(buffer.getvalue())

Después de esto, un epub.write_epub(...) genera el libro con las imágenes comprimidas sin cambiar nada más.

Esto encaja bien con lo que puedes hacer usando Python como herramienta de procesamiento de datos: la misma lógica que aplicas a arrays o dataframes la puedes aplicar a los bytes de una imagen dentro de un libro.

Añadir una tabla de contenidos automática

Muchos libros generados automáticamente llegan sin TOC, lo que los hace incómodos en Kindle o Kobo. Puedes construir la tabla de contenidos leyendo los <h1> de cada capítulo:

toc_items = []

for item in book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
    soup = BeautifulSoup(item.get_content().decode('utf-8'), 'html.parser')
    h1   = soup.find('h1')

    if h1:
        toc_items.append(
            epub.Link(item.get_name(), h1.get_text(strip=True), item.id)
        )

book.toc = toc_items

Para EPUB 3 necesitas dos ficheros de navegación. El NCX es el formato antiguo (EPUB 2) pero muchos lectores todavía lo exigen, incluido el propio Kindle. El NAV es el estándar actual de EPUB 3:

# NCX (compatibilidad EPUB 2 / Kindle)
ncx = epub.EpubNcx()
book.add_item(ncx)

# NAV (EPUB 3)
nav = epub.EpubNav()
book.add_item(nav)

# Actualizar el spine para que empiece por el nav
book.spine = ['nav'] + [item for item in book.spine if item != 'nav']

Si solo incluyes NAV y omites NCX, el libro se abre sin problemas en la mayoría de lectores modernos, pero algunos Kindle antiguos y versiones viejas de Adobe Digital Editions no muestran la tabla de contenidos. Lo más seguro es incluir ambos.

Un par de cosas a tener en cuenta

  • ebooklib tiene algunos comportamientos peculiares con EPUB que mezclan versiones 2 y 3. Si el libro original es un EPUB 2 puro, algunos campos de metadatos pueden aparecer vacíos.
  • El parser html.parser de BeautifulSoup no es tan tolerante con XHTML malformado como lxml. Si encuentras errores al parsear, prueba con 'lxml-xml' como parser, aunque necesita instalar lxml aparte.
  • Los nombres de fichero dentro del EPUB son sensibles a mayúsculas en algunas plataformas. Al reemplazar imágenes, asegúrate de conservar el nombre exacto del ítem original o actualiza las referencias en el HTML.

Con estas herramientas puedes hacer desde extracciones simples hasta pipelines completos que procesen cientos de libros en lote. El formato EPUB no tiene ningún misterio una vez que entiendes que todo es ZIP + HTML.

Imagen: Pexels / Nemuel Sereti

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP