下面是一个基于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元数据查看器具有以下功能:
主要功能:
界面特点:
附加功能:
D:20230101120000Z格式)这个查看器提供了一个简单而有效的方式来查看PDF文件的元数据信息,适合用于文档管理、元数据分析等场景。