Python 批量处理文件:路径操作与正则匹配批量重命名高效指南

5次阅读
没有评论

共计 9438 个字符,预计需要花费 24 分钟才能阅读完成。

在日常工作和学习中,我们经常会遇到需要对大量文件进行整理和重命名的情况。无论是整理照片、管理文档,还是处理代码仓库中的资源文件,手动操作不仅效率低下,还极易出错。幸运的是,Python 作为一门强大的脚本语言,为我们提供了优雅且高效的解决方案。本文将深入探讨如何利用 Python 的文件路径操作(ospathlib 模块)与正则表达式(re 模块)来批量处理文件,特别是实现灵活的批量重命名功能。

为什么选择 Python 进行批量文件处理?

Python 在文件操作方面拥有得天独厚的优势:

  1. 简洁易读的语法:Python 代码通常比其他语言更短,更易于理解和维护。
  2. 强大的标准库osshutilpathlibre 等模块提供了丰富的文件系统和字符串处理功能,无需安装第三方库即可完成复杂任务。
  3. 跨平台兼容性:Python 脚本可以在 Windows、macOS 和 Linux 等不同操作系统上运行,处理文件路径时会自动适应平台差异。
  4. 高度自动化:一旦脚本编写完成,只需运行即可批量处理文件,极大节省时间。

文件批量处理最常见的需求之一就是重命名。例如,你可能需要:

  • 统一文件命名格式(如 image-001.jpg, image-002.jpg)。
  • 根据文件内容或创建日期添加前缀或后缀。
  • 从文件名中提取特定信息并重新组织。
  • 删除文件名中不必要的字符或部分。

这些任务通过 Python 结合路径操作和正则表达式,都能轻松实现。

理解文件路径:ospathlib 的双剑合璧

在 Python 中处理文件,首先要掌握文件路径的表示和操作。os 模块提供了传统的基于字符串的路径操作,而 pathlib 模块则引入了面向对象的路径处理方式,使得代码更加直观和现代化。

os 模块:传统而强大的路径操作

os 模块是 Python 处理文件和目录的基础。它提供了许多用于操作系统交互的函数,包括文件路径的构建、分割、判断等。

import os

# 1. 获取当前工作目录
current_dir = os.getcwd()
print(f"当前工作目录: {current_dir}")

# 2. 列出目录内容
# os.listdir() 返回目录中所有文件和子目录的名称列表
files_and_dirs = os.listdir(current_dir)
print(f"当前目录内容: {files_and_dirs[:5]}...") # 只打印前 5 个

# 3. 拼接路径:os.path.join() 是跨平台安全的拼接方式
# 避免直接使用字符串拼接,因为不同操作系统的路径分隔符不同
file_name = "example.txt"
full_path = os.path.join(current_dir, "data", file_name)
print(f"完整路径示例: {full_path}")

# 4. 分割路径:os.path.split() 分割为目录和文件名
# os.path.splitext() 分割为文件名和扩展名
dir_path, base_name = os.path.split(full_path)
name, ext = os.path.splitext(base_name)
print(f"目录部分: {dir_path}")
print(f"文件名部分 (带扩展名): {base_name}")
print(f"文件名 (无扩展名): {name}")
print(f"扩展名: {ext}")

# 5. 判断路径类型
print(f"'{full_path}' 是文件吗? {os.path.isfile(full_path)}")
print(f"'{current_dir}' 是目录吗? {os.path.isdir(current_dir)}")
print(f"'{full_path}' 存在吗? {os.path.exists(full_path)}")

# 6. 重命名文件 (os.rename)
# os.rename("old_name.txt", "new_name.txt")
# 注意:若新文件名已存在,会覆盖,或在某些系统上报错

os 模块的函数通常接受并返回字符串形式的路径,这使得它与早期 Python 代码和习惯了字符串操作的开发者兼容性良好。

pathlib 模块:现代面向对象的路径管理

