前言
在處理大量 PDF 文件時,常會遇到檔案內嵌的 Metadata Title 與實際檔名不符的問題,這會影響文件管理與搜尋效率。本文示範如何利用純 Python 的 pypdf 套件,自動走訪指定資料夾(含子資料夾)內所有 PDF,將其 Metadata 中的 Title 欄位批量更新為檔名(不含副檔名),同時保留其他 Metadata,並備份原始檔案。適合需要批量整理 PDF 檔案屬性的工程師與自學者。
使用 pypdf 讀寫 PDF Metadata
pypdf 是一個純 Python 的 PDF 操作套件,能讀取與修改 PDF 的內容與屬性。本文重點在修改 PDF 的 Metadata,特別是 /Title 欄位。
讀取與複製 PDF 頁面
為了保留 PDF 內容,先使用 PdfReader 讀取原始 PDF,再用 PdfWriter 複製所有頁面:
reader = PdfReader(str(src_for_edit))
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
這樣做是為了在不破壞內容的前提下,更新 Metadata。
修改 Metadata 中的 Title
原始 Metadata 透過 reader.metadata 取得,是一個字典。為避免遺失其他欄位,先複製除了 /Title 以外的欄位,並將值轉成字串:
orig_meta = reader.metadata or {}
new_meta = {}
for key, value in orig_meta.items():
if key == "/Title":
continue
if value is None:
continue
new_meta[key] = str(value)
接著設定新的 Title 為檔名(不含副檔名):
new_meta["/Title"] = pdf_path.stem
writer.add_metadata(new_meta)
這樣可以確保只改 Title,其他 Metadata 保持不變。
備份原始檔案與覆寫
為防止資料遺失,程式會先建立備份檔案(檔名加上 .bak),如果備份已存在,則直接使用原檔:
backup_path = pdf_path.with_suffix(pdf_path.suffix + ".bak")
if not backup_path.exists():
shutil.copy2(pdf_path, backup_path)
src_for_edit = backup_path
else:
src_for_edit = pdf_path
最後將修改後的 PDF 直接覆寫原始檔案。
遞迴走訪資料夾
利用 pathlib 的 rglob 方法,可以遞迴搜尋指定資料夾底下所有 .pdf 檔案,並過濾掉備份檔:
for path in root.rglob("*.pdf"):
if path.name.endswith(".pdf.bak"):
continue
fix_pdf_title(path)
實際應用與延伸
此工具適合用於資料整理、文件管理系統,或是需要統一 PDF Metadata 的場景。未來可擴充功能,例如修改其他 Metadata 欄位、支援多種檔案格式,或加入多線程提升處理效率。
常見問題與注意事項
- 確保執行環境有安裝 pypdf 套件。
- 備份機制避免資料遺失,但仍建議先手動備份重要檔案。
- 修改 Metadata 不會改變 PDF 內容,但部分 PDF 可能因加密或損毀導致處理失敗。
完整程式碼
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批量修正 PDF Metadata Title(使用 pypdf,純 Python,不需編譯)
功能:
- 走訪指定資料夾(含子資料夾)所有 .pdf
- 把 PDF 內嵌的 /Title 改成「檔名(不含 .pdf)」
- 原檔先備份成 xxx.pdf.bak
"""
import sys
from pathlib import Path
import shutil
from pypdf import PdfReader, PdfWriter
def fix_pdf_title(pdf_path: Path):
"""把單一 PDF 的 Title 改成檔名(不含副檔名)"""
try:
print(f"處理:{pdf_path}")
# 建立備份:xxx.pdf.bak(如果已存在就略過)
backup_path = pdf_path.with_suffix(pdf_path.suffix + ".bak")
if not backup_path.exists():
shutil.copy2(pdf_path, backup_path)
src_for_edit = backup_path # 從備份讀
else:
src_for_edit = pdf_path # 已備份過就直接用目前檔案
# 讀 PDF
reader = PdfReader(str(src_for_edit))
writer = PdfWriter()
# 複製所有頁
for page in reader.pages:
writer.add_page(page)
# 讀取原有 metadata
orig_meta = reader.metadata or {}
new_meta = {}
# 保留其他欄位,只改 /Title
for key, value in orig_meta.items():
if key == "/Title":
continue
if value is None:
continue
new_meta[key] = str(value)
# 設定新的 Title = 檔名(不含 .pdf)
new_title = pdf_path.stem
new_meta["/Title"] = new_title
writer.add_metadata(new_meta)
# 寫回原本檔名(覆蓋)
with open(pdf_path, "wb") as f_out:
writer.write(f_out)
print(f" ✅ 已更新 Title 為:{new_title}\n")
except Exception as e:
print(f" ❌ 發生錯誤:{e}\n")
def scan_folder(root: Path):
"""走訪資料夾底下所有 .pdf"""
for path in root.rglob("*.pdf"):
# 跳過備份檔
if path.name.endswith(".pdf.bak"):
continue
fix_pdf_title(path)
def main():
if len(sys.argv) >= 2:
target_dir = Path(sys.argv[1]).expanduser().resolve()
else:
# 沒帶參數就用目前工作目錄
target_dir = Path.cwd()
if not target_dir.exists() or not target_dir.is_dir():
print(f"找不到資料夾:{target_dir}")
sys.exit(1)
print(f"開始處理資料夾:{target_dir}\n")
scan_folder(target_dir)
print("全部處理完成 🎉")
if __name__ == "__main__":
main()