本地运行的检索PDF文件中出现关键字的python程序

发布于:2025-08-31 ⋅ 阅读:(21) ⋅ 点赞:(0)

如果PDF不是OCR的,要在一大堆PDF中检索某个关键词,比较麻烦,
下面的程序可以实现,功能有:
1)本地运行
2)指定某个文件夹,检索出结果,并且可以本地打开。

import os
import sys
import pdfplumber
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import webbrowser
import threading
from queue import Queue


class PDFSearchApp:
    def __init__(self, root):
        self.root = root
        self.root.title("PDF关键词搜索工具")
        self.root.geometry("900x650")

        # 设置主题样式
        self.style = ttk.Style()
        if sys.platform == "win32":
            self.style.theme_use('vista')
        elif sys.platform == "darwin":
            self.style.theme_use('aqua')
        else:
            self.style.theme_use('clam')

        self.setup_ui()

        # 用于线程间通信的队列
        self.result_queue = Queue()

        # 检查是否在搜索中
        self.searching = False

    def setup_ui(self):
        """设置用户界面"""
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 文件夹选择区域
        folder_frame = ttk.LabelFrame(main_frame, text="PDF文件夹选择", padding="10")
        folder_frame.pack(fill=tk.X, pady=(0, 10))

        self.folder_var = tk.StringVar()
        folder_entry = ttk.Entry(folder_frame, textvariable=self.folder_var, width=80)
        folder_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))

        browse_btn = ttk.Button(folder_frame, text="浏览...", command=self.browse_folder)
        browse_btn.pack(side=tk.RIGHT)

        # 关键词搜索区域
        search_frame = ttk.LabelFrame(main_frame, text="搜索设置", padding="10")
        search_frame.pack(fill=tk.X, pady=(0, 10))

        ttk.Label(search_frame, text="关键词:").pack(side=tk.LEFT)

        self.keyword_var = tk.StringVar()
        keyword_entry = ttk.Entry(search_frame, textvariable=self.keyword_var, width=40)
        keyword_entry.pack(side=tk.LEFT, padx=(5, 10))
        keyword_entry.bind("<Return>", lambda event: self.start_search())

        self.search_btn = ttk.Button(search_frame, text="开始搜索", command=self.start_search)
        self.search_btn.pack(side=tk.RIGHT)

        # 结果区域
        results_frame = ttk.LabelFrame(main_frame, text="搜索结果", padding="10")
        results_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))

        # 创建带滚动条的树形视图
        tree_frame = ttk.Frame(results_frame)
        tree_frame.pack(fill=tk.BOTH, expand=True)

        # 创建垂直滚动条
        tree_scroll = ttk.Scrollbar(tree_frame)
        tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)

        # 创建水平滚动条
        h_scroll = ttk.Scrollbar(tree_frame, orient="horizontal")
        h_scroll.pack(side=tk.BOTTOM, fill=tk.X)

        # 创建树形视图
        self.tree = ttk.Treeview(tree_frame,
                                 yscrollcommand=tree_scroll.set,
                                 xscrollcommand=h_scroll.set,
                                 selectmode="browse")

        # 配置滚动条
        tree_scroll.config(command=self.tree.yview)
        h_scroll.config(command=self.tree.xview)

        # 定义列
        self.tree["columns"] = ("path", "count")
        self.tree.column("#0", width=0, stretch=tk.NO)  # 隐藏默认列
        self.tree.column("path", anchor=tk.W, width=600)
        self.tree.column("count", anchor=tk.CENTER, width=100)

        # 设置列标题
        self.tree.heading("path", text="文件路径", anchor=tk.W)
        self.tree.heading("count", text="出现次数", anchor=tk.CENTER)

        # 绑定双击事件打开文件
        self.tree.bind("<Double-1>", self.open_selected_file)

        self.tree.pack(fill=tk.BOTH, expand=True)

        # 状态栏
        self.status_var = tk.StringVar()
        self.status_var.set("就绪")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)

        # 添加右键菜单
        self.context_menu = tk.Menu(self.tree, tearoff=0)
        self.context_menu.add_command(label="打开文件", command=self.open_selected_file)
        self.context_menu.add_command(label="复制文件路径", command=self.copy_file_path)
        self.tree.bind("<Button-3>", self.show_context_menu)

    def browse_folder(self):
        """浏览并选择文件夹"""
        folder_path = filedialog.askdirectory(title="选择包含PDF文件的文件夹")
        if folder_path:
            self.folder_var.set(folder_path)

    def start_search(self):
        """开始搜索"""
        folder_path = self.folder_var.get().strip()
        keyword = self.keyword_var.get().strip()

        # 验证输入
        if not folder_path or not os.path.isdir(folder_path):
            messagebox.showerror("错误", "请选择有效的PDF文件夹")
            return

        if not keyword:
            messagebox.showerror("错误", "请输入搜索关键词")
            return

        # 清空之前的搜索结果
        for item in self.tree.get_children():
            self.tree.delete(item)

        # 更新状态
        self.status_var.set(f"正在搜索 '{keyword}' 在文件夹: {folder_path}...")
        self.search_btn.config(state=tk.DISABLED)
        self.searching = True

        # 在后台线程执行搜索
        threading.Thread(target=self.search_worker, args=(folder_path, keyword), daemon=True).start()

        # 开始检查结果队列
        self.root.after(100, self.check_results)

    def search_worker(self, folder_path, keyword):
        """搜索工作线程"""
        try:
            results = self.search_pdfs_for_keyword(folder_path, keyword)
            self.result_queue.put(('results', results, keyword, folder_path))
        except Exception as e:
            self.result_queue.put(('error', str(e)))

    def check_results(self):
        """检查结果队列"""
        while not self.result_queue.empty():
            msg_type, *data = self.result_queue.get()

            if msg_type == 'results':
                results, keyword, folder_path = data
                self.display_results(results, keyword, folder_path)
            elif msg_type == 'error':
                error_msg = data[0]
                messagebox.showerror("搜索错误", f"搜索过程中发生错误:\n{error_msg}")
                self.status_var.set("搜索出错")

            self.searching = False
            self.search_btn.config(state=tk.NORMAL)

        if self.searching:
            self.root.after(100, self.check_results)

    def search_pdfs_for_keyword(self, folder_path, keyword):
        """
        在指定文件夹中搜索包含关键字的PDF文件并统计出现次数

        参数:
        folder_path (str): 要搜索的文件夹路径
        keyword (str): 要搜索的关键字

        返回:
        list: 包含关键字的PDF文件信息列表,每个元素包含文件路径和出现次数
        """
        results = []
        keyword_lower = keyword.lower()
        total_files = 0
        processed_files = 0

        # 首先统计PDF文件总数(用于进度显示)
        for _, _, files in os.walk(folder_path):
            total_files += sum(1 for f in files if f.lower().endswith('.pdf'))

        # 遍历文件夹中的所有文件
        for root, _, files in os.walk(folder_path):
            for file in files:
                if file.lower().endswith('.pdf'):
                    file_path = os.path.join(root, file)
                    processed_files += 1

                    try:
                        # 更新状态
                        self.status_var.set(f"正在处理 ({processed_files}/{total_files}): {file}")

                        # 使用pdfplumber打开PDF文件
                        with pdfplumber.open(file_path) as pdf:
                            text = ""
                            # 提取所有页面的文本
                            for page in pdf.pages:
                                page_text = page.extract_text()
                                if page_text:
                                    text += page_text + "\n"

                            # 检查关键字并统计出现次数
                            text_lower = text.lower()
                            count = text_lower.count(keyword_lower)

                            if count > 0:
                                # 保留相对路径显示,更简洁
                                rel_path = os.path.relpath(file_path, folder_path)
                                results.append({
                                    'file_path': file_path,
                                    'display_path': rel_path,
                                    'count': count,
                                    'absolute_path': os.path.abspath(file_path)
                                })
                    except Exception as e:
                        print(f"处理文件 {file_path} 时出错: {e}")

        # 按出现次数排序(降序)
        results.sort(key=lambda x: x['count'], reverse=True)
        return results

    def display_results(self, results, keyword, folder_path):
        """显示搜索结果"""
        if not results:
            self.status_var.set(f"未找到包含 '{keyword}' 的PDF文件")
            messagebox.showinfo("搜索完成", f"在文件夹 '{folder_path}' 中未找到包含 '{keyword}' 的PDF文件")
            return

        # 添加结果到树形视图
        for result in results:
            self.tree.insert("", tk.END, values=(result['display_path'], result['count']),
                             tags=(result['absolute_path'],))

        self.status_var.set(f"找到 {len(results)} 个包含 '{keyword}' 的PDF文件")
        messagebox.showinfo("搜索完成", f"找到 {len(results)} 个包含 '{keyword}' 的PDF文件")

    def open_selected_file(self, event=None):
        """打开选中的文件"""
        selected_items = self.tree.selection()
        if not selected_items:
            return

        item = selected_items[0]
        file_path = self.tree.item(item, "tags")[0]

        if not file_path or not os.path.isfile(file_path):
            messagebox.showerror("错误", "无法获取有效的文件路径")
            return

        try:
            # 尝试使用系统默认程序打开
            if sys.platform == 'win32':
                os.startfile(file_path)
            elif sys.platform == 'darwin':
                subprocess.call(('open', file_path))
            else:
                subprocess.call(('xdg-open', file_path))
            self.status_var.set(f"已尝试打开: {file_path}")
        except Exception as e:
            messagebox.showerror("打开文件错误", f"无法打开文件:\n{str(e)}")

    def copy_file_path(self):
        """复制选中文件的路径到剪贴板"""
        selected_items = self.tree.selection()
        if not selected_items:
            return

        item = selected_items[0]
        file_path = self.tree.item(item, "tags")[0]

        if file_path:
            self.root.clipboard_clear()
            self.root.clipboard_append(file_path)
            self.status_var.set(f"已复制文件路径到剪贴板: {file_path}")

    def show_context_menu(self, event):
        """显示右键菜单"""
        item = self.tree.identify_row(event.y)
        if item:
            self.tree.selection_set(item)
            self.context_menu.post(event.x_root, event.y_root)


