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")

嗯,这套流程我用了好几年。从 ospathlib,从手动改名到自动化日志管理,每一步都能省下大量时间。

你想想看,每天省下半小时,一年就是 180 多个小时。这些时间用来学新知识、陪家人,不香吗?