自 Python 3.4 引入的 pathlib 模块提供了一种面向对象的方式来处理文件系统路径。它将路径视为对象,提供了更直观、更链式调用的 API,大大简化了路径操作。

from pathlib import Path

# 1. 创建 Path 对象
current_path = Path.cwd()
print(f"当前工作路径 (Path 对象): {current_path}")

# 2. 列出目录内容
# Path.iterdir() 返回一个迭代器,包含目录中的 Path 对象
for item in current_path.iterdir():
    print(f"{item.name} (是文件: {item.is_file()}, 是目录: {item.is_dir()})")
    if item.is_file():
        break # 只打印第一个文件

# 3. 拼接路径:使用 / 运算符,更直观
new_file_path = current_path / "data" / "example_pathlib.txt"
print(f"Pathlib 拼接路径示例: {new_file_path}")

# 4. 获取路径的各个部分
print(f"父目录: {new_file_path.parent}")
print(f"文件名 (带扩展名): {new_file_path.name}")
print(f"文件名 (无扩展名): {new_file_path.stem}")
print(f"扩展名: {new_file_path.suffix}")

# 5. 判断路径类型
print(f"'{new_file_path}' 是文件吗? {new_file_path.is_file()}")
print(f"'{current_path}' 是目录吗? {current_path.is_dir()}")
print(f"'{new_file_path}' 存在吗? {new_file_path.exists()}")

# 6. 重命名文件 (Path.rename)
# source_path = Path("old_file.txt")
# dest_path = Path("new_file.txt")
# source_path.rename(dest_path) # 返回新的 Path 对象

# 7. 遍历指定类型文件 (更方便)
# for file in current_path.glob("*.txt"): # 查找所有.txt 文件
#     print(f"找到 txt 文件: {file.name}")

pathlib 的优势在于其直观性和链式调用能力,它将路径操作从字符串处理提升到对象操作的层面,减少了错误,并提高了代码可读性。在现代 Python 开发中,pathlib 往往是更推荐的选择。

正则表达式的魔法:精确匹配与替换的利器

当我们处理文件名时,往往需要根据复杂的模式来识别、提取或替换其中的特定部分。这时,正则表达式(Regular Expressions,简称 regex 或 regexp)就成了不可或缺的强大工具。Python 的 re 模块提供了完整的正则表达式支持。

re 模块基础

正则表达式通过一种特殊的字符序列来定义一个搜索模式。

import re

# re.search() 查找字符串中第一个匹配项
# re.match() 检查字符串开头是否匹配
# re.findall() 查找字符串中所有非重叠匹配项
# re.sub() 替换字符串中匹配的模式

text = "文件名_图片_20231026_编号 001.jpg"

# 1. 匹配日期模式 (YYYYMMDD)
pattern_date = r"d{8}" # d 表示数字,{8} 表示重复 8 次
match_date = re.search(pattern_date, text)
if match_date:
    print(f"找到日期: {match_date.group(0)}") # group(0) 返回整个匹配到的字符串

# 2. 匹配编号模式 (如 "编号 001")
pattern_id = r"编号(d+)" # () 用于捕获组,d+ 匹配一个或多个数字
match_id = re.search(pattern_id, text)
if match_id:
    print(f"找到编号 (完整): {match_id.group(0)}")
    print(f"找到编号 (仅数字): {match_id.group(1)}") # group(1) 返回第一个捕获组的内容

# 3. 替换模式:re.sub(pattern, replacement, string)
new_text = re.sub(r"图片_", "Photo-", text)
print(f"替换' 图片_': {new_text}")

# 4. 使用捕获组进行复杂替换
# 假设我们想把 "文件名_图片_20231026_编号 001.jpg"
# 变为 "IMG-20231026-001.jpg"
original_filename = "document_archive_20231026_001.pdf"
# 模式:匹配日期和编号
# r".*?(d{8}).*?(d+).(.+)"
# .*? 非贪婪匹配任意字符,(d{8}) 捕获日期,(d+) 捕获编号,(.+) 捕获扩展名
pattern_complex = r".*?(d{8}).*?(d+).(.+)"
match_complex = re.search(pattern_complex, original_filename)

if match_complex:
    date_part = match_complex.group(1) # 20231026
    id_part = match_complex.group(2)   # 001
    ext_part = match_complex.group(3)  # pdf

    new_filename_format = f"FILE-{date_part}-{id_part}.{ext_part}"
    print(f"复杂替换结果: {new_filename_format}")

常用的正则表达式元字符和语法:

  • .:匹配除换行符以外的任何单个字符。
  • *:匹配前一个字符零次或多次。
  • +:匹配前一个字符一次或多次。
  • ?:匹配前一个字符零次或一次。
  • ^:匹配字符串的开头。
  • $:匹配字符串的结尾。
  • []:匹配括号内任何一个字符(如 [aeiou] 匹配任何元音字母)。
  • [^]:匹配除括号内字符以外的任何字符。
  • |:或运算符,匹配 | 前或后的表达式。
  • ():捕获组,用于分组表达式和提取匹配到的子字符串。
  • :转义字符,将特殊字符转为普通字符,或将普通字符转为特殊字符(如 d)。
    • d:匹配任何数字(等同于 [0-9])。
    • D:匹配任何非数字字符。
    • w:匹配任何单词字符(字母、数字、下划线,等同于 [a-zA-Z0-9_])。
    • W:匹配任何非单词字符。
    • s:匹配任何空白字符(空格、制表符、换行符等)。
    • S:匹配任何非空白字符。
  • {n}:匹配前一个字符恰好 n 次。
  • {n,}:匹配前一个字符至少 n 次。
  • {n,m}:匹配前一个字符至少 n 次,但不超过 m 次。

理解并熟练运用这些语法是实现复杂文件名匹配和替换的关键。

实战演练:批量重命名文件

现在,我们将结合 pathlibre 模块,演示一个常见的批量重命名场景:假设你有一堆命名格式不统一的图片文件,例如:

  • DSC_1234.JPG
  • IMG_20231026_103005.jpg
  • Screenshot (2023-10-26 at 11.00.00 AM).png
  • vacation_photo_01.jpeg

我们希望将它们统一重命名为 Trip_Greece_YYYYMMDD_编号. 扩展名 的格式。对于没有日期的文件,可以自动生成一个通用日期或使用文件修改日期。为了简化,我们先处理带有日期和编号模式的文件,并为其他文件提供一个默认处理方案。

目标:
IMG_20231026_103005.jpg 改为 Trip_Greece_20231026_001.jpg
DSC_1234.JPG 改为 Trip_Greece_20240101_002.JPG (假设默认日期和编号)

核心步骤:

  1. 定义目标目录和重命名规则。
  2. 遍历目录中的所有文件。
  3. 对每个文件,尝试用正则表达式提取信息(如日期、原编号)。
  4. 根据提取的信息构造新的文件名。
  5. (关键!)先进行“试运行”:打印出旧文件名和新文件名,不实际执行重命名,以确保逻辑正确。
  6. 确认无误后,执行实际的重命名操作。
import re
from pathlib import Path
import os # 用于获取文件修改时间

def batch_rename_files(target_dir_path, prefix="Trip_Greece", default_date="20240101"):
    """
    批量重命名指定目录下的图片文件。尝试从文件名中提取日期和编号,若无则使用默认值。Args:
        target_dir_path (str): 目标目录的路径。prefix (str): 新文件名的前缀,例如 "Trip_Greece"。default_date (str): 无法从文件名中提取日期时使用的默认日期,格式 YYYYMMDD。"""
    target_path = Path(target_dir_path)
    if not target_path.is_dir():
        print(f"错误: 目录'{target_dir_path}'不存在或不是一个目录。")
        return

    print(f"开始在目录'{target_path}'中查找文件进行重命名...")

    # 正则表达式模式:# 1. 匹配 IMG_YYYYMMDD_HHMMSS 或 DSC_XXXX.EXT
    # 2. 匹配 YYYYMMDD 格式的日期
    # 3. 匹配数字序列作为编号

    # 模式一:匹配 IMG_YYYYMMDD_HHMMSS.EXT
    # 捕获组 1: 日期 (YYYYMMDD)
    # 捕获组 2: 扩展名
    pattern_img_date = re.compile(r"IMG_(d{8})_d{6}.(.+)", re.IGNORECASE)

    # 模式二:匹配 DSC_XXXX.EXT
    # 捕获组 1: 编号 (XXXX)
    # 捕获组 2: 扩展名
    pattern_dsc = re.compile(r"DSC_(d{4,}).(.+)", re.IGNORECASE) # 匹配 DSC_后至少 4 位数字

    # 模式三:匹配任意文件,尝试提取中间的日期和数字作为编号,最后是扩展名
    # 捕获组 1: 日期 (YYYYMMDD)
    # 捕获组 2: 编号 (d+)
    # 捕获组 3: 扩展名
    pattern_generic = re.compile(r".*?(d{8}).*?(d+).(.+)", re.IGNORECASE) # 泛型模式,用于捕获日期和数字编号

    # 用于生成新的序列号
    counter = 1

    # 存储重命名计划 (旧路径, 新路径)
    rename_plan = []

    for file_path in target_path.iterdir():
        if file_path.is_file():
            old_name = file_path.name
            new_name = old_name # 默认情况下不改变

            # 尝试匹配各种模式
            match_img = pattern_img_date.search(old_name)
            match_dsc = pattern_dsc.search(old_name)
            match_generic = pattern_generic.search(old_name)

            extracted_date = default_date
            extracted_id = f"{counter:03d}" # 格式化为三位数字,如 001

            if match_img:
                extracted_date = match_img.group(1)
                extension = match_img.group(2)
                new_name = f"{prefix}_{extracted_date}_{extracted_id}.{extension}"
            elif match_dsc:
                # 对于 DSC 文件,直接使用其编号作为新的编号
                extracted_id = match_dsc.group(1)
                extension = match_dsc.group(2)
                # 尝试获取文件修改时间作为日期
                try:
                    mtime = os.path.getmtime(file_path)
                    extracted_date = Path(file_path).stat().st_mtime # 或者 os.path.getmtime(file_path)
                    extracted_date = datetime.fromtimestamp(extracted_date).strftime('%Y%m%d')
                except Exception:
                    print(f"警告: 无法获取'{old_name}'的修改日期,使用默认日期。")
                    extracted_date = default_date
                new_name = f"{prefix}_{extracted_date}_{extracted_id}.{extension}"
            elif match_generic:
                # 尝试从泛型模式中提取日期和编号
                extracted_date = match_generic.group(1)
                extracted_id = match_generic.group(2)
                extension = match_generic.group(3)
                new_name = f"{prefix}_{extracted_date}_{extracted_id}.{extension}"
            else:
                # 如果都没有匹配到,使用默认日期和自动递增编号
                extension = file_path.suffix.lstrip('.') # 获取扩展名,并移除前导点
                # 尝试获取文件修改时间作为日期
                try:
                    import datetime
                    mtime = os.path.getmtime(file_path)
                    extracted_date = datetime.datetime.fromtimestamp(mtime).strftime('%Y%m%d')
                except Exception:
                    print(f"警告: 无法获取'{old_name}'的修改日期,使用默认日期。")
                    extracted_date = default_date

                new_name = f"{prefix}_{extracted_date}_{extracted_id}.{extension}"

            new_file_path = file_path.parent / new_name

            # 只有当新旧名称不同时才加入计划
            if new_file_path != file_path:
                rename_plan.append((file_path, new_file_path))

            counter += 1 # 无论是否重命名,序列号都递增

    if not rename_plan:
        print("没有需要重命名的文件。")
        return

    print("n--- 重命名计划 (Dry Run) ---")
    for old_p, new_p in rename_plan:
        print(f"将'{old_p.name}'重命名为'{new_p.name}'")

    confirm = input("n 以上是重命名计划。是否确认执行? (y/N):")
    if confirm.lower() == 'y':
        print("n--- 执行重命名 ---")
        for old_p, new_p in rename_plan:
            try:
                # 检查新文件是否已经存在,避免覆盖重要文件
                if new_p.exists():
                    print(f"警告:'{new_p.name}'已存在,跳过重命名'{old_p.name}'。")
                    continue
                old_p.rename(new_p)
                print(f"成功重命名'{old_p.name}'为'{new_p.name}'")
            except OSError as e:
                print(f"错误: 无法重命名'{old_p.name}'为'{new_p.name}'- {e}")
        print("n 批量重命名完成。")
    else:
        print("重命名操作已取消。")

# 示例使用:# 1. 创建一些测试文件
# 如果在实际环境中运行,请确保在一个测试目录中进行操作!# current_test_dir = Path("./test_files")
# current_test_dir.mkdir(exist_ok=True)
# (current_test_dir / "IMG_20231026_103005.jpg").touch()
# (current_test_dir / "DSC_00123.JPG").touch()
# (current_test_dir / "Screenshot (2023-10-26 at 11.00.00 AM).png").touch()
# (current_test_dir / "vacation_photo_01.jpeg").touch()
# (current_test_dir / "document_20231201_project.docx").touch()

# 在这里替换为你的目标目录
# batch_rename_files("./test_files", prefix="MyPhotos_Summer")

重要提示 :在运行任何批量文件操作脚本之前, 务必在非重要文件上进行测试,或备份你的文件!上述代码包含了“试运行”阶段 (Dry Run),会先打印出计划执行的重命名操作,等待用户确认后才实际执行。这是一个非常重要的安全机制。

进阶技巧与最佳实践

  1. 错误处理:使用 try-except 块来捕获可能发生的 OSError (文件不存在、权限不足、新文件名已存在等) 或其他异常,确保脚本的健壮性。
  2. 处理子目录:如果需要递归处理子目录中的文件,可以使用 os.walk() 函数,或者 pathlib 结合 Path.rglob() (递归 glob)。
    # Path.rglob() 示例:# for file in target_path.rglob("*.jpg"):
    #     # 处理所有子目录下的.jpg 文件
    #     pass
  3. 用户交互:通过 input() 函数获取用户输入的目录路径、新文件名格式等参数,使脚本更加通用和灵活。
  4. 日志记录:对于大型或复杂的批量操作,记录详细的日志(哪些文件被重命名、重命名前后的名称、遇到的错误等)非常有帮助。可以使用 Python 的 logging 模块。
  5. 版本控制:如果你经常需要对文件进行整理,可以考虑将你的重命名脚本放入版本控制系统(如 Git),方便管理和回溯。

总结

Python 凭借其强大的文件路径操作模块(ospathlib)和正则表达式模块(re),为我们提供了高效、灵活的批量文件处理能力。通过本文的讲解和实战演练,你应该已经掌握了如何:

  • 使用 ospathlib 进行文件路径的创建、拼接、分割和查询。
  • 运用 re 模块进行复杂的文件名模式匹配、信息提取和替换。
  • 编写一个安全的批量重命名脚本,并包含重要的“试运行”机制。

掌握这些技能,你将能够告别繁琐的手动操作,大幅提升文件管理的效率。从现在开始,尝试将 Python 应用到你的日常文件整理工作中,体验编程带来的便利吧!

正文完
 0
评论(没有评论)