if __name__ == "__main__":
    # 检查是否安装了必要的库
    try:
        import pdfplumber
    except ImportError:
        result = messagebox.askyesno("依赖缺失",
                                     "未找到pdfplumber库。需要安装才能提取PDF文本。\n\n"
                                     "是否现在安装? (需要网络连接)\n\n"
                                     "点击'是'将运行: pip install pdfplumber\n"
                                     "点击'否'将退出程序")

        if result:
            import subprocess

            try:
                subprocess.check_call([sys.executable, "-m", "pip", "install", "pdfplumber"])
                messagebox.showinfo("安装完成", "pdfplumber库已成功安装!")
            except Exception as e:
                messagebox.showerror("安装失败", f"无法安装pdfplumber库:\n{str(e)}\n\n请手动安装后重新运行程序")
                sys.exit(1)
        else:
            sys.exit(0)

    # 创建并运行GUI
    root = tk.Tk()
    app = PDFSearchApp(root)
    root.mainloop()

功能特点

1. 用户友好界面

  • 简洁直观的Tkinter界面
  • 支持Windows、macOS和Linux
  • 响应式布局,适应不同屏幕尺寸

2. 核心功能

  • 文件夹选择:通过系统原生对话框选择PDF文件夹
  • 关键词搜索:输入关键词后按回车或点击按钮开始搜索
  • 结果展示
    • 显示文件相对路径(简洁)
    • 显示关键词出现次数
    • 按出现次数降序排列
  • 文件操作
    • 双击文件或右键菜单可直接打开PDF
    • 右键菜单提供"复制文件路径"功能

3. 高级特性

  • 后台搜索:搜索在独立线程中进行,不会冻结UI
  • 进度显示:状态栏显示当前处理的文件和进度
  • 错误处理:友好的错误提示和异常处理
  • 自动依赖检查:如果没有安装pdfplumber,会提示安装

使用说明

1. 安装依赖

首次运行时,程序会自动检查并安装pdfplumber库(需要网络连接):

  • 如果没有安装,会弹出提示询问是否安装
  • 点击"是"将自动安装
  • 如果不想自动安装,可以手动运行:pip install pdfplumber

网站公告

今日签到

点亮在社区的每一天
去签到