Python 批量处理文件:路径操作与正则匹配,解锁高效重命名工作流

4次阅读
没有评论

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

想象一下,你的硬盘里散落着成千上万个文件:相机导出的混乱照片名,下载的文献资料编码不一,项目报告版本杂乱无章。面对这些无序的数据,手动逐一重命名无疑是一场噩梦,耗时耗力,且极易出错。此时,自动化工具就显得尤为珍贵。Python,凭借其简洁的语法和强大的库生态,成为了文件批量处理和重命名的不二之选。

本篇文章将带你深入探索 Python 在文件批量处理中的两大核心能力:路径操作 正则表达式匹配。我们将学习如何利用这些工具,从文件系统中精准定位目标,通过复杂的模式匹配来识别并提取信息,最终实现高效、智能的文件批量重命名。无论你是数据分析师、开发人员,还是仅仅想让自己的文件系统井井有条,本文都将为你提供一套全面的解决方案,帮助你解锁前所未有的文件管理效率。

文件处理的基石:Python 路径操作

在进行任何文件操作之前,我们首先需要知道如何与文件系统进行交互,即如何处理文件和目录的路径。Python 提供了两个主要模块来实现这一点:os 模块和 pathlib 模块。

os 模块:传统而强大的工具箱

os 模块是 Python 内置的,提供了与操作系统交互的函数。对于路径操作,我们主要关注 os.path 子模块。

  • 列出目录内容:os.listdir()
    用于获取指定目录下的所有文件和子目录的名称列表。

    import os
    # 假设 'my_files' 目录存在
    # files_in_dir = os.listdir('my_files')
    # print(f"目录内容: {files_in_dir}")
  • 路径拼接:os.path.join()
    用于安全地拼接路径,自动处理不同操作系统路径分隔符的差异。

    import os
    base_path = 'my_folder'
    file_name = 'document.txt'
    full_path = os.path.join(base_path, file_name)
    # print(f"拼接后的路径: {full_path}")
  • 判断文件 / 目录类型:os.path.isfile()os.path.isdir()
    在遍历目录时,用于区分文件和目录。

    import os
    # path_example = 'my_files/file1.txt'
    # print(f"是文件吗? {os.path.isfile(path_example)}")
  • 获取文件名和扩展名:os.path.splitext()
    将完整文件名分割成“文件主名”和“扩展名”两部分。

    import os
    filename_with_ext = 'report.2023.final.pdf'
    name, ext = os.path.splitext(filename_with_ext)
    # print(f"文件名: {name}, 扩展名: {ext}") # 输出: 文件名: report.2023.final, 扩展名: .pdf

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

pathlib 是 Python 3.4 引入的模块,它以面向对象的方式提供了更直观、更强大的路径操作功能。它封装了路径字符串,使其成为具有各种方法的 Path 对象,极大提高了代码的可读性和可维护性。

  • 创建 Path 对象与路径拼接
    Path 对象支持使用 / 运算符进行路径拼接,非常直观。

    from pathlib import Path
    base_path = Path('my_folder')
    file_name = 'document.txt'
    full_path = base_path / file_name # 自动处理分隔符
    # print(f"Path 对象: {full_path}")
  • 遍历目录:iterdir()
    Path 对象的 iterdir() 方法以迭代器形式返回目录中的所有文件和子目录的 Path 对象。

    from pathlib import Path
    # target_dir = Path('my_files')
    # if target_dir.is_dir():
    #     for item in target_dir.iterdir():
    #         print(f"目录项: {item}, 是文件吗? {item.is_file()}")
  • 获取路径信息
    Path 对象提供了丰富的属性来获取路径的各个部分。

    from pathlib import Path
    p = Path('my_files/report.2023.final.pdf')
    # print(f"文件名 (不含路径): {p.name}")       # report.2023.final.pdf
    # print(f"文件名 (不含扩展名): {p.stem}")      # report.2023.final
    # print(f"扩展名: {p.suffix}")             # .pdf
    # print(f"父目录: {p.parent}")             # my_files
  • 重命名文件:rename()
    Path 对象提供了 rename() 方法,可以直接对文件进行重命名。

    from pathlib import Path
    # original_path = Path('old_name.txt') # 假设文件存在
    # new_path = original_path.parent / 'new_name.txt'
    # original_path.rename(new_path)
    # print(f"文件已重命名为: {new_path.name}")

