""" 纯中文 EPUB 构建器模块 负责: 1. 复制原始 EPUB 结构 2. 使用译文替换原文 3. 调用 FormatRestorer 将译文占位符还原为 HTML 标签 """ from ebooklib import epub import ebooklib from bs4 import BeautifulSoup from typing import Dict, List, Any from pathlib import Path from loguru import logger import uuid from .fine_grained_extractor import FineGrainedExtractor class ChineseEPUBBuilder: """纯中文 EPUB 构建器 (DOM Safe)""" def __init__(self, original_book, config: Dict): self.original_book = original_book self.config = config self.translate_toc = config['translation'].get('translate_toc', False) def create_chinese_epub_with_mapping(self, items: List[Any], # List[ManifestItem] output_path: str) -> str: """ 创建纯中文 EPUB。 """ try: new_book = epub.EpubBook() self._copy_metadata(new_book) # 安全清理 TOC new_book.toc = self._sanitize_toc(self.original_book.toc) # 1. 按文件分组 Manifest Items # 假设 items 已经是按全局 ID 排序的 (ManifestManager.get_items 返回有序列表) file_items_map = {} for item in items: fname = item.source_file if fname not in file_items_map: file_items_map[fname] = [] file_items_map[fname].append(item) processed_item_ids = set() item_map = {} # 特殊处理:封面图片 # 复制资源 for item in self.original_book.get_items(): if item.get_type() != ebooklib.ITEM_DOCUMENT: if item.id not in processed_item_ids: new_book.add_item(item) processed_item_ids.add(item.id) item_map[item.id] = item # 重建 Spine new_spine = [] for spine_id, linear in self.original_book.spine: item = self.original_book.get_item_with_id(spine_id) if not item: continue if item.get_type() == ebooklib.ITEM_DOCUMENT: file_name = item.get_name() if file_name in file_items: new_item = self._create_chinese_document( item, file_items[file_name] ) new_item.id = item.id else: new_item = item if new_item.id not in processed_item_ids: new_book.add_item(new_item) processed_item_ids.add(new_item.id) new_spine.append(new_item) else: if item.id in item_map: new_spine.append(item_map[item.id]) new_book.spine = new_spine new_book.add_item(epub.EpubNcx()) new_book.add_item(epub.EpubNav()) output_file = self._generate_output_filename(output_path) epub.write_epub(output_file, new_book, {}) return output_file except Exception as e: logger.error(f"创建纯中文 EPUB 失败: {e}", exc_info=True) raise def _sanitize_toc(self, toc): """确保 TOC 中的所有节点都有 ID""" for item in toc: if isinstance(item, (epub.Link, epub.Section)): if not getattr(item, 'uid', None): item.uid = f"navPoint-{uuid.uuid4().hex[:8]}" elif isinstance(item, tuple) and len(item) == 2: section, children = item if isinstance(section, (epub.Link, epub.Section)): if not getattr(section, 'uid', None): section.uid = f"navPoint-{uuid.uuid4().hex[:8]}" self._sanitize_toc(children) return toc def _copy_metadata(self, new_book): try: for namespace, meta_dict in self.original_book.metadata.items(): for name, values in meta_dict.items(): for value, other in values: if name and hasattr(name, 'lower') and name.lower() == 'identifier': continue new_book.add_metadata(namespace, name, value, other) new_book.add_metadata('DC', 'language', 'zh-CN') new_book.set_identifier(f"chinese-{uuid.uuid4().hex[:12]}") except Exception as e: logger.error(f"元数据复制出错: {e}") def _create_chinese_document(self, original_item, manifest_items: list): try: from .text_processor import TextProcessor soup = BeautifulSoup(original_item.get_content().decode('utf-8'), 'html.parser') # 获取文本元素 text_elements = TextProcessor.get_valid_text_elements(soup) current_idx = 0 for element in text_elements: if not TextProcessor.clean_element_text(element): continue if current_idx < len(manifest_items): m_item = manifest_items[current_idx] # 只有当非导航元素时才尝试替换内容 if not TextProcessor.is_navigation_element(element): # 检查是否是嵌套容器(需要特殊处理) is_nested = TextProcessor.is_nested_container(element) # 优先使用带格式的翻译,降级到纯文本翻译 if m_item.translation_with_original_html: if is_nested: self._replace_direct_content(element, m_item.translation_with_original_html) else: self._replace_content(element, m_item.translation_with_original_html) elif m_item.translation: # 降级:使用纯文本翻译(无格式) if is_nested: self._replace_direct_content(element, m_item.translation) else: self._replace_content(element, m_item.translation) current_idx += 1 new_item = epub.EpubHtml(title=original_item.title, file_name=original_item.get_name(), lang='zh-CN') new_item.set_content(str(soup).encode('utf-8')) return new_item except Exception as e: logger.error(f"创建中文文档失败 {original_item.get_name()}: {e}") return original_item def _replace_content(self, element, translated_html: str): """用译文替换元素的 inner_html""" try: # 将译文 HTML 字符串解析为 BeautifulSoup 对象 new_soup = BeautifulSoup(translated_html, 'html.parser') # 清空原元素并填入新内容 element.clear() # 重要:必须先转换为 list,否则 append 会修改 contents 导致跳过元素 for content in list(new_soup.contents): element.append(content) except Exception as e: logger.error(f"替换内容失败: {e}") def _replace_direct_content(self, element, translated_html: str): """ 替换嵌套容器元素的直接文本内容(保留子块级元素) 策略: 1. 保存所有子块级元素 2. 清空元素内容 3. 填入译文 4. 在末尾追加保存的子块 """ try: from bs4 import NavigableString block_tags = ['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'li', 'td'] # 1. 保存所有子块级元素 saved_blocks = [] for child in element.find_all(block_tags, recursive=False): # 只保存直接子元素中的块 saved_blocks.append(child.extract()) # 2. 额外保存嵌套在 span 等内联元素中的块 for inline in element.find_all(['span', 'a', 'em', 'strong'], recursive=True): for child in inline.find_all(block_tags, recursive=False): saved_blocks.append(child.extract()) # 3. 清空并填入译文 new_soup = BeautifulSoup(translated_html, 'html.parser') element.clear() for content in list(new_soup.contents): element.append(content) # 4. 追加保存的子块 for block in saved_blocks: element.append(block) except Exception as e: logger.error(f"替换嵌套内容失败: {e}") # 降级到普通替换 self._replace_content(element, translated_html) def _generate_output_filename(self, output_path: str) -> str: from .utils import sanitize_filename title = self.original_book.get_metadata('DC', 'title') clean_title = sanitize_filename(title[0][0]) if title else "chinese_book" Path(output_path).mkdir(parents=True, exist_ok=True) return str(Path(output_path) / f"{clean_title}_chinese.epub")