4. 文件操作基础
说实话,在自动化构建这条路上,文件操作是你绕不开的基本功。
我刚开始写构建脚本那会儿,总觉得文件操作嘛,不就是复制粘贴?后来被坑过几次才明白——文件操作看似简单,但坑全藏在细节里。比如路径分隔符、权限问题、目录不存在时的异常处理……这些你都得心里有数。
今天我们就来聊聊 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 文件的读写与解析」。到时候我会分享一些在配置管理中踩过的坑,敬请期待。
课后练习:
- 写一个脚本,遍历当前目录下所有
.log文件,将它们复制到logs_backup目录。 - 实现一个函数,删除指定目录下所有
.tmp文件(递归删除)。 - 模拟构建流程:创建
build目录,在里面生成几个文件,然后复制到release目录,最后删除build目录。
记住,文件操作是自动化构建的基石。把这些基础打牢了,后面的路会顺很多。