news 2026/5/12 21:02:18

【原创实践】手把手实现 PDF 原版式翻译:PyMuPDF + Ollama 大模型实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【原创实践】手把手实现 PDF 原版式翻译:PyMuPDF + Ollama 大模型实战

一、背景与目标

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

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

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

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


二、整体技术方案

技术选型

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

核心思路

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

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

三、核心类设计:PDFTranslator

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

✅ 必须安装(核心依赖)

pipinstallpymupdf pipinstallollama pipinstallpillow

完整代码

importfitz# PyMuPDFimportosimporttempfilefromPILimportImageimportioimportollamaclassPDFTranslator: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=modeldefextract_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"]forblockinblocks:if"lines"inblock:# 文本块forlineinblock["lines"]:forspaninline["spans"]:text_blocks.append({"text":span["text"],"bbox":span["bbox"],# 边界框 [x0, y0, x1, y1]"size":span["size"],"font":span["font"],"color":span["color"]})returntext_blocksdefextract_images(self,page_num):""" 提取指定页面的图片 Args: page_num (int): 页面编号(从0开始) Returns: list: 包含图片信息的列表 """page=self.doc[page_num]image_list=[]# 获取页面上的图片image_list_raw=page.get_images()forimg_index,imginenumerate(image_list_raw):xref=img[0]pix=fitz.Pixmap(self.doc,xref)# 如果是CMYK图片,转换为RGBifpix.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]ifimg_rectselseNone,"xref":xref}image_list.append(image_info)pix=Nonereturnimage_listdeftranslate_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]}..."iflen(text)>50elsef"正在翻译文本:{text}")# 调用Ollama模型response=ollama.chat(model=self.model,messages=[{'role':'user','content':prompt}])translated=response['message']['content'].strip()print(f"翻译结果:{translated[:50]}..."iflen(translated)>50elsef"翻译结果:{translated}")returntranslatedexceptExceptionase:print(f"翻译错误:{e}")returntext# 如果翻译失败,返回原文本defcreate_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表示到最后一页) """# 确定页面范围ifend_pageisNone:end_page=len(self.doc)-1else:end_page=min(end_page,len(self.doc)-1)# 处理指定范围的页面forpage_numinrange(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)forimg_infoinimages:ifimg_info["rect"]:# 插入原始图片new_page.insert_image(img_info["rect"],stream=img_info["image_data"])# 提取并翻译文本text_blocks=self.extract_text_and_positions(page_num)# 为了更好地保持版面,我们需要更精确地处理文本forblockintext_blocks:ifblock["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来保持更好的版面# 首先擦除原始文本区域(可选)# 然后插入翻译后的文本# 检查文本是否包含中文字符,如果是,则使用中文字体ifany(ord(char)>127forcharintranslated_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"])# 对于空文本块,也检查是否包含中文字符ifany(ord(char)>127forcharinblock["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"所有页面翻译完成!")defget_page_count(self):""" 获取PDF总页数 Returns: int: PDF总页数 """returnlen(self.doc)defclose(self):""" 关闭文档 """ifself.doc:self.doc.close()defmain():# 使用示例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()

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 18:15:35

制造业质检新思路:HunyuanOCR识别产品标签确保一致性

制造业质检新思路&#xff1a;HunyuanOCR识别产品标签确保一致性 在一条高速运转的电子产品装配线上&#xff0c;每分钟都有数百台设备完成封装。它们即将发往全球不同国家——中国、德国、日本、巴西……每一台机器上的标签都必须准确无误地标注语言、型号、批次和合规信息。一…

作者头像 李华
网站建设 2026/5/9 15:24:35

你还在复制数组?现代C#数据操作的正确打开方式

第一章&#xff1a;你还在复制数组&#xff1f;现代C#数据操作的正确打开方式在现代C#开发中&#xff0c;手动复制数组不仅效率低下&#xff0c;还容易引入边界错误和内存浪费。.NET 提供了更高级的数据结构和语言特性&#xff0c;使开发者能够以声明式、安全且高效的方式处理集…

作者头像 李华
网站建设 2026/5/11 9:26:39

Uber全球运营:HunyuanOCR适应不同城市驾驶执照格式

Uber全球运营&#xff1a;HunyuanOCR适应不同城市驾驶执照格式 在旧金山的清晨&#xff0c;一位新司机正通过Uber App上传他的加州驾照&#xff1b;与此同时&#xff0c;在曼谷&#xff0c;另一位申请者提交了泰文版的驾驶证照片&#xff1b;而在迪拜&#xff0c;系统接收到一张…

作者头像 李华
网站建设 2026/5/11 21:33:43

为什么你的C#集合合并这么慢?一文看懂表达式优化的4个关键点

第一章&#xff1a;C#集合合并性能问题的根源在处理大规模数据时&#xff0c;C#开发者常面临集合合并操作的性能瓶颈。这些问题并非源于语言本身的能力不足&#xff0c;而是由底层数据结构的选择、内存分配模式以及算法复杂度共同导致。低效的数据结构选择 使用不合适的集合类型…

作者头像 李华
网站建设 2026/5/9 3:50:32

矿山安全管理:HunyuanOCR识别井下设备铭牌确保合规运行

矿山安全管理&#xff1a;HunyuanOCR识别井下设备铭牌确保合规运行 在深埋地下的矿井中&#xff0c;每一台通风机、水泵和电气柜都承载着生命的重量。它们是否在设计寿命内运行&#xff1f;是否经过正规备案&#xff1f;这些看似基础的问题&#xff0c;却直接关系到数百名矿工的…

作者头像 李华
网站建设 2026/4/23 14:44:41

基于引导向量场GVF和分布式星形通信的5艘欠驱动USV菱形编队控制Matlab仿真,实现USV沿预设路径稳定编队,同时避开直线安全边界

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;擅长数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。&#x1f34e; 往期回顾关注个人主页&#xff1a;Matlab科研工作室&#x1f447; 关注我领取海量matlab电子书和数学建模资料 &#x1f34…

作者头像 李华