4、文件操作基础:使用 os 和 shutil 模块进行文件和目录的复制、移动、删除

说实话,在自动化构建这条路上,文件操作是你绕不开的基本功。

我刚开始写构建脚本那会儿,总觉得文件操作嘛,不就是复制粘贴?后来被坑过几次才明白——文件操作看似简单,但坑全藏在细节里。比如路径分隔符、权限问题、目录不存在时的异常处理……这些你都得心里有数。

今天我们就来聊聊 Python 里最常用的两个文件操作模块:os 和 shutil。它们就像你的左右手,一个负责底层操作,一个负责高级操作。

4.1 os 模块:文件操作的基础设施

os 模块是 Python 和操作系统交互的桥梁。它提供了很多底层接口,比如创建目录、删除文件、获取文件信息等。

我个人习惯把 os 模块当作「基础设施」来用。它不花哨,但很稳。

4.1.1 文件和目录的创建与删除

先看几个最常用的操作:

import os

# 创建单级目录
os.mkdir('my_folder')

# 创建多级目录(如果中间目录不存在,会自动创建)
os.makedirs('parent/child/grandchild', exist_ok=True)

# 删除空目录
os.rmdir('my_folder')

# 删除文件
os.remove('temp.txt')

# 重命名文件或目录
os.rename('old_name.txt', 'new_name.txt')
注意:os.rmdir() 只能删除空目录。如果目录里有文件,会抛出 OSError。我曾经在清理构建产物时,就因为忘了先删文件,导致脚本直接崩了。

4.1.2 路径操作:别再用字符串拼接了

很多新手喜欢用字符串拼接路径,比如 path + '/' + filename。这在 Windows 上会直接翻车——因为 Windows 用的是反斜杠 \。

我建议你养成习惯,用 os.path 来处理路径:

import os

# 拼接路径(自动处理分隔符)
full_path = os.path.join('parent', 'child', 'file.txt')
# 输出:parent/child/file.txt(Linux)或 parent\child\file.txt(Windows)

# 获取文件名
filename = os.path.basename('/home/user/data.csv')
# 输出:data.csv

# 获取目录名
dirname = os.path.dirname('/home/user/data.csv')
# 输出:/home/user

# 分离文件名和扩展名
name, ext = os.path.splitext('data.csv')
# 输出:('data', '.csv')

# 判断路径是否存在
if os.path.exists('some_file.txt'):
    print('文件存在')

# 判断是文件还是目录
os.path.isfile('file.txt')
os.path.isdir('folder')
小技巧:用 os.path.join() 拼接路径,你的脚本就能在 Windows、Linux、macOS 上无缝运行。我参与过一个跨平台项目,就因为统一用了 os.path,省了不少适配的功夫。

4.1.3 遍历目录:os.walk 的妙用

在自动化构建中,经常需要遍历整个目录树。比如查找所有 .py 文件,或者清理构建产物。

os.walk() 是我最常用的遍历工具。它会递归地遍历目录,返回三个值:当前目录路径、子目录列表、文件列表。

import os

for root, dirs, files in os.walk('project_folder'):
    for file in files:
        if file.endswith('.py'):
            full_path = os.path.join(root, file)
            print(f'找到 Python 文件:{full_path}')

你想想看,如果让你手动写递归遍历,得写多少代码?os.walk 一行就搞定了。

4.2 shutil 模块:高级文件操作

shutil 模块是 os 的「升级版」。它提供了更高级的文件操作,比如复制整个目录、移动文件、删除非空目录等。

说白了,os 管的是「底层」,shutil 管的是「业务」。在自动化构建中,shutil 的使用频率其实更高。

4.2.1 复制文件和目录

复制操作是构建脚本里的常客。比如把编译好的产物复制到发布目录。

import shutil

# 复制文件(目标路径可以是文件或目录)
shutil.copy('source.txt', 'dest.txt')
shutil.copy('source.txt', 'dest_folder/')

# 复制文件并保留元数据(权限、时间戳等)
shutil.copy2('source.txt', 'dest.txt')

# 复制整个目录(递归复制)
shutil.copytree('source_folder', 'dest_folder', dirs_exist_ok=True)
重点:shutil.copy() 和 shutil.copy2() 的区别在于,copy2 会尽量保留文件的元数据(比如修改时间、权限)。如果你在构建过程中需要保持文件属性不变,用 copy2 更稳妥。

4.2.2 移动文件和目录

移动操作本质上就是「复制 + 删除」。但 shutil.move() 帮你一步到位:

import shutil

# 移动文件到目标目录
shutil.move('source.txt', 'dest_folder/')

