>>> from env_helper import info; info()
页面更新时间: 2023-06-30 19:19:17
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-9-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

3.6. 项目:将带有美国风格日期的文件改名为欧洲风格日期

假定你的老板用电子邮件发给你上千个文件,文件名包含美国风格的日期 (MM-DD-YYYY),需要将它们改名为欧洲风格的曰期(DD-MM-YYYY)。 手工完成这个无聊的任务可能需要几天时间!让我们写一个程序来完成它。

下面是程序要做的事:

  • 检查当前工作目录的所有文件名,寻找美国风格的日期。

  • 如果找到,将该文件改名,交换月份和日期的位置,使之成为欧洲风格。这意味着代码需要做下面的事情:

  • 创建一个正则表达式,可以识别美国风格日期的文本模式。

  • 调用 os.listdir(),找出工作目录中的所有文件。

  • 循环遍历每个文件名,利用该正则表达式检查它是否包含曰期。

  • 如果它包含日期,用 shutil.move() 对该文件改名。

对于这个项目,打开一个新的文件编辑器窗口, 将代码保存为 renameDates.py

3.6.1. 第1步:为美国风格的日期创建一个正则表达式

程序的第一部分需要导入必要的模块,并创建一个正则表达式, 它能识别 MM-DD-YYYY 格式的日期。TODO 注释将提醒你, 这个程序还要写什么。将它们作为 TODO,就很容易利用 IDLE的 Ctrl-F 查找功能找到它们。让你的代码看起来像这样:

#! python3
# renameDates.py - Renames filenames with American MM-DD-YYYY date format
# to European DD-MM-YYYY.

import shutil, os, re

# Create a regex that matches files with the American date format.
datePattern = re.compile(r"""^(.*?) # all text before the date
    ((0|1)?\d)- # one or two digits for the month
    ((0|1|2|3)?\d)- # one or two digits for the day
    ((19|20)\d\d) # four digits for the year
    (.*?)$ # all text after the date
    """, re.VERBOSE)

# TODO: Loop over the files in the working directory.

# TODO: Skip files without a date.

# TODO: Get the different parts of the filename.

# TODO: Form the European-style filename.

# TODO: Get the full, absolute file paths.

# TODO: Rename the files.

通过本章,你知道 shutil.move() 函数可以用于文件改名: 它的参数是要改名的文件名,以及新的文件名。 因为这个函数存在于 shutil 模块中, 所以你必须导入该模块。

在为这些文件改名之前,需要确定哪些文件要改名。 文件名如果包含 spam4-4-1984.txt 和01-03-2014eggs.zip 这样的日期, 就应该改名,而文件名不包含日期的应该忽略, 诸如 littlebrother.epub

可以用正则表达式来识别该模式。在开始导入 re 模块后,调用 rexompile() 创建 一个 Regex 对象。 传入 re.VERBOSE 作为第二参数, 这将在正则表达式字符串中允许空白字符和注释, 让它更可读。

正则表达式字符串以 ^(.*?) 开始, 匹配文件名开始处、日期出现之前的任何文本。 ((0|l)?\d) 分组匹配月份。第一个数字可以是0或1, 所以正则表达式匹配12,作为十二月份,也会匹配02, 作为二月份。这个数字也是可选的, 所以四月份可以是04或4。 日期的分组是 ((0|l|2|3)?\d) , 它遵循类似的逻辑。3、03和31是有效的日期数字 (是的,这个正则表达式会接受一些无效的日期, 诸如4-31-2014、2-29-2013和0-15-2014。 日期有许多特例,很容易被遗漏。为了简单, 这个程序中的正则表达式已经足够好了)。

虽然1885是一个有效的年份,但你可能只在寻找20世纪和21世纪的年份。 这防止了程序不小心匹配非日期的文件名,它们和日期格式类似,诸如 10-10-1000.txt

正则表达式的 (.*?)$ 部分,将匹配日期之后的任何文本。

3.6.2. 第2步:识别文件名中的日期部分

接下来,程序将循环遍历 os.listdir() 返回的文件 名字符串列表,用这个正则表达式匹配它们。 文件名不包含日期的文件将被忽略。如果文件名包含日期, 匹配的文本将保存在几个变量中。 用下面的代码代替程序中前3个 TODO:

#! python3
# renameDates.py - Renames filenames with American MM-DD-YYYY date format
# to European DD-MM-YYYY.
--snip--
# Loop over the files in the working directory.
for amerFilename in os.listdir('.'):
    mo = datePattern.search(amerFilename)

    # Skip files without a date.
    if mo == None:
        continue
        # Get the different parts of the filename.
        beforePart = mo.group(1)
        monthPart = mo.group(2)
        dayPart = mo.group(4)
        yearPart = mo.group(6)
        afterPart = mo.group(8)

--snip--

如果 search() 方法返回的 Match 对象是 None, 那么 amerFilename 中的文件名 不匹配该正则表达式。 continue 语句将跳过 循环剩下的部分,转向下一个文件名。

否则,该正则表达式分组匹配的不同字符串, 将保存在名为 beforePartmonthPartdayPartyearPartafterPar 的变量中。这些变量中的字符串 将在下一步中使用,用于构成欧洲风格的文件名。

为了让分组编号直观,请尝试从头阅读该正则表达式, 每遇到一个左括号就计数加一。不要考虑代码, 只是写下该正则表达式的框架。 这有助于使分组变得直观,例如:

datePattern = re.compile(r"""^(1) # all text before the date
(2 (3) )-  # one or two digits for the month
(4 (5) )-  # one or two digits for the day
(6 (7) )   # four digits for the year
(8)$       # all text after the date
""", re.VERBOSE)

这里,编号1至8代表了该正则表达式中的分组。 写出该正则表达式的框架, 其中只包含括号和分组编号。 这让你更清楚地理解所写的正则表达式, 然后再转向程序中剩下的部分。

3.6.3. 第3步:构成新文件名,并对文件改名

作为最后一步,连接前一步生成的变量中的字符串, 得到欧洲风格的日期:曰期在月份之前。 用下面的代码代替程序中最后3个 TODO :

>>> #! python3
>>> # renameDates.py - Renames filenames with American MM-DD-YYYY date format
>>> # to European DD-MM-YYYY.
>>>
>>> import shutil, os, re
>>>
>>> # Create a regex that matches files with the American date format.
>>> datePattern = re.compile(r"""^(.*?) # all text before the date
>>>     ((0|1)?\d)- # one or two digits for the month
>>>     ((0|1|2|3)?\d)- # one or two digits for the day
>>>     ((19|20)\d\d) # four digits for the year
>>>     (.*?)$ # all text after the date
>>>     """, re.VERBOSE)
>>>
>>>
>>> for amerFilename in os.listdir('.'):
>>>     mo = datePattern.search(amerFilename)
>>>     print(mo)
>>>
>>>     # Skip files without a date.
>>>     if mo == None:
>>>         continue
>>>     else:
>>>         # Get the different parts of the filename.
>>>         beforePart = mo.group(1)
>>>         monthPart = mo.group(2)
>>>         dayPart = mo.group(4)
>>>         yearPart = mo.group(6)
>>>         afterPart = mo.group(8)
>>>
>>>         # Form the European-style filename.
>>>         euroFilename = beforePart + dayPart + '-' + monthPart + '-' + yearPart + afterPart
>>>
>>>         # Get the full, absolute file paths.
>>>         absWorkingDir = os.path.abspath('.')
>>>         amerFilename = os.path.join(absWorkingDir, amerFilename)
>>>         euroFilename = os.path.join(absWorkingDir, euroFilename)
>>>
>>>         # Rename the files.
>>>         print('Renaming "%s" to "%s"...' % (amerFilename, euroFilename))
>>>         shutil.move(amerFilename, euroFilename) # uncomment after testing
None
None
None
None
None
None
None
None
None
None
None
None
None
None
None
None
None

将连接的字符串保存在名为 euroHlename 的变量中。 然后将 amerFilename 中原来的文件名和新 的 euroFilename 变量传递给 shutil.move() 函数, 将该文件改名。

这个程序将 shutil.move() 调用注释掉, 代之以打印出将被改名的文件名。 先像这样运行程序,你可以确认文件改名是正确的。 然后取消 shutil.move() 调用的注释, 再次运行该程序,确实将这些文件改名。

3.6.4. 第4步:类似程序的想法

有很多其他的理由,导致你需要对大量的文件改名。

  • 为文件名添加前缀,诸如添加 spam_ ,将 eggs.txt 改名为 spam_eggs.txt

  • 将欧洲风格日期的文件改名为美国风格日期。

  • 删除文件名中的0,诸如 spam0042.txt