Python结合PyMuPDF手把手实现PDF原版式翻译的实战指南

 更新时间:2026年01月04日 08:49:17   作者:weixin_46244623  
本文提出了一种基于PyMuPDF和Ollama大模型的PDF翻译方案,解决了传统PDF翻译中的版式错乱,图片丢失等问题,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下

一、背景与目标

在处理英文技术文档、论文或说明书时,常见的 PDF 翻译方案存在几个痛点:

  • 翻译后版式错乱
  • 图片、公式丢失
  • 代码、URL 被误翻译
  • 中文字体无法正常显示
  • 只能整页 OCR,无法保持原始排版

本文介绍一种基于 PyMuPDF(fitz)+ Ollama 大模型的 PDF 翻译方案,目标是:

逐页、逐文本块翻译 PDF,自然语言翻译为中文,图片与版式完全保持不变

翻译后的效果如下图

二、整体技术方案

技术选型

模块技术
PDF 解析PyMuPDF(fitz)
图片处理PyMuPDF Pixmap
大模型推理Ollama
翻译模型qwen2.5:7b
输出方式重新生成 PDF(逐页)

核心思路

整个流程可拆解为 5 个步骤:

  • 逐页解析 PDF
  • 提取文本 span(包含字体、字号、位置)
  • 提取并复制原始图片
  • 使用大模型翻译自然语言文本
  • 在原坐标位置重新绘制文本,生成新 PDF

三、核心类设计:PDFTranslator

整个翻译逻辑被封装在 PDFTranslator 类中,职责清晰、结构合理。

必须安装(核心依赖)

pip install pymupdf
pip install ollama
pip install pillow

完整代码

import fitz  # PyMuPDF
import os
import tempfile
from PIL import Image
import io
import ollama


