工作过程要在pdf的文件上查找内容,但是图片扫描的pdf文件,无法查找。为此用python写一个程序用于转换成ocr识别出透明文字,在原图片上的pdf文件。这个过程有点不容易,就是我喜欢使用paddleocr,且要用gpu识别,比较快。
python
# -*- coding: utf-8 -*-
"""
Created on Sun Aug 30 10:58:00 2026
@author: YBK
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
import os
import sys
import json
import shutil
from PyPDF2 import PdfReader, PdfWriter
from paddleocr import PaddleOCR
# 修复 hashlib 问题
import hashlib
_original_md5 = hashlib.md5
def patched_md5(*args, **kwargs):
if 'usedforsecurity' in kwargs:
del kwargs['usedforsecurity']
return _original_md5(*args, **kwargs)
hashlib.md5 = patched_md5
# 设置 Ghostscript 环境
gs_bin = r'C:\Program Files\gs\gs10.07.1\bin'
os.environ['PATH'] = gs_bin + os.pathsep + os.environ.get('PATH', '')
os.environ['GS_PROG'] = os.path.join(gs_bin, 'gswin64c.exe')
# 导入 PdfOCRer
from pdfocrer.pdf_ocrer import PdfOCRer
# 每批处理的页数(用于降低显存峰值,但不会彻底释放显存)
PAGES_PER_BATCH = 30 # 可根据显存调整,设为 0 表示一次性处理所有页(但容易爆显存)
class PDFOCRApp:
def __init__(self, root):
self.root = root
self.root.title("PDF OCR 转换工具 (PdfOCRer) - 断点续传")
self.root.geometry("600x400")
self.root.resizable(False, False)
self.input_path = tk.StringVar()
self.output_path = tk.StringVar()
self.lang = tk.StringVar(value="ch")
self.is_processing = False
self.debug = False # 添加这一行
self._create_widgets()
def _create_widgets(self):
main_frame = ttk.Frame(self.root, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(main_frame, text="1. 选择输入的PDF文件:").grid(row=0, column=0, sticky=tk.W, pady=(0, 5))
input_frame = ttk.Frame(main_frame)
input_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
input_frame.columnconfigure(0, weight=1)
ttk.Entry(input_frame, textvariable=self.input_path, state="readonly").pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
ttk.Button(input_frame, text="浏览...", command=self._browse_input).pack(side=tk.RIGHT)
ttk.Label(main_frame, text="2. 选择输出PDF的位置:").grid(row=2, column=0, sticky=tk.W, pady=(0, 5))
output_frame = ttk.Frame(main_frame)
output_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
output_frame.columnconfigure(0, weight=1)
ttk.Entry(output_frame, textvariable=self.output_path, state="readonly").pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
ttk.Button(output_frame, text="浏览...", command=self._browse_output).pack(side=tk.RIGHT)
ttk.Label(main_frame, text="3. 选择识别语言:").grid(row=4, column=0, sticky=tk.W, pady=(0, 5))
lang_frame = ttk.Frame(main_frame)
lang_frame.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 20))
languages = [("中文", "ch"), ("英文", "en"), ("韩文", "korean"), ("日文", "japan"), ("拉丁文", "latin"), ("阿拉伯文", "arabic")]
for text, value in languages:
ttk.Radiobutton(lang_frame, text=text, variable=self.lang, value=value).pack(side=tk.LEFT, padx=(0, 15))
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=6, column=0, columnspan=3, pady=(0, 10))
self.start_button = ttk.Button(button_frame, text="开始转换", command=self._start_ocr, width=15)
self.start_button.pack(side=tk.LEFT, padx=(0, 20))
self.progress_bar = ttk.Progressbar(button_frame, mode='indeterminate', length=200)
self.progress_bar.pack(side=tk.LEFT)
ttk.Label(main_frame, text="处理日志:").grid(row=7, column=0, sticky=tk.W, pady=(10, 5))
self.log_text = tk.Text(main_frame, height=10, state='disabled', wrap=tk.WORD)
self.log_text.grid(row=8, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.log_text.yview)
scrollbar.grid(row=8, column=3, sticky=(tk.N, tk.S))
self.log_text['yscrollcommand'] = scrollbar.set
main_frame.rowconfigure(8, weight=1)
main_frame.columnconfigure(0, weight=1)
def _browse_input(self):
file_path = filedialog.askopenfilename(title="选择PDF文件", filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")])
if file_path:
self.input_path.set(file_path)
base, ext = os.path.splitext(file_path)
self.output_path.set(f"{base}_ocr{ext}")
def _browse_output(self):
file_path = filedialog.asksaveasfilename(title="保存PDF文件为", defaultextension=".pdf", filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")])
if file_path:
self.output_path.set(file_path)
def _log(self, message):
self.log_text.config(state='normal')
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.log_text.config(state='disabled')
self.root.update_idletasks()
# ============== 断点续传核心函数 ==============
def _get_temp_dir(self, input_pdf):
"""获取临时文件夹路径(在输入PDF同目录下创建)"""
base_dir = os.path.dirname(input_pdf)
basename = os.path.splitext(os.path.basename(input_pdf))[0]
temp_dir = os.path.join(base_dir, basename + "_ocr_temp")
return temp_dir
def _get_progress_file(self, temp_dir):
return os.path.join(temp_dir, "progress.json")
def _load_progress(self, temp_dir):
"""读取进度文件,返回 last_completed(已完成的最后一页页码)和 total_pages"""
prog_file = self._get_progress_file(temp_dir)
if os.path.exists(prog_file):
try:
with open(prog_file, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get("last_completed", 0), data.get("total_pages", 0)
except:
return 0, 0
return 0, 0
def _save_progress(self, temp_dir, last_completed, total_pages):
prog_file = self._get_progress_file(temp_dir)
with open(prog_file, 'w', encoding='utf-8') as f:
json.dump({"last_completed": last_completed, "total_pages": total_pages}, f, indent=2)
def _clear_progress(self, temp_dir):
prog_file = self._get_progress_file(temp_dir)
if os.path.exists(prog_file):
os.remove(prog_file)
# ============== 合并单页PDF ==============
def _merge_page_pdfs(self, page_pdfs, output_path):
"""按页码顺序合并所有单页PDF"""
writer = PdfWriter()
for pdf_path in page_pdfs:
if os.path.exists(pdf_path):
reader = PdfReader(pdf_path)
for page in reader.pages:
writer.add_page(page)
else:
self._log(f"警告:找不到页面文件 {pdf_path}")
with open(output_path, 'wb') as f:
writer.write(f)
self._log(f"合并完成,输出文件:{output_path}")
# ============== 核心处理 ==============
def _run_ocr(self):
try:
input_pdf = self.input_path.get()
output_pdf = self.output_path.get()
lang = self.lang.get()
self._log(f"开始处理: {input_pdf}")
self._log(f"输出文件: {output_pdf}")
self._log(f"识别语言: {lang}")
self._log("-" * 40)
# 创建临时目录
temp_dir = self._get_temp_dir(input_pdf)
os.makedirs(temp_dir, exist_ok=True)
self._log(f"临时目录: {temp_dir}")
# 读取进度
last_completed, total_pages_saved = self._load_progress(temp_dir)
# 获取总页数
reader = PdfReader(input_pdf)
total_pages = len(reader.pages)
if total_pages_saved != total_pages:
# 如果文件页数有变化,重置进度
self._log("检测到文件页数变化,重置进度。")
last_completed = 0
total_pages_saved = total_pages
self._save_progress(temp_dir, 0, total_pages)
# 检查是否已全部完成
if last_completed >= total_pages:
self._log("所有页面已处理完毕,直接合并...")
# 收集所有单页PDF(假设已存在)
page_files = [os.path.join(temp_dir, f"page_{i+1:04d}.pdf") for i in range(total_pages)]
self._merge_page_pdfs(page_files, output_pdf)
self._clear_progress(temp_dir)
self._log("✅ 转换成功完成!")
self._log(f"可搜索PDF已保存至: {output_pdf}")
return
# 初始化 OCR 引擎(低内存参数)
self._log("加载OCR模型...")
ocr_engine = PaddleOCR(
lang=lang,
use_gpu=True,
gpu_mem=2048, # 限制显存
use_angle_cls=False,
rec_batch_num=1,
det_batch_num=1,
max_text_length=50
)
self._log("OCR模型加载完成。")
# 初始化 PdfOCRer 实例(统一临时目录)
ocr = PdfOCRer(debug=False, temp_dir=temp_dir)
# 从 last_completed+1 开始处理
start_page = last_completed + 1
self._log(f"从第 {start_page} 页开始处理...")
for page_num in range(start_page, total_pages + 1):
self._log(f"正在处理第 {page_num}/{total_pages} 页...")
try:
# 处理单页,返回该页的OCR PDF路径
page_pdf_path = ocr.process_single_page(input_pdf, page_num, ocr_engine)
# 将生成的页面文件重命名为统一格式(方便合并)
target_name = os.path.join(temp_dir, f"page_{page_num:04d}.pdf")
if os.path.exists(page_pdf_path):
os.rename(page_pdf_path, target_name)
else:
self._log(f"警告:第 {page_num} 页未生成有效文件。")
# 继续处理下一页?或者抛异常?视情况而定
# 更新进度
last_completed = page_num
self._save_progress(temp_dir, last_completed, total_pages)
self._log(f"第 {page_num} 页完成,进度已保存。")
except Exception as e:
self._log(f"第 {page_num} 页处理失败: {e}")
# 发生异常时,进度停留在当前-1,下次可继续
raise # 重新抛出,终止流程
# 所有页面处理完毕,合并
self._log("所有页面处理完毕,开始合并...")
page_files = [os.path.join(temp_dir, f"page_{i+1:04d}.pdf") for i in range(total_pages)]
self._merge_page_pdfs(page_files, output_pdf)
# 清理临时目录(可选)
try:
shutil.rmtree(temp_dir)
self._log("临时目录已清理。")
except Exception as e:
self._log(f"清理临时目录时出错: {e}")
# 删除进度文件
self._clear_progress(temp_dir)
self._log("-" * 40)
self._log("✅ 转换成功完成!")
self._log(f"可搜索PDF已保存至: {output_pdf}")
except Exception as e:
self._log(f"❌ 转换过程中出现错误: {e}")
import traceback
self._log(traceback.format_exc())
messagebox.showerror("转换错误", f"处理失败,请查看日志了解详情。\n错误信息: {e}")
finally:
self.root.after(0, self._reset_ui)
def _start_ocr(self):
if self.is_processing:
return
if not self.input_path.get():
messagebox.showwarning("提示", "请先选择一个输入的PDF文件。")
return
if not self.output_path.get():
messagebox.showwarning("提示", "请先设置输出文件的位置。")
return
if not os.path.exists(self.input_path.get()):
messagebox.showerror("错误", "输入的PDF文件不存在。")
return
self.is_processing = True
self.start_button.config(state='disabled', text="处理中...")
self.progress_bar.start(10)
self.log_text.config(state='normal')
self.log_text.delete(1.0, tk.END)
self.log_text.config(state='disabled')
thread = threading.Thread(target=self._run_ocr, daemon=True)
thread.start()
def _reset_ui(self):
self.is_processing = False
self.start_button.config(state='normal', text="开始转换")
self.progress_bar.stop()
if __name__ == "__main__":
root = tk.Tk()
app = PDFOCRApp(root)
root.mainloop()
主要是装一些库和Ghostscript,我在官网下不了,在这个网站下载。https://www.pdfblog.at/
https://www.pdfblog.at/
然后要给python建一个自己的环境:
conda activate paddle_ocr_env
安装:
以 CUDA 11.2 为例,安装对应的 GPU 版本reference:8reference:9
如果你的 CUDA 版本不同,请到 PaddlePaddle 官网查找对应的安装命令
pip install paddlepaddle-gpu==2.4.2.post112 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html
直接安装 PaddleOCR
pip install paddleocr
这一步会出错,需要conda install -c conda-forge python-lmdb -y
然后:pip install paddlepaddle==2.4.2 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html
pip install paddleocr==2.6.1.3
pip install pdfocrer
将C:\Program Files\gs\gs10.07.1\bin里面的gswin64c.exe修改为gs.exe
然后用我上面的程序应该就好用了,还是有点复杂的。在过程中,我还遇到处理pdf到125页就爆内存,所以我后来改了这个,可以"续传",不怕它爆内存。这些都是deepseek帮忙的,但它就不会有我这种续传的想法,可见还是人比较聪明。