阅读第 1 部分:使用 Python 为数字艺术家自动化重复性任务
如果您从事数字艺术工作已经有一段时间了,那么文件管理的重要性对您来说应该是显而易见的。如果您与其他艺术家合作,这一点就更加重要。每个人都有自己喜欢的命名约定和项目目录结构。当您尝试查找本应以某种方式命名的文件时,但您的合作伙伴之一认为以三个臭皮匠的双关语命名每个文件会更有趣时,这可能会非常令人沮丧。(嘿,这种情况会发生的!)
一旦您开始使用脚本自动化流程的某些部分,这种挫败感就会加剧。现在是您的代码,而不是您,无法找到正确的文件。更糟糕的是,大多数脚本都不会寻找轻微命名更改的解决方法。它们根本无法工作。
幸运的是,通过几行相对简单的代码,您可以帮助缓解此类问题。让我们来看一个并非由自娱自乐的合作者引起的问题示例。有时问题可能是您自己的错误。我个人从不(咳咳)犯错。但偶尔,我使用的程序会完全按照我告诉它们的方式执行,而不是按照我打算让它们执行的方式执行。
案例分析
动画是我工作的重要组成部分。在创建动画或视觉效果时,将动画作品的每一帧输出(渲染)为单独的图像文件是一种好的做法。(有时您每帧渲染多个图像,但请允许我从简化开始。)通常,这些动画帧都放在硬盘上的单独目录中。
现在,我不确定这种情况是否发生在其他人身上,或者只是我的一个怪癖,但有时(在多个软件包中)我选择了我的渲染文件应该去的目录,但随后出现了一个小故障。(有些人可能会说“用户错误”,但请记住,我从不犯错。)我的渲染并没有保存到 project/render/
,而是遗漏了最后一个斜杠,并且每个渲染的图像文件都以单词“render”开头,而不是进入 render
目录。也就是说,我希望我的动画的第 1 帧是 project/render/frame0001.png
,但程序却创建了 project/renderframe0001.png
。现在我的主项目目录中充斥着数千个渲染文件。真糟糕。
我有几个选择。简单的解决方案是将所有这些渲染文件移动到正确的目录,并简单地容忍糟糕的命名。但是,如果我的后期制作步骤期望没有以 render 单词开头的命名结构,这可能会有问题。如果我需要重新渲染,情况可能会变得更加复杂。
说到重新渲染,这可能是另一种选择。我可以删除所有命名不正确的文件,修复动画程序中的输出路径,然后重新进行所有渲染。然而,问题在于,渲染动画帧有时非常耗时。对于复杂的场景,单帧可能需要一个多小时。将其乘以每秒 24-30 帧的动画,我们可以很快看到重新渲染并不是我们希望的快速简便的解决方案。
当然,总是有手动选项:将所有渲染文件移动到正确的目录,然后逐个更改每个文件的名称。当然,如果您只有几十帧,那可能不算太麻烦。如果您有数千帧动画,那非常麻烦。
Python 脚本解决方案
那么还剩下什么?没错:编写脚本!与本系列的第 1 部分一样,我们将使用 Python 来完成这项工作。在第 1 部分中,我们使用了 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 条评论