本文 AI 產出,尚未審核

Python 檔案操作(File I/O)─ shutil、glob、pathlib 模組深入探討

簡介

在日常的 Python 專案中,檔案與目錄的操作是不可或缺的基礎工作。無論是資料前處理、日誌管理、或是自動化部署,都需要可靠且易讀的程式碼來搬移、搜尋或建立檔案路徑。Python 標準庫提供了三個功能強大的模組:shutilglobpathlib

  • shutil 專注於 高階的檔案與目錄搬移/複製,相當於作業系統的 cpmv 指令。
  • glob 則是 通配符搜尋的好幫手,讓你可以用類似 Unix shell 的模式一次找出多個符合條件的檔案。
  • pathlib 把路徑抽象為 物件導向Path,讓路徑操作變得直覺且跨平台。

掌握這三個模組,不僅能寫出更簡潔的程式碼,還能減少因平台差異或手動字串處理產生的錯誤。下面我們將逐一說明概念、提供實用範例,並討論常見陷阱與最佳實踐,最後以真實案例示範如何結合使用。


核心概念

1️⃣ shutil – 高階檔案與目錄操作

功能 常用函式 說明
複製檔案 shutil.copy(src, dst) 只複製內容,保留檔案權限。
複製檔案與權限 shutil.copy2(src, dst) cp -p,保留所有 metadata。
複製目錄 shutil.copytree(src, dst, dirs_exist_ok=False) 會遞迴複製子目錄與檔案。
移動/重新命名 shutil.move(src, dst) 自動判斷同磁碟或跨磁碟搬移。
刪除目錄 shutil.rmtree(path) 直接遞迴刪除整個目錄樹。
磁碟使用量 shutil.disk_usage(path) 回傳 (total, used, free)

範例 1:安全複製檔案(保留時間戳記)

import shutil
from pathlib import Path

src = Path('data/source.txt')
dst = Path('backup/source.txt')

# copy2 會保留檔案的最終修改時間與權限
shutil.copy2(src, dst)
print(f'已將 {src} 複製到 {dst}')

範例 2:遞迴複製整個資料夾,遇到同名目錄自動合併

import shutil
from pathlib import Path

src_dir = Path('project/templates')
dst_dir = Path('project_backup/templates')

# Python 3.8 之後加入 dirs_exist_ok 參數
shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True)
print('資料夾複製完成')

範例 3:搬移大量檔案到分散的子目錄

import shutil
from pathlib import Path

src_root = Path('logs')
dst_root = Path('archive')

for file in src_root.glob('*.log'):
    # 依檔名的日期部分建立子目錄,例如 2023-09-01.log → 2023-09-01/
    date_part = file.stem.split('-')[0]  # 假設檔名格式為 2023-09-01.log
    sub_dir = dst_root / date_part
    sub_dir.mkdir(parents=True, exist_ok=True)
    shutil.move(str(file), str(sub_dir / file.name))

print('日誌檔案已依日期搬移')

2️⃣ glob – 通配符搜尋

glob 允許使用 *?[seq][!seq] 等模式匹配檔名,返回符合條件的路徑列表。它支援 遞迴搜尋**)以及 相對/絕對路徑

範例 4:一次找出所有 Python 檔案(含子目錄)

import glob
from pathlib import Path

# ** 代表任意層級的子目錄,recursive 必須設為 True
py_files = glob.glob('**/*.py', recursive=True)

print('專案中的 .py 檔案:')
for p in py_files:
    print(p)

範例 5:使用 Path.glob 取得特定前綴的 CSV 檔案

from pathlib import Path

data_dir = Path('data')
csv_files = list(data_dir.glob('sales_202?.csv'))  # 匹配 2020~2029 年

for f in csv_files:
    print(f.name, f.stat().st_size, 'bytes')