小结:对于现代 Python 开发,pathlib 通常是更推荐的选择,因为它提供了更清晰、更符合直觉的 API。在批量重命名时,我们会结合两者或选择其一。

精确打击:Python 正则表达式匹配

当文件名遵循某种复杂模式,或者需要从文件名中提取特定部分来构建新名称时,正则表达式(Regular Expressions,简称 RegEx 或 Regex)就成了你的利器。Python 的 re 模块提供了强大的正则表达式功能。

正则表达式基础与常用模式

正则表达式是一种用于描述字符串模式的工具。通过组合特定的字符和元字符,你可以创建出能够匹配各种文本模式的表达式。
re 模块的主要函数包括 re.search()(在字符串中查找匹配模式的第一个位置)、re.match()(从字符串开头匹配模式)、re.findall()(查找所有匹配模式)和 re.sub()(替换所有匹配模式的部分)。

常用的正则表达式元字符和特殊序列:

  • .: 匹配除换行符以外的任何单个字符。
  • *, +, ?: 分别匹配前一个字符零次或多次、一次或多次、零次或一次。
  • {n}, {n,m}: 匹配前一个字符恰好 n 次、n 到 m 次。
  • [], [^...]: 字符集,匹配方括号中的任何一个字符或不在方括号中的任何字符。
  • |: 或,匹配左边或右边的表达式。
  • (), (?:): 捕获组用于分组并捕获匹配内容,非捕获组用于分组但不捕获。
  • ^, $: 分别匹配字符串的开头和结尾。
  • d, D, w, W, s, S: 分别匹配数字、非数字、字母数字下划线、非字母数字下划线、空白字符、非空白字符。

捕获组在重命名中的应用

捕获组是正则表达式中最强大的特性之一,它允许你从匹配的字符串中提取特定部分。这些被捕获的部分可以在替换操作中重新使用,是实现智能重命名的关键。

import re

# 场景:文件名 'DSC00123_holiday_2023.jpg',提取照片编号和年份
filename = 'DSC00123_holiday_2023.jpg'
# 模式: DSC + (任意数字一次或多次) + _holiday_ + (四个数字) + . + (jpg 或 png)
# (d+) 捕获数字 ID, (d{4}) 捕获年份, (jpg|png) 捕获扩展名
pattern = r'DSC(d+)_.*?_(d{4}).(jpg|png)'

match = re.search(pattern, filename)

if match:
    photo_id = match.group(1) # 第一个捕获组的内容
    year = match.group(2)     # 第二个捕获组的内容
    ext = match.group(3)      # 第三个捕获组的内容
    # print(f"照片 ID: {photo_id}, 年份: {year}, 扩展名: {ext}")

    # 使用捕获组构建新文件名,例如:Vacation_Photo_2023_00123.jpg
    new_filename_example = f"Vacation_Photo_{year}_{photo_id}.{ext}"
    # print(f"新文件名示例: {new_filename_example}")
else:
    # print("文件名不匹配模式。")
    pass

# re.sub() 的应用:将文件名中的日期格式从 YYYY-MM-DD 转换为 YYYYMMDD
filename_with_date = 'report_2023-10-26_final.docx'
# 匹配 YYYY-MM-DD 格式的日期,并捕获年、月、日
# 1, 2, 3 引用捕获组的内容
new_filename_transformed = re.sub(r'_(d{4})-(d{2})-(d{2})', r'_123', filename_with_date)
# print(f"转换后的文件名: {new_filename_transformed}") # report_20231026_final.docx

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

现在,我们将路径操作和正则表达式结合起来,实现一些常见的批量重命名场景。重要提示:在执行任何批量文件操作时,强烈建议先进行“模拟运行”(dry run),只打印将要执行的操作,而不实际修改文件。确认无误后再执行实际操作。同时,对重要文件进行备份是明智之举。

场景一:为所有 JPG 图片添加前缀

假设你有一个文件夹,里面有许多 IMG_xxxx.JPG 格式的照片,你想把它们都改成 Holiday_IMG_xxxx.JPG

import os
import re
from pathlib import Path
import shutil # 用于创建和清理演示目录

