把英语电子书翻译为中文 epub

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

  • 整天搞这些一时兴起的事情。
  • 我还是没有找到真正值得做的事情。

希望对大家有帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

waterHBO

老哥,支持一下啊

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值