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
book_to_skill/parsers/epub.py– Spine discovery, ZIP traversal, and chapter assemblybook_to_skill/parsers/html.py– HTML‑to‑text conversionbook_to_skill/sanitize.py– Unicode normalization and control character removalbook_to_skill/utils.py–Chapterdataclass definitiontests/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) 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:
- Locates
META-INF/container.xmlto find the root OPF path - Parses the OPF XML for
<spine>element andtoc.ncxreferences - Maps
itemrefIDs 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
chardetfallback for legacy encodings - Dispatches raw HTML to
book_to_skill/parsers/html.pyfor 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
srcattributes 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
EpubParserinbook_to_skill/parsers/epub.pyimplements streaming EPUB extraction using Python'szipfilemodule- OPF spine parsing ensures correct chapter sequencing per EPUB specification
- HTML sanitization via
html.pyandsanitize.pyproduces agent‑ready plain text - Asset tracking with explicit missing‑image reporting prevents silent data loss
Chapterdataclass 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →