阅读第一部分:使用 Python 为数字艺术家自动化重复性任务
如果您从事数字艺术创作有相当一段时间了,那么文件管理的重要性对您来说应该是显而易见的。如果您与其他艺术家合作,这一点就更加重要。每个人都有自己喜欢的命名约定和项目目录结构。当您尝试查找应该以某种方式命名的文件时,但您的合作伙们认为以《三傻瓜》的双关语命名每个文件会更有趣时,这可能会非常令人沮丧。(嘿,这种情况会发生的!)
一旦您开始使用脚本自动化流程的某些部分,这种挫败感就会加剧。现在是您的代码,而不是您,找不到正确的文件。更糟糕的是,大多数脚本都不会寻找轻微命名更改的解决方法。它们根本无法工作。
幸运的是,只需几行相对简单的代码,您就可以帮助缓解此类问题。让我们使用一个并非由自娱自乐的合作者引起的例子。有时问题可能是您自己的错误。我个人从不(咳咳)犯错。但偶尔,我使用的程序会完全按照我告诉它们的方式执行,而不是按照我希望它们执行的方式执行。
举例说明
动画是我工作的重要组成部分。在创建动画或视觉效果时,将动画作品的每一帧输出(渲染)为单独的图像文件是一种好的做法。(有时您每帧渲染多个图像,但请允许我先简化一下。)通常,这些动画帧都放在硬盘驱动器上的单独目录中。
现在,我不确定这是否发生在其他人身上,或者只是我的怪癖,但有时(在多个软件包中),我选择了我的渲染文件应该存放的目录,但随后出现了故障。(有些人可能会说“用户错误”,但请记住,我从不犯错。)我的本意是将渲染文件保存到 `project/render/`,但最后一个斜杠被遗漏了,每个渲染的图像文件都以单词 'render' 开头,而不是进入 `render` 目录。也就是说,我希望动画的第 1 帧是 `project/render/frame0001.png`,但程序却创建了 `project/renderframe0001.png`。现在我的主项目目录充斥着数千个渲染文件。真糟糕。
我有几个选择。简单的解决方案是将所有这些渲染文件移动到正确的目录,并简单地容忍糟糕的命名。但是,如果我的后期制作步骤期望的命名结构中没有每个文件名前面的 `render` 单词,这可能会有问题。如果我需要重新渲染,情况可能会变得更加复杂。
说到重新渲染,这可能是另一种选择。我可以删除命名不正确的文件,修复动画程序中的输出路径,然后重新进行所有渲染。然而,问题在于,渲染动画帧有时非常耗时。对于复杂的场景,单帧可能需要一个多小时。将其乘以每秒 24-30 帧的动画,我们可以很快看出重新渲染并不是我们希望的快速简便的解决方案。
当然,总是有手动选项:将所有渲染文件移动到正确的目录,然后逐个更改名称。当然,如果您只有几十帧,那可能不会太麻烦。如果您有数千帧动画,那将非常麻烦。
Python 脚本解决方案
那么还剩下什么?没错:编写脚本!与本系列的第一部分一样,我们将使用 Python 来完成。在第一部分中,我们使用了 `subprocess` 模块。这个例子不需要它,但它确实使用了 Python 的另一个内置模块 `os`。`os` 模块提供了一种执行任务的方法,例如移动和重命名文件,这些任务由您的操作系统处理。而且由于 Python 是跨平台的,因此 `os` 模块可以在 Python 运行的任何地方工作,而无需考虑您实际使用的操作系统。
因此,您的脚本的快速而简陋的版本可能如下所示
import os
for filename in os.listdir('./'):
if filename.startswith('renderframe'):
os.rename(filename, filename[:6]+'/'+filename[6:])
如果您以前从未编写过代码,那么此脚本中有一些您可能不熟悉的内容。让我们从您已经知道的内容开始。第一行 `import os` 使您的脚本意识到 Python 的内置 `os` 模块(类似于您在上一篇文章中导入 `subprocess` 的方式)。
下一行代码(尽管有额外的换行符)指示循环的开始。循环是脚本编写和编程中主要的节省时间的方法之一。基本上,如果您需要一遍又一遍地执行某个过程(例如重命名一堆文件),那么循环可以帮助您节省时间和保持理智。
在这个特定的例子中,您正在使用 `for` 循环,这是一种特殊的循环,您可以使用它来遍历一系列事物。在这种情况下,您正在遍历当前目录中所有文件的名称。您是如何做到的?让我向您介绍 `os.listdir`。
Python `os` 模块有一个名为 `listdir` 的函数。该函数将接受任何目录路径作为输入,并返回该目录中所有文件的列表。在本例中,您正在使用 `os.listdir('./')`。`'./'` 位是一段文本字符串(因此带有引号),它是“我现在所在的当前目录”的简写。
“太好了,”您可能会说,“所以 `os.listdir('./')` 创建了当前目录中所有文件的列表,但这与 `for` 循环有什么关系?”
好问题!您不想一次处理整个文件列表。您需要一次处理一个。`for` 循环遍历 `os.listdir('./')` 提供的文件列表。因为您的 for 循环需要一个通用名称来在处理每个文件时调用它,所以我们使用变量 `filename` 作为占位符。
了解了这一点,请看一下设置 for 循环的完整行:`for filename in os.listdir('./'):`。这行的英文翻译是,“创建当前目录中每个文件的列表。然后循环遍历文件名列表。为了简单起见,在处理每个文件时,只需将每个文件称为 `filename`。”
在脚本的下一行代码中,您位于循环中(您可以看出来,因为该行是缩进的)。因为您在循环中,所以您在此处执行的所有操作都将为当前目录中的每个文件重复执行。请记住,我们开始这样做是因为我的渲染文件最终出现在错误的位置,可能这里还有其他文件不是我的渲染帧。我们需要确保此脚本不会重命名和移动任何文件;它需要将自身限制为仅重命名和移动那些渲染帧。
过滤和重命名
幸运的是,我们有一种很好的方法可以做到这一点。我们可以根据我告诉我的动画软件产生的可怕的错误命名进行过滤。当前目录中所有位置不佳、命名不正确的文件都以 `renderframe` 开头。
对于脚本循环遍历的每个文件,它需要检查并查看该文件是否以 `renderframe` 开头。这正是这行代码的作用:`if filename.startswith('renderframe'):`。这行代码使用了脚本编写和编程中的另一个常见构造,即 `if` 语句或条件语句。它以单词 `if` 开头,然后是测试条件。该测试条件必须是真或假。如果测试条件为真,则脚本可以执行 if 语句规定的特定代码。如果测试条件为假,则会跳过该代码。
在本例中,测试条件使用 `startswith` 函数,该函数内置于 Python 中的所有字符串。顾名思义,如果字符串以您作为输入提供的任何文本位开头,则 `startswith` 函数返回 true。否则,它返回 false。因此,将 `if filename.startswith('renderframe'):` 翻译成英文,它将读作,“如果我们的文件名列表中的当前文件名以文本 `renderframe` 开头,则执行下一段代码。”
好的。您已经获得了当前目录中的文件列表,并且您已经将该列表缩小到仅包含我们放错位置的渲染文件。现在开始实际工作,将这些文件重命名并移动到它们应该在的位置。幸运的是,可以使用 `os` 模块的 `rename` 函数,在一行代码中完成此重命名和移动步骤。
`os.rename` 函数接受两个输入参数:您要重命名的文件和您要将其重命名为什么。不过,很酷的部分是,这些输入参数将文件的路径视为其名称的一部分。因此,如果您在第二个输入中包含不同的路径作为一部分,则可以一次重命名和移动文件。为减少打字欢呼吧!
现在,看看第二行文本 (`os.rename(filename, filename[:6]+'/'+filename[6:])`),前半部分非常简单。后半部分(逗号之后)是您以前可能没有遇到过的另一个小小的怪异之处。不过,这并不难理解。这只是理解您想做什么的问题。
假设您的脚本已启动,并且正在处理文件 `renderframe0001.png`。要重命名和移动文件,您只需在单词 `render` 后添加一个 `/` 字符。单词 `render` 有六个字母长。有了这个小信息,您可以使用 `filename` 变量中已有的内容为您的文件构造新路径。您只需要正确的表示法。
我一直很喜欢 Python 获取文本字符串子集的表示法。它是 `[start:end]`,其中 `start` 是子集的第一个字符,`end` 是子集中最后一个字符之后的字符。与所有理智的编程语言一样,Python 从数字零开始计数。因此,在我们的示例中,我们正在处理一个具有文本 `renderframe0001.png` 的 `filename` 变量,您可以使用 `filename[3:7]`,Python 会给您 `derf` 作为结果。
您可能会注意到您的代码不仅两次使用了此表示法,而且在每种情况下,它都缺少起始字符或结束字符的值。这是一个很酷的小技巧。如果您仅提供起始字符,但保留表示法中的冒号,Python 会假定您想要字符串中该点之后的所有字符。同样,如果您仅包含结束字符值,Python 会给您字符串中该字符之前的所有字符。在我们的示例中,`filename[:6]` 获取 `render`。`filename[6:]` 表示法获取 `frame0001.png`。
使用此技术,您在单词 `render` 后将文件名分成两半。现在您要做的就是使用额外的斜杠 (`/`) 重新组装它。因此,将所有内容放在一起,这行代码 (`os.rename(filename, filename[:6]+'/'+filename[6:])`) 翻译为,“通过在文件名的第六个字符后插入斜杠来重命名我的文件。”
添加用户反馈
这就是描述您的快速而简陋的脚本的全部内容,该脚本用于移动和重命名一大堆放错位置的文件。唯一可能值得添加到它的是一些用户反馈。如果您要移动和重命名数千个文件,可能需要一分钟左右。了解您的脚本正在处理的文件将很有用。您可以使用一个小的打印语句在重命名前执行此操作。您完成的脚本可能如下所示
import os
for filename in os.listdir('./'):
if filename.startswith('renderframe'):
print('Moving and renaming:', filename)
os.rename(filename, filename[:6]+'/'+filename[6:])
就这样!如果您像我一样,您的软件错误地完全按照您的要求执行操作,那么这几行代码可以为您节省大量时间。
此代码块还可以作为其他有用的文件管理脚本的良好起点。例如,我喜欢 Blender 合成器中的文件输出节点。即使在渲染静态(非动画)帧的多个通道时,我也使用它。然而,缺点是文件输出节点总是将当前帧号附加到它生成的每个文件的末尾。这对于动画来说很好,但当我只是渲染静态图像时,这有点烦人。我最终得到一堆以 `0001.png` 结尾的文件。
幸运的是,通过对我移动和重命名脚本进行一些小的修改,我可以轻松地一次性删除这些 `0001`。
import os
for filename in os.listdir('./'):
if filename.endswith('0001.png'):
print('Renaming:', filename)
os.rename(filename, filename[:-8]+filename[-4:])
此脚本和上一个脚本之间的差异非常小。此脚本不是处理文件名的开头,而是从文件名的末尾开始处理。因此,此脚本没有使用 `filename.startswith`,而是使用 `filename.endswith` 作为其过滤机制。并且此脚本不是在第六个字符后插入斜杠,而是修改 `0001.png` 之前的字符(即,直到倒数第八个字符的所有字符)。请注意 `filename[:-8]` 中的负数。该负值告诉 Python 从字符串的末尾而不是开头开始。
就这样!现在您有一种一次性更改一堆文件开头或结尾(或中间!)的方法。您可以节省时间并避免执行无聊的重复性任务,从而可以将精力集中在进行更有趣的创造性工作上。
5 条评论