欢迎光临汕头市华金智网
详情描述
基于Python开发一个PDF文件元数据查看器
PDF文件元数据查看器

下面是一个基于Python开发的PDF文件元数据查看器,使用PyPDF2库来提取PDF文件的元数据信息,并提供一个简单的GUI界面。

#!/usr/bin/env python3
"""
PDF文件元数据查看器
支持查看PDF文件的标题、作者、主题、关键词、创建者、创建日期等元数据信息
"""

import os
import sys
import json
from datetime import datetime
from pathlib import Path

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import PyPDF2
from PIL import Image, ImageTk


class PDFMetadataViewer:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("PDF元数据查看器")
        self.root.geometry("900x700")
        self.root.minsize(800, 600)

        # 设置窗口图标
        try:
            self.root.iconbitmap(self.get_icon_path())
        except:
            pass  # 如果找不到图标文件,忽略错误

        self.current_file = None
        self.setup_ui()

    def get_icon_path(self):
        """获取图标文件路径"""
        # 尝试几种可能的图标路径
        possible_paths = [
            "pdf_icon.ico",
            os.path.join(os.path.dirname(__file__), "pdf_icon.ico"),
            os.path.join(os.path.dirname(__file__), "assets", "pdf_icon.ico"),
        ]

        for path in possible_paths:
            if os.path.exists(path):
                return path
        return None

    def setup_ui(self):
        """设置用户界面"""
        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # 配置网格权重
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(3, weight=1)

        # 标题
        title_label = ttk.Label(
            main_frame, 
            text="PDF文件元数据查看器", 
            font=("Arial", 16, "bold")
        )
        title_label.grid(row=0, column=0, columnspan=3, pady=(0, 15))

        # 文件选择区域
        ttk.Label(main_frame, text="PDF文件:").grid(row=1, column=0, sticky=tk.W, pady=5)

        self.file_path_var = tk.StringVar()
        file_entry = ttk.Entry(main_frame, textvariable=self.file_path_var, width=50)
        file_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(5, 5), pady=5)

        browse_btn = ttk.Button(main_frame, text="浏览...", command=self.browse_file)
        browse_btn.grid(row=1, column=2, padx=(0, 5), pady=5)

        # 按钮区域
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=2, column=0, columnspan=3, pady=(10, 15))

        load_btn = ttk.Button(button_frame, text="加载元数据", command=self.load_metadata)
        load_btn.pack(side=tk.LEFT, padx=(0, 10))

        clear_btn = ttk.Button(button_frame, text="清空", command=self.clear_display)
        clear_btn.pack(side=tk.LEFT, padx=(0, 10))

        export_btn = ttk.Button(button_frame, text="导出为JSON", command=self.export_json)
        export_btn.pack(side=tk.LEFT)

        # 创建笔记本(标签页)控件
        self.notebook = ttk.Notebook(main_frame)
        self.notebook.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))

        # 元数据标签页
        self.metadata_frame = ttk.Frame(self.notebook, padding="5")
        self.notebook.add(self.metadata_frame, text="元数据")

        # 创建带滚动条的文本区域用于显示元数据
        self.metadata_text = tk.Text(self.metadata_frame, wrap=tk.WORD, font=("Consolas", 10))
        metadata_scrollbar = ttk.Scrollbar(self.metadata_frame, orient=tk.VERTICAL, command=self.metadata_text.yview)
        self.metadata_text.configure(yscrollcommand=metadata_scrollbar.set)

        self.metadata_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        metadata_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))

        self.metadata_frame.columnconfigure(0, weight=1)
        self.metadata_frame.rowconfigure(0, weight=1)

        # 详细信息标签页
        self.details_frame = ttk.Frame(self.notebook, padding="5")
        self.notebook.add(self.details_frame, text="详细信息")

        # 创建树状视图显示详细信息
        columns = ("属性", "值")
        self.details_tree = ttk.Treeview(self.details_frame, columns=columns, show="headings", height=20)

        # 设置列标题
        for col in columns:
            self.details_tree.heading(col, text=col)
            self.details_tree.column(col, width=300)

        # 添加滚动条
        details_scrollbar = ttk.Scrollbar(self.details_frame, orient=tk.VERTICAL, command=self.details_tree.yview)
        self.details_tree.configure(yscrollcommand=details_scrollbar.set)

        self.details_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        details_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))

        self.details_frame.columnconfigure(0, weight=1)
        self.details_frame.rowconfigure(0, weight=1)

        # 文件信息标签页
        self.file_info_frame = ttk.Frame(self.notebook, padding="5")
        self.notebook.add(self.file_info_frame, text="文件信息")

        # 文件信息显示区域
        self.file_info_text = tk.Text(self.file_info_frame, wrap=tk.WORD, font=("Consolas", 10))
        file_info_scrollbar = ttk.Scrollbar(self.file_info_frame, orient=tk.VERTICAL, command=self.file_info_text.yview)
        self.file_info_text.configure(yscrollcommand=file_info_scrollbar.set)

        self.file_info_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        file_info_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))

        self.file_info_frame.columnconfigure(0, weight=1)
        self.file_info_frame.rowconfigure(0, weight=1)

        # 状态栏
        self.status_var = tk.StringVar()
        self.status_var.set("就绪 - 请选择PDF文件")
        status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
        status_bar.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(10, 0))

    def browse_file(self):
        """浏览并选择PDF文件"""
        file_path = filedialog.askopenfilename(
            title="选择PDF文件",
            filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")]
        )

        if file_path:
            self.file_path_var.set(file_path)
            self.current_file = file_path
            self.status_var.set(f"已选择文件: {os.path.basename(file_path)}")

    def load_metadata(self):
        """加载并显示PDF文件的元数据"""
        file_path = self.file_path_var.get()

        if not file_path:
            messagebox.showwarning("警告", "请先选择PDF文件!")
            return

        if not os.path.exists(file_path):
            messagebox.showerror("错误", "文件不存在!")
            return

        try:
            with open(file_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)

                # 获取元数据
                metadata = pdf_reader.metadata

                # 显示原始元数据
                self.metadata_text.delete(1.0, tk.END)

                if metadata:
                    self.metadata_text.insert(tk.END, "=== PDF文档元数据 ===\n\n")

                    # 将元数据转换为字典
                    metadata_dict = {}
                    for key in metadata:
                        # 处理可能的二进制数据
                        value = metadata[key]
                        if isinstance(value, bytes):
                            try:
                                value = value.decode('utf-8', errors='ignore')
                            except:
                                value = str(value)
                        metadata_dict[key] = value

                        # 在文本区域显示
                        self.metadata_text.insert(tk.END, f"{key}: {value}\n")
                else:
                    self.metadata_text.insert(tk.END, "该PDF文件没有元数据信息。\n")

                # 在树状视图中显示详细信息
                self.details_tree.delete(*self.details_tree.get_children())

                if metadata:
                    for key in metadata:
                        value = metadata[key]
                        if isinstance(value, bytes):
                            try:
                                value = value.decode('utf-8', errors='ignore')
                            except:
                                value = str(value)
                        self.details_tree.insert("", tk.END, values=(key, value))

                # 显示文件信息
                self.file_info_text.delete(1.0, tk.END)

                file_info = self.get_file_info(file_path, pdf_reader)
                self.file_info_text.insert(tk.END, file_info)

                self.status_var.set(f"成功加载: {os.path.basename(file_path)}")

        except Exception as e:
            messagebox.showerror("错误", f"读取PDF文件时出错:\n{str(e)}")
            self.status_var.set("加载文件时出错")

    def get_file_info(self, file_path, pdf_reader):
        """获取文件详细信息"""
        file_stat = os.stat(file_path)
        file_size = file_stat.st_size

        info = "=== 文件信息 ===\n\n"
        info += f"文件路径: {file_path}\n"
        info += f"文件名: {os.path.basename(file_path)}\n"
        info += f"文件大小: {self.format_file_size(file_size)}\n"
        info += f"创建时间: {datetime.fromtimestamp(file_stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S')}\n"
        info += f"修改时间: {datetime.fromtimestamp(file_stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')}\n\n"

        info += "=== PDF文档信息 ===\n\n"
        info += f"页数: {len(pdf_reader.pages)}\n"
        info += f"是否加密: {'是' if pdf_reader.is_encrypted else '否'}\n"

        # 尝试获取更多信息
        try:
            if hasattr(pdf_reader, 'documentInfo'):
                info += f"文档信息对象: 已加载\n"
        except:
            pass

        return info

    def format_file_size(self, size_bytes):
        """格式化文件大小"""
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size_bytes < 1024.0:
                return f"{size_bytes:.2f} {unit}"
            size_bytes /= 1024.0
        return f"{size_bytes:.2f} TB"

    def clear_display(self):
        """清空显示内容"""
        self.metadata_text.delete(1.0, tk.END)
        self.details_tree.delete(*self.details_tree.get_children())
        self.file_info_text.delete(1.0, tk.END)
        self.status_var.set("显示内容已清空")

    def export_json(self):
        """将元数据导出为JSON文件"""
        file_path = self.file_path_var.get()

        if not file_path or not os.path.exists(file_path):
            messagebox.showwarning("警告", "请先选择有效的PDF文件!")
            return

        try:
            with open(file_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                metadata = pdf_reader.metadata

                if not metadata:
                    messagebox.showinfo("提示", "该PDF文件没有元数据可导出。")
                    return

                # 转换为可序列化的字典
                metadata_dict = {}
                for key in metadata:
                    value = metadata[key]
                    if isinstance(value, bytes):
                        try:
                            value = value.decode('utf-8', errors='ignore')
                        except:
                            value = str(value)
                    metadata_dict[key] = value

                # 添加文件信息
                file_stat = os.stat(file_path)
                metadata_dict['_file_info'] = {
                    'filename': os.path.basename(file_path),
                    'file_size': file_stat.st_size,
                    'page_count': len(pdf_reader.pages),
                    'is_encrypted': pdf_reader.is_encrypted,
                    'export_timestamp': datetime.now().isoformat()
                }

                # 选择保存位置
                save_path = filedialog.asksaveasfilename(
                    title="保存JSON文件",
                    defaultextension=".json",
                    filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]
                )

                if save_path:
                    with open(save_path, 'w', encoding='utf-8') as json_file:
                        json.dump(metadata_dict, json_file, indent=2, ensure_ascii=False)

                    messagebox.showinfo("成功", f"元数据已导出到:\n{save_path}")
                    self.status_var.set(f"元数据已导出: {os.path.basename(save_path)}")

        except Exception as e:
            messagebox.showerror("错误", f"导出JSON时出错:\n{str(e)}")

    def run(self):
        """运行应用程序"""
        self.root.mainloop()


