epub2text 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epub2text/__init__.py +96 -0
- epub2text/__main__.py +6 -0
- epub2text/_version.py +34 -0
- epub2text/cleaner.py +151 -0
- epub2text/cli.py +601 -0
- epub2text/formatters.py +382 -0
- epub2text/models.py +66 -0
- epub2text/parser.py +753 -0
- epub2text/py.typed +0 -0
- epub2text-0.1.0.dist-info/METADATA +380 -0
- epub2text-0.1.0.dist-info/RECORD +15 -0
- epub2text-0.1.0.dist-info/WHEEL +5 -0
- epub2text-0.1.0.dist-info/entry_points.txt +2 -0
- epub2text-0.1.0.dist-info/licenses/LICENSE +21 -0
- epub2text-0.1.0.dist-info/top_level.txt +1 -0
epub2text/__init__.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
epub2text - Extract text from EPUB files with smart cleaning.
|
|
3
|
+
|
|
4
|
+
A niche CLI tool for extracting and processing text from EPUB files.
|
|
5
|
+
Supports selective chapter extraction, smart text cleaning, and
|
|
6
|
+
both CLI and library usage.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import tempfile
|
|
10
|
+
import urllib.request
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Union
|
|
13
|
+
from .parser import EPUBParser
|
|
14
|
+
from .models import Chapter, Metadata
|
|
15
|
+
from .cleaner import clean_text, TextCleaner
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"EPUBParser",
|
|
19
|
+
"Chapter",
|
|
20
|
+
"Metadata",
|
|
21
|
+
"clean_text",
|
|
22
|
+
"TextCleaner",
|
|
23
|
+
"epub2txt",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from ._version import version as __version__
|
|
28
|
+
except ImportError:
|
|
29
|
+
__version__ = "0.0.0+unknown"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def epub2txt(
|
|
33
|
+
filepath: str,
|
|
34
|
+
outputlist: bool = False,
|
|
35
|
+
clean: bool = True,
|
|
36
|
+
) -> Union[str, list[str]]:
|
|
37
|
+
"""
|
|
38
|
+
Extract text from EPUB file (compatibility function for old epub2txt API).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
filepath: Path to EPUB file or URL
|
|
42
|
+
outputlist: If True, return list of chapter texts; if False,
|
|
43
|
+
return single string
|
|
44
|
+
clean: If True, apply text cleaning; if False, minimal processing
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Either a single string of all text (outputlist=False) or list of
|
|
48
|
+
chapter texts (outputlist=True)
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> text = epub2txt("book.epub")
|
|
52
|
+
>>> chapters = epub2txt("book.epub", outputlist=True)
|
|
53
|
+
>>> raw_text = epub2txt("book.epub", clean=False)
|
|
54
|
+
>>> text = epub2txt("https://example.com/book.epub")
|
|
55
|
+
"""
|
|
56
|
+
# Check if filepath is a URL
|
|
57
|
+
is_url = filepath.startswith("http://") or filepath.startswith("https://")
|
|
58
|
+
|
|
59
|
+
tmp_path = None
|
|
60
|
+
if is_url:
|
|
61
|
+
# Download to temporary file
|
|
62
|
+
with tempfile.NamedTemporaryFile(suffix=".epub", delete=False) as tmp:
|
|
63
|
+
tmp_path = tmp.name
|
|
64
|
+
urllib.request.urlretrieve(filepath, tmp_path)
|
|
65
|
+
actual_path = tmp_path
|
|
66
|
+
else:
|
|
67
|
+
actual_path = filepath
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Use compact format (single newlines) to match old epub2txt behavior
|
|
71
|
+
parser = EPUBParser(actual_path, paragraph_separator="\n")
|
|
72
|
+
|
|
73
|
+
# Get all chapters
|
|
74
|
+
chapters = parser.get_chapters()
|
|
75
|
+
|
|
76
|
+
if outputlist:
|
|
77
|
+
# Return list of chapter texts
|
|
78
|
+
result = []
|
|
79
|
+
for chapter in chapters:
|
|
80
|
+
text = parser.extract_chapters([chapter.id])
|
|
81
|
+
if clean:
|
|
82
|
+
cleaner = TextCleaner(preserve_single_newlines=True)
|
|
83
|
+
text = cleaner.clean(text)
|
|
84
|
+
result.append(text)
|
|
85
|
+
return result
|
|
86
|
+
else:
|
|
87
|
+
# Return single concatenated string
|
|
88
|
+
text = parser.extract_chapters()
|
|
89
|
+
if clean:
|
|
90
|
+
cleaner = TextCleaner(preserve_single_newlines=True)
|
|
91
|
+
text = cleaner.clean(text)
|
|
92
|
+
return text
|
|
93
|
+
finally:
|
|
94
|
+
# Clean up temporary file if we downloaded one
|
|
95
|
+
if is_url and tmp_path:
|
|
96
|
+
Path(tmp_path).unlink(missing_ok=True)
|
epub2text/__main__.py
ADDED
epub2text/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
epub2text/cleaner.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Text cleaning utilities extracted from abogen."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# Pre-compile frequently used regex patterns for better performance
|
|
6
|
+
_BRACKETED_NUMBERS_PATTERN = re.compile(r"\[\s*\d+\s*\]")
|
|
7
|
+
_STANDALONE_PAGE_NUMBERS_PATTERN = re.compile(r"^\s*\d+\s*$", re.MULTILINE)
|
|
8
|
+
_PAGE_NUMBERS_AT_END_PATTERN = re.compile(r"\s+\d+\s*$", re.MULTILINE)
|
|
9
|
+
_PAGE_NUMBERS_WITH_DASH_PATTERN = re.compile(
|
|
10
|
+
r"\s+[-–—]\s*\d+\s*[-–—]?\s*$", re.MULTILINE
|
|
11
|
+
)
|
|
12
|
+
_WHITESPACE_PATTERN = re.compile(r"[^\S\n]+")
|
|
13
|
+
_MULTIPLE_NEWLINES_PATTERN = re.compile(r"\n{3,}")
|
|
14
|
+
_SINGLE_NEWLINE_PATTERN = re.compile(r"(?<!\n)\n(?!\n)")
|
|
15
|
+
_CHAPTER_MARKER_PATTERN = re.compile(r"<<CHAPTER_MARKER:[^>]*>>")
|
|
16
|
+
_METADATA_TAG_PATTERN = re.compile(r"<<METADATA_[^:]+:[^>]*>>")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TextCleaner:
|
|
20
|
+
"""Smart text cleaning with configurable options."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
remove_page_numbers: bool = True,
|
|
25
|
+
remove_footnotes: bool = True,
|
|
26
|
+
replace_single_newlines: bool = True,
|
|
27
|
+
normalize_whitespace: bool = True,
|
|
28
|
+
preserve_single_newlines: bool = False,
|
|
29
|
+
):
|
|
30
|
+
"""Initialize text cleaner with options.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
remove_page_numbers: Remove standalone and end-of-line page
|
|
34
|
+
numbers
|
|
35
|
+
remove_footnotes: Remove bracketed footnote markers like [1]
|
|
36
|
+
replace_single_newlines: Replace single newlines with spaces
|
|
37
|
+
(preserve paragraphs)
|
|
38
|
+
normalize_whitespace: Collapse multiple spaces to single space
|
|
39
|
+
preserve_single_newlines: Keep single newlines as paragraph
|
|
40
|
+
separators (overrides replace_single_newlines)
|
|
41
|
+
"""
|
|
42
|
+
self.remove_page_numbers = remove_page_numbers
|
|
43
|
+
self.remove_footnotes = remove_footnotes
|
|
44
|
+
self.replace_single_newlines = replace_single_newlines
|
|
45
|
+
self.normalize_whitespace = normalize_whitespace
|
|
46
|
+
self.preserve_single_newlines = preserve_single_newlines
|
|
47
|
+
|
|
48
|
+
def clean(self, text: str) -> str:
|
|
49
|
+
"""Clean text with configured options.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
text: Raw text to clean
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Cleaned text
|
|
56
|
+
"""
|
|
57
|
+
# Remove metadata tags
|
|
58
|
+
text = _METADATA_TAG_PATTERN.sub("", text)
|
|
59
|
+
|
|
60
|
+
# Remove page numbers and footnotes if requested
|
|
61
|
+
if self.remove_page_numbers:
|
|
62
|
+
text = _BRACKETED_NUMBERS_PATTERN.sub("", text)
|
|
63
|
+
text = _STANDALONE_PAGE_NUMBERS_PATTERN.sub("", text)
|
|
64
|
+
text = _PAGE_NUMBERS_AT_END_PATTERN.sub("", text)
|
|
65
|
+
text = _PAGE_NUMBERS_WITH_DASH_PATTERN.sub("", text)
|
|
66
|
+
|
|
67
|
+
if self.remove_footnotes:
|
|
68
|
+
text = _BRACKETED_NUMBERS_PATTERN.sub("", text)
|
|
69
|
+
|
|
70
|
+
# Normalize whitespace
|
|
71
|
+
if self.normalize_whitespace:
|
|
72
|
+
# Collapse all whitespace (excluding newlines) into single spaces per line
|
|
73
|
+
lines = [
|
|
74
|
+
_WHITESPACE_PATTERN.sub(" ", line).strip() for line in text.splitlines()
|
|
75
|
+
]
|
|
76
|
+
text = "\n".join(lines)
|
|
77
|
+
|
|
78
|
+
# Standardize paragraph breaks (multiple newlines become exactly two)
|
|
79
|
+
# Unless preserve_single_newlines is True (for compact format)
|
|
80
|
+
if not self.preserve_single_newlines:
|
|
81
|
+
text = _MULTIPLE_NEWLINES_PATTERN.sub("\n\n", text).strip()
|
|
82
|
+
else:
|
|
83
|
+
# For compact format, collapse multiple newlines to single
|
|
84
|
+
text = _MULTIPLE_NEWLINES_PATTERN.sub("\n", text).strip()
|
|
85
|
+
|
|
86
|
+
# Replace single newlines with spaces, but preserve double newlines
|
|
87
|
+
# Unless preserve_single_newlines is True (for compact format)
|
|
88
|
+
if self.replace_single_newlines and not self.preserve_single_newlines:
|
|
89
|
+
text = _SINGLE_NEWLINE_PATTERN.sub(" ", text)
|
|
90
|
+
|
|
91
|
+
return text
|
|
92
|
+
|
|
93
|
+
def calculate_length(self, text: str) -> int:
|
|
94
|
+
"""Calculate text length excluding chapter markers and metadata.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
text: Text to measure
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Character count
|
|
101
|
+
"""
|
|
102
|
+
# Ignore chapter markers and metadata patterns
|
|
103
|
+
text = _CHAPTER_MARKER_PATTERN.sub("", text)
|
|
104
|
+
text = _METADATA_TAG_PATTERN.sub("", text)
|
|
105
|
+
# Ignore newlines and leading/trailing spaces
|
|
106
|
+
text = text.replace("\n", "").strip()
|
|
107
|
+
return len(text)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def clean_text(
|
|
111
|
+
text: str,
|
|
112
|
+
remove_page_numbers: bool = True,
|
|
113
|
+
remove_footnotes: bool = True,
|
|
114
|
+
replace_single_newlines: bool = True,
|
|
115
|
+
normalize_whitespace: bool = True,
|
|
116
|
+
preserve_single_newlines: bool = False,
|
|
117
|
+
) -> str:
|
|
118
|
+
"""Clean text with smart options (convenience function).
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
text: Raw text to clean
|
|
122
|
+
remove_page_numbers: Remove standalone and end-of-line page numbers
|
|
123
|
+
remove_footnotes: Remove bracketed footnote markers like [1]
|
|
124
|
+
replace_single_newlines: Replace single newlines with spaces
|
|
125
|
+
normalize_whitespace: Collapse multiple spaces to single space
|
|
126
|
+
preserve_single_newlines: Keep single newlines as paragraph separators
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Cleaned text
|
|
130
|
+
"""
|
|
131
|
+
cleaner = TextCleaner(
|
|
132
|
+
remove_page_numbers=remove_page_numbers,
|
|
133
|
+
remove_footnotes=remove_footnotes,
|
|
134
|
+
replace_single_newlines=replace_single_newlines,
|
|
135
|
+
normalize_whitespace=normalize_whitespace,
|
|
136
|
+
preserve_single_newlines=preserve_single_newlines,
|
|
137
|
+
)
|
|
138
|
+
return cleaner.clean(text)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def calculate_text_length(text: str) -> int:
|
|
142
|
+
"""Calculate text length excluding markers and metadata.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
text: Text to measure
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Character count
|
|
149
|
+
"""
|
|
150
|
+
cleaner = TextCleaner()
|
|
151
|
+
return cleaner.calculate_length(text)
|