epubchapterize 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.
- epub_chapterize/__init__.py +4 -0
- epub_chapterize/chapterize.py +320 -0
- epubchapterize-0.1.0.dist-info/METADATA +156 -0
- epubchapterize-0.1.0.dist-info/RECORD +8 -0
- epubchapterize-0.1.0.dist-info/WHEEL +5 -0
- epubchapterize-0.1.0.dist-info/entry_points.txt +2 -0
- epubchapterize-0.1.0.dist-info/licenses/LICENSE +21 -0
- epubchapterize-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from glob import glob
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import ebooklib
|
|
5
|
+
from ebooklib import epub
|
|
6
|
+
from bs4 import BeautifulSoup
|
|
7
|
+
import nltk
|
|
8
|
+
nltk.download('punkt_tab')
|
|
9
|
+
from lxml import etree
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import syntok.segmenter as segmenter
|
|
12
|
+
import spacy
|
|
13
|
+
import sys
|
|
14
|
+
from spacy.util import is_package
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_model(name="en_core_web_sm"):
|
|
18
|
+
if not is_package(name):
|
|
19
|
+
raise RuntimeError(
|
|
20
|
+
f"SpaCy model '{name}' not installed. Run: python -m spacy download {name}"
|
|
21
|
+
)
|
|
22
|
+
return spacy.load(name)
|
|
23
|
+
|
|
24
|
+
def get_nlp_model(language_code):
|
|
25
|
+
if language_code == 'en':
|
|
26
|
+
return load_model('en_core_web_trf')
|
|
27
|
+
elif language_code == 'de':
|
|
28
|
+
return load_model('de_dep_news_trf')
|
|
29
|
+
elif language_code == 'it':
|
|
30
|
+
return load_model('it_core_news_trf')
|
|
31
|
+
elif language_code == 'es':
|
|
32
|
+
return load_model('es_dep_news_trf')
|
|
33
|
+
elif language_code == 'fr':
|
|
34
|
+
return load_model('fr_dep_news_trf')
|
|
35
|
+
else:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
nlp_models = {}
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class NavItem:
|
|
42
|
+
nav_label: str
|
|
43
|
+
doc_href: str
|
|
44
|
+
element_id: str
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class HeaderMatch:
|
|
48
|
+
header: object
|
|
49
|
+
header_text: str
|
|
50
|
+
header_xpath: str
|
|
51
|
+
nav_item: NavItem
|
|
52
|
+
sent_method = "nltk" # Options: "nltk", "spacy", "syntok"
|
|
53
|
+
|
|
54
|
+
def syntok_segmenter(text):
|
|
55
|
+
sentences = []
|
|
56
|
+
for paragraph in segmenter.process(text):
|
|
57
|
+
for sentence in paragraph:
|
|
58
|
+
sentence = ''.join(token.spacing + token.value for token in sentence)
|
|
59
|
+
sentences.append(sentence)
|
|
60
|
+
return sentences
|
|
61
|
+
|
|
62
|
+
def get_punkt_tokenizer(langage_code):
|
|
63
|
+
language_tokenizer_map = {
|
|
64
|
+
'en': 'tokenizers/punkt/english.pickle',
|
|
65
|
+
'fr': 'tokenizers/punkt/french.pickle',
|
|
66
|
+
'de': 'tokenizers/punkt/german.pickle',
|
|
67
|
+
'es': 'tokenizers/punkt/spanish.pickle',
|
|
68
|
+
'it': 'tokenizers/punkt/italian.pickle',
|
|
69
|
+
'nl': 'tokenizers/punkt/dutch.pickle',
|
|
70
|
+
'pt': 'tokenizers/punkt/portuguese.pickle',
|
|
71
|
+
}
|
|
72
|
+
return language_tokenizer_map.get(langage_code, 'tokenizers/punkt/english.pickle')
|
|
73
|
+
|
|
74
|
+
def get_sent_method(language_code):
|
|
75
|
+
if sent_method == "nltk":
|
|
76
|
+
tokenizer = nltk.data.load(get_punkt_tokenizer(language_code))
|
|
77
|
+
def custom_sent_tokenize(text):
|
|
78
|
+
return tokenizer.tokenize(text)
|
|
79
|
+
return custom_sent_tokenize
|
|
80
|
+
elif sent_method == "spacy":
|
|
81
|
+
nlp = get_nlp_model(language_code)
|
|
82
|
+
if nlp:
|
|
83
|
+
return lambda text: [sent.text for sent in nlp(text).sents]
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Unsupported language code for spaCy: {language_code}")
|
|
86
|
+
elif sent_method == "syntok":
|
|
87
|
+
return lambda text: [sent for sent in syntok_segmenter(text)]
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError(f"Unknown sentence segmentation method: {sent_method}")
|
|
90
|
+
|
|
91
|
+
def get_matched_header_for_nav_item(nav_item: NavItem, book) -> HeaderMatch:
|
|
92
|
+
nav_label = nav_item.nav_label
|
|
93
|
+
doc_href = nav_item.doc_href
|
|
94
|
+
element_id = nav_item.element_id
|
|
95
|
+
linked_item = book.get_item_with_href(doc_href)
|
|
96
|
+
if linked_item:
|
|
97
|
+
linked_content = linked_item.get_content().decode()
|
|
98
|
+
linked_soup = BeautifulSoup(linked_content, 'html.parser') # Parse as HTML
|
|
99
|
+
extracted_text = None
|
|
100
|
+
if element_id:
|
|
101
|
+
linked_element = linked_soup.find(id=element_id)
|
|
102
|
+
if linked_element:
|
|
103
|
+
# Extract headers (h1, h2, h3) from the linked content
|
|
104
|
+
h1s = linked_soup.find_all('h1')
|
|
105
|
+
h2s = linked_soup.find_all('h2')
|
|
106
|
+
h3s = linked_soup.find_all('h3')
|
|
107
|
+
|
|
108
|
+
# Combine headers into a single list with their tag type
|
|
109
|
+
headers = [(header, 'h1') for header in h1s] + \
|
|
110
|
+
[(header, 'h2') for header in h2s] + \
|
|
111
|
+
[(header, 'h3') for header in h3s]
|
|
112
|
+
|
|
113
|
+
print(f"Found {len(headers)} headers in {doc_href} with element ID: {element_id}")
|
|
114
|
+
|
|
115
|
+
best_match = None
|
|
116
|
+
pattern = generate_header_pattern(nav_label)
|
|
117
|
+
print(f"Searching for header matching pattern: {pattern.pattern} in {doc_href} with element ID: {element_id}")
|
|
118
|
+
for header, _ in headers:
|
|
119
|
+
header_text = header.get_text(' ', strip=True).replace('\n', ' ')
|
|
120
|
+
print(f"Checking header: {header_text}")
|
|
121
|
+
if pattern.search(header_text):
|
|
122
|
+
print(f"Pattern matched in header: {header_text}")
|
|
123
|
+
if not best_match or len(header_text) < len(best_match.header_text):
|
|
124
|
+
def get_xpath(element):
|
|
125
|
+
tree = etree.HTML(str(element))
|
|
126
|
+
return tree.getroottree().getpath(tree)
|
|
127
|
+
header_xpath = get_xpath(header)
|
|
128
|
+
best_match = HeaderMatch(header, header_text, header_xpath, nav_item)
|
|
129
|
+
extracted_text = best_match if best_match else None
|
|
130
|
+
return extracted_text
|
|
131
|
+
|
|
132
|
+
def generate_header_pattern(target_text):
|
|
133
|
+
words = re.findall(r'\w+', target_text.lower())
|
|
134
|
+
pattern = r'.*'.join(re.escape(word) for word in words)
|
|
135
|
+
return re.compile(pattern, re.IGNORECASE)
|
|
136
|
+
|
|
137
|
+
def get_nav_items_standard_gutenberg_epub3(file_path) -> list[NavItem]:
|
|
138
|
+
book = epub.read_epub(file_path)
|
|
139
|
+
nav_items = []
|
|
140
|
+
|
|
141
|
+
for item in book.get_items():
|
|
142
|
+
if item.get_type() == ebooklib.ITEM_NAVIGATION:
|
|
143
|
+
html_content = item.get_content().decode()
|
|
144
|
+
soup = BeautifulSoup(html_content, 'xml')
|
|
145
|
+
navpoints = soup.find_all('navPoint')
|
|
146
|
+
for navpoint in navpoints:
|
|
147
|
+
nav_label = navpoint.find('navLabel').find("text").get_text(strip=True)
|
|
148
|
+
content = navpoint.find('content')
|
|
149
|
+
doc_href = None
|
|
150
|
+
element_id = None
|
|
151
|
+
if content and content.has_attr('src'):
|
|
152
|
+
src = content['src']
|
|
153
|
+
src_parts = src.split('#')
|
|
154
|
+
doc_href = src_parts[0]
|
|
155
|
+
element_id = src_parts[1] if len(src_parts) > 1 else None # ID within the document
|
|
156
|
+
nav_items.append(NavItem(nav_label, doc_href, element_id))
|
|
157
|
+
return nav_items
|
|
158
|
+
|
|
159
|
+
def filter_by_chapter_class(combined_header_info: list[tuple[any, NavItem]], book) -> list[tuple[any, NavItem]]:
|
|
160
|
+
found_chapter_divs = False
|
|
161
|
+
filtered_header_info = []
|
|
162
|
+
for header, nav_item in combined_header_info:
|
|
163
|
+
for item in book.get_items():
|
|
164
|
+
if item.get_type() == ebooklib.ITEM_DOCUMENT:
|
|
165
|
+
soup = BeautifulSoup(item.get_body_content(), 'html.parser')
|
|
166
|
+
chapter_divs = soup.find_all('div', class_='chapter')
|
|
167
|
+
if chapter_divs:
|
|
168
|
+
found_chapter_divs = True
|
|
169
|
+
for chapter_div in chapter_divs:
|
|
170
|
+
if header in chapter_div.descendants:
|
|
171
|
+
filtered_header_info.append((header, nav_item))
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
if not found_chapter_divs:
|
|
175
|
+
return combined_header_info
|
|
176
|
+
|
|
177
|
+
return filtered_header_info
|
|
178
|
+
|
|
179
|
+
def chapterize(file_path):
|
|
180
|
+
book = epub.read_epub(file_path)
|
|
181
|
+
nav_item_infos = get_nav_items_standard_gutenberg_epub3(file_path)
|
|
182
|
+
language = book.get_metadata('DC', 'language')
|
|
183
|
+
language = language[0][0] if language else 'en'
|
|
184
|
+
if language in nlp_models:
|
|
185
|
+
get_sentences = nlp_models[language]
|
|
186
|
+
else:
|
|
187
|
+
get_sentences = get_sent_method(language)
|
|
188
|
+
nlp_models[language] = get_sentences
|
|
189
|
+
chapters = []
|
|
190
|
+
matched_candidate_headers: list[HeaderMatch] = []
|
|
191
|
+
for nav_item_info in nav_item_infos:
|
|
192
|
+
matched_candidate_headers.append(get_matched_header_for_nav_item(nav_item_info, book))
|
|
193
|
+
print(f"Matched header for nav item '{nav_item_info.nav_label}': {matched_candidate_headers[-1]}")
|
|
194
|
+
|
|
195
|
+
matched_candidate_headers = [candidate_header for candidate_header in matched_candidate_headers if candidate_header is not None]
|
|
196
|
+
|
|
197
|
+
for matched_header in matched_candidate_headers:
|
|
198
|
+
print(f"Matched Header: {matched_header.header_text}, XPath: {matched_header.header_xpath}, Nav Label: {matched_header.nav_item.nav_label}")
|
|
199
|
+
|
|
200
|
+
for item in book.get_items():
|
|
201
|
+
if item.get_type() == ebooklib.ITEM_DOCUMENT:
|
|
202
|
+
soup = BeautifulSoup(item.get_body_content(), 'html.parser')
|
|
203
|
+
|
|
204
|
+
current_document_all_headers = []
|
|
205
|
+
for header_tag in ['h1', 'h2', 'h3']:
|
|
206
|
+
current_document_all_headers.extend(soup.find_all(header_tag))
|
|
207
|
+
|
|
208
|
+
headers_with_nav_items = []
|
|
209
|
+
for header in current_document_all_headers:
|
|
210
|
+
for header_match in matched_candidate_headers:
|
|
211
|
+
if str(header_match.header) == str(header):
|
|
212
|
+
headers_with_nav_items.append((header, header_match.nav_item))
|
|
213
|
+
|
|
214
|
+
headers_with_nav_items = filter_by_chapter_class(headers_with_nav_items, book)
|
|
215
|
+
print(f"Number of headers with nav items: {len(headers_with_nav_items)}")
|
|
216
|
+
|
|
217
|
+
sections = []
|
|
218
|
+
for i, combined_header in enumerate(headers_with_nav_items):
|
|
219
|
+
header_match = combined_header[0]
|
|
220
|
+
nav_item_info = combined_header[1]
|
|
221
|
+
heading_text = nav_item_info.nav_label
|
|
222
|
+
if "THE FULL PROJECT GUTENBERG LICENSE" in heading_text:
|
|
223
|
+
continue
|
|
224
|
+
section = {'title': heading_text, 'paragraphs': []}
|
|
225
|
+
next_heading = headers_with_nav_items[i + 1] if i + 1 < len(headers_with_nav_items) else None
|
|
226
|
+
current_element = header_match
|
|
227
|
+
while current_element and current_element != next_heading:
|
|
228
|
+
if current_element.name == 'p':
|
|
229
|
+
section['paragraphs'].append(current_element.get_text(separator=" ", strip=True))
|
|
230
|
+
current_element = current_element.find_next()
|
|
231
|
+
sections.append(section)
|
|
232
|
+
|
|
233
|
+
no_sentences_heading = ''
|
|
234
|
+
for section in sections:
|
|
235
|
+
chapter_title = no_sentences_heading + section['title']
|
|
236
|
+
chapter_title = chapter_title.replace('\n', ' ')
|
|
237
|
+
paragraphs = section['paragraphs']
|
|
238
|
+
sentences = []
|
|
239
|
+
for paragraph in paragraphs:
|
|
240
|
+
# Replace all occurrences of chapter_title and newlines in the paragraph
|
|
241
|
+
transformed_sentences = get_sentences(paragraph.replace(chapter_title, '').replace('\n', ' '))
|
|
242
|
+
for sentence in transformed_sentences:
|
|
243
|
+
stripped_sentence = sentence.strip()
|
|
244
|
+
if stripped_sentence and not all(char in '.,!?;:"\'-()[]{}' for char in stripped_sentence): # Check if the sentence is not empty, whitespace, or only punctuation
|
|
245
|
+
sentences.append(sentence)
|
|
246
|
+
if sentences:
|
|
247
|
+
chapters.append({
|
|
248
|
+
'title': chapter_title,
|
|
249
|
+
'sentences': [s.strip() for s in sentences]
|
|
250
|
+
})
|
|
251
|
+
no_sentences_heading = ''
|
|
252
|
+
else:
|
|
253
|
+
no_sentences_heading += chapter_title + ' '
|
|
254
|
+
|
|
255
|
+
return chapters, language
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
|
|
261
|
+
books_to_add = []
|
|
262
|
+
books_directory = "books"
|
|
263
|
+
|
|
264
|
+
#["/Users/matthewgrant/Source/EpubChapterize/epub_chapterize/books/to_import/Röschen-Jaköble-und-andere-kleine-Leute-De.epub"]:
|
|
265
|
+
for file_path in glob(os.path.join(books_directory, "**", "*.epub"), recursive=True):
|
|
266
|
+
if "archive" in file_path: # Include only files in the archive folder
|
|
267
|
+
continue
|
|
268
|
+
book = epub.read_epub(file_path)
|
|
269
|
+
language = book.get_metadata('DC', 'language')
|
|
270
|
+
language = language[0][0] if language else 'en'
|
|
271
|
+
|
|
272
|
+
title = book.get_metadata('DC', 'title')
|
|
273
|
+
author = book.get_metadata('DC', 'creator')
|
|
274
|
+
title = title[0][0] if title else "Unknown Title"
|
|
275
|
+
author = author[0][0] if author else "Unknown Author"
|
|
276
|
+
|
|
277
|
+
books_to_add.append({
|
|
278
|
+
'file_path': os.path.relpath(file_path, books_directory),
|
|
279
|
+
'title': title,
|
|
280
|
+
'author': author
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
output_test_files = True
|
|
284
|
+
unable_to_parse_file = os.path.join(books_directory, "unable_to_parse.txt")
|
|
285
|
+
if os.path.exists(unable_to_parse_file):
|
|
286
|
+
os.remove(unable_to_parse_file)
|
|
287
|
+
for book_to_add in books_to_add:
|
|
288
|
+
if len(sys.argv) > 1:
|
|
289
|
+
input_file_path = sys.argv[1]
|
|
290
|
+
if os.path.exists(input_file_path):
|
|
291
|
+
chapters, language = chapterize(input_file_path)
|
|
292
|
+
else:
|
|
293
|
+
print(f"File {input_file_path} does not exist. Falling back to default behavior.")
|
|
294
|
+
chapters, language = chapterize(os.path.join(books_directory, book_to_add["file_path"]))
|
|
295
|
+
else:
|
|
296
|
+
chapters, language = chapterize(os.path.join(books_directory, book_to_add["file_path"]))
|
|
297
|
+
|
|
298
|
+
print("Chapters found:", len(chapters))
|
|
299
|
+
if not chapters:
|
|
300
|
+
unable_to_parse_file = os.path.join(books_directory, "unable_to_parse.txt")
|
|
301
|
+
os.makedirs(os.path.dirname(unable_to_parse_file), exist_ok=True)
|
|
302
|
+
with open(unable_to_parse_file, "a", encoding="utf-8") as f:
|
|
303
|
+
f.write(os.path.join(books_directory, book_to_add["file_path"]) + "\n")
|
|
304
|
+
for chapter in chapters:
|
|
305
|
+
print(chapter["title"])
|
|
306
|
+
print(chapter["sentences"][:1])
|
|
307
|
+
if output_test_files:
|
|
308
|
+
book_folder = os.path.join("output", book_to_add["title"][:100])
|
|
309
|
+
os.makedirs(book_folder, exist_ok=True)
|
|
310
|
+
print("Book folder created:", book_folder)
|
|
311
|
+
chapter_number = chapters.index(chapter) + 1
|
|
312
|
+
chapter_file_path = os.path.join(book_folder, f"{chapter_number} - {chapter['title'][:100]}.txt")
|
|
313
|
+
|
|
314
|
+
with open(chapter_file_path, "w", encoding="utf-8") as chapter_file:
|
|
315
|
+
chapter_file.write(chapter["title"] + "\n\n")
|
|
316
|
+
chapter_file.write("\n".join(f"<start> {sentence} <end>" for sentence in chapter["sentences"]))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: epubchapterize
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package for parsing chapters from EPUBs.
|
|
5
|
+
Author-email: Matthew Grant <nzmattgrant@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nzmattgrant/epubchapterize
|
|
8
|
+
Project-URL: Documentation, https://github.com/nzmattgrant/epubchapterize/wiki
|
|
9
|
+
Project-URL: Source, https://github.com/nzmattgrant/epubchapterize
|
|
10
|
+
Project-URL: Tracker, https://github.com/nzmattgrant/epubchapterize/issues
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: annotated-types==0.7.0
|
|
18
|
+
Requires-Dist: beautifulsoup4==4.13.3
|
|
19
|
+
Requires-Dist: blis==1.3.0
|
|
20
|
+
Requires-Dist: bs4==0.0.2
|
|
21
|
+
Requires-Dist: build==1.3.0
|
|
22
|
+
Requires-Dist: catalogue==2.0.10
|
|
23
|
+
Requires-Dist: certifi==2025.1.31
|
|
24
|
+
Requires-Dist: charset-normalizer==3.4.1
|
|
25
|
+
Requires-Dist: click==8.1.8
|
|
26
|
+
Requires-Dist: cloudpathlib==0.21.0
|
|
27
|
+
Requires-Dist: confection==0.1.5
|
|
28
|
+
Requires-Dist: cymem==2.0.11
|
|
29
|
+
Requires-Dist: EbookLib==0.18
|
|
30
|
+
Requires-Dist: idna==3.10
|
|
31
|
+
Requires-Dist: importlib_metadata==8.7.0
|
|
32
|
+
Requires-Dist: Jinja2==3.1.6
|
|
33
|
+
Requires-Dist: joblib==1.4.2
|
|
34
|
+
Requires-Dist: langcodes==3.5.0
|
|
35
|
+
Requires-Dist: language_data==1.3.0
|
|
36
|
+
Requires-Dist: lxml==5.3.2
|
|
37
|
+
Requires-Dist: marisa-trie==1.2.1
|
|
38
|
+
Requires-Dist: markdown-it-py==3.0.0
|
|
39
|
+
Requires-Dist: MarkupSafe==3.0.2
|
|
40
|
+
Requires-Dist: mdurl==0.1.2
|
|
41
|
+
Requires-Dist: murmurhash==1.0.12
|
|
42
|
+
Requires-Dist: nltk==3.9.1
|
|
43
|
+
Requires-Dist: numpy==2.0.2
|
|
44
|
+
Requires-Dist: packaging==24.2
|
|
45
|
+
Requires-Dist: preshed==3.0.9
|
|
46
|
+
Requires-Dist: pydantic==2.11.3
|
|
47
|
+
Requires-Dist: pydantic_core==2.33.1
|
|
48
|
+
Requires-Dist: Pygments==2.19.1
|
|
49
|
+
Requires-Dist: pyproject_hooks==1.2.0
|
|
50
|
+
Requires-Dist: regex==2024.11.6
|
|
51
|
+
Requires-Dist: requests==2.32.3
|
|
52
|
+
Requires-Dist: rich==14.0.0
|
|
53
|
+
Requires-Dist: shellingham==1.5.4
|
|
54
|
+
Requires-Dist: six==1.17.0
|
|
55
|
+
Requires-Dist: smart-open==7.1.0
|
|
56
|
+
Requires-Dist: soupsieve==2.6
|
|
57
|
+
Requires-Dist: spacy==3.8.7
|
|
58
|
+
Requires-Dist: spacy-legacy==3.0.12
|
|
59
|
+
Requires-Dist: spacy-loggers==1.0.5
|
|
60
|
+
Requires-Dist: srsly==2.5.1
|
|
61
|
+
Requires-Dist: syntok==1.4.4
|
|
62
|
+
Requires-Dist: thinc==8.3.6
|
|
63
|
+
Requires-Dist: tomli==2.2.1
|
|
64
|
+
Requires-Dist: tqdm==4.67.1
|
|
65
|
+
Requires-Dist: typer==0.15.2
|
|
66
|
+
Requires-Dist: typing-inspection==0.4.0
|
|
67
|
+
Requires-Dist: typing_extensions==4.13.1
|
|
68
|
+
Requires-Dist: urllib3==2.4.0
|
|
69
|
+
Requires-Dist: wasabi==1.1.3
|
|
70
|
+
Requires-Dist: weasel==0.4.1
|
|
71
|
+
Requires-Dist: wrapt==1.17.2
|
|
72
|
+
Requires-Dist: zipp==3.23.0
|
|
73
|
+
Dynamic: license-file
|
|
74
|
+
|
|
75
|
+
# EpubChapterize
|
|
76
|
+
### A tool to split out chapters from ePub documents. Initially just for Project Gutenberg ePub3s.
|
|
77
|
+
|
|
78
|
+
## Setup
|
|
79
|
+
|
|
80
|
+
To set up the project, follow these steps:
|
|
81
|
+
|
|
82
|
+
1. Clone the repository:
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/yourusername/EpubChapterize.git
|
|
85
|
+
cd EpubChapterize
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
2. Create a virtual environment:
|
|
89
|
+
```bash
|
|
90
|
+
python -m venv venv
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
3. Activate the virtual environment:
|
|
94
|
+
- On macOS/Linux:
|
|
95
|
+
```bash
|
|
96
|
+
source venv/bin/activate
|
|
97
|
+
```
|
|
98
|
+
- On Windows:
|
|
99
|
+
```bash
|
|
100
|
+
venv\Scripts\activate
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
4. Install the required dependencies:
|
|
104
|
+
```bash
|
|
105
|
+
pip install -r requirements.txt
|
|
106
|
+
```
|
|
107
|
+
5. Install additional language models for spaCy (if needed):
|
|
108
|
+
|
|
109
|
+
Depending on the languages you plan to process, you may need to install specific spaCy language models. Use the following commands to install them:
|
|
110
|
+
|
|
111
|
+
- For English:
|
|
112
|
+
```bash
|
|
113
|
+
python -m spacy download en_core_web_trf
|
|
114
|
+
```
|
|
115
|
+
- For German:
|
|
116
|
+
```bash
|
|
117
|
+
python -m spacy download de_dep_news_trf
|
|
118
|
+
```
|
|
119
|
+
- For Italian:
|
|
120
|
+
```bash
|
|
121
|
+
python -m spacy download it_core_news_trf
|
|
122
|
+
```
|
|
123
|
+
- For Spanish:
|
|
124
|
+
```bash
|
|
125
|
+
python -m spacy download es_dep_news_trf
|
|
126
|
+
```
|
|
127
|
+
- For French:
|
|
128
|
+
```bash
|
|
129
|
+
python -m spacy download fr_dep_news_trf
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If you are not using spacy then skip this step
|
|
133
|
+
|
|
134
|
+
## Usage
|
|
135
|
+
|
|
136
|
+
This tool is primarily designed to extract chapters from Project Gutenberg ePub3 files. It works by analyzing the navigation structure, matching headers, and attempting to identify chapter divisions. Note that it may also include some preamble content, and its accuracy is not guaranteed.
|
|
137
|
+
|
|
138
|
+
To use the tool, run:
|
|
139
|
+
```bash
|
|
140
|
+
python chapterize.py /path/to/your/epub/files/
|
|
141
|
+
```
|
|
142
|
+
or
|
|
143
|
+
```bash
|
|
144
|
+
python chapterize.py
|
|
145
|
+
```
|
|
146
|
+
which will use the books directory by default
|
|
147
|
+
|
|
148
|
+
## Notes
|
|
149
|
+
|
|
150
|
+
- The tool is not perfect and may require manual adjustments to the output.
|
|
151
|
+
- It is currently a standalone script but may be packaged in the future.
|
|
152
|
+
- Feel free to fork the repository and modify it as needed.
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
If you encounter any issues, please raise a ticket in the repository. Contributions are welcome!
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
epub_chapterize/__init__.py,sha256=2m7lAQJFn2QxO4taQdBM8hqD7ifpRv9J3MkV4k1AdWI,74
|
|
2
|
+
epub_chapterize/chapterize.py,sha256=LOcdSVzVMhEVB3sFg6zxF978aRYGt1KvXL5GP-yjvlA,14102
|
|
3
|
+
epubchapterize-0.1.0.dist-info/licenses/LICENSE,sha256=LMLma5_BtgDYi_jPdgFCnyyyrDWR9d85t2Wlt7cFGmo,1068
|
|
4
|
+
epubchapterize-0.1.0.dist-info/METADATA,sha256=LW70yZl4LWEzq2685WegVXmWLEkvYrJ93OW_u8roRqk,4710
|
|
5
|
+
epubchapterize-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
epubchapterize-0.1.0.dist-info/entry_points.txt,sha256=QNNVQU0TvFN3Xel-m3CkxDbhPntdTuwbhJNOKtRKNAs,74
|
|
7
|
+
epubchapterize-0.1.0.dist-info/top_level.txt,sha256=rhi6aeSlCEp9HyPXXCwp5DrO9QOQ0YSMSqJeSuhXH5g,16
|
|
8
|
+
epubchapterize-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 nzmattgrant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
epub_chapterize
|