- Refactor codebase into src/ (preprocessing, translation, assembly) - Add pipeline/ scripts for individual stages - Externalize configuration to config/config.yaml - Fix Cover Image preservation - Update documentation and manuals
175 lines
6.8 KiB
Python
175 lines
6.8 KiB
Python
"""
|
|
EPUB 解析器模块 (EPUB Parser Module)
|
|
|
|
该模块负责读取 EPUB 文件,提取元数据和内容项目。
|
|
它使用 ebooklib 库来处理 EPUB 格式的底层细节。
|
|
|
|
Classes:
|
|
EPUBParser: 负责 EPUB 文件的加载、元数据提取和内容项遍历。
|
|
"""
|
|
|
|
import ebooklib
|
|
from ebooklib import epub
|
|
from bs4 import BeautifulSoup
|
|
from typing import List, Dict, Any, Set, Optional
|
|
from pathlib import Path
|
|
from loguru import logger
|
|
|
|
|
|
class EPUBParser:
|
|
"""
|
|
EPUB 文件解析器。
|
|
|
|
负责加载 EPUB 文件,提取书籍元数据(如标题、作者),并提供方法来遍历和提取
|
|
书中的文档内容(HTML/XHTML)。
|
|
|
|
Attributes:
|
|
epub_path (Path): EPUB 文件的路径对象。
|
|
book (epub.EpubBook): ebooklib 加载的书籍对象。
|
|
metadata (Dict[str, str]): 提取的书籍元数据字典。
|
|
"""
|
|
|
|
def __init__(self, epub_path: str):
|
|
"""
|
|
初始化 EPUB 解析器。
|
|
|
|
Args:
|
|
epub_path (str): EPUB 文件的文件路径。
|
|
|
|
Raises:
|
|
FileNotFoundError: 如果指定的文件不存在。
|
|
Exception: 如果 EPUB 文件加载失败(格式错误等)。
|
|
"""
|
|
self.epub_path = Path(epub_path)
|
|
if not self.epub_path.exists():
|
|
raise FileNotFoundError(f"EPUB 文件不存在: {epub_path}")
|
|
|
|
try:
|
|
# ignore_ncx=True 是为了避免某些旧版 epub 的警告,但新版 ebooklib 可能行为不同
|
|
# 这里直接读取,让 ebooklib 处理
|
|
self.book = epub.read_epub(str(self.epub_path))
|
|
logger.info(f"成功加载 EPUB: {self.epub_path.name}")
|
|
except Exception as e:
|
|
logger.error(f"加载 EPUB 失败: {e}")
|
|
raise
|
|
|
|
self.metadata = self._extract_metadata()
|
|
|
|
def _extract_metadata(self) -> Dict[str, str]:
|
|
"""
|
|
从 EPUB 对象中提取标准元数据。
|
|
|
|
提取 Dublin Core (DC) 元数据,包括标题、作者和语言。
|
|
|
|
Returns:
|
|
Dict[str, str]: 包含 'title', 'author', 'language' 的字典。
|
|
如果提取失败,会使用默认值 ("Unknown", "en")。
|
|
"""
|
|
metadata = {}
|
|
|
|
try:
|
|
# get_metadata 返回的是 (value, dict) 的列表,我们取第一个结果
|
|
title_meta = self.book.get_metadata('DC', 'title')
|
|
metadata['title'] = title_meta[0][0] if title_meta else "Unknown"
|
|
|
|
author_meta = self.book.get_metadata('DC', 'creator')
|
|
metadata['author'] = author_meta[0][0] if author_meta else "Unknown"
|
|
|
|
lang_meta = self.book.get_metadata('DC', 'language')
|
|
metadata['language'] = lang_meta[0][0] if lang_meta else "en"
|
|
|
|
logger.info(f"书籍: {metadata['title']} - {metadata['author']}")
|
|
except Exception as e:
|
|
logger.warning(f"提取元数据时出错: {e}")
|
|
# 设置保底值
|
|
metadata.setdefault('title', 'Unknown')
|
|
metadata.setdefault('author', 'Unknown')
|
|
metadata.setdefault('language', 'en')
|
|
|
|
return metadata
|
|
|
|
def get_toc(self):
|
|
"""
|
|
获取书籍的目录结构 (Table of Contents)
|
|
|
|
Returns:
|
|
book.toc: ebooklib 的原始 TOC 结构
|
|
"""
|
|
return self.book.toc
|
|
|
|
def extract_all_content_items(self, include_files: Optional[Set[str]] = None) -> List[Dict[str, Any]]:
|
|
"""
|
|
提取所有可翻译的内容项目(文档)。
|
|
|
|
遍历 EPUB 中的所有 Item,筛选出类型为 ITEM_DOCUMENT 的项目。
|
|
同时会进行简单的过滤,跳过内容过短(<100字符)或看起来像非正文的文件(如 nav, toc, cover)。
|
|
|
|
Returns:
|
|
List[Dict[str, Any]]: 内容项目列表。每个字典包含:
|
|
- item (epub.EpubItem): 原始 Item 对象。
|
|
- file_name (str): 文件名。
|
|
- content (str): 解码后的 HTML 内容。
|
|
- text_length (int): 纯文本长度(用于统计)。
|
|
"""
|
|
content_items = []
|
|
|
|
# 获取所有文档类型的项目
|
|
for item in self.book.get_items():
|
|
if item.get_type() == ebooklib.ITEM_DOCUMENT:
|
|
# 如果指定了 include_files,则只处理其中的文件
|
|
if include_files is not None:
|
|
item_name = item.get_name()
|
|
if item_name not in include_files:
|
|
logger.debug(f"跳过未选中的文件: {item_name}")
|
|
continue
|
|
|
|
try:
|
|
# 获取内容 (bytes -> str)
|
|
content = item.get_content().decode('utf-8')
|
|
|
|
# 简单的内容验证:提取纯文本检查长度
|
|
soup = BeautifulSoup(content, 'html.parser')
|
|
text = soup.get_text().strip()
|
|
|
|
# 跳过太短的内容(可能是只有图片的页面、空页面)
|
|
if len(text) < 100:
|
|
logger.debug(f"跳过短内容: {item.get_name()} ({len(text)} 字符)")
|
|
continue
|
|
|
|
# 注意:文件名跳过逻辑已移至 TOCParser.get_skip_files()
|
|
# 通过 include_files 参数在调用前过滤
|
|
|
|
content_items.append({
|
|
'item': item,
|
|
'file_name': item.get_name(),
|
|
'content': content,
|
|
'text_length': len(text)
|
|
})
|
|
|
|
logger.debug(f"添加内容项: {item.get_name()} ({len(text)} 字符)")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"处理项目失败 {item.get_name()}: {e}")
|
|
continue
|
|
|
|
logger.info(f"提取了 {len(content_items)} 个内容项目")
|
|
return content_items
|
|
|
|
def get_book_info(self) -> Dict[str, str]:
|
|
"""
|
|
获取书籍的摘要信息。
|
|
|
|
Returns:
|
|
Dict[str, str]: 包含文件名、标题、作者、语言和文档数量的字典。
|
|
"""
|
|
# 统计内容项
|
|
document_count = sum(1 for item in self.book.get_items()
|
|
if item.get_type() == ebooklib.ITEM_DOCUMENT)
|
|
|
|
return {
|
|
'filename': self.epub_path.name,
|
|
'title': self.metadata.get('title', 'Unknown'),
|
|
'author': self.metadata.get('author', 'Unknown'),
|
|
'language': self.metadata.get('language', 'en'),
|
|
'document_count': document_count
|
|
} |