source_dir = Path('my_photos_prefix_demo')

# 为演示目的创建临时目录和文件
if source_dir.exists():
    shutil.rmtree(source_dir)
source_dir.mkdir()
(source_dir / 'IMG_1234.JPG').touch()
(source_dir / 'IMG_5678.JPG').touch()
(source_dir / 'document.pdf').touch()

prefix = "Holiday_"
# 匹配 IMG_xxxx.JPG,不区分大小写,并捕获原始文件名(不含扩展名)jpg_pattern = re.compile(r'^(IMG_d{4}).JPG$', re.IGNORECASE)

print(f"n--- 批量添加前缀(模拟运行)---")
for file_path in source_dir.iterdir():
    if file_path.is_file():
        match = jpg_pattern.search(file_path.name)
        if match:
            original_name = file_path.name
            new_file_name = prefix + original_name
            print(f"将'{original_name}'重命名为'{new_file_name}'")
            # 若要实际执行重命名,取消下一行的注释,并注释掉上面的 print 语句
            # file_path.rename(file_path.parent / new_file_name)

# 实际操作前,请务必进行模拟运行。以下为清理代码:# shutil.rmtree(source_dir)

场景二:根据捕获组重组文件名

假设你的文件名为 Report_ProjectX_20231026_Final.pdf,你希望将其重命名为 2023-10-26_ProjectX_Report_Final.pdf,即重新组织项目名、日期和报告类型。

import re
from pathlib import Path
import shutil

source_dir = Path('my_reports_reorganize_demo')

# 为演示目的创建临时目录和文件
if source_dir.exists():
    shutil.rmtree(source_dir)
source_dir.mkdir()
(source_dir / 'Report_ProjectA_20231026_Final.pdf').touch()
(source_dir / 'Report_ProjectB_20231101_Draft.pdf').touch()
(source_dir / 'image.jpg').touch()

# 模式:Report_ (ProjectName) _ (YYYYMMDD) _ (Version) . (ext)
# 捕获组:1:ProjectName, 2:YYYY, 3:MM, 4:DD, 5:Version, 6:ext
report_pattern = re.compile(r'^Report_([a-zA-Z0-9]+)_(d{4})(d{2})(d{2})_([a-zA-Z]+).(pdf|doc|docx)$')

print(f"n--- 批量重组文件名(模拟运行)---")
for file_path in source_dir.iterdir():
    if file_path.is_file():
        match = report_pattern.search(file_path.name)
        if match:
            project_name = match.group(1)
            year = match.group(2)
            month = match.group(3)
            day = match.group(4)
            version = match.group(5)
            ext = match.group(6)

            # 构建新文件名:YYYY-MM-DD_ProjectName_Report_Version.ext
            new_file_name = f"{year}-{month}-{day}_{project_name}_Report_{version}.{ext}"

            print(f"将'{file_path.name}'重命名为'{new_file_name}'")
            # 若要实际执行重命名,取消下一行的注释,并注释掉上面的 print 语句
            # file_path.rename(file_path.parent / new_file_name)

# 实际操作前,请务必进行模拟运行。以下为清理代码:# shutil.rmtree(source_dir)

综合案例:递归重命名子目录中的特定文件

现在我们来解决一个更复杂的场景:你有一个包含多个子目录的项目文件夹。每个子目录中都可能有一些 backup_old_document_v1.txt 这样的文件,你希望将它们统一重命名为 document_v1_backup.txt,并且这个操作要递归地应用于所有子目录。

import os
import re
from pathlib import Path
import shutil

# 定义根目录
root_dir = Path('project_data_recursive_demo')

# --- 准备演示数据 ---
if root_dir.exists():
    shutil.rmtree(root_dir) # 清理旧的演示目录
root_dir.mkdir()
(root_dir / 'dir_a').mkdir()
(root_dir / 'dir_a' / 'sub_dir_x').mkdir()
(root_dir / 'dir_b').mkdir()

(root_dir / 'important.txt').touch()
(root_dir / 'dir_a' / 'backup_old_document_v1.txt').touch()
(root_dir / 'dir_a' / 'report_2023.pdf').touch()
(root_dir / 'dir_a' / 'sub_dir_x' / 'backup_old_spreadsheet_v2.xlsx').touch()
(root_dir / 'dir_b' / 'another_file.log').touch()
(root_dir / 'dir_b' / 'backup_old_presentation_v3.pptx').touch()

