Shutil 標(biāo)準(zhǔn)庫(kù): Python 文件操作的萬(wàn)用刀
今天來(lái)聊一個(gè)被低估的 Python 標(biāo)準(zhǔn)庫(kù) - shutil 。工作中我們用 Python (尤其是寫(xiě)一些短小輕快的腳本)雖然經(jīng)常和文件打交道,卻很少用到 shutil 。但實(shí)際上, shutil 提供了比 os 模塊更高級(jí)的文件操作接口,能讓我們寫(xiě)出更 Pythonic 的代碼。
從一個(gè)真實(shí)場(chǎng)景說(shuō)起
最近在整理項(xiàng)目代碼時(shí),需要將散落在各處的配置文件歸類(lèi)到統(tǒng)一目錄。按以往的習(xí)慣,我會(huì)這樣寫(xiě):
import os
# 創(chuàng)建目標(biāo)目錄
if not os.path.exists("configs"):
os.makedirs("configs")
# 移動(dòng)文件
for root, dirs, files in os.walk("."):
for file in files:
if file.endswith(".conf"):
src = os.path.join(root, file)
dst = os.path.join("configs", file)
os.rename(src, dst)
這段代碼能完成任務(wù),但存在幾個(gè)問(wèn)題:
- 如果目標(biāo)路徑已存在同名文件會(huì)報(bào)錯(cuò)
- 不支持跨設(shè)備移動(dòng)
- 沒(méi)有保留文件的元數(shù)據(jù)(權(quán)限、時(shí)間戳等)
用 shutil 可以?xún)?yōu)雅地解決這些問(wèn)題:
import shutil
import os
os.makedirs("configs", exist_ok=True)
for root, dirs, files in os.walk("."):
for file in files:
if file.endswith(".conf"):
src = os.path.join(root, file)
dst = os.path.join("configs", file)
shutil.move(src, dst)
看起來(lái)差別不大,但 shutil.move() 會(huì):
當(dāng)目標(biāo)路徑已存在同名文件時(shí), shutil.move() 的行為取決于操作系統(tǒng)
- Windows:如果目標(biāo)文件存在且正在使用,會(huì)拋出 PermissionError ;否則會(huì)靜默覆蓋目標(biāo)文件
- Unix/Linux:會(huì)遵循操作系統(tǒng)的規(guī)則。如果用戶(hù)有權(quán)限,會(huì)覆蓋目標(biāo)文件;否則拋出 PermissionError
支持跨設(shè)備移動(dòng)
"跨設(shè)備"指的是在不同的文件系統(tǒng)或存儲(chǔ)設(shè)備之間移動(dòng)文件,比如從 C 盤(pán)移動(dòng)到 D 盤(pán)、從本地磁盤(pán)移動(dòng)到網(wǎng)絡(luò)驅(qū)動(dòng)器、從固態(tài)硬盤(pán)移動(dòng)到 U 盤(pán)。
shutil.move() 會(huì)首先嘗試使用 os.rename() ,如果失敗且錯(cuò)誤是跨設(shè)備錯(cuò)誤 errno.EXDEV ,則復(fù)制文件到目標(biāo)位置,驗(yàn)證復(fù)制成功,最后刪除源文件。
保留源文件的所有元數(shù)據(jù)。
shutil 常用操作詳解
1. 復(fù)制文件和目錄
# 復(fù)制文件
shutil.copy("source.txt", "dest.txt") # 復(fù)制文件內(nèi)容
shutil.copy2("source.txt", "dest.txt") # 復(fù)制文件內(nèi)容和元數(shù)據(jù)
# 復(fù)制目錄
shutil.copytree("src_dir", "dst_dir") # 遞歸復(fù)制整個(gè)目錄樹(shù)
copy2() 比 copy() 多了preserving metadata 的功能,在需要保留文件屬性時(shí)很有用。
2. 刪除目錄
# 刪除目錄樹(shù)
shutil.rmtree("dir_to_remove") # 遞歸刪除目錄及其內(nèi)容
比 os.rmdir() 強(qiáng)大,后者只能刪除空目錄。
3. 磁盤(pán)使用統(tǒng)計(jì)
total, used, free = shutil.disk_usage(".")
print(f"總空間: {total // (2**30)} GiB")
print(f"已使用: {used // (2**30)} GiB")
print(f"可用: {free // (2**30)} GiB")
直觀地獲取磁盤(pán)使用情況,免去了手動(dòng)計(jì)算的麻煩。
4. 文件打包與壓縮
# 創(chuàng)建壓縮包
shutil.make_archive("backup", "zip", "source_dir") # 支持zip、tar等格式
# 解壓縮
shutil.unpack_archive("backup.zip", "extract_dir")
實(shí)用腳本示例
1. 項(xiàng)目備份工具
import shutil
from datetime import datetime
import os
def backup_project(project_path, backup_dir="backups"):
# 創(chuàng)建以時(shí)間戳命名的備份文件
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"backup_{timestamp}"
# 確保備份目錄存在
os.makedirs(backup_dir, exist_ok=True)
# 創(chuàng)建壓縮包
archive_path = shutil.make_archive(
os.path.join(backup_dir, backup_name),
"zip",
project_path
)
print(f"備份完成: {archive_path}")
2. 大文件搬運(yùn)工具
import shutil
import os
from pathlib import Path
def move_large_files(src_dir, dst_dir, min_size_mb=100):
"""移動(dòng)大于指定大小的文件到目標(biāo)目錄"""
dst_path = Path(dst_dir)
dst_path.mkdir(exist_ok=True)
for root, _, files in os.walk(src_dir):
for file in files:
file_path = Path(root) / file
if file_path.stat().st_size > min_size_mb * 1024 * 1024:
try:
shutil.move(str(file_path), dst_path / file)
print(f"已移動(dòng): {file}")
except Exception as e:
print(f"移動(dòng)失敗 {file}: {e}")
3. 智能文件分類(lèi)器
import shutil
from pathlib import Path
import mimetypes
def organize_files(directory):
"""根據(jù)文件類(lèi)型自動(dòng)分類(lèi)文件"""
directory = Path(directory)
# 遍歷所有文件
for file_path in directory.rglob("*"):
if file_path.is_file():
# 獲取文件類(lèi)型
mime_type, _ = mimetypes.guess_type(str(file_path))
if mime_type:
category = mime_type.split("/")[0]
# 創(chuàng)建分類(lèi)目錄
dest_dir = directory / category
dest_dir.mkdir(exist_ok=True)
# 移動(dòng)文件
try:
shutil.move(str(file_path), str(dest_dir / file_path.name))
except Exception as e:
print(f"處理{file_path}時(shí)出錯(cuò): {e}")
性能提示
對(duì)于大文件操作,shutil 提供了 copyfileobj() 方法,支持設(shè)置緩沖區(qū)大?。?/p>
with open("source.dat", "rb") as fsrc:
with open("dest.dat", "wb") as fdst:
shutil.copyfileobj(fsrc, fdst, length=1024*1024) # 1MB buffer
copytree() 支持多進(jìn)程并行復(fù)制:
from multiprocessing import Pool
def copy_with_progress(src, dst):
shutil.copy2(src, dst)
return dst
with Pool(processes=4) as pool:
shutil.copytree("src_dir", "dst_dir", copy_function=pool.map)
文件屬性操作
1. 權(quán)限和所有權(quán)
import shutil
import os
def mirror_permissions(src, dst):
# 復(fù)制權(quán)限位
shutil.copymode(src, dst)
# 復(fù)制所有權(quán)(需要root權(quán)限)
try:
shutil.chown(dst,
user=os.stat(src).st_uid,
group=os.stat(src).st_gid)
except PermissionError:
print("需要管理員權(quán)限來(lái)修改所有權(quán)")
2. 元數(shù)據(jù)復(fù)制
import shutil
import os
from datetime import datetime
def show_metadata(path):
stat = os.stat(path)
print(f"訪(fǎng)問(wèn)時(shí)間: {datetime.fromtimestamp(stat.st_atime)}")
print(f"修改時(shí)間: {datetime.fromtimestamp(stat.st_mtime)}")
print(f"創(chuàng)建時(shí)間: {datetime.fromtimestamp(stat.st_ctime)}")
print(f"權(quán)限: {oct(stat.st_mode)[-3:]}")
print(f"大小: {stat.st_size} bytes")
# 復(fù)制文件并保留所有元數(shù)據(jù)
src = "source.txt"
dst = "dest.txt"
shutil.copy2(src, dst)
print("源文件元數(shù)據(jù):")
show_metadata(src)
print("\n目標(biāo)文件元數(shù)據(jù):")
show_metadata(dst)
總結(jié)
shutil 是一個(gè)設(shè)計(jì)優(yōu)雅的文件操作庫(kù):
- 提供了比 os 模塊更高級(jí)的接口
- 自動(dòng)處理各種邊界情況
- 保持了 Python "batteries included" 的理念
下次遇到文件操作需求,不妨先看看 shutil 是否已經(jīng)提供了合適的工具。畢竟,"不要重復(fù)發(fā)明輪子"也是 Python 的哲學(xué)之一。