範例 6:排除特定目錄的搜尋(結合 fnmatch

import glob
import fnmatch
from pathlib import Path

all_md = glob.glob('**/*.md', recursive=True)
# 排除 docs/ 目錄下的檔案
filtered = [p for p in all_md if not fnmatch.fnmatch(p, 'docs/*')]

print('除 docs 目錄外的 Markdown 檔案:')
for p in filtered:
    print(p)

3️⃣ pathlib – 物件導向的路徑操作

自 Python 3.4 起,pathlib 成為官方推薦的路徑處理方式。它把路徑視為 Path 物件,提供直覺的屬性與方法,且自動處理 Windows 與 POSIX 路徑差異。

常用屬性與方法

方法/屬性 說明
Path.cwd() 取得當前工作目錄的 Path 物件。
Path.home() 使用者的家目錄。
p / 'sub' 使用 / 運算子拼接子路徑(跨平台安全)。
p.iterdir() 產生目錄下的所有子項目(Path 物件)。
p.is_file() / p.is_dir() 判斷類型。
p.read_text()/p.write_text() 直接讀寫文字檔。
p.read_bytes()/p.write_bytes() 直接讀寫二進位檔。
p.rename(new_path) 重新命名或搬移。
p.with_name('new.txt') 替換檔名但保留路徑。
p.with_suffix('.csv') 替換副檔名。

範例 7:使用 Path 產生日期目錄並寫入日誌

from pathlib import Path
from datetime import datetime

log_root = Path('logs')
today = datetime.now().strftime('%Y-%m-%d')
log_dir = log_root / today
log_dir.mkdir(parents=True, exist_ok=True)

log_file = log_dir / 'run.log'
log_file.write_text('程式執行開始\n', encoding='utf-8')
print(f'已建立日誌檔案:{log_file}')

範例 8:遍歷目錄,統計檔案大小,並以表格方式列印

from pathlib import Path

def folder_size(path: Path) -> int:
    return sum(f.stat().st_size for f in path.rglob('*') if f.is_file())

root = Path('project')
for sub in root.iterdir():
    if sub.is_dir():
        size_mb = folder_size(sub) / (1024 * 1024)
        print(f'{sub.name:<20} {size_mb:>6.2f} MB')

範例 9:結合 shutilPath 完成安全備份

import shutil
from pathlib import Path
from datetime import datetime

def backup(src: Path, dst_root: Path):
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    dst = dst_root / f'{src.name}_bak_{timestamp}'
    shutil.copytree(src, dst)
    print(f'已備份 {src} 至 {dst}')

backup(Path('config'), Path('backup'))

常見陷阱與最佳實踐

陷阱 可能的問題 解決方式
使用字串拼接路徑 在 Windows \ 與 POSIX / 混用,導致 FileNotFoundError 使用 pathlib.Pathos.path.join,讓 Python 自動處理分隔符。
shutil.copytree 目錄已存在 預設會拋出 FileExistsError 在 Python 3.8+ 使用 dirs_exist_ok=True,或先手動刪除目標目錄。
glob 非遞迴時忘記 recursive=True 只能搜尋當前層級,找不到子目錄檔案。 設定 recursive=True 並使用 ** 通配符。
忽略檔案權限 複製或搬移後檔案權限不正確,可能導致執行失敗。 使用 shutil.copy2 以保留 metadata;或手動設定 os.chmod
大量小檔案搬移效能低 每次搬移都會觸發磁碟 I/O,耗時。 考慮先壓縮成 zip (shutil.make_archive) 再搬移,或使用多執行緒/進程批次搬移。
未處理例外 檔案不存在或權限不足會中斷整個腳本。 使用 try/except 包住 I/O 操作,並記錄錯誤日誌。

最佳實踐

  1. 盡量使用 pathlib,讓程式碼在不同作業系統上保持一致。
  2. 在搬移/複製前先檢查目標是否已存在,避免不小心覆寫重要檔案。
  3. 使用 with 方式開啟檔案(或 Path.read_text/write_text),確保檔案自動關閉。
  4. 在大量 I/O 任務中加入進度條(如 tqdm),提升使用者體驗。
  5. 將檔案操作封裝成函式或類別,便於重複使用與單元測試。

實際應用場景

場景 1:每日自動備份資料庫匯出檔

每天晚上 02:00,系統會把 PostgreSQL 匯出的 .sql 檔案搬到備份伺服器,並依日期分目錄保存 30 天。

import shutil
from pathlib import Path
from datetime import datetime, timedelta

EXPORT_DIR = Path('/var/backups/db')
REMOTE_DIR = Path('/mnt/backup_server/db')
RETENTION_DAYS = 30

def daily_backup():
    today = datetime.now().strftime('%Y-%m-%d')
    src_file = EXPORT_DIR / f'db_{today}.sql'
    dst_dir = REMOTE_DIR / today
    dst_dir.mkdir(parents=True, exist_ok=True)
    shutil.copy2(src_file, dst_dir / src_file.name)
    print(f'已備份 {src_file} 到 {dst_dir}')

    # 清理過期備份
    cutoff = datetime.now() - timedelta(days=RETENTION_DAYS)
    for old_dir in REMOTE_DIR.iterdir():
        if old_dir.is_dir() and datetime.strptime(old_dir.name, '%Y-%m-%d') < cutoff:
            shutil.rmtree(old_dir)
            print(f'已刪除過期備份 {old_dir}')

daily_backup()

場景 2:批次處理影像檔案,將所有 .png 轉成 .jpg 並搬移至 processed/

需要遍歷多層目錄,找到所有 PNG,使用 Pillow 轉檔,最後保留原始結構。

from pathlib import Path
from PIL import Image
import shutil

SRC_ROOT = Path('raw_images')
DST_ROOT = Path('processed_images')

for png_path in SRC_ROOT.rglob('*.png'):
    rel_path = png_path.relative_to(SRC_ROOT).with_suffix('.jpg')
    dst_path = DST_ROOT / rel_path
    dst_path.parent.mkdir(parents=True, exist_ok=True)

    # 轉檔
    with Image.open(png_path) as im:
        rgb_im = im.convert('RGB')
        rgb_im.save(dst_path, quality=85)

    # 可選:保留原檔或刪除
    # png_path.unlink()
    print(f'轉換 {png_path} → {dst_path}')

場景 3:建立專案的檔案清單(Markdown)供文件生成器使用

使用 glob 收集所有 .md 檔案,排除 node_modules/tests/,產生目錄清單。

import glob
import fnmatch
from pathlib import Path

exclude_patterns = ['node_modules/*', 'tests/*']
all_md = glob.glob('**/*.md', recursive=True)

filtered = [p for p in all_md if not any(fnmatch.fnmatch(p, pat) for pat in exclude_patterns)]

output = Path('DOCS_INDEX.md')
with output.open('w', encoding='utf-8') as f:
    f.write('# 專案文件索引\n\n')
    for p in sorted(filtered):
        f.write(f'- [{Path(p).name}]({p})\n')

print(f'已產生文件索引 {output}')

總結

  • shutil 為檔案與目錄的 高階搬移/複製 提供簡潔 API,配合 Path 可寫出安全且跨平台的備份腳本。
  • glob 讓你以 通配符一次搜尋多個檔案,特別適合批次處理與自動化任務。
  • pathlib 把路徑抽象為物件,使 路徑拼接、檔案屬性讀取、讀寫操作 都變得直觀且不易出錯。

在實務開發中,將三者結合,先用 glob/Path.rglob 找出目標檔案,再用 shutil 完成搬移或備份,最後以 Path 產生或更新相關記錄,能大幅提升程式碼的可讀性與維護性。

實務建議:在每個 I/O 任務前加入日誌、例外捕捉與測試,確保在不同作業系統或權限環境下仍能穩定運行。
持續學習:Python 標準庫不斷演進,未來的 pathlib 可能會加入更多便利方法,保持關注官方文件是最好的投資。

祝你在 Python 檔案操作的旅程中,寫出更乾淨、更可靠的程式碼! 🚀