How Book‑to‑Skill Extracts Content from EPUB Files: A Deep‑Dive into the EPUB Extraction Pipeline

Book‑to‑Skill treats EPUB files as ZIP‑archived XHTML documents, extracting chapters via a streaming parser that reads the OPF spine, sanitizes HTML content, and yields structured Chapter objects without writing temporary files to disk.

The epub extraction workflow in Book‑to‑Skill is engineered for security, determinism, and agent‑ready output. This article examines the complete pipeline—from archive parsing to sanitized text generation—based on the source code in virgiliojr94/book-to-skill.


Core Architecture of EPUB Extraction

EPUB files are fundamentally ZIP archives containing XML metadata, XHTML content documents, and media assets. The EPUB parser (book_to_skill/parsers/epub.py) treats this structure as a read‑only stream, avoiding disk‑based temporary extraction.

File Locations and Responsibilities


Step‑by‑Step EPUB Extraction Process

Opening the Archive In‑Memory

The parser uses Python's zipfile module (accessed through book_to_skill/dependencies.py) to open the EPUB without extracting to disk. This approach:

  • Eliminates temporary file vulnerabilities
  • Reduces I/O overhead for large ebooks
  • Enables processing in restricted environments

# Simplified excerpt from book_to_skill/parsers/epub.py

import zipfile
from pathlib import Path

class EpubParser:
    def __init__(self, epub_path: Path):
        self._archive = zipfile.ZipFile(epub_path, 'r')
        self._opf_path = self._locate_opf()  # Find content.opf

Discovering the Reading Spine

EPUBs define content order via the OPF spine, not filesystem sequence. The parser:

  1. Locates META-INF/container.xml to find the root OPF path
  2. Parses the OPF XML for <spine> element and toc.ncx references
  3. Maps itemref IDs to manifest items, producing an ordered file list

This guarantees chapters process in author‑intended sequence regardless of ZIP entry order.

from xml.etree import ElementTree as ET

def _build_spine(self) -> list[str]:
    """Return ordered list of content document paths from OPF spine."""
    opf_content = self._archive.read(self._opf_path)
    root = ET.fromstring(opf_content)
    # Namespace handling omitted for clarity

    spine = root.find('.//{http://www.idpf.org/2007/opf}spine')
    itemrefs = spine.findall('{http://www.idpf.org/2007/opf}itemref')
    return [self._id_to_path(ir.get('idref')) for ir in itemrefs]

Extracting and Converting XHTML Chapters

For each spine item, the parser:

  • Retrieves the XHTML document from the ZIP archive
  • Decodes as UTF‑8 with chardet fallback for legacy encodings
  • Dispatches raw HTML to book_to_skill/parsers/html.py for structured extraction

The HTML parser handles semantic elements—headings, paragraphs, tables, lists—producing clean intermediate text.


# From book_to_skill/parsers/html.py

from bs4 import BeautifulSoup

def extract_text(self, html_bytes: bytes) -> str:
    soup = BeautifulSoup(html_bytes, 'lxml')
    # Remove script/style elements

    for tag in soup(["script", "style", "nav"]):
        tag.decompose()
    # Extract text with structure preservation

    return self._normalize_whitespace(soup.get_text(separator='\n'))

Sanitizing Output for Agent Consumption

Raw extracted text passes through book_to_skill/sanitize.py, which performs:

  • Unicode control character stripping (C0/C1 controls, bidi overrides, annotation markers)
  • Whitespace normalization (collapse multiple newlines, trim leading/trailing space)
  • Boilerplate removal via heuristic patterns for publisher headers/footers

This sanitization step is critical for downstream LLM‑based skill generation, ensuring predictable token sequences.

Asset Tracking and Missing Image Reporting

EPUBs embed images via relative paths in XHTML. The parser:

  • Records all src attributes encountered during HTML traversal
  • Validates existence against ZIP entries
  • Exposes missing_images() method for caller inspection

