1. 起因, 目的:
- 英语的电子书,勉强也能读,只是不方便,尤其是速读。
- 把英文的电子书,翻译为中文,下次再遇到这种电子书,直接能用。
2. 先看效果
能用。
3. 过程:
代码 1, 使用远程的 api 来翻译
- 涉及大量的网络请求,很慢。
import ebooklib
from ebooklib import epub
from bs4 import BeautifulSoup, Comment, CData # 导入 Comment 和 CData 用于更精确地过滤
from googletrans import Translator
import time
import os
# --- 配置 ---
INPUT_EPUB_FILE = 'a.epub'
OUTPUT_EPUB_FILE = 'a_chinese_opt_v1.epub' # 修改输出文件名以作区分
TARGET_LANGUAGE = 'zh-cn' # 目标语言代码 (中文简体)
DELAY_BETWEEN_REQUESTS = 1 # 每次翻译API请求之后的延迟(秒)
MAX_CHARS_PER_REQUEST = 4500 # 单次API请求的最大字符数 (保守值)
# --- 初始化翻译器 ---
translator = Translator(service_urls=['translate.google.com']) # 可以指定服务URL
def translate_text_robust(text_to_translate, retries=3, delay=5):
"""
带重试机制的翻译函数,处理可能的网络问题或API限制。
"""
if not text_to_translate or text_to_translate.isspace():
return text_to_translate
for attempt in range(retries):
try:
# print(f" Attempting translation for: '{text_to_translate[:60]}...'") # Debug
translated = translator.translate(text_to_translate, dest=TARGET_LANGUAGE)
# print(f" Original: {text_to_translate[:30]}... Translated: {translated.text[:30]}...")
return translated.text
except Exception as e:
print(f" 翻译错误: {e}. 尝试次数 {attempt + 1}/{retries}.")
if attempt < retries - 1:
print(f" 等待 {delay} 秒后重试...")
time.sleep(delay)
else:
print(f" 翻译失败: {text_to_translate[:50]}...")
return f"[翻译失败] {text_to_translate}"
return text_to_translate # 如果所有重试都失败
def translate_html_content(html_content_str):
"""
解析HTML内容,合并块级元素的文本进行翻译。
注意:此方法会丢失块内原有的HTML子标签。
"""
soup = BeautifulSoup(html_content_str, 'html.parser')
# 定义我们关心的主要文本容器标签
# 'div' 有时用于布局,可能包含非常大的、非纯文本内容,需谨慎处理。
# 我们先从段落、标题、列表项开始。
block_tags_to_translate = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'caption']
# 你可以根据EPUB的具体内容调整这个列表,例如加入 'td', 'th' 等
elements_for_translation = []
for tag_name in block_tags_to_translate:
elements_for_translation.extend(soup.find_all(tag_name))
print(f" 找到 {len(elements_for_translation)} 个指定的块级元素进行处理。")
api_calls_count = 0
for i, element in enumerate(elements_for_translation):
# 检查元素是否仍存在于DOM树中 (如果父元素被处理并清空,子元素可能已脱离)
if not element.parent:
continue
original_block_text = element.get_text(separator=' ', strip=True)
if original_block_text:
print(
f" 处理块 {i + 1}/{len(elements_for_translation)} (标签: <{element.name}>): '{original_block_text[:70].replace(chr(10), ' ').replace(chr(13), ' ')}...'")
translated_parts = []
# 处理长文本块,分片翻译
if len(original_block_text) > MAX_CHARS_PER_REQUEST:
num_chunks = (len(original_block_text) + MAX_CHARS_PER_REQUEST - 1) // MAX_CHARS_PER_REQUEST
print(f" 文本过长 ({len(original_block_text)} chars), 将分割成 {num_chunks} 个片段进行翻译。")
for chunk_idx in range(0, len(original_block_text), MAX_CHARS_PER_REQUEST):
text_chunk = original_block_text[chunk_idx: chunk_idx + MAX_CHARS_PER_REQUEST]
if text_chunk and not text_chunk.isspace():
# print(f" 翻译片段 {chunk_idx // MAX_CHARS_PER_REQUEST + 1}/{num_chunks}: '{text_chunk[:50]}...'")
translated_chunk_text = translate_text_robust(text_chunk)
translated_parts.append(translated_chunk_text)
api_calls_count += 1
# print(f" API call {api_calls_count}. Pausing for {DELAY_BETWEEN_REQUESTS}s...")
time.sleep(DELAY_BETWEEN_REQUESTS) # 每次API调用后延迟
else:
# 文本长度合适,直接翻译
translated_parts.append(translate_text_robust(original_block_text))
api_calls_count += 1
# print(f" API call {api_calls_count}. Pausing for {DELAY_BETWEEN_REQUESTS}s...")
time.sleep(DELAY_BETWEEN_REQUESTS) # 每次API调用后延迟
final_translated_text = "".join(translated_parts)
# 清空元素内所有原有子节点和文本,然后设置新的翻译后文本
# 这会丢失块内如 <b>, <i>, <a> 等子标签
element.clear()
element.string = final_translated_text
# else:
# print(f" 块 {i+1}/{len(elements_for_translation)} (标签: <{element.name}>) 无有效文本内容,跳过。")
# 对于不在上述 block_tags_to_translate 中的文本节点(例如直接在 <body> 下的文本,或在 <span> 等行内元素中的文本)
# 此简化版本不会处理它们,除非它们被包含在被处理的块级元素内。
# 一个更彻底的方案会复杂得多,需要仔细遍历和重建HTML结构。
return str(soup)
def main():
overall_start_time = time.time() # 程序总开始时间
if not os.path.exists(INPUT_EPUB_FILE):
print(f"错误:输入文件 '{INPUT_EPUB_FILE}' 不存在。")
return
print(f"正在加载 EPUB 文件: {INPUT_EPUB_FILE}")
book = epub.read_epub(INPUT_EPUB_FILE)
print("开始翻译内容 (方案一:合并块级元素优化)...")
# new_items = [] # epublib 的 item 是可变的,可以直接修改book.items中的对象
for item_idx, item in enumerate(list(book.get_items())): # 使用list副本迭代,以防万一修改影响迭代
if item.get_type() == ebooklib.ITEM_DOCUMENT: # HTML/XHTML内容文件
print(f"\n处理文件 {item_idx + 1}/{len(list(book.get_items()))}: {item.get_name()}")
try:
html_content_bytes = item.get_content()
# 尝试UTF-8解码,如果失败则尝试常见的备选编码
try:
html_content_str = html_content_bytes.decode('utf-8')
except UnicodeDecodeError:
print(f" 警告: 文件 {item.get_name()} UTF-8解码失败,尝试使用 'latin-1'。")
try:
html_content_str = html_content_bytes.decode('latin-1')
except UnicodeDecodeError:
print(f" 警告: 文件 {item.get_name()} latin-1解码失败,尝试使用 'cp1252'。")
html_content_str = html_content_bytes.decode('cp1252', errors='replace') # 使用replace避免程序中断
except Exception as e_decode:
print(f" 错误: 文件 {item.get_name()} 解码失败: {e_decode}。跳过此文件。")
# new_items.append(item) # 如果创建新列表,则加入未修改项
continue
file_process_start_time = time.time()
translated_html_str = translate_html_content(html_content_str)
file_process_end_time = time.time()
print(f" 文件 {item.get_name()} 处理完毕,耗时: {file_process_end_time - file_process_start_time:.2f} 秒。")
item.set_content(translated_html_str.encode('utf-8'))
# new_items.append(item)
# else:
# new_items.append(item) # 非HTML内容直接保留
# book.items = new_items # 如果之前创建了new_items并填充
print("\n更新书本元数据中的语言信息...")
book.set_language(TARGET_LANGUAGE)
print(f"书本语言元数据已更新为: {TARGET_LANGUAGE}")
print(f"\n正在保存翻译后的 EPUB 文件: {OUTPUT_EPUB_FILE}")
epub.write_epub(OUTPUT_EPUB_FILE, book, {})
print("翻译完成!")
overall_end_time = time.time() # 程序总结束时间
total_processing_time = overall_end_time - overall_start_time
print(f"\n--- 任务完成 ---")
print(f"总耗时: {total_processing_time:.2f} 秒 ({total_processing_time / 60:.2f} 分钟)")
if __name__ == '__main__':
# 确保你有一个名为 'a.epub' 的文件在脚本同目录下,或者修改 INPUT_EPUB_FILE 的路径
# 例如: INPUT_EPUB_FILE = 'path/to/your/ebook.epub'
if not os.path.exists(INPUT_EPUB_FILE):
# 创建一个简单的 a.epub 用于测试
print(f"警告: 输入文件 '{INPUT_EPUB_FILE}' 不存在。将尝试创建一个简单的测试EPUB。")
book = epub.EpubBook()
book.set_identifier('id123456')
book.set_title('Sample Book for Translation')
book.set_language('en')
c1 = epub.EpubHtml(title='Intro', file_name='chap_01.xhtml', lang='en')
c1.content = u'<h1>Introduction</h1><p>This is a sample paragraph. It has some سنا ہے text to translate.</p><p>Another paragraph here. Followed by a list:<ul><li>First item</li><li>Second item to check</li><li>Third one for the road.</li></ul></p><p>Short text.</p><p>A very long text string to test chunking, this needs to be repeated many times to exceed 4500 characters. Let\'s repeat this sentence multiple times to simulate a very long paragraph that will require splitting by the translation API character limit. ' + (
'This is a long sentence fragment. ' * 200) + ' End of the long paragraph.</p>'
book.add_item(c1)
book.toc = (epub.Link('chap_01.xhtml', 'Introduction', 'intro'),)
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
style = 'BODY {color: white;}'
nav_css = epub.EpubItem(uid="style_nav", file_name="style/nav.css", media_type="text/css", content=style)
book.add_item(nav_css)
book.spine = ['nav', c1]
epub.write_epub(INPUT_EPUB_FILE, book, {})
print(f"测试EPUB '{INPUT_EPUB_FILE}' 已创建。请重新运行脚本。")
else:
main()
# 依赖: pip install ebooklib beautifulsoup4 googletrans==4.0.0rc1
# --- 任务完成 ---
# 总耗时: 166.86 秒 (2.78 分钟)
代码 2, 使用本地的模型, huggingface + tensorflow
- 会快一点。
# 聊天记录
# https://gemini.google.com/gem/coding-partner/f82fda8d1724f03a
import ebooklib
from ebooklib import epub
from bs4 import BeautifulSoup, Comment, CData # 导入 Comment 和 CData 用于更精确地过滤
from googletrans import Translator
import time
import os
# --- 新增:Hugging Face Transformers 初始化 ---
from transformers import MarianMTModel, MarianTokenizer
import torch # 如果你安装了 PyTorch
# --- 配置 ---
INPUT_EPUB_FILE = 'a.epub'
OUTPUT_EPUB_FILE = 'a_chinese.epub'
TARGET_LANGUAGE = 'zh-cn' # 目标语言代码 (中文简体)
DELAY_BETWEEN_REQUESTS = 1 # 每次翻译请求之间的延迟(秒),防止IP被封
# --- 初始化翻译器 ---
# translator = Translator()
# --- 初始化本地翻译模型 ---
MODEL_NAME = 'Helsinki-NLP/opus-mt-en-zh' # 英文到中文的模型
tokenizer = None
model = None
device = None # 用于GPU加速
try:
print(f"正在加载本地翻译模型: {MODEL_NAME}...")
# 检查是否有可用的GPU
if torch.cuda.is_available():
device = torch.device("cuda")
print("检测到 GPU,将使用 GPU 进行翻译。")
else:
device = torch.device("cpu")
print("未检测到 GPU,将使用 CPU 进行翻译 (可能会比较慢)。")
tokenizer = MarianTokenizer.from_pretrained(MODEL_NAME)
model = MarianMTModel.from_pretrained(MODEL_NAME).to(device) # 将模型移到GPU或CPU
print("本地翻译模型加载成功!")
except Exception as e:
print(f"错误:无法加载本地翻译模型 {MODEL_NAME}。错误信息: {e}")
print("请确保已安装 'transformers', 'torch', 'sentencepiece' 库,并且模型名称正确。")
print("将无法使用本地翻译功能。")
# 这里可以选择退出程序或回退到其他翻译方式(如果实现了的话)
exit()
# --- 重写翻译函数 ---
# translate_text_local
def translate_text_local(text_to_translate, max_length=512):
"""
使用本地 MarianMT 模型翻译文本。
"""
if not text_to_translate or text_to_translate.isspace() or not model or not tokenizer:
return text_to_translate
try:
inputs = tokenizer(text_to_translate, return_tensors="pt", truncation=False).to(device) # 不立即截断,先获取所有input_ids
input_ids = inputs['input_ids'][0] # 获取单个样本的input_ids
translated_text_parts = []
# 定义一个合理的块大小,略小于模型的最大长度以留有余地
chunk_size = max_length - 2 # 减去特殊token的长度
for i in range(0, len(input_ids), chunk_size):
chunk = input_ids[i: i + chunk_size]
# 需要确保chunk是torch.Tensor并且有正确的维度 (1, sequence_length)
chunk_tensor = torch.tensor([chunk.tolist()], device=device) # 转换为list再转tensor以避免slice问题
# 创建 attention_mask (全1即可,因为我们自己做了分块)
attention_mask = torch.ones_like(chunk_tensor)
translation_tokens = model.generate(input_ids=chunk_tensor, attention_mask=attention_mask,
max_length=max_length) # 可以增加其他生成参数
translated_text_parts.append(tokenizer.decode(translation_tokens[0], skip_special_tokens=True))
translated_text = "".join(translated_text_parts) # 根据需要用空格或其他连接符
# print(f"Original: {text_to_translate[:30]}... Translated: {translated_text[:30]}...")
return translated_text
except Exception as e:
print(f"本地翻译错误: {e}. 对于文本: {text_to_translate[:50]}...")
return f"[本地翻译失败] {text_to_translate}"
def translate_html_content(html_content):
"""
解析HTML内容,翻译其中的文本节点。
"""
soup = BeautifulSoup(html_content, 'html.parser')
text_nodes = soup.find_all(string=True)
count = 0
total_nodes = len(text_nodes)
for i, text_node in enumerate(text_nodes):
# 1. 忽略脚本和样式标签内的文本
if text_node.parent.name in ['script', 'style', 'title', 'meta', 'link']:
continue
# 2. 忽略HTML注释
if isinstance(text_node, Comment):
continue
# 3. 忽略CDATA块 (虽然EPUB中不常见,但以防万一)
if isinstance(text_node, CData):
continue
original_text = text_node.string
# 4. 忽略纯粹的空白文本节点
if original_text and not original_text.isspace():
stripped_text = original_text.strip() # 去除首尾空白再判断
if stripped_text: # 确保strip后还有内容
print(f" 正在翻译片段 { i +1}/{total_nodes}: '{stripped_text[:50]}...'")
# translated_text = translate_text_robust(stripped_text)
translated_text = translate_text_local(stripped_text)
# 直接替换节点内容
# 如果原始文本有前后空格,我们需要保留它们
prefix = original_text[:len(original_text) - len(original_text.lstrip())]
suffix = original_text[len(original_text.rstrip()):]
text_node.replace_with(prefix + translated_text + suffix)
count += 1
# if count % 5 == 0: # 每翻译5个片段,稍微休息一下
# time.sleep(DELAY_BETWEEN_REQUESTS)
return str(soup)
def main():
if not os.path.exists(INPUT_EPUB_FILE):
print(f"错误:输入文件 '{INPUT_EPUB_FILE}' 不存在。")
return
print(f"正在加载 EPUB 文件: {INPUT_EPUB_FILE}")
book = epub.read_epub(INPUT_EPUB_FILE)
print("开始翻译内容...")
new_items = []
for item in book.get_items():
if item.get_type() == ebooklib.ITEM_DOCUMENT: # 这是HTML/XHTML内容文件
print(f"处理文件: {item.get_name()}")
# 解码内容
try:
html_content = item.get_content().decode('utf-8')
except UnicodeDecodeError:
print(f" 警告: 文件 {item.get_name()} UTF-8解码失败,尝试使用 'latin-1'。")
try:
html_content = item.get_content().decode('latin-1') # 有些EPUB可能编码不规范
except Exception as e_decode:
print(f" 错误: 文件 {item.get_name()} 解码失败: {e_decode}。跳过此文件。")
new_items.append(item) # 保留原样
continue
translated_html = translate_html_content(html_content)
# 更新item的内容
item.set_content(translated_html.encode('utf-8'))
new_items.append(item)
print(f" 文件 {item.get_name()} 处理完毕。")
# 也可以在这里加一个更长的延时,如果单个文件翻译了很多片段
# time.sleep(DELAY_BETWEEN_REQUESTS * 2)
else:
# 对于非HTML内容(如图片、CSS、字体等),直接保留
new_items.append(item)
# 更新书本元数据中的语言信息
book.set_language(TARGET_LANGUAGE)
print(f"书本语言元数据已更新为: {TARGET_LANGUAGE}")
# 将所有处理过的(或未处理的)items重新赋值给book对象
# (其实我们是直接修改了book.items列表中的对象,所以这一步理论上不是必须的,
# 但为了清晰,可以这样做,或者直接用原来的book对象写入)
# book.items = new_items # 如果创建了new_items列表并想用它替换
print(f"正在保存翻译后的 EPUB 文件: {OUTPUT_EPUB_FILE}")
epub.write_epub(OUTPUT_EPUB_FILE, book, {})
print("翻译完成!")
if __name__ == '__main__':
main()
# pip install ebooklib beautifulsoup4 googletrans==4.0.0-rc1
# pip install transformers torch sentencepiece
4. 结论 + todo
- 整天搞这些一时兴起的事情。
- 我还是没有找到真正值得做的事情。
希望对大家有帮助。