Python 檔案操作(File I/O)─ shutil、glob、pathlib 模組深入探討
簡介
在日常的 Python 專案中,檔案與目錄的操作是不可或缺的基礎工作。無論是資料前處理、日誌管理、或是自動化部署,都需要可靠且易讀的程式碼來搬移、搜尋或建立檔案路徑。Python 標準庫提供了三個功能強大的模組:shutil、glob、pathlib。
shutil專注於 高階的檔案與目錄搬移/複製,相當於作業系統的cp、mv指令。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:結合 shutil 與 Path 完成安全備份
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.Path 或 os.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 操作,並記錄錯誤日誌。 |
最佳實踐
- 盡量使用
pathlib,讓程式碼在不同作業系統上保持一致。 - 在搬移/複製前先檢查目標是否已存在,避免不小心覆寫重要檔案。
- 使用
with方式開啟檔案(或Path.read_text/write_text),確保檔案自動關閉。 - 在大量 I/O 任務中加入進度條(如
tqdm),提升使用者體驗。 - 將檔案操作封裝成函式或類別,便於重複使用與單元測試。
實際應用場景
場景 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 檔案操作的旅程中,寫出更乾淨、更可靠的程式碼! 🚀