3. 文件与目录操作:使用 os 和 pathlib 模块进行批量文件遍历、创建、删除与重命名

文件操作,说白了就是脚本里最基础也最绕不开的活儿。我刚开始写自动化脚本那会儿,总觉得文件遍历不就是个 for 循环嘛,结果一跑起来,各种路径问题、权限问题、编码问题全冒出来了。后来我才明白,文件操作这事儿,看着简单,坑是真不少。

Python 给了我们两套工具:os 模块是老牌选手,功能全面但 API 设计得有点糙;pathlib 是 Python 3.4 之后加入的新秀,用面向对象的方式处理路径,我个人更偏爱它。不过话说回来,实际项目中往往是两者混着用,看场景选工具。

3.1 文件遍历:从 os.walk 到 pathlib.rglob

批量操作的第一步,就是找到你要处理的文件。我见过不少新手用 os.listdir 一层层递归,代码写得又长又容易漏文件。其实 Python 早就给我们准备好了更优雅的方式。

3.1.1 使用 os.walk 遍历目录树

os.walk 是个生成器,它会递归遍历目录树,每次返回三个值:当前目录路径、子目录列表、文件列表。嗯,这里要注意,它返回的是字符串路径,不是对象。

import os

# 遍历 /data/logs 下所有 .log 文件
for root, dirs, files in os.walk('/data/logs'):
    for file in files:
        if file.endswith('.log'):
            full_path = os.path.join(root, file)
            print(f'找到日志文件: {full_path}')

我在项目中遇到过一个问题:os.walk 默认会进入所有子目录,包括那些你不想碰的隐藏目录。比如 .git 或者 __pycache__,遍历进去不仅慢,还可能报错。解决办法是修改 dirs 列表,把不需要的目录删掉。

for root, dirs, files in os.walk('/project'):
    # 跳过 .git 和 __pycache__ 目录
    dirs[:] = [d for d in dirs if d not in ['.git', '__pycache__']]
    # 继续处理文件...

你想想看,这个技巧能省多少时间?尤其是项目大了以后,少遍历一个 .git 目录,速度能快好几倍。

3.1.2 使用 pathlib.rglob 更简洁

如果你用的是 Python 3.4 以上版本,我强烈建议你试试 pathlib。它的 rglob 方法支持通配符匹配,代码写起来特别清爽。

from pathlib import Path

# 遍历 /data/logs 下所有 .log 文件
log_dir = Path('/data/logs')
for log_file in log_dir.rglob('*.log'):
    print(f'找到日志文件: {log_file}')

你看,一行 rglob('*.log') 就搞定了。而且返回的是 Path 对象,可以直接调用各种方法,比如 .name、.suffix、.parent,比字符串拼接舒服多了。

核心区别:

  • os.walk 返回字符串路径,需要手动拼接
  • pathlib.rglob 返回 Path 对象,支持链式调用
  • rglob 支持通配符模式,代码更简洁
  • os.walk 可以动态修改遍历行为(比如跳过目录)

3.2 文件创建与目录管理

创建文件和目录,听起来简单,但有个细节很多人会忽略:如果父目录不存在,直接创建文件会报错。我曾经因为这个 bug 被线上告警折腾了一整晚,后来学乖了,创建文件前先确保目录存在。

3.2.1 使用 os.makedirs 创建目录

os.makedirs 可以递归创建目录,加上 exist_ok=True 参数,即使目录已存在也不会报错。这个参数是 Python 3.2 之后加的,老版本没有,如果你还在用 Python 2,嗯……该升级了。

import os

# 递归创建目录,如果已存在则忽略
os.makedirs('/data/backup/2024/01', exist_ok=True)

# 创建文件
with open('/data/backup/2024/01/report.txt', 'w') as f:
    f.write('备份报告')

3.2.2 使用 pathlib 更优雅

pathlib 的 mkdir 方法同样支持 exist_ok,而且 parents=True 表示递归创建父目录。我个人习惯用这种方式,因为 Path 对象可以一步到位。

from pathlib import Path

# 创建目录并写入文件
backup_dir = Path('/data/backup/2024/01')
backup_dir.mkdir(parents=True, exist_ok=True)

# 直接创建文件
report_file = backup_dir / 'report.txt'
report_file.write_text('备份报告')

这里有个小技巧:Path 对象用 / 运算符拼接路径,比 os.path.join 直观多了。你想想看,写 backup_dir / 'report.txt' 是不是比 os.path.join(backup_dir, 'report.txt') 顺眼?

3.3 批量删除与重命名

批量删除和重命名,是自动化运维里最常见的场景。比如清理过期日志、重命名备份文件。但这里有个大坑:删除操作不可逆,一定要先确认再执行。