The test suite in tests/test_epub_image_reporting.py asserts this behavior:


# Example: Detecting broken image references

from book_to_skill.parsers.epub import EpubParser

parser = EpubParser("novel_with_broken_assets.epub")
for chapter in parser.iter_chapters():
    pass  # Process content

unresolved = parser.missing_images()

# Returns: ['images/chapter3_missing.png', 'cover/broken.jpg']

Yielding Structured Chapter Objects

The final output is a stream of Chapter instances defined in book_to_skill/utils.py:

from dataclasses import dataclass
from typing import Optional

@dataclass(frozen=True)
class Chapter:
    title: str
    content: str
    source_path: str
    metadata: Optional[dict] = None

Chapter titles are extracted from:

  • XHTML <title> element
  • First <h1><h6> heading (fallback)
  • Spine item ID (last resort)

CLI Integration and Usage

The book-to-skill command dispatches to the EPUB parser via extension detection in book_to_skill/cli.py:

Programmatic Usage

from book_to_skill.parsers.epub import EpubParser
from pathlib import Path

epub_path = Path("technical_manual.epub")
parser = EpubParser(epub_path)

for idx, chapter in enumerate(parser.iter_chapters(), 1):
    print(f"Chapter {idx}: {chapter.title}")
    print(f"Word count: {len(chapter.content.split())}")
    # Feed chapter.content to skill generation pipeline

Command‑Line Usage


# Extract EPUB to structured skill segments

$ book-to-skill extract ./docs/api_reference.epub --output ./skills/

# Output directory structure:

# ./skills/

# ├── 01-introduction.md

# ├── 02-authentication.md

# └── 03-endpoints.md

EPUB Extraction vs. Other Formats

Book‑to‑Skill implements a unified parser interface (parsers/__init__.py). The EPUB parser distinguishes itself through:

Aspect EPUB Parser Plain Text / Markdown Parsers
Archive handling ZIP stream, no extraction Direct file read
Content ordering OPF spine‑driven Linear file sequence
HTML processing Full XHTML parsing Minimal or no markup handling
Asset awareness Image reference tracking N/A
Memory profile Generator‑based streaming Fully loaded

Summary

  • EpubParser in book_to_skill/parsers/epub.py implements streaming EPUB extraction using Python's zipfile module
  • OPF spine parsing ensures correct chapter sequencing per EPUB specification
  • HTML sanitization via html.py and sanitize.py produces agent‑ready plain text
  • Asset tracking with explicit missing‑image reporting prevents silent data loss
  • Chapter dataclass provides a structured, immutable interface for downstream consumers

Frequently Asked Questions

How does Book‑to‑Skill handle corrupted or non‑standard EPUB files?

The parser validates mandatory EPUB structure (META-INF/container.xml, content.opf) and raises EpubStructureError for missing elements. Malformed XML within content documents triggers graceful degradation: the parser logs warnings, attempts encoding detection, and continues with remaining chapters rather than failing entirely.

Can Book‑to‑Skill extract EPUB content without installing optional dependencies?

Core extraction requires only standard‑library modules (zipfile, xml.etree). However, high‑fidelity HTML parsing depends on beautifulsoup4 and lxml (specified in book_to_skill/dependencies.py). Without these, the parser falls back to regex‑based extraction with reduced structural accuracy.

Why does the EPUB parser not extract images by default?

Book‑to‑Skill targets text‑centric skill generation. Images are tracked via references but not decoded, keeping memory usage bounded and avoiding binary processing complexity. Callers requiring image extraction can access raw ZIP entries through parser._archive.read(image_path) using recorded reference paths.

How are chapter titles determined when metadata is sparse?

The parser applies a priority cascade: (1) XHTML <title> element, (2) first semantic heading <h1><h6>, (3) toc.ncx navigation label, (4) spine idref with numeric prefix. This heuristic approach maximizes usable titles across publisher‑quality and self‑published EPUBs with inconsistent markup.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →