3、文件系统操作:os模块基础、pathlib库使用、批量遍历目录、文件筛选与过滤

做标定数据处理,说白了第一步就是跟文件打交道。

我刚开始写脚本那会儿,最头疼的不是算法逻辑,而是怎么把几百个文件夹里的数据文件找出来。你想想看,一个标定项目下来,原始数据、中间结果、最终标定表,散落在十几个子目录里。手动翻?那得翻到猴年马月去。

所以这一章,咱们就把文件系统操作这块彻底捋清楚。我保证,学完这些,你写批处理脚本的效率至少翻一倍。

3.1 os模块:老牌劲旅,但依然能打

Python 的 os 模块,算是文件操作的元老级人物了。我入行那会儿,几乎所有文件路径操作都靠它。虽然现在有了更现代的 pathlib,但 os 模块在一些底层操作上依然不可替代。

3.1.1 路径拼接与判断

标定数据通常按日期或项目编号分文件夹存放。比如:

D:\CalibrationData\2024_ProjectA\raw_data\sensor_01.dat

os 模块拼接路径,我习惯这么写:

import os

base_dir = r"D:\CalibrationData\2024_ProjectA"
sub_dir = "raw_data"
file_name = "sensor_01.dat"

full_path = os.path.join(base_dir, sub_dir, file_name)
print(full_path)
# 输出: D:\CalibrationData\2024_ProjectA\raw_data\sensor_01.dat

这里有个坑,我踩过好几次——路径分隔符。Windows 用反斜杠 \,Linux 用正斜杠 /。用 os.path.join 就对了,它会自动适配当前系统。

我的小技巧: 路径字符串前面加个 r,变成原始字符串,这样就不用担心反斜杠转义的问题了。比如 r"D:\data"

3.1.2 判断文件或目录是否存在

批处理脚本最怕什么?路径不存在,程序直接崩掉。我一般会在脚本开头做一次存在性检查:

import os

data_path = r"D:\CalibrationData\2024_ProjectA"

if os.path.exists(data_path):
    print("路径存在,继续处理...")
else:
    print(f"警告:路径 {data_path} 不存在!")
    # 可以在这里退出或创建目录
    os.makedirs(data_path, exist_ok=True)  # 自动创建多级目录

os.makedirs 配合 exist_ok=True,是我最常用的组合。不管目录存不存在,都能安全处理,不会报错。

3.1.3 获取文件信息

标定数据文件往往有命名规范,但有时候也需要看文件本身的属性。比如文件大小、修改时间:

import os
import time

file_path = r"D:\CalibrationData\2024_ProjectA\raw_data\sensor_01.dat"

file_size = os.path.getsize(file_path)  # 字节数
mod_time = os.path.getmtime(file_path)  # 时间戳

print(f"文件大小: {file_size / 1024:.2f} KB")
print(f"最后修改时间: {time.ctime(mod_time)}")

我曾经遇到过一个情况:某批次标定数据文件大小异常,只有正常文件的十分之一。用 os.path.getsize 一查,发现是采集过程中断导致的。嗯,从那以后,我每次批处理都会先检查文件大小是否在合理范围内。

3.2 pathlib库:更现代、更优雅的方式

说实话,我第一次用 pathlib 的时候,心里是有点抵触的——毕竟 os 模块用了那么多年。但用了一个月之后,我就真香了。

pathlib 把路径当作对象来处理,而不是字符串。这意味着你可以用 . 来调用各种方法,代码读起来就像在说人话。

3.2.1 基本用法

from pathlib import Path

# 定义一个路径对象
data_dir = Path(r"D:\CalibrationData\2024_ProjectA")

# 拼接路径
raw_dir = data_dir / "raw_data" / "sensor_01.dat"
print(raw_dir)
# 输出: D:\CalibrationData\2024_ProjectA\raw_data\sensor_01.dat

# 判断是否存在
if raw_dir.exists():
    print("文件存在")

# 获取父目录
print(raw_dir.parent)       # D:\CalibrationData\2024_ProjectA\raw_data
print(raw_dir.parents[1])   # D:\CalibrationData\2024_ProjectA

# 获取文件名和后缀
print(raw_dir.name)         # sensor_01.dat
print(raw_dir.stem)         # sensor_01
print(raw_dir.suffix)       # .dat

看到没?用 / 运算符拼接路径,比 os.path.join 直观多了。我个人特别喜欢 .stem.suffix 这两个属性,在筛选文件时简直不要太方便。

3.2.2 创建与删除目录

from pathlib import Path

# 创建目录(自动创建父目录)
output_dir = Path(r"D:\CalibrationData\2024_ProjectA\processed")
output_dir.mkdir(parents=True, exist_ok=True)

# 删除空目录
empty_dir = Path(r"D:\CalibrationData\temp")
if empty_dir.exists():
    empty_dir.rmdir()  # 只能删除空目录

# 删除文件
file_to_delete = Path(r"D:\CalibrationData\old_data.dat")
if file_to_delete.exists():
    file_to_delete.unlink()
注意: rmdir() 只能删除空目录。如果目录里有文件,会报错。要删除非空目录,可以用 shutil.rmtree(),但一定要谨慎——删了就找不回来了。

3.3 批量遍历目录:从手动到自动

这才是重头戏。标定工程师每天面对的就是成百上千个文件,手动处理?不存在的。

3.3.1 使用 os.walk 遍历

os.walk 是我早期最依赖的函数。它会递归遍历目录树,返回三个值:当前目录路径、子目录列表、文件列表。

import os

root_dir = r"D:\CalibrationData\2024_ProjectA"

for dirpath, dirnames, filenames in os.walk(root_dir):
    print(f"当前目录: {dirpath}")
    print(f"子目录数: {len(dirnames)}")
    print(f"文件数: {len(filenames)}")
    print("-" * 40)

    # 处理每个文件
    for filename in filenames:
        full_path = os.path.join(dirpath, filename)
        # 这里可以写你的处理逻辑
        print(f"  处理文件: {full_path}")

我记得有一次,客户要求把所有子目录下的 .csv 文件汇总到一个表格里。用 os.walk 配合列表推导式,十几行代码就搞定了。客户还以为我熬了一整夜呢。

3.3.2 使用 pathlib 的 rglob 方法

如果你喜欢 pathlib 的风格,那 rglob 绝对是你的菜。它支持通配符匹配,写起来更简洁。

from pathlib import Path

data_dir = Path(r"D:\CalibrationData\2024_ProjectA")

# 查找所有 .dat 文件
dat_files = list(data_dir.rglob("*.dat"))
print(f"找到 {len(dat_files)} 个 .dat 文件")

# 查找所有子目录下的 .csv 文件
csv_files = list(data_dir.rglob("**/*.csv"))
print(f"找到 {len(csv_files)} 个 .csv 文件")

# 遍历并处理
for file_path in data_dir.rglob("*.dat"):
    print(f"处理: {file_path.name}")
    # 你的处理逻辑...

rglob 里的 ** 表示递归所有子目录。说白了,就是「不管藏多深,我都能给你揪出来」。

3.4 文件筛选与过滤:只拿我想要的

遍历目录只是第一步。真正麻烦的是,怎么从一堆乱七八糟的文件里,精准地找到你要处理的那一批。

3.4.1 按扩展名筛选

这是最基础的需求。标定数据常见的格式有 .dat.csv.h5.mat 等。

from pathlib import Path

data_dir = Path(r"D:\CalibrationData\2024_ProjectA")

# 只筛选 .dat 和 .csv 文件
target_extensions = {".dat", ".csv"}
filtered_files = [
    f for f in data_dir.rglob("*")
    if f.suffix.lower() in target_extensions
]

print(f"筛选出 {len(filtered_files)} 个目标文件")

3.4.2 按文件名模式筛选

有时候文件名里包含了关键信息,比如传感器编号、采集时间。我们可以用正则表达式来精确匹配。

import re
from pathlib import Path

data_dir = Path(r"D:\CalibrationData\2024_ProjectA")

# 匹配 sensor_ 开头,后跟两位数字,扩展名为 .dat 的文件
pattern = re.compile(r"sensor_(\d{2})\.dat$")