3.3.1 批量删除文件

我曾经在测试环境写过一个清理脚本,结果路径写错了,把生产环境的日志全删了……嗯,从那以后我写删除逻辑都会加一个 dry_run 模式,先模拟执行,确认无误再真删。

import os
from pathlib import Path

def clean_old_logs(log_dir, days=30, dry_run=True):
    """清理指定天数之前的日志文件"""
    import time
    now = time.time()
    cutoff = now - days * 86400
    
    log_path = Path(log_dir)
    for log_file in log_path.rglob('*.log'):
        if log_file.stat().st_mtime < cutoff:
            if dry_run:
                print(f'[模拟] 将删除: {log_file}')
            else:
                log_file.unlink()
                print(f'[已删除] {log_file}')

# 先模拟运行
clean_old_logs('/data/logs', days=30, dry_run=True)

# 确认无误后真正执行
# clean_old_logs('/data/logs', days=30, dry_run=False)

警告: 批量删除操作一定要先模拟运行!我建议你在脚本里默认开启 dry_run=True,通过命令行参数控制是否真删。另外,删除前最好先备份或移动到回收站目录,给自己留条后路。

3.3.2 批量重命名文件

重命名操作相对安全一些,但也要注意文件名冲突的问题。比如你想把所有 .txt 文件改成 .md,如果目标文件名已经存在,os.rename 会直接覆盖,不会报错。这又是一个坑。

from pathlib import Path

def rename_files(directory, old_ext, new_ext):
    """批量修改文件扩展名"""
    dir_path = Path(directory)
    for file in dir_path.rglob(f'*{old_ext}'):
        new_name = file.with_suffix(new_ext)
        if new_name.exists():
            print(f'[跳过] 目标文件已存在: {new_name}')
            continue
        file.rename(new_name)
        print(f'[重命名] {file.name} -> {new_name.name}')

# 将所有 .log 文件改为 .txt
rename_files('/data/logs', '.log', '.txt')

你看,用 pathlib 的 with_suffix 方法,改扩展名就是一行代码的事。而且我加了个检查,避免覆盖已有文件。这个细节,我在项目里吃过亏才加上的。

3.4 实战:日志文件归档脚本

说了这么多,咱们来写一个完整的实战脚本。这个脚本是我在运维一个 Web 服务时写的,功能是把昨天的日志文件打包压缩,然后删除原始文件。

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

def archive_logs(log_dir, archive_dir):
    """归档昨天的日志文件"""
    log_path = Path(log_dir)
    archive_path = Path(archive_dir)
    archive_path.mkdir(parents=True, exist_ok=True)
    
    # 计算昨天的日期
    yesterday = datetime.now() - timedelta(days=1)
    date_str = yesterday.strftime('%Y-%m-%d')
    
    # 遍历所有 .log 文件
    for log_file in log_path.rglob('*.log'):
        # 检查文件修改日期是否为昨天
        mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
        if mtime.date() == yesterday.date():
            # 构建归档文件名
            archive_name = f'{log_file.stem}_{date_str}.gz'
            archive_file = archive_path / archive_name
            
            # 压缩文件
            with open(log_file, 'rb') as f_in:
                with gzip.open(archive_file, 'wb') as f_out:
                    shutil.copyfileobj(f_in, f_out)
            
            # 删除原始文件
            log_file.unlink()
            print(f'[归档] {log_file} -> {archive_file}')

# 执行归档
archive_logs('/var/log/myapp', '/data/archives')

小提示: 实际生产环境中,我建议你把归档脚本放到 crontab 里,每天凌晨执行一次。另外,归档目录最好按月份分目录,比如 /data/archives/2024/01/,方便后续管理。

3.5 本章知识体系

下面这张图总结了文件与目录操作的核心知识点,你可以把它当作速查表。

文件与目录操作知识体系 文件与目录操作 文件遍历 创建与删除 重命名与移动 os.walk pathlib.rglob os.makedirs Path.mkdir Path.unlink os.rename Path.rename 最佳实践:先模拟 → 再执行 → 留备份 推荐优先使用 pathlib,兼容性要求高时使用 os 模块

这张图把文件操作的核心知识点串起来了。你从中间的主模块出发,往三个方向走:遍历、创建删除、重命名移动。每个方向都有对应的 API 和注意事项。最下面那条「先模拟 → 再执行 → 留备份」是我用血泪教训换来的经验,希望你记住。

好了,文件与目录操作就讲到这里。记住一句话:操作文件前多想想,操作文件后多检查。自动化脚本跑得快是本事,跑得稳才是真功夫。

可可精品资料库