3. 文件系统操作:os模块、pathlib模块、批量文件重命名与移动、日志文件管理
做验证的,谁没跟文件系统打过交道?
我入行那会儿,每天要处理几百个仿真波形文件。手动改名?那得改到天亮。后来我学乖了,写脚本批量处理。嗯,今天就把这些经验分享给你。
3.1 os模块:老牌选手,功能全面
os 模块是 Python 自带的,不用装任何第三方库。说白了,它就是让你用 Python 代码去执行操作系统命令。
3.1.1 常用操作一览
| 功能 | 函数 | 说明 |
|---|---|---|
| 获取当前目录 | os.getcwd() |
返回字符串路径 |
| 切换目录 | os.chdir(path) |
类似 cd 命令 |
| 列出目录内容 | os.listdir(path) |
返回文件名列表 |
| 创建目录 | os.mkdir(path) |
只能建一层 |
| 递归创建目录 | os.makedirs(path) |
可以建多层 |
| 删除文件 | os.remove(path) |
永久删除,小心 |
| 重命名 | os.rename(src, dst) |
也可用于移动文件 |
| 判断路径是否存在 | os.path.exists(path) |
返回布尔值 |
| 拼接路径 | os.path.join(a, b) |
自动处理斜杠 |
核心要点:用 os.path.join() 拼接路径,别自己拼字符串。Windows 用反斜杠,Linux 用正斜杠,join() 会自动适配。
3.1.2 实战:批量重命名仿真日志
我在项目中遇到过这种情况:仿真跑了一晚上,生成了 100 多个 sim.log 文件。每个都在不同子目录里,根本分不清哪个对应哪个测试用例。
import os
def rename_logs(base_dir):
"""给仿真日志加上时间戳,避免覆盖"""
for root, dirs, files in os.walk(base_dir):
for f in files:
if f == "sim.log":
old_path = os.path.join(root, f)
# 用目录名作为标识
dir_name = os.path.basename(root)
new_name = f"{dir_name}_sim.log"
new_path = os.path.join(root, new_name)
os.rename(old_path, new_path)
print(f"已重命名: {old_path} -> {new_path}")
rename_logs("./sim_results")
我的习惯:重命名前先打印一遍,确认无误再执行真正的 os.rename()。你想想看,要是把关键文件改坏了,那可就麻烦了。
3.2 pathlib模块:面向对象,更现代
Python 3.4 开始引入了 pathlib。我个人更喜欢用它,因为代码读起来更自然。它把路径当成对象来处理,而不是字符串。
3.2.1 基本用法对比
| 操作 | os 模块写法 | pathlib 写法 |
|---|---|---|
| 获取当前目录 | os.getcwd() |
Path.cwd() |
| 拼接路径 | os.path.join("a", "b") |
Path("a") / "b" |
| 判断是否为文件 | os.path.isfile(p) |
Path(p).is_file() |
| 获取文件名 | os.path.basename(p) |
Path(p).name |
| 获取后缀 | os.path.splitext(p)[1] |
Path(p).suffix |
看到没?pathlib 用 / 运算符拼接路径,是不是很直观?
3.2.2 实战:按文件类型分类整理
验证项目里,文件类型特别多:.v、.sv、.log、.vcd、.fsdb……混在一起看着就头疼。
from pathlib import Path
def organize_by_type(src_dir):
"""按文件后缀分类到不同文件夹"""
src = Path(src_dir)
if not src.exists():
print(f"目录不存在: {src_dir}")
return
for f in src.iterdir():
if f.is_file():
# 获取后缀,去掉点号
ext = f.suffix[1:] if f.suffix else "no_ext"
target_dir = src / ext
target_dir.mkdir(exist_ok=True)
f.rename(target_dir / f.name)
print(f"移动: {f.name} -> {ext}/")
organize_by_type("./work_dir")
我曾经踩过的坑:用 iterdir() 遍历时,如果同时进行重命名操作,可能会漏掉文件或重复处理。建议先收集所有文件路径,再统一处理。
3.3 批量文件重命名与移动
这部分是验证工程师的日常。我几乎每周都要写类似的脚本。
3.3.1 批量重命名:添加前缀或后缀
from pathlib import Path
def add_prefix_suffix(directory, prefix="", suffix=""):
"""给目录下所有 .log 文件添加前缀或后缀"""
for f in Path(directory).glob("*.log"):
new_name = f"{prefix}{f.stem}{suffix}{f.suffix}"
new_path = f.with_name(new_name)
f.rename(new_path)
print(f"{f.name} -> {new_name}")
# 给所有日志加上日期前缀
add_prefix_suffix("./logs", prefix="20250101_")
3.3.2 按规则移动文件
仿真跑完后,我想把通过的用例和失败的用例分开存放。怎么做?
from pathlib import Path
import re
def move_by_result(log_dir):
"""根据日志内容移动文件"""
log_dir = Path(log_dir)
pass_dir = log_dir / "PASS"
fail_dir = log_dir / "FAIL"
pass_dir.mkdir(exist_ok=True)
fail_dir.mkdir(exist_ok=True)
for log_file in log_dir.glob("*.log"):
content = log_file.read_text()
if "TEST PASSED" in content:
log_file.rename(pass_dir / log_file.name)
elif "TEST FAILED" in content:
log_file.rename(fail_dir / log_file.name)
else:
print(f"无法判断结果: {log_file.name}")
move_by_result("./sim_logs")
小技巧:移动文件前,先检查目标目录是否存在。用 exist_ok=True 可以避免重复创建报错。
3.4 日志文件管理
验证脚本跑起来,日志是命根子。出了问题,全靠日志来排查。
3.4.1 使用 logging 模块
别用 print() 打日志了。用 logging 模块,可以控制输出级别、写入文件、格式化内容。
import logging
def setup_logger(name, log_file):
"""配置日志记录器"""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# 文件处理器:记录所有信息
fh = logging.FileHandler(log_file)
fh.setLevel(logging.DEBUG)
# 控制台处理器:只显示重要信息
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# 格式化
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
logger.addHandler(fh)
logger.addHandler(ch)
return logger
# 使用示例
log = setup_logger("sim_monitor", "simulation.log")
log.info("仿真开始")
log.debug("加载测试用例: test_abc")
log.warning("时序违例,但仍在容忍范围内")
log.error("仿真崩溃,请检查波形")
3.4.2 日志轮转:防止日志撑爆磁盘
仿真跑一整天,日志文件可能几个 GB。不轮转的话,磁盘很快就满了。
from logging.handlers import RotatingFileHandler
def setup_rotating_logger(name, log_file, max_bytes=10*1024*1024, backup_count=5):
"""配置带轮转的日志记录器"""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
handler = RotatingFileHandler(
log_file,
maxBytes=max_bytes, # 每个文件最大 10MB
backupCount=backup_count # 保留 5 个备份
)
handler.setFormatter(logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s"
))
logger.addHandler(handler)
return logger
log = setup_rotating_logger("eda_sim", "eda_sim.log")
for i in range(10000):
log.info(f"第 {i} 次迭代完成")
为什么这么做?日志文件太大,打开都费劲。轮转后,每个文件 10MB,最多保留 5 个历史文件。旧的自动删除,不会撑爆磁盘。
3.4.3 日志分析小脚本
日志写好了,怎么快速找出错误?我一般写个简单的分析脚本:
from pathlib import Path
from collections import Counter
def analyze_log(log_path):
"""统计日志中的错误类型"""
log_path = Path(log_path)
if not log_path.exists():
print("日志文件不存在")
return
errors = []
with open(log_path, "r") as f:
for line in f:
if "ERROR" in line or "FATAL" in line:
errors.append(line.strip())
print(f"总错误数: {len(errors)}")
if errors:
# 统计最常见的错误
error_types = Counter(
[e.split(" - ")[-1] for e in errors]
)
print("最常见的错误:")
for err, count in error_types.most_common(5):
print(f" [{count}次] {err}")
analyze_log("simulation.log")
嗯,这套流程我用了好几年。从 os 到 pathlib,从手动改名到自动化日志管理,每一步都能省下大量时间。
你想想看,每天省下半小时,一年就是 180 多个小时。这些时间用来学新知识、陪家人,不香吗?