def main():
    """主函数"""
    # 检查必要的库
    try:
        import PyPDF2
    except ImportError:
        print("错误: 需要安装PyPDF2库")
        print("请运行: pip install PyPDF2")
        return

    try:
        from PIL import Image
    except ImportError:
        print("警告: Pillow库未安装,某些功能可能受限")
        print("可以运行: pip install Pillow")

    # 创建并运行应用程序
    app = PDFMetadataViewer()
    app.run()


if __name__ == "__main__":
    main()

安装依赖

在运行此程序之前,需要安装必要的Python库:

pip install PyPDF2 pillow

功能说明

这个PDF元数据查看器具有以下功能:

主要功能

  • 浏览和选择PDF文件
  • 查看PDF文件的元数据(标题、作者、主题、关键词等)
  • 显示文件详细信息(大小、创建时间、修改时间等)
  • 显示PDF文档信息(页数、是否加密等)

界面特点

  • 标签页界面,分为"元数据"、"详细信息"和"文件信息"三个标签
  • 元数据以原始格式显示
  • 详细信息以表格形式显示
  • 文件信息显示文件属性和PDF文档属性

附加功能

  • 将元数据导出为JSON格式文件
  • 清空显示内容
  • 状态栏显示操作状态

使用说明

运行程序后,点击"浏览..."按钮选择PDF文件 点击"加载元数据"按钮查看文件元数据 使用不同的标签页查看不同格式的信息 可以使用"导出为JSON"将元数据保存到文件 使用"清空"按钮清除当前显示的内容

注意事项

  • 某些PDF文件可能不包含元数据信息
  • 元数据中的日期可能需要特殊解析(如D:20230101120000Z格式)
  • 如果遇到加密的PDF文件,可能无法读取其元数据

这个查看器提供了一个简单而有效的方式来查看PDF文件的元数据信息,适合用于文档管理、元数据分析等场景。