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

> Learn how Book-to-Skill efficiently extracts content from EPUB files by treating them as ZIP archives and streaming XHTML chapters. Discover the EPUB extraction pipeline and structured chapter object generation.

- Repository: [Virgilio Junior/book-to-skill](https://github.com/virgiliojr94/book-to-skill)
- Tags: deep-dive
- Published: 2026-08-31

---

**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`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/parsers/epub.py)) treats this structure as a read‑only stream, avoiding disk‑based temporary extraction.

### File Locations and Responsibilities

- **[`book_to_skill/parsers/epub.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/parsers/epub.py)** – Spine discovery, ZIP traversal, and chapter assembly
- **[`book_to_skill/parsers/html.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/parsers/html.py)** – HTML‑to‑text conversion
- **[`book_to_skill/sanitize.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/sanitize.py)** – Unicode normalization and control character removal
- **[`book_to_skill/utils.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/utils.py)** – `Chapter` dataclass definition
- **[`tests/test_epub_image_reporting.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/tests/test_epub_image_reporting.py)** – Validation of missing asset detection

---

## 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`](https://github.com/virgiliojr94/book-to-skill/blob/main/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

```python

# 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`](https://github.com/virgiliojr94/book-to-skill/blob/main/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.

```python
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`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/parsers/html.py) for structured extraction

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

```python

# 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`](https://github.com/virgiliojr94/book-to-skill/blob/main/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`](https://github.com/virgiliojr94/book-to-skill/blob/main/tests/test_epub_image_reporting.py) asserts this behavior:

```python

# 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`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/utils.py):

```python
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`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/cli.py):

### Programmatic Usage

```python
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

```bash

# 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`](https://github.com/virgiliojr94/book-to-skill/blob/main/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`](https://github.com/virgiliojr94/book-to-skill/blob/main/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`](https://github.com/virgiliojr94/book-to-skill/blob/main/html.py) and [`sanitize.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/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`](https://github.com/virgiliojr94/book-to-skill/blob/main/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`](https://github.com/virgiliojr94/book-to-skill/blob/main/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.