回顾系列文章“艺术家 Python 技巧”
脚本是自动化创意工作中繁琐部分的好方法,让您可以将大部分精力集中在有趣和有意思的部分。然而,有时一个快速的一次性脚本会被频繁使用,以至于它变成了一个实用工具。当这种情况发生时,仅仅让脚本成为一个快速的东西,在没有与您交互的情况下自行运行通常是不够的。脚本需要接受输入参数,向您提出有意义的问题,然后根据这些问题采取行动。
让我们以本系列前一篇文章中的脚本为例,第 3 部分:使用 Python 查找损坏的图像。它是一个方便的小工具,用于列出目录中任何损坏的图像文件。这很棒,但是不必将该脚本复制到您想要测试坏图像的每个目录中肯定会更方便。此外,如果您的脚本可以在找到损坏的图像后为您删除它们,那就更好了。当然,删除文件总是有点可怕。您想确保只删除坏文件,因此脚本提供一些提示和确认会很好,所以我们来做这件事。
接下来是上一篇文章末尾存在的脚本
from os import listdir
from PIL import Image
for filename in listdir('./'):
if filename.endswith('.png'):
try:
img = Image.open('./'+filename) # open the image file
img.verify() # verify that it is, in fact an image
except (IOError, SyntaxError) as e:
print('Bad file:', filename) # print out the names of corrupt files
接受命令行参数
要添加的第一个有用的便利之处是让您的脚本在您选择的任何任意目录中工作。您可以让脚本提示您输入目录名称,但我更喜欢在实际运行脚本时将其作为输入参数。这两种方式都有道理,但对我而言,主要是为了偷懒。如果我在运行脚本时指定目录路径
python remove-corrupt-pngs.py /path/to/directory
我可以利用 shell 内置的制表符补全功能,因此,输入更少(好吧,通过更少地按下键盘上的按钮来输入相同的量)。
要从命令行获取输入(或参数),最简单的方法是使用另一个 Python 的默认模块:sys。具体来说,您对 sys.argv 感兴趣,这是一个单词列表(以空格分隔),在您启动脚本时同时输入。列表是在 Python 脚本中经常使用的基本数据结构。将它们视为所有分组在同一名称下的变量序列。
接下来是一个快速的小测试示例。创建一个名为 list_test.py 的新 Python 脚本,并用以下代码填充它
import sys
print('Length of list:', len(sys.argv))
print(sys.arv)
保存您的脚本。现在,当您从命令行运行它时(python list_test.py),您的脚本的输出应如下所示
Length of list: 1
['list_test.py']
尝试在命令行上添加一个或两个参数(例如,python list_test.py blahblah blah)。脚本将输出如下内容
Length of list: 3
['list_test.py, 'blahblah', 'blah']
首先要注意的是如何找到列表的长度,这就像在列表上使用 len() 函数一样简单。接下来要注意的是列表本身的表示法。这在本系列的第 1 篇文章中已经介绍过,第 1 部分:使用 Python 为数字艺术家自动化重复性任务,但为了快速回顾,列表用方括号 ([ ]) 包围,每个列表项用逗号分隔。在 sys.argv 列表的情况下,您的脚本名称始终是该列表中的第一个项目。之后的每个单词都是列表中的后续项目。您可以通过指定其索引(一个整数),在列表名称后的方括号内(例如,sys.argv[0])来单独访问列表中的每个项目。
关于索引的快速题外话
现在值得花点时间来谈谈索引。虽然大多数人(和一些奇怪的语言——我看着你 Lua 和 MATLAB)从 1 开始计数,但 Python 和大多数现代编程语言不是;它们的计数从零开始。因此,即使您的脚本名称是 sys.argv 列表中的第一个项目,您也可以使用 sys.argv[0] 访问它。如果您使用 sys.argv[1],您将获得列表中的第二个项目(在前面的示例中,这将是 blahblah)。
现在回到脚本
既然您知道如何检查列表的长度并访问其中的各个元素,那么您可以使您的损坏图像查找脚本更智能一些。首先,假设脚本名称后的第一个参数是您要检查的图像目录的路径。将该路径分配给它自己的变量是明智的。我将其称为 imgdir。要将您的路径参数分配给 imgdir 变量,您只需使用 imgdir = sys.argv[1]。
但有一个问题:如果您忘记在启动脚本时指定目录名称怎么办?如果您的脚本会检查这一点,那就太好了。幸运的是,您可以检查 sys.argv 列表的长度。如果 sys.argv 列表不是 2,那么您就知道尚未指定路径。代码块看起来像这样
if len(sys.argv) == 2:
imgdir = sys.argv[1]
else:
print('You need to include a path to the directory you want to check.')
exit(2)
这个小代码块检查 sys.argv 列表中是否有两个项目,并将第二个项目(索引 1)分配给 imgdir 变量。否则,它会打印错误消息并退出 (exit(2))。作为奖励,如果您的目录路径中有空格,但您没有正确地将其括在引号中,它也会退出。(文件名中的空格对于脚本编写来说可能很麻烦。避免在您的文件命名约定中出现这种糟糕的情况。)
将此代码块添加到脚本的顶部。然后,您需要使您的脚本利用该 imgdir 目录,这非常简单,因为它主要只是将 './' 替换为 imgdir。完成此操作后,您的脚本应如下所示
import sys
from os import listdir
from PIL import Image
if len(sys.argv) == 2:
imgdir = sys.argv[1]
else:
print('You need to include a path to the directory you want to check.')
exit(2)
for filename in listdir(imgdir):
if filename.endswith('.png'):
try:
img = Image.open(imgdir+'/'+filename) # open the image file
img.verify() # verify that it is, in fact an image
except (IOError, SyntaxError) as e:
print('Bad file:', filename) # print out the names of corrupt files
现在您可以从任何地方运行您的脚本并检查您的计算机可以访问的任何目录;不再需要在各处复制相同的脚本。
提示用户输入
让您的脚本为您删除这些坏文件实际上非常容易。您只需要使用 os 模块中内置的 remove() 函数即可。但是,现在还不要这样做。
当您删除文件时,始终要格外小心。文件一旦被删除就消失了。因此,您应该让删除文件的脚本首先告诉您它要删除哪些文件,然后征求您的确认,因此您应该首先将这一部分添加到脚本中。
在 Python 脚本中提示输入的最简单方法是使用 raw_input() 函数。从表面上看,它看起来很像 print() 函数。您在括号之间放置一个字符串,Python 会将其输出到屏幕上。但是,不同之处在于 raw_input() 然后会等待您输入内容并按 Enter 键。您输入的任何内容都存储在一个变量中。然后,您的脚本可以读取该变量的内容并决定如何采取行动。
对于此特定脚本,您希望它询问是否应删除特定文件。该行代码可能如下所示
delete_files = raw_input('Would you like to remove this bad image? (y/N)')
用户输入的任何内容都将作为字符串存储在 delete_files 变量中。对于删除文件,我喜欢处理它的方式是将“no”用作默认响应。只有当用户输入“yes”的某些变体时,脚本才会继续删除任何内容。此代码块看起来像这样
if delete_files in ['Y', 'y', 'Yes', 'yes', 'YES']:
print('Removing bad image.')
# code for deleting image goes here (we'll get to it, promise)
else:
print('Leaving bad image in place.')
将到目前为止的所有部分组合在一起,您的脚本应如下所示
import sys
from os import listdir
from PIL import Image
if len(sys.argv) == 2:
imgdir = sys.argv[1]
else:
print('You need to include a path to the directory you want to check.')
exit(2)
for filename in listdir(imgdir):
if filename.endswith('.png'):
try:
img = Image.open(imgdir+'/'+filename) # open the image file
img.verify() # verify that it is, in fact an image
except (IOError, SyntaxError) as e:
print('Bad file:', filename) # print out the names of corrupt files
delete_files = raw_input('Would you like to remove this bad image? (y/N)')
if delete_files in ['Y', 'y', 'Yes', 'yes', 'YES']:
print('Removing bad image.')
# code for deleting image goes here (we'll get to it, promise)
else:
print('Leaving bad image in place.')
但是,此设置有一个问题。如果您的脚本找到很多损坏的图像怎么办?必须单独确认删除每个图像将非常乏味和烦人。让您的脚本告诉您所有坏图像,并要求您一次性删除它们会更好。
虽然这是一个好主意,但您需要重新思考脚本中的一些逻辑。别担心,代码方面不是什么大事。只是一个小的“编排”调整。到目前为止,您的脚本一直在立即处理坏文件,在找到它们时立即打印其名称。但是,如果您的脚本要同时处理所有坏文件,那么您将需要某种机制来存储每个坏文件的名称。您将需要另一个列表。
您的脚本将首先将坏文件的名称存储在列表中,您可以将其命名为 badfiles。您可以在 Python 中创建一个空列表,如下所示
badfiles = []
您只需使用等号和一对空方括号,就完成了。但您可能会发现自己问:“我为什么需要一个空列表?我以为这应该是一个损坏图像的列表,而不是一个空列表。”
好问题。简短的回答是,如果列表不存在,您就无法向其中添加内容。如果您不从一个空篮子开始,就很难为野餐收集食物。有了空列表,您需要一种机制将每个坏文件的名称添加到列表中。幸运的是,Python 的内置列表有一种非常好的处理方式。每个列表都有一个 append() 函数,您可以使用它向列表中添加项目。它的用法如下所示
badfile.append('corrupt.png')
这假设坏图像的名称是“corrupt.png”。当然,您不需要键入实际的文件名;您已经有一个变量用于此目的。在您的脚本中,不是在找到坏文件时打印其名称,而是将该文件的名称附加到列表中。然后,一旦脚本完成检查目录中是否有坏图像,它就可以将该文件列表打印到屏幕上,然后询问您是否要删除它们。
当您这样做时,您的脚本开始看起来像这样
import sys
from os import listdir
from PIL import Image
if len(sys.argv) == 2:
imgdir = sys.argv[1]
else:
print('You need to include a path to the directory you want to check.')
exit(2)
badfiles = [] # Empty list to store names of corrupt images
for filename in listdir(imgdir):
if filename.endswith('.png'):
try:
img = Image.open(imgdir+'/'+filename) # open the image file
img.verify() # verify that it is, in fact an image
except (IOError, SyntaxError) as e:
badfiles.append(filename) # Add bad file name to list
print('List of bad images:')
for filename in badfiles:
print(imgdir+'/'+filename) # Print each corrupted file's name with full path
print('There are,' len(badfiles), 'bad images that need to be deleted and replaced.'
delete_files = raw_input('Would you like to remove these bad images? (y/N)')
if delete_files in ['Y', 'y', 'Yes', 'yes', 'YES']:
print('Removing bad images.')
# code for deleting image goes here (we'll get to it, promise)
else:
print('Leaving bad images in place.')
删除文件
此时,您的脚本愉快地创建损坏图像的列表,并在完成检查整个目录时将该列表打印到屏幕上。现在让我们谈谈实际删除这些文件。正如我在上一节中提到的,删除就像使用 os 模块中的 remove() 函数一样简单。当然,您需要让您的脚本知道该函数。请记住,现在您的脚本不知道整个 os 模块;您只在脚本顶部导入了 listdir。
您可以只在脚本顶部添加 from os import remove 并完成它,但让我们偷懒一点。您已经从 os 导入了一个函数,所以您不妨尝试将该行重用于 remove() 函数,实际上,您可以这样做。您要从模块导入的每个函数只需要在该行中列出,并用逗号分隔。您的导入行应如下所示
from os import listdir, remove
现在您的脚本知道如何删除文件了。为减少输入而欢呼!
至于实际使用 remove() 函数,它非常容易。您只需向其提供要删除文件的完整路径,文件就会消失得无影无踪。您只需要对 badfiles 列表中的每个文件执行此操作。该过程的代码只是使用另一个 for 循环
for filename in badfiles:
remove(imgdir+'/'+filename)
将其插入您已编写的代码中(就在关于承诺删除文件的注释所在的位置),您完成的脚本应如下所示
import sys
from os import listdir, remove
from PIL import Image
if len(sys.argv) == 2:
imgdir = sys.argv[1]
else:
print('You need to include a path to the directory you want to check.')
exit(2)
badfiles = [] # Empty list to store names of corrupt images
for filename in listdir(imgdir):
if filename.endswith('.png'):
try:
img = Image.open(imgdir+'/'+filename) # open the image file
img.verify() # verify that it is, in fact an image
except (IOError, SyntaxError) as e:
badfiles.append(filename) # Add bad file name to list
print('List of bad images:')
for filename in badfiles:
print(imgdir+'/'+filename) # Print each corrupted file's name with full path
print('There are,' len(badfiles), 'bad images that need to be deleted and replaced.'
delete_files = raw_input('Would you like to remove these bad images? (y/N)')
if delete_files in ['Y', 'y', 'Yes', 'yes', 'YES']:
print('Removing bad images.')
for filename in badfiles:
remove(imgdir+'/'+filename) # Permanently delete files
else:
print('Leaving bad images in place.')
现在您有了一个可以从任何地方运行的脚本,因为它在命令行接受用户输入。更重要的是,您的脚本在执行任何危险操作(如删除文件)之前会征求许可。此外,现在您知道如何从命令行获取参数,并为您的脚本添加一些简单的用户交互。
您可以使用我介绍的技术为您编写的任何 Python 脚本添加交互性,这意味着您不必在工作目录中随机放置脚本。您可以将所有脚本保存在一个位置,并从那里使用它们。步骤更少,维护更容易,而且您仍然可以自动化繁琐的工作,以便您可以专注于您的创造性工作——有什么理由不喜欢呢?
1 条评论