class PDFTranslator:
    def __init__(self, source_pdf_path, model="qwen2.5:7b"):
        """
        初始化PDF翻译器
        
        Args:
            source_pdf_path (str): 源PDF文件路径
            model (str): Ollama模型名称
        """
        self.source_pdf_path = source_pdf_path
        self.doc = fitz.open(source_pdf_path)
        self.model = model
        
    def extract_text_and_positions(self, page_num):
        """
        提取指定页面的文本及其位置信息
        
        Args:
            page_num (int): 页面编号(从0开始)
            
        Returns:
            list: 包含文本块信息的列表
        """
        page = self.doc[page_num]
        text_blocks = []
        
        # 获取页面上的文本块
        blocks = page.get_text("dict")["blocks"]
        
        for block in blocks:
            if "lines" in block:  # 文本块
                for line in block["lines"]:
                    for span in line["spans"]:
                        text_blocks.append({
                            "text": span["text"],
                            "bbox": span["bbox"],  # 边界框 [x0, y0, x1, y1]
                            "size": span["size"],
                            "font": span["font"],
                            "color": span["color"]
                        })
        
        return text_blocks
    
    def extract_images(self, page_num):
        """
        提取指定页面的图片
        
        Args:
            page_num (int): 页面编号(从0开始)
            
        Returns:
            list: 包含图片信息的列表
        """
        page = self.doc[page_num]
        image_list = []
        
        # 获取页面上的图片
        image_list_raw = page.get_images()
        
        for img_index, img in enumerate(image_list_raw):
            xref = img[0]
            pix = fitz.Pixmap(self.doc, xref)
            
            # 如果是CMYK图片,转换为RGB
            if pix.n < 5:
                img_data = pix.tobytes("png")
            else:
                pix1 = fitz.Pixmap(fitz.csRGB, pix)
                img_data = pix1.tobytes("png")
                pix1 = None
            
            # 获取图片在页面上的位置
            img_rects = page.get_image_rects(xref)
            
            image_info = {
                "image_data": img_data,
                "rect": img_rects[0] if img_rects else None,
                "xref": xref
            }
            
            image_list.append(image_info)
            pix = None
        
        return image_list
    
    def translate_text(self, text, dest_lang='zh'):
        """
        使用Ollama大模型翻译文本
        
        Args:
            text (str): 要翻译的文本
            dest_lang (str): 目标语言
            
        Returns:
            str: 翻译后的文本
        """
        try:
            # 构建翻译提示,明确要求将英文翻译成中文,只返回翻译结果,保持原文格式
            prompt = f"""请将以下英文文本翻译成中文,保持原文的格式、空格和标点符号,只返回翻译结果,不要添加任何解释。

翻译要求:
1. 所有代码内容(包括代码块、函数名、类名、变量名、命令、配置项等)必须保持原样,不得翻译。
2. 所有网址(以 http://、https://、www. 开头的链接)必须保持原样,不得翻译。
3. 所有数字保持原样,不得翻译或改写(包括整数、小数、百分比、版本号、时间、日期、编号等)。
4. 保持原文中的空格、换行、缩进等格式,确保翻译后格式一致。
5. 仅翻译自然语言描述性的英文文本,其余内容全部保持不变。

英文文本如下:
{text}
"""
            
            print(f"正在翻译文本: {text[:50]}..." if len(text) > 50 else f"正在翻译文本: {text}")
            
            # 调用Ollama模型
            response = ollama.chat(
                model=self.model,
                messages=[
                    {
                        'role': 'user',
                        'content': prompt
                    }
                ]
            )
            
            translated = response['message']['content'].strip()
            print(f"翻译结果: {translated[:50]}..." if len(translated) > 50 else f"翻译结果: {translated}")
            
            return translated
        except Exception as e:
            print(f"翻译错误: {e}")
            return text  # 如果翻译失败,返回原文本
    
    def create_translated_pdf(self, output_path, dest_lang='zh', start_page=0, end_page=None):
        """
        创建翻译后的PDF文件,保持原有版面布局
        
        Args:
            output_path (str): 输出PDF文件路径
            dest_lang (str): 目标语言
            start_page (int): 开始页面(从0开始)
            end_page (int): 结束页面(从0开始,None表示到最后一页)
        """
        # 确定页面范围
        if end_page is None:
            end_page = len(self.doc) - 1
        else:
            end_page = min(end_page, len(self.doc) - 1)
        
        # 处理指定范围的页面
        for page_num in range(start_page, end_page + 1):
            print(f"正在处理第 {page_num + 1} 页...(共{len(self.doc)}页)")
            
            # 创建新的PDF文档
            new_doc = fitz.open()
            
            # 获取原始页面信息
            original_page = self.doc.load_page(page_num)
            page_width = original_page.rect.width
            page_height = original_page.rect.height
            
            # 创建新页面
            new_page = new_doc.new_page(width=page_width, height=page_height)
            
            # 复制原始页面的布局和图片
            original_page = self.doc[page_num]
            
            # 复制图片
            images = self.extract_images(page_num)
            for img_info in images:
                if img_info["rect"]:
                    # 插入原始图片
                    new_page.insert_image(img_info["rect"], stream=img_info["image_data"])
            
            # 提取并翻译文本
            text_blocks = self.extract_text_and_positions(page_num)
            
            # 为了更好地保持版面,我们需要更精确地处理文本
            for block in text_blocks:
                if block["text"].strip():  # 避免空文本
                    translated_text = self.translate_text(block["text"], dest_lang)
                    
                    # 计算文本位置
                    bbox = block["bbox"]
                    x0, y0, x1, y1 = bbox
                    
                    # 创建文本插入点(使用左下角作为基点)
                    # fitz使用笛卡尔坐标系,y轴向上
                    point = fitz.Point(x0, y0 + block["size"])
                    
                    # 使用text writer来保持更好的版面
                    # 首先擦除原始文本区域(可选)
                    # 然后插入翻译后的文本
                    # 检查文本是否包含中文字符,如果是,则使用中文字体
                    if any(ord(char) > 127 for char in translated_text):  # 包含非ASCII字符(如中文)
                        try:
                            new_page.insert_text(
                                point,
                                translated_text,
                                fontsize=block["size"],
                                fontname="china-ss",
                                color=(0, 0, 0)  # 强制使用黑色文字以确保可见
                            )
                        except:
                            # 如果内置字体失败,使用默认字体
                            new_page.insert_text(
                                point,
                                translated_text,
                                fontsize=block["size"],
                                color=(0, 0, 0)  # 强制使用黑色文字以确保可见
                            )
                    else:
                        # 如果是英文文本,使用原始字体
                        try:
                            new_page.insert_text(
                                point,
                                translated_text,
                                fontsize=block["size"],
                                fontname=block["font"],
                                color=(0, 0, 0)  # 强制使用黑色文字以确保可见
                            )
                        except:
                            new_page.insert_text(
                                point,
                                translated_text,
                                fontsize=block["size"],
                                color=(0, 0, 0)  # 强制使用黑色文字以确保可见
                            )
                else:
                    # 如果是空文本,直接复制原始文本(如空格等)
                    bbox = block["bbox"]
                    x0, y0, x1, y1 = bbox
                    point = fitz.Point(x0, y0 + block["size"])
                    
                    # 对于空文本块,也检查是否包含中文字符
                    if any(ord(char) > 127 for char in block["text"]):  # 包含非ASCII字符(如中文)
                        try:
                            new_page.insert_text(
                                point,
                                block["text"],
                                fontsize=block["size"],
                                fontname="china-ss",
                                color=(0, 0, 0)  # 强制使用黑色文字以确保可见
                            )
                        except:
                            # 如果内置字体失败,使用默认字体
                            new_page.insert_text(
                                point,
                                block["text"],
                                fontsize=block["size"],
                                color=(0, 0, 0)  # 强制使用黑色文字以确保可见
                            )
                    else:
                        # 如果是英文文本,使用原始字体
                        try:
                            new_page.insert_text(
                                point,
                                block["text"],
                                fontsize=block["size"],
                                fontname=block["font"],
                                color=(0, 0, 0)  # 强制使用黑色文字以确保可见
                            )
                        except:
                            new_page.insert_text(
                                point,
                                block["text"],
                                fontsize=block["size"],
                                color=(0, 0, 0)  # 强制使用黑色文字以确保可见
                            )
            
            # 为每一页创建单独的PDF文件
            page_output_path = output_path.replace('.pdf', f'_page_{page_num + 1}.pdf')
            new_doc.save(page_output_path)
            new_doc.close()
            print(f"第 {page_num + 1} 页翻译完成,输出文件: {page_output_path}")
        
        print(f"所有页面翻译完成!")
    
    def get_page_count(self):
        """
        获取PDF总页数
        
        Returns:
            int: PDF总页数
        """
        return len(self.doc)
    
    def close(self):
        """
        关闭文档
        """
        if self.doc:
            self.doc.close()


