前言
Python 的文件操作能力是其”电池包含”理念的典型体现。无论是读取配置文件、处理日志文件,还是批量操作文件系统,Python 都提供了简洁而强大的 API。
本文将系统讲解:
- 路径处理的新旧两代方案对比
- 文件读写的标准操作与进阶技巧
- 安全实践与异常处理
- 常见场景的代码模板
路径处理:pathlib vs os.path
技术选型对比
| 特性 | pathlib.Path | os.path |
|---|---|---|
| 面向对象 | ✅ 是 | ❌ 否 |
| 链式调用 | ✅ 支持 | ❌ 不支持 |
| 跨平台 | ✅ 自动适配 | ✅ 需手动处理 |
| 可读性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Python 版本 | 3.4+ | 所有版本 |
结论:优先使用 pathlib,它是 Python 3.4+ 的现代标准。
基础路径操作
from pathlib import Path
# 1. 创建路径对象
p = Path("/home/user/documents/report.txt")
# 2. 路径拼接(跨平台安全的 / 运算符)
docs = Path("/home/user") / "documents" / "report.txt"
# 等价于: os.path.join("/home/user", "documents", "report.txt")
# 3. 获取路径组成部分
print(p.name) # "report.txt" — 文件名
print(p.stem) # "report" — 不含扩展名
print(p.suffix) # ".txt" — 扩展名
print(p.parent) # Path("/home/user/documents") — 父目录
print(p.parent.parent) # Path("/home/user")
# 4. 判断路径类型
print(p.is_file()) # True — 是否为文件
print(p.is_dir()) # False — 是否为目录
print(p.exists()) # True — 是否存在
路径信息提取
from pathlib import Path
p = Path("/home/user/documents/images/photo.jpg")
# 拆解路径
print(p.parts) # ('/', 'home', 'user', 'documents', 'images', 'photo.jpg')
# 获取绝对路径
print(p.resolve()) # PosixPath('/home/user/documents/images/photo.jpg')
# 获取文件大小
print(p.stat().st_size) # 字节数
# 获取修改时间
from datetime import datetime
mtime = datetime.fromtimestamp(p.stat().st_mtime)
print(mtime) # 2026-05-15 10:30:45
目录遍历
from pathlib import Path
base = Path("/home/user/projects")
# 列出目录下所有文件(含子目录)
for item in base.rglob("*"): # rglob 递归搜索
print(item)
# 仅列出特定扩展名的文件
for py_file in base.rglob("*.py"):
print(py_file)
# 排除特定目录
for item in base.rglob("*"):
if ".git" not in item.parts:
print(item)
# 列出直接子项(不递归)
for item in base.iterdir():
print(item.name)
文件基础操作
读取文件
from pathlib import Path
# 方法1:read_text() — 一次性读取全部文本
content = Path("config.ini").read_text(encoding="utf-8")
# 方法2:read_bytes() — 读取二进制
data = Path("image.png").read_bytes()
# 方法3:逐行读取(大文件推荐)
for line in Path("large_log.txt").open(encoding="utf-8"):
print(line.strip())
写入文件
from pathlib import Path
output = Path("output.txt")
# 方法1:write_text() — 写入文本(覆盖)
output.write_text("Hello, World!\n第二行", encoding="utf-8")
# 方法2:write_bytes() — 写入二进制
output.write_bytes(b"\x89PNG\r\n\x1a\n")
# 方法3:append_text() — 追加模式
output.append_text("\n追加的内容", encoding="utf-8")
文本编码注意事项
from pathlib import Path
# 最佳实践:始终指定编码
config = Path("config.yaml").read_text(encoding="utf-8")
# 处理未知编码(回退方案)
def safe_read(path, encodings=("utf-8", "gbk", "latin-1")):
for enc in encodings:
try:
return Path(path).read_text(encoding=enc)
except UnicodeDecodeError:
continue
raise ValueError(f"无法解码文件: {path}")
content = safe_read("mixed_encoding.txt")
高级文件操作
文件复制
import shutil
from pathlib import Path
src = Path("source.txt")
dst = Path("backup/source_copy.txt")
# 方法1:shutil.copy — 仅复制内容
shutil.copy(src, dst)
# 方法2:shutil.copy2 — 复制内容+元数据(时间戳等)
shutil.copy2(src, dst)
# 方法3:Path.copy() (Python 3.26+)
dst.parent.mkdir(parents=True, exist_ok=True)
src.copy(dst)
# 复制整个目录
shutil.copytree("source_dir", "backup_dir", dirs_exist_ok=True)
文件移动与重命名
import shutil
from pathlib import Path
# 移动文件
shutil.move("old_location.txt", "new_location.txt")
# 重命名
Path("old_name.txt").rename("new_name.txt")
# 安全重命名(目标存在则自动添加序号)
def safe_rename(src, dst):
"""目标文件存在时,自动命名为 file_1.txt, file_2.txt"""
if not dst.exists():
return src.rename(dst)
stem = dst.stem
suffix = dst.suffix
parent = dst.parent
for i in range(1, 1000):
new_name = f"{stem}_{i}{suffix}"
new_path = parent / new_name
if not new_path.exists():
return src.rename(new_path)
raise FileExistsError("重命名次数超过上限")
safe_rename(Path("test.txt"), Path("output.txt"))
# 如果 output.txt 存在,则保存为 output_1.txt
文件删除
from pathlib import Path
# 删除单个文件
Path("temp.txt").unlink(missing_ok=True) # missing_ok=True 避免文件不存在报错
# 删除空目录
Path("empty_dir").rmdir()
# 删除目录树(包含内容)
import shutil
shutil.rmtree("old_project", ignore_errors=True)
创建临时文件/目录
import tempfile
from pathlib import Path
# 临时文件
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=True) as f:
f.write("临时数据")
temp_path = f.name
# 临时目录
with tempfile.TemporaryDirectory() as tmpdir:
temp_file = Path(tmpdir) / "data.json"
temp_file.write_text('{"key": "value"}')
# 使用完毕自动清理
# 自定义临时目录位置
temp_dir = Path(tempfile.gettempdir()) / "myapp_cache"
temp_dir.mkdir(exist_ok=True)
上下文管理器与资源安全
为什么必须使用 with 语句
# ❌ 错误示范:忘记关闭文件
def bad_read():
f = open("large_file.txt", encoding="utf-8")
data = f.read() # 如果这里抛异常,文件永不关闭
f.close()
return data
# ✅ 正确做法:with 语句自动管理资源
def good_read():
with open("large_file.txt", encoding="utf-8") as f:
return f.read() # 离开 with 块自动关闭文件
# ✅ pathlib 同样支持上下文管理器
from pathlib import Path
with Path("data.txt").open("r", encoding="utf-8") as f:
lines = f.readlines()
二进制文件操作
from pathlib import Path
# 图片复制
src = Path("photo.jpg")
dst = Path("backup/photo_copy.jpg")
dst.parent.mkdir(parents=True, exist_ok=True)
with src.open("rb") as fsrc:
with dst.open("wb") as fdst:
# 分块复制(处理大文件)
block_size = 8192
while chunk := fsrc.read(block_size):
fdst.write(chunk)
# 或者一行搞定
src.copy(dst) # Python 3.26+
异常处理模板
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
def safe_file_operation(src_path, dst_path):
"""安全的文件操作包装器"""
src = Path(src_path)
dst = Path(dst_path)
# 前置检查
if not src.exists():
raise FileNotFoundError(f"源文件不存在: {src}")
if src.is_dir():
raise IsADirectoryError(f"源路径是目录而非文件: {src}")
try:
# 确保目标目录存在
dst.parent.mkdir(parents=True, exist_ok=True)
# 执行操作
src.copy(dst)
logger.info(f"文件复制成功: {src} -> {dst}")
except PermissionError:
logger.error(f"权限不足: {dst}")
raise
except OSError as e:
logger.error(f"系统错误: {e}")
raise
# 使用示例
try:
safe_file_operation("input.txt", "output/result.txt")
except Exception as e:
print(f"操作失败: {e}")
常见场景实战
场景1:批量重命名文件
from pathlib import Path
def batch_rename(directory, pattern, replacement):
"""批量重命名:替换文件名中的特定字符串"""
dir_path = Path(directory)
renamed = []
for file in dir_path.iterdir():
if pattern in file.name:
new_name = file.name.replace(pattern, replacement)
new_path = file.rename(file.parent / new_name)
renamed.append((file.name, new_name))
return renamed
# 示例:将所有 "IMG_" 替换为 "Photo_"
results = batch_rename("./photos", "IMG_", "Photo_")
for old, new in results:
print(f"{old} -> {new}")
场景2:配置文件处理
from pathlib import Path
import json
class ConfigManager:
"""配置文件管理器"""
def __init__(self, config_dir=None):
self.config_dir = Path(config_dir) if config_dir else Path.home() / ".myapp"
self.config_dir.mkdir(parents=True, exist_ok=True)
self.config_file = self.config_dir / "config.json"
def load(self):
"""加载配置,带默认值"""
if self.config_file.exists():
return json.loads(self.config_file.read_text(encoding="utf-8"))
return {"theme": "light", "language": "zh-CN"}
def save(self, config):
"""保存配置"""
self.config_file.write_text(
json.dumps(config, indent=2, ensure_ascii=False),
encoding="utf-8"
)
# 使用
config = ConfigManager()
settings = config.load()
settings["theme"] = "dark"
config.save(settings)
场景3:日志文件轮转
from pathlib import Path
from datetime import datetime
import gzip
import shutil
def rotate_log(log_path, max_size_mb=10, keep_count=5):
"""日志轮转:超过大小则压缩归档"""
log = Path(log_path)
if not log.exists():
return
size_mb = log.stat().st_size / (1024 * 1024)
if size_mb < max_size_mb:
return
# 生成带时间戳的归档文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archive = log.parent / f"{log.stem}_{timestamp}.log.gz"
# 压缩并创建新日志
with log.open("rb") as f_in:
with gzip.open(archive, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
# 清空原日志
log.write_text("", encoding="utf-8")
# 清理旧归档
pattern = f"{log.stem}_*.log.gz"
archives = sorted(log.parent.glob(pattern), key=lambda p: p.stat().st_ctime)
for old in archives[:-keep_count]:
old.unlink()
# 使用
rotate_log("app.log", max_size_mb=10, keep_count=5)
场景4:递归搜索与过滤
from pathlib import Path
from datetime import datetime, timedelta
def find_files(directory, extensions=None, modified_after=None, min_size=None):
"""
高级文件搜索
Args:
directory: 搜索目录
extensions: 文件扩展名列表,如 ['.py', '.txt']
modified_after: 仅返回修改时间在指定日期之后的文件
min_size: 仅返回大于指定大小的文件(字节)
"""
dir_path = Path(directory)
results = []
for file in dir_path.rglob("*"):
if not file.is_file():
continue
# 扩展名过滤
if extensions and file.suffix.lower() not in extensions:
continue
# 时间过滤
if modified_after:
mtime = datetime.fromtimestamp(file.stat().st_mtime)
if mtime < modified_after:
continue
# 大小过滤
if min_size and file.stat().st_size < min_size:
continue
results.append(file)
return sorted(results)
# 示例:查找近7天修改的 Python 文件
one_week_ago = datetime.now() - timedelta(days=7)
recent_files = find_files(
"./projects",
extensions=[".py"],
modified_after=one_week_ago
)
for f in recent_files:
size_kb = f.stat().st_size / 1024
print(f"{f} ({size_kb:.1f} KB)")
最佳实践
安全规范清单
| 规范 | 说明 | 代码示例 |
|---|---|---|
| ✅ 始终指定编码 | 避免跨平台乱码 | read_text(encoding="utf-8") |
| ✅ 使用绝对路径 | 避免相对路径歧义 | Path.resolve() |
| ✅ 创建目录前检查 | 避免覆盖已有文件 | mkdir(parents=True, exist_ok=True) |
| ✅ 异常处理 | 防止程序崩溃 | try/except 包裹关键操作 |
| ✅ 使用 with 语句 | 自动释放资源 | with open(...) as f: |
| ❌ 避免字符串拼接路径 | 不同系统分隔符不同 | 用 Path / "dir" 替代 |
性能优化技巧
from pathlib import Path
# 1. 大文件分块读写
def read_large_file(path, chunk_size=65536):
with open(path, "rb") as f:
while chunk := f.read(chunk_size):
yield chunk
# 2. 批量操作减少系统调用
from pathlib import Path
def bulk_copy(files, target_dir):
"""批量复制文件,减少目录切换开销"""
target = Path(target_dir)
target.mkdir(parents=True, exist_ok=True)
for src in files:
src = Path(src)
if src.is_file():
src.copy(target / src.name) # Python 3.26+
# 3. 使用 glob 替代 listdir + filter
# ❌ 低效
files = [f for f in Path(".").iterdir() if f.suffix == ".py"]
# ✅ 高效
files = list(Path(".").glob("*.py"))
跨平台兼容
from pathlib import Path
import os
# pathlib 自动处理路径分隔符
# Linux/Mac: /home/user/file.txt
# Windows: C:\Users\user\file.txt
# 检测操作系统
HOME = Path.home()
TEMP = Path(os.environ.get("TEMP", "/tmp"))
# 使用环境变量
USER_DATA = Path(os.environ.get("APPDATA" if os.name == "nt" else "HOME")) / "MyApp"
USER_DATA.mkdir(parents=True, exist_ok=True)
排错指南
常见错误与解决方案
错误1:FileNotFoundError
# ❌ 常见错误
content = Path("data/config.txt").read_text()
# ✅ 解决方案1:确保目录存在
Path("data/config.txt").parent.mkdir(parents=True, exist_ok=True)
# ✅ 解决方案2:检查文件是否存在
config_path = Path("data/config.txt")
if config_path.exists():
content = config_path.read_text()
else:
content = "{}" # 使用默认空配置
# ✅ 解决方案3:使用默认值
content = Path("data/config.txt").read_text(encoding="utf-8") if Path("data/config.txt").exists() else "{}"
错误2:PermissionError
# 问题:文件被占用或无权限
# 解决方案1:关闭所有文件句柄
import gc
gc.collect() # 强制垃圾回收释放文件句柄
# 解决方案2:检查文件属性
from pathlib import Path
import stat
p = Path("locked_file.txt")
if not p.stat().st_mode & stat.S_IWRITE:
print("文件是只读的")
# 解决方案3:Windows 下检查进程
import subprocess
result = subprocess.run(
["handle", "locked_file.txt"],
capture_output=True, text=True
)
错误3:IsADirectoryError / FileExistsError
from pathlib import Path
# 场景:目标路径是已存在的目录
src = Path("my_folder")
dst = Path("existing_folder")
# 解决方案1:shutil.copytree
import shutil
shutil.copytree(src, dst / src.name) # 在目标目录下创建子目录
# 场景:想覆盖但 rename() 不允许
# rename() 在目标存在时会报错
# 解决方案2:先删除后重命名
if dst.exists():
if dst.is_dir():
shutil.rmtree(dst)
else:
dst.unlink()
src.rename(dst)
错误4:路径中文字符问题
from pathlib import Path
# Windows 中文路径问题
# 解决方案1:使用原始字符串
path = Path(r"C:\用户\管理员\文档")
# 解决方案2:Path 对象确保编码正确
path = Path("C:/用户/管理员/文档") # 正斜杠在 Windows 也有效
# 解决方案3:统一编码
import sys
sys.stdout.reconfigure(encoding='utf-8')
错误5:编码检测与乱码
from pathlib import Path
import chardet
def detect_and_read(path):
"""自动检测文件编码并读取"""
raw = Path(path).read_bytes()
result = chardet.detect(raw)
encoding = result["encoding"]
# 常用编码映射
encoding_map = {
"GB2312": "gbk",
"Windows-1252": "cp1252",
}
encoding = encoding_map.get(encoding, encoding or "utf-8")
return raw.decode(encoding)
# 安装 chardet: pip install chardet
content = detect_and_read("chinese_text.txt")
调试技巧
from pathlib import Path
# 1. 打印完整路径信息
p = Path("relative/path.txt")
print(f"""
绝对路径: {p.absolute()}
解析路径: {p.resolve()}
规范路径: {p.resolve().as_posix()}
是否存在: {p.exists()}
是否文件: {p.is_file()}
是否目录: {p.is_dir()}
""")
# 2. 递归查看目录树
def tree(path, prefix="", max_depth=3, _current_depth=0):
"""可视化目录树"""
if _current_depth >= max_depth:
return
path = Path(path)
if not path.exists():
return
# 当前节点
if path.is_dir():
print(f"{prefix}📁 {path.name}/")
new_prefix = prefix + "│ "
for child in sorted(path.iterdir()):
tree(child, new_prefix, max_depth, _current_depth + 1)
else:
size = path.stat().st_size
print(f"{prefix}📄 {path.name} ({size} bytes)")
tree("./projects", max_depth=2)
总结
核心要点
- 优先使用
pathlib: 现代、链式、跨平台 - 始终指定编码:
encoding="utf-8"是最佳实践 - 使用 with 语句: 自动资源管理,防止泄漏
- 检查再操作:
exists()、is_file()等判断方法 - 异常处理: 关键操作包裹 try/except
一图总结
┌─────────────────────────────────────────────────────────┐
│ 文件操作速查 │
├─────────────────┬─────────────────┬─────────────────────┤
│ 操作 │ pathlib │ os/shutil │
├─────────────────┼─────────────────┼─────────────────────┤
│ 读取文本 │ read_text() │ open().read() │
│ 写入文本 │ write_text() │ open().write() │
│ 复制文件 │ Path.copy() │ shutil.copy() │
│ 移动文件 │ Path.rename() │ shutil.move() │
│ 删除文件 │ Path.unlink() │ os.remove() │
│ 创建目录 │ mkdir() │ os.makedirs() │
│ 遍历文件 │ glob() / rglob()│ os.walk() │
│ 检查存在 │ exists() │ os.path.exists() │
└─────────────────┴─────────────────┴─────────────────────┘
💡 提示: 本文代码适用于 Python 3.10+,部分高级特性(如
Path.copy())需要 Python 3.26+。 📚 扩展阅读:
原创内容,版权所有。未经授权,禁止转载。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END













暂无评论内容