matched_files = []
for file_path in data_dir.rglob("*.dat"):
    match = pattern.search(file_path.name)
    if match:
        sensor_id = match.group(1)
        matched_files.append((sensor_id, file_path))
        print(f"传感器 {sensor_id}: {file_path.name}")

print(f"匹配到 {len(matched_files)} 个传感器文件")

我曾经遇到过一个项目,文件名里包含了采集日期,但格式不统一——有的用 20240101,有的用 2024-01-01。用正则表达式一把梭,全给标准化了。

3.4.3 按文件属性筛选

除了文件名,文件大小和修改时间也是重要的筛选条件。

from pathlib import Path
import time

data_dir = Path(r"D:\CalibrationData\2024_ProjectA")

# 筛选最近7天内修改过的 .dat 文件
seven_days_ago = time.time() - 7 * 24 * 3600

recent_files = [
    f for f in data_dir.rglob("*.dat")
    if f.stat().st_mtime > seven_days_ago
]

print(f"最近7天修改的文件数: {len(recent_files)}")

# 筛选大小在 1KB 到 10MB 之间的文件
min_size = 1024          # 1KB
max_size = 10 * 1024 * 1024  # 10MB

valid_files = [
    f for f in data_dir.rglob("*.dat")
    if min_size <= f.stat().st_size <= max_size
]

print(f"大小在范围内的文件数: {len(valid_files)}")
核心思路: 文件筛选的本质就是「条件组合」。把扩展名、文件名模式、文件属性这些条件用逻辑运算符(and、or)组合起来,就能精准定位你要的数据。

3.5 实战:一个完整的标定数据筛选脚本

说了这么多,咱们来写一个实际能用的脚本。假设场景是这样的:

  • 数据目录结构:项目名/日期/传感器ID/*.dat
  • 需要筛选出:2024年1月之后、传感器ID为01-10、文件大小大于100KB的 .dat 文件
from pathlib import Path
import re

def filter_calibration_data(root_dir):
    """
    筛选标定数据文件
    """
    root = Path(root_dir)
    if not root.exists():
        print(f"错误:目录 {root_dir} 不存在")
        return []

    # 编译正则:匹配 sensor_01.dat 到 sensor_10.dat
    pattern = re.compile(r"sensor_(0[1-9]|10)\.dat$")

    # 日期过滤:2024年1月1日之后
    from datetime import datetime
    cutoff_date = datetime(2024, 1, 1).timestamp()

    result = []

    for file_path in root.rglob("*.dat"):
        # 条件1:文件名匹配
        if not pattern.search(file_path.name):
            continue

        # 条件2:文件大小大于100KB
        if file_path.stat().st_size < 100 * 1024:
            continue

        # 条件3:修改时间在2024年1月之后
        if file_path.stat().st_mtime < cutoff_date:
            continue

        result.append(file_path)

    return result

# 使用示例
if __name__ == "__main__":
    data_dir = r"D:\CalibrationData"
    files = filter_calibration_data(data_dir)
    print(f"筛选到 {len(files)} 个符合条件的文件")
    for f in files:
        print(f"  - {f}")

这个脚本我实际用过类似的版本。当时项目组有5个人,每个人手动筛选数据,每次要花半小时。写成脚本后,5秒钟搞定。你想想看,这效率提升有多大?

3.6 避坑指南

我曾经踩过的坑:
  1. 路径编码问题: 中文路径在 Windows 下可能乱码。建议项目路径全用英文,或者统一用 UTF-8 编码。
  2. 权限问题: 有些系统目录(如 Program Files)普通用户没有写权限。脚本里最好加个 try-except 捕获 PermissionError。
  3. 符号链接: os.walk 默认会进入符号链接指向的目录,可能导致无限循环。用 followlinks=False 可以避免。
  4. 文件占用: 有些文件正在被其他程序使用,删除或移动时会报错。操作前先检查文件是否可访问。

好了,文件系统操作这块就讲到这里。下一章咱们要进入真正的数据处理环节——用 Python 读写各种标定数据格式。到时候你会发现,文件操作学好了,后面的路会顺很多。