# --- 定义重命名逻辑 ---
# 匹配模式:backup_old_ (documentName) _ (version) . (extension)
# 捕获组:1: documentName, 2: version, 3: extension
rename_pattern = re.compile(r'^backup_old_([a-zA-Z]+)_(vd+).(txt|xlsx|pptx)$')

def batch_rename_recursive(directory: Path, pattern: re.Pattern, dry_run: bool = True):
    """
    递归地遍历目录,并根据正则表达式模式重命名文件。Args:
        directory (Path): 要处理的根目录。pattern (re.Pattern): 用于匹配文件名的正则表达式模式。dry_run (bool): 如果为 True,则只打印将执行的操作,不实际修改文件。"""
    for item_path in directory.iterdir():
        if item_path.is_dir():
            batch_rename_recursive(item_path, pattern, dry_run) # 递归调用
        elif item_path.is_file():
            match = pattern.search(item_path.name)
            if match:
                original_name = item_path.name
                doc_name = match.group(1)
                version = match.group(2)
                extension = match.group(3)

                # 构建新文件名:{documentName}_{version}_backup.{extension}
                new_file_name = f"{doc_name}_{version}_backup.{extension}"
                new_file_path = item_path.parent / new_file_name

                if dry_run:
                    print(f"[模拟运行] 将'{original_name}'(在'{item_path.parent}') 重命名为'{new_file_name}'")
                else:
                    try:
                        if new_file_path.exists():
                            print(f"[警告] 新文件'{new_file_name}'已存在于'{item_path.parent}',跳过重命名'{original_name}'。")
                            continue
                        item_path.rename(new_file_path)
                        print(f"[成功] 将'{original_name}'(在'{item_path.parent}') 重命名为'{new_file_name}'")
                    except OSError as e:
                        print(f"[错误] 重命名'{original_name}'失败: {e}")

print(f"n--- 递归重命名(模拟运行)---")
batch_rename_recursive(root_dir, rename_pattern, dry_run=True)

print(f"n--- 递归重命名(实际执行)---")
# batch_rename_recursive(root_dir, rename_pattern, dry_run=False) # 实际运行时取消注释

# --- 再次打印目录结构,查看结果 (仅在实际运行后才可见变更) ---
# print(f"n--- 重命名后的文件结构 ---")
# for p in root_dir.rglob('*'):
#     print(f"- {p}")

# --- 清理演示数据 ---
# shutil.rmtree(root_dir)
# print(f"n--- 删除演示目录'{root_dir}'---")

高级技巧与注意事项

  1. 错误处理:在实际应用中,务必添加 try-except 块来捕获可能发生的错误,如文件不存在、权限不足、新旧文件名相同、新文件名已存在等,增强脚本的健壮性。
  2. 编码问题:在处理包含非 ASCII 字符的文件名时,确保你的操作系统和 Python 环境使用正确的字符编码(通常是 UTF-8)。
  3. 性能考虑:对于包含大量文件(数十万甚至上百万)的目录,直接遍历可能会比较慢。可以考虑使用 os.scandir() 替代 os.listdir()Path.iterdir(),因为它返回的是一个迭代器,效率更高。
  4. 跨平台兼容性os.pathpathlib 都已处理了路径分隔符的跨平台问题。但文件名本身可能存在跨平台兼容性问题(例如 Windows 不允许文件名包含某些字符)。
  5. 版本控制:对于重要的文件或项目,在进行大规模重命名操作前,考虑将其置于版本控制系统(如 Git)下,以便在出现问题时能够轻松回滚。

总结

Python 结合其强大的 ospathlib 模块进行路径操作,以及 re 模块进行正则表达式匹配,为文件批量处理和重命名提供了无与伦比的灵活性和效率。从简单的文件筛选到复杂的模式提取与重组,Python 都能游刃有余。掌握这些技能,你将能够将繁琐的手动文件管理任务转化为自动化脚本,极大地提升你的工作效率。

记住,安全第一:永远先进行模拟运行,并对重要数据进行备份,以确保自动化操作万无一失。现在,就拿起你的 Python 武器,开始整理你的数字世界吧!

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