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 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
@@ -0,0 +1,6 @@
1
+ """Allow running epub2text as a module: python -m epub2text."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
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)