def main():
    # 使用示例
    source_pdf = "1.pdf"
    output_pdf = "translated_1.pdf"
    
    # 创建翻译器实例,使用Ollama模型
    translator = PDFTranslator(source_pdf, model="qwen2.5:7b")
    
    try:
        print(f"PDF总页数: {translator.get_page_count()}")
        
        # 翻译PDF并保持版面(只翻译前3页作为示例)
        translator.create_translated_pdf(
            output_path=output_pdf, 
            dest_lang='zh',
            start_page=8,
            end_page=211  # 翻译前3页(0-2)
        )
    finally:
        # 关闭文档
        translator.close()


if __name__ == "__main__":
    main()

运行结果 

到此这篇关于Python结合PyMuPDF手把手实现PDF原版式翻译的实战指南的文章就介绍到这了,更多相关Python PyMuPDF翻译PDF内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 基于python win32setpixel api 实现计算机图形学相关操作(推荐)

    基于python win32setpixel api 实现计算机图形学相关操作(推荐)

    这篇文章主要介绍了基于python win32setpixel api 实现计算机图形学相关操作,这次的主要分为2个主要模块,一个是实现画线,画圆的算法,还有填充的算法,以及裁剪的算法,需要的朋友可以参考下
    2021-12-12
  • 详解使用Python写一个向数据库填充数据的小工具(推荐)

    详解使用Python写一个向数据库填充数据的小工具(推荐)

    这篇文章主要介绍了用Python写一个向数据库填充数据的小工具,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • python删除xml中的w:ascii属性的步骤

    python删除xml中的w:ascii属性的步骤

    使用xml.etree.ElementTree删除Word XML中w:ascii属性,需注册命名空间并定位rFonts元素,通过del操作删除属性,建议配合python-docx库,注意备份及文档保护设置,确保修改目标样式,本文给大家介绍python删除xml中的w:ascii属性的步骤,感兴趣的朋友一起看看吧
    2025-06-06
  • 基于Python绘制子图及子图刻度的变换等的问题

    基于Python绘制子图及子图刻度的变换等的问题

    这篇文章主要介绍了基于Python绘制子图及子图刻度的变换等的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-05-05
  • 学习Python爬虫的几点建议

    学习Python爬虫的几点建议

    这篇文章主要介绍了学习Python爬虫的几点建议,对新手学习爬虫有很大的帮助,感兴趣的朋友可以了解下
    2020-08-08
  • 在pycharm中配置Anaconda以及pip源配置详解

    在pycharm中配置Anaconda以及pip源配置详解

    这篇文章主要介绍了在pycharm中配置Anaconda以及pip源配置详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • python 读取数据库并绘图的实例

    python 读取数据库并绘图的实例

    今天小编就为大家分享一篇python 读取数据库并绘图的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-12-12
  • Pandas中Series和DataFrame的索引实现

    Pandas中Series和DataFrame的索引实现

    这篇文章主要介绍了Pandas中Series和DataFrame的索引实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-06-06
  • python基于property()函数定义属性

    python基于property()函数定义属性

    这篇文章主要介绍了python基于property()函数定义属性,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • python openpyxl使用方法详解

    python openpyxl使用方法详解

    这篇文章主要介绍了python openpyxl使用方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-07-07

最新评论