epub-blocks
epub-blocks is a dependency-free Python library for reproducibly extracting
ordered, structured text blocks from EPUB 2 and EPUB 3 files. A declarative
recipe selects an edition’s XHTML, assigns block types, and generates stable
identifiers without edition-specific Python code or a pre-existing output
table.
It requires Python 3.13 or later. The project is pre-1.0, so its public API and recipe format may still make breaking changes.
Installation
python -m pip install epub-blocks
The runtime package has no third-party dependencies.
What it does
The package:
- safely reads EPUB package metadata and XHTML spine documents;
- selects source documents and blocks with case-insensitive globs;
- normalizes extracted text and omits configured EPUB semantic elements;
- derives groups from source paths, heading markers, or explicit transitions;
- generates flat and nested identifiers with per-group counters;
- assigns output types through ordered source-block rules;
- handles joins, splits, omissions, skipped blocks, and inserted material;
- selects whole compound blocks and intentional empty structural blocks;
- preserves recipe-selected spans and milestones as XML fragments or configurable Unicode delimiters, without requiring new paragraph numbers;
- pins both the EPUB and the compiled extraction plan with SHA-256; and
- writes headerless
id,type,textTSV records.
A recipe is sufficient to generate a new base text. It does not need a
separately stored list of expected identifiers or types. Existing outputs may
be useful as test oracles while authoring a recipe, but they are not recipe
inputs and are not read by epub-blocks.
The package does not compare editions, select preferred readings, apply editorial corrections, create witness manifests, or emit stand-off annotations. It can generate a marked-up TSV for a separate converter to derive plain text and stand-off together. Recipes describe source structure, not expected prose.
Inspecting an EPUB
Use the lower-level API to discover source blocks before writing a recipe:
from epub_blocks import extract_blocks
for block in extract_blocks(
"book.epub",
include_documents=["*chapter*.xhtml"],
exclude_classes=["image-caption"],
):
print(block.source_locator, block.tag, sorted(block.classes), block.text)
A locator such as s008:text/chapter-01.xhtml#1.3.2 combines the one-based
spine position, EPUB-internal document path, and element path within the XHTML
body. Recipes use the EPUB-local portion: text/chapter-01.xhtml#1.3.2.
Recipe example
Recipe version 1 remains the serialized format in epub-blocks 0.4.0. Existing 0.3.0 recipes keep their extracted fields and compiled digest when the optional new features are absent. TSV serialization now uses literal fields instead of CSV-style quoting:
{
"recipe_version": "1",
"metadata": {"name": "Example edition"},
"epub": {
"identifier": "9780000000000",
"sha256": "f4f9c2d902a41b80732b2dce7ad01a57f615859c21417a5019e3cd8e4d271282"
},
"normalization": {
"collapse_whitespace": true,
"strip": true,
"unicode_normalization": "NFC"
},
"omit_epub_types": ["noteref", "pagebreak"],
"source_blocks": {
"include_documents": ["text/chapter-*.xhtml"],
"exclude_classes": ["image-caption"]
},
"output": {
"groups": {
"source_marker": {
"pattern": "^CHAPTER\\s+([IVXLCDM]+)\\b",
"case_insensitive": true
},
"capture_kind": "roman",
"capture_width": 2
},
"identifiers": {
"block": {
"template": "{group}.{number:03d}",
"start": 1
},
"line": {
"template": "{group}.{block:03d}.{number:02d}",
"start": 1
}
},
"default": {"type": "paragraph", "role": "block"},
"rules": [
{
"match": {"tag": "h1"},
"type": "heading",
"role": "fixed",
"id": "{group}.000",
"remove_prefix": {
"pattern": "^CHAPTER\\s+[IVXLCDM]+\\s+",
"case_insensitive": true
}
},
{
"match": {"classes": ["verse-first"]},
"type": "line",
"role": "line-start"
},
{
"match": {"classes": ["verse"]},
"type": "line",
"role": "line"
}
],
"skip_source": [],
"replacements": [],
"insertions": [],
"compiled_sha256": "28354978484f35525c616d138bdeb8f801039d665235ac88bd02f1a25791c410"
}
}
This recipe generates identifiers such as 01.000, 01.001, 01.002.01,
and 01.002.02. Rules are matched against source structure; no expected text
or identifier table is consulted.
See the complete recipe specification and packaged JSON Schema.
Optional marked-up text
The recipe's text.markup object chooses "format": "xml" or
"format": "delimiters". Both serializers use the same structural rules,
references, normalized text, and annotation boundaries. For example:
XML: A <em>quiet</em> word.<page label="iv"/>
Delimiters: A ⧼quiet⧽ word.⟦iv⟧
Milestones between text blocks can attach to the following block without consuming a number. Trailing milestones require an explicit policy. Literal text is escaped so it cannot be mistaken for markup. See the markup contract for the complete configuration, grammar, escaping rules, normalization, slicing, and attachment semantics.
Command line
epub-blocks book.epub recipe.json records.tsv
The output is headerless TSV with id, type, and extracted text columns.
Fields are separated by literal tabs and records by LF. Quotes and backslashes
are written unchanged: there is no CSV quoting, quote doubling, or TSV escaping.
Embedded TAB, CR, or LF characters in any field are rejected rather than
silently altering text or producing ambiguous rows. Normalize text or encode
such characters in the markup before writing. A failed write leaves any
existing output file intact.
Python API
from epub_blocks import extract_recipe_file
records = extract_recipe_file("book.epub", "recipe.json")
for record in records:
print(record.block_id, record.block_type, record.text)
compile_recipe_file returns the immutable in-memory extraction plan.
Fragment and extract_fragments provide lower-level access to selected XHTML
subtrees. The supported import surface is the names exported by epub_blocks.
Safety
EPUB files are untrusted ZIP and XML input. Default APIs bound archive size, individual and cumulative reads, compression ratios, XML size, element count, and nesting depth. Duplicate or unsafe paths and encrypted members are rejected. External DTDs are never loaded; internal subsets, entity declarations, other doctypes, and unknown named character references are rejected.
Recipes are trusted local configuration because they contain regular
expressions. epub-blocks never executes EPUB scripts or fetches resources from
the network.
Development
uv sync
uv run ruff format --check .
uv run ruff check .
uv run pyright
uv run coverage erase
uv run coverage run -m unittest discover -s tests
uv run coverage report
uv run python -m build
uv run twine check dist/*
uv run pyright --verifytypes epub_blocks --ignoreexternal
Coverage includes branches and enforces a 90% minimum. Tests build synthetic EPUBs; no EPUB files are committed. See CONTRIBUTING.md and CHANGELOG.md.
License
epub-blocks is available under the
MIT License.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file epub_blocks-0.4.0.tar.gz.
File metadata
- Download URL: epub_blocks-0.4.0.tar.gz
- Upload date:
- Size: 82.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b1b822bcb884ec50efc733249d1a4bd4b86193fd4dfab3cf509c21601c2a18a
|
|
| MD5 |
d9d34dda23a9a0c9c8459b37e205249d
|
|
| BLAKE2b-256 |
7e07f45d6079012b70a30b637e371c17e3ee5f94bb73dce892d1a5f598580219
|
Provenance
The following attestation bundles were made for epub_blocks-0.4.0.tar.gz:
Publisher:
release.yml on jtauber/epub-blocks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
epub_blocks-0.4.0.tar.gz -
Subject digest:
6b1b822bcb884ec50efc733249d1a4bd4b86193fd4dfab3cf509c21601c2a18a - Sigstore transparency entry: 2728761263
- Sigstore integration time:
-
Permalink:
jtauber/epub-blocks@9a121dad67862243c120ac6bee7cf246d83c115e -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/jtauber
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9a121dad67862243c120ac6bee7cf246d83c115e -
Trigger Event:
release
-
Statement type:
File details
Details for the file epub_blocks-0.4.0-py3-none-any.whl.
File metadata
- Download URL: epub_blocks-0.4.0-py3-none-any.whl
- Upload date:
- Size: 48.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d04a9af128df9d908df93782d5c956a7bb0be9359f290a340e78090ec1b8689
|
|
| MD5 |
d4b5782bc6a2bef24c6bea03f1f60150
|
|
| BLAKE2b-256 |
6ff5ab84bb45744db9824d5575d08a1896d53c5dc7e2b61ec2f3fe3f1d7b0c93
|
Provenance
The following attestation bundles were made for epub_blocks-0.4.0-py3-none-any.whl:
Publisher:
release.yml on jtauber/epub-blocks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
epub_blocks-0.4.0-py3-none-any.whl -
Subject digest:
7d04a9af128df9d908df93782d5c956a7bb0be9359f290a340e78090ec1b8689 - Sigstore transparency entry: 2728761555
- Sigstore integration time:
-
Permalink:
jtauber/epub-blocks@9a121dad67862243c120ac6bee7cf246d83c115e -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/jtauber
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9a121dad67862243c120ac6bee7cf246d83c115e -
Trigger Event:
release
-
Statement type: