uxoxo/eb2ab
0
1#!/usr/bin/env python32"""3Ebook Text Extractor - Extract text and metadata from multiple ebook formats4 5Supports: EPUB, PDF, TXT, HTML6Returns: Structured metadata and chapter content7 8Author: Claude Code9Date: 2025-10-1810"""11 12import os13import re14from pathlib import Path15from dataclasses import dataclass16from typing import List, Tuple, Optional17 18# Import with fallbacks19try:20 import ebooklib21 from ebooklib import epub22 HAS_EBOOKLIB = True23except ImportError:24 HAS_EBOOKLIB = False25 26try:27 from bs4 import BeautifulSoup28 HAS_BS4 = True29except ImportError:30 HAS_BS4 = False31 32try:33 import pymupdf4llm34 HAS_PYMUPDF = True35except ImportError:36 HAS_PYMUPDF = False37 38 39@dataclass40class EbookMetadata:41 """Metadata extracted from ebook"""42 title: str43 author: str44 format: str45 word_count: int46 character_count: int47 chapter_count: int48 49 50@dataclass51class ChapterContent:52 """Content of a single chapter"""53 title: str54 text: str55 chapter_number: int56 word_count: int57 58 59def extract_ebook_text(ebook_path: str) -> Tuple[EbookMetadata, List[ChapterContent]]:60 """61 Extract text and metadata from an ebook file62 63 Args:64 ebook_path: Path to ebook file65 66 Returns:67 Tuple of (metadata, chapters)68 69 Raises:70 ValueError: If format is unsupported71 FileNotFoundError: If file doesn't exist72 """73 74 if not os.path.exists(ebook_path):75 raise FileNotFoundError(f"Ebook file not found: {ebook_path}")76 77 # Detect format78 ext = Path(ebook_path).suffix.lower()79 80 if ext == '.epub':81 return _extract_epub(ebook_path)82 elif ext == '.pdf':83 return _extract_pdf(ebook_path)84 elif ext in ['.txt', '.html', '.htm']:85 return _extract_text(ebook_path, ext)86 else:87 raise ValueError(88 f"Unsupported format: {ext}\n"89 f"Supported: .epub, .pdf, .txt, .html"90 )91 92 93def _extract_epub(ebook_path: str) -> Tuple[EbookMetadata, List[ChapterContent]]:94 """Extract text from EPUB file"""95 96 if not HAS_EBOOKLIB:97 raise ImportError("ebooklib required for EPUB. Install with: pip install ebooklib")98 99 if not HAS_BS4:100 raise ImportError("beautifulsoup4 required for EPUB. Install with: pip install beautifulsoup4")101 102 try:103 book = epub.read_epub(ebook_path)104 except Exception as e:105 raise ValueError(f"Failed to read EPUB file: {e}")106 107 # Extract metadata108 title = book.get_metadata('DC', 'title')109 title = title[0][0] if title else Path(ebook_path).stem110 111 author = book.get_metadata('DC', 'creator')112 author = author[0][0] if author else "Unknown"113 114 # Extract chapters115 chapters = []116 chapter_num = 0117 118 for item in book.get_items():119 if item.get_type() == ebooklib.ITEM_DOCUMENT:120 # Parse HTML content121 soup = BeautifulSoup(item.get_content(), 'html.parser')122 123 # Extract text124 text = soup.get_text()125 text = _clean_text(text)126 127 if len(text.strip()) < 50: # Skip very short sections128 continue129 130 chapter_num += 1131 132 # Try to extract chapter title133 chapter_title = f"Chapter {chapter_num}"134 h_tags = soup.find_all(['h1', 'h2', 'h3'])135 if h_tags:136 chapter_title = h_tags[0].get_text().strip()137 138 word_count = len(text.split())139 140 chapters.append(ChapterContent(141 title=chapter_title,142 text=text,143 chapter_number=chapter_num,144 word_count=word_count145 ))146 147 # Calculate totals148 total_words = sum(c.word_count for c in chapters)149 total_chars = sum(len(c.text) for c in chapters)150 151 metadata = EbookMetadata(152 title=title,153 author=author,154 format="EPUB",155 word_count=total_words,156 character_count=total_chars,157 chapter_count=len(chapters)158 )159 160 return metadata, chapters161 162 163def _extract_pdf(ebook_path: str) -> Tuple[EbookMetadata, List[ChapterContent]]:164 """Extract text from PDF file"""165 166 if not HAS_PYMUPDF:167 raise ImportError(168 "pymupdf4llm required for PDF extraction. "169 "Install with: pip install pymupdf4llm"170 )171 172 try:173 # Extract markdown from PDF174 md_text = pymupdf4llm.to_markdown(ebook_path)175 except Exception as e:176 raise ValueError(f"Failed to extract PDF: {e}")177 178 # Split into chapters (look for markdown headers)179 chapters = []180 current_chapter = []181 current_title = "Chapter 1"182 chapter_num = 0183 184 for line in md_text.split('\n'):185 # Check for chapter markers (# headers)186 if re.match(r'^#+\s+', line):187 # Save previous chapter if exists188 if current_chapter:189 chapter_num += 1190 text = '\n'.join(current_chapter)191 text = _clean_text(text)192 193 chapters.append(ChapterContent(194 title=current_title,195 text=text,196 chapter_number=chapter_num,197 word_count=len(text.split())198 ))199 current_chapter = []200 201 # Start new chapter202 current_title = line.lstrip('#').strip()203 else:204 current_chapter.append(line)205 206 # Add final chapter207 if current_chapter:208 chapter_num += 1209 text = '\n'.join(current_chapter)210 text = _clean_text(text)211 212 chapters.append(ChapterContent(213 title=current_title,214 text=text,215 chapter_number=chapter_num,216 word_count=len(text.split())217 ))218 219 # If no chapters detected, treat whole doc as one chapter220 if not chapters:221 text = _clean_text(md_text)222 chapters = [ChapterContent(223 title="Full Document",224 text=text,225 chapter_number=1,226 word_count=len(text.split())227 )]228 229 # Calculate totals230 total_words = sum(c.word_count for c in chapters)231 total_chars = sum(len(c.text) for c in chapters)232 233 title = Path(ebook_path).stem234 235 metadata = EbookMetadata(236 title=title,237 author="Unknown",238 format="PDF",239 word_count=total_words,240 character_count=total_chars,241 chapter_count=len(chapters)242 )243 244 return metadata, chapters245 246 247def _extract_text(ebook_path: str, ext: str) -> Tuple[EbookMetadata, List[ChapterContent]]:248 """Extract text from TXT or HTML file"""249 250 try:251 with open(ebook_path, 'r', encoding='utf-8') as f:252 content = f.read()253 except UnicodeDecodeError:254 # Try with different encoding255 with open(ebook_path, 'r', encoding='latin-1') as f:256 content = f.read()257 258 # Parse HTML if needed259 if ext in ['.html', '.htm']:260 if not HAS_BS4:261 raise ImportError("beautifulsoup4 required for HTML. Install with: pip install beautifulsoup4")262 263 soup = BeautifulSoup(content, 'html.parser')264 content = soup.get_text()265 266 content = _clean_text(content)267 268 # Try to detect chapters (look for "Chapter" markers)269 chapter_pattern = r'(?:^|\n)(?:Chapter|CHAPTER|Ch\.?)\s+(\d+|[IVXLCDM]+)'270 chapter_splits = list(re.finditer(chapter_pattern, content))271 272 chapters = []273 274 if chapter_splits:275 # Split by detected chapters276 for i, match in enumerate(chapter_splits):277 start = match.start()278 end = chapter_splits[i+1].start() if i+1 < len(chapter_splits) else len(content)279 280 chapter_text = content[start:end]281 chapter_title = match.group(0).strip()282 chapter_text = _clean_text(chapter_text)283 284 chapters.append(ChapterContent(285 title=chapter_title,286 text=chapter_text,287 chapter_number=i+1,288 word_count=len(chapter_text.split())289 ))290 else:291 # No chapters detected, treat as single chapter292 chapters = [ChapterContent(293 title="Full Text",294 text=content,295 chapter_number=1,296 word_count=len(content.split())297 )]298 299 # Calculate totals300 total_words = sum(c.word_count for c in chapters)301 total_chars = sum(len(c.text) for c in chapters)302 303 title = Path(ebook_path).stem304 format_name = ext.upper().lstrip('.')305 306 metadata = EbookMetadata(307 title=title,308 author="Unknown",309 format=format_name,310 word_count=total_words,311 character_count=total_chars,312 chapter_count=len(chapters)313 )314 315 return metadata, chapters316 317 318def _clean_text(text: str) -> str:319 """Clean extracted text"""320 321 # Remove excessive whitespace322 text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text)323 324 # Remove page numbers (common pattern)325 text = re.sub(r'\n\s*\d+\s*\n', '\n', text)326 327 # Normalize spaces328 text = re.sub(r'[ \t]+', ' ', text)329 330 # Remove leading/trailing whitespace331 text = text.strip()332 333 return text334 335 336# Export main function337__all__ = ['extract_ebook_text', 'EbookMetadata', 'ChapterContent']338 