# 移动并重命名
shutil.move('source.txt', 'dest_folder/new_name.txt')

# 移动整个目录
shutil.move('source_folder', 'dest_folder/')

嗯,这里要注意:shutil.move() 在跨文件系统移动时,会先复制再删除。如果文件很大,可能会有点慢。我曾在一次构建中移动了一个 10GB 的日志文件,结果等了半天……后来我改成直接复制,再手动删除源文件,反而更可控。

4.2.3 删除非空目录

这是 shutil 最实用的功能之一。os.rmdir() 只能删空目录,但 shutil.rmtree() 可以删掉整个目录树,不管里面有多少文件。

import shutil

# 删除整个目录(包括所有子目录和文件)
shutil.rmtree('build_output/')
警告:shutil.rmtree() 是不可逆操作!删了就没了。我曾经在测试环境里写了个脚本,不小心把 rmtree('..') 写成了 rmtree('.'),结果当前目录被清空了……还好是测试环境。建议你在使用前,先打印一下路径确认。

4.3 实战:自动化构建中的文件操作

光说不练假把式。我们来看一个真实的自动化构建场景:

需求:编译项目后,把生成的 .exe 文件复制到发布目录,删除旧的构建产物,并生成一个版本号文件。

import os
import shutil
from datetime import datetime

def build_and_publish(source_dir, publish_dir):
    # 1. 确保发布目录存在
    os.makedirs(publish_dir, exist_ok=True)
    
    # 2. 查找所有 .exe 文件
    exe_files = []
    for root, dirs, files in os.walk(source_dir):
        for file in files:
            if file.endswith('.exe'):
                exe_files.append(os.path.join(root, file))
    
    if not exe_files:
        print('没有找到可执行文件,构建可能失败')
        return False
    
    # 3. 复制到发布目录
    for exe_path in exe_files:
        shutil.copy2(exe_path, publish_dir)
        print(f'已复制:{exe_path} -> {publish_dir}')
    
    # 4. 删除旧的构建产物(保留源码)
    build_temp = os.path.join(source_dir, 'build_temp')
    if os.path.exists(build_temp):
        shutil.rmtree(build_temp)
        print(f'已清理临时目录:{build_temp}')
    
    # 5. 生成版本号文件
    version_info = {
        'build_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
        'files': [os.path.basename(f) for f in exe_files]
    }
    
    version_path = os.path.join(publish_dir, 'version.txt')
    with open(version_path, 'w') as f:
        f.write(f"构建时间:{version_info['build_time']}\n")
        f.write(f"包含文件:{', '.join(version_info['files'])}\n")
    
    print(f'版本信息已保存到:{version_path}')
    return True

# 执行构建发布
build_and_publish('./build', './release')

这个脚本虽然简单,但涵盖了 os 和 shutil 的常用操作。我在实际项目中,类似的脚本改一改就能用在很多场景。

4.4 常见错误与避坑指南

错误场景 原因 解决方案
FileNotFoundError 源文件或目录不存在 操作前用 os.path.exists() 检查
PermissionError 没有文件读写权限 检查文件权限,或用 os.chmod() 修改
NotADirectoryError 把文件路径当目录用了 用 os.path.isdir() 确认类型
shutil.Error 复制过程中出错(如磁盘空间不足) 捕获异常,打印具体错误信息
我的经验:写文件操作代码时,一定要加异常处理。尤其是 shutil.rmtree() 和 os.remove(),一个不小心就是灾难。我习惯在关键操作前后加日志,这样出问题时能快速定位。

4.5 小结

好了,这一章的内容就到这里。我们来回顾一下:

  • os 模块:负责底层文件操作,如创建目录、删除文件、路径处理、遍历目录。
  • shutil 模块:负责高级文件操作,如复制、移动、删除非空目录。
  • 实战技巧:用 os.path.join() 处理路径,用 os.walk() 遍历目录,用 shutil.rmtree() 清理构建产物。

下一章,我们会聊聊 「5、配置管理:YAML 和 JSON 文件的读写与解析」。到时候我会分享一些在配置管理中踩过的坑,敬请期待。

课后练习:

  1. 写一个脚本,遍历当前目录下所有 .log 文件,将它们复制到 logs_backup 目录。
  2. 实现一个函数,删除指定目录下所有 .tmp 文件(递归删除)。
  3. 模拟构建流程:创建 build 目录,在里面生成几个文件,然后复制到 release 目录,最后删除 build 目录。

记住,文件操作是自动化构建的基石。把这些基础打牢了,后面的路会顺很多。