首页 » Python编程快速上手 » Python编程快速上手全文在线阅读

《Python编程快速上手》第9章 组织文件

关灯直达底部

在上一章中,你学习了如何用Python创建并写入新文件。你的程序也可以组织硬盘上已经存在的文件。也许你曾经经历过查找一个文件夹,里面有几十个、几百个,甚至上千个文件,需要手工进行复制、改名、移动或压缩。或者考虑下面这样的任务:

  • 在一个文件夹及其所有子文件夹中,复制所有的pdf文件(且只复制pdf文件)
  • 针对一个文件夹中的所有文件,删除文件名中前导的零,该文件夹中有数百个文件,名为spam001.txt、spam002.txt、spam003.txt等。
  • 将几个文件夹的内容压缩到一个ZIP文件中(这可能是一个简单的备份系统)

所有这种无聊的任务,正是在请求用Python实现自动化。通过对电脑编程来完成这些任务,你就把它变成了一个快速工作的文件职员,而且从不犯错。

在开始处理文件时你会发现,如果能够很快查看文件的扩展名(.txt、.pdf、.jpg等),是很有帮助的。在OS X和Linux上,文件浏览器很有可能自动显示扩展名。在Windows上,文件扩展名可能默认是隐藏的。要显示扩展名,请点开Start►Control Panel►Appearance和Personalization►Folder选项。在View选项卡中,Advanced Settings之下,取消Hide extensions for known file types复选框。

9.1 shutil模块

shutil(或称为shell工具)模块中包含一些函数,让你在Python程序中复制、移动、改名和删除文件。要使用shutil的函数,首先需要import shutil。

9.1.1 复制文件和文件夹

shutil模块提供了一些函数,用于复制文件和整个文件夹。

调用shutil.copy(source, destination),将路径source处的文件复制到路径destination处的文件夹(source和destination都是字符串)。如果destination是一个文件名,它将作为被复制文件的新名字。该函数返回一个字符串,表示被复制文件的路径。

在交互式环境中输入以下代码,看看shutil.copy的效果:

 >>> import shutil, os >>> os.chdir('C://')❶ >>> shutil.copy('C://spam.txt', 'C://delicious') 'C://delicious//spam.txt'❷ >>> shutil.copy('eggs.txt', 'C://delicious//eggs2.txt') 'C://delicious//eggs2.txt'  

第一个shutil.copy调用将文件C:/spam.txt复制到文件夹C:/delicious。返回值是刚刚被复制的文件的路径。请注意,因为指定了一个文件夹作为目的地❶,原来的文件名spam.txt就被用作新复制的文件名。第二个shutil.copy调用❷也将文件C:/eggs.txt复制到文件夹C:/delicious,但为新文件提供了一个名字eggs2.txt。

shutil.copy将复制一个文件,shutil.copytree将复制整个文件夹,以及它包含的文件夹和文件。调用shutil.copytree(source, destination),将路径source处的文件夹,包括它的所有文件和子文件夹,复制到路径destination处的文件夹。source和destination参数都是字符串。该函数返回一个字符串,是新复制的文件夹的路径。

在交互式环境中输入以下代码:

>>> import shutil, os>>> os.chdir('C://')>>> shutil.copytree('C://bacon', 'C://bacon_backup')'C://bacon_backup'  

shutil.copytree调用创建了一个新文件夹,名为bacon_backup,其中的内容与原来的bacon文件夹一样。现在你已经备份了非常非常宝贵的“bacon”。

9.1.2 文件和文件夹的移动与改名

调用shutil.move(source, destination),将路径source处的文件夹移动到路径destination,并返回新位置的绝对路径的字符串。

如果destination指向一个文件夹,source文件将移动到destination中,并保持原来的文件名。例如,在交互式环境中输入以下代码:

>>> import shutil>>> shutil.move('C://bacon.txt', 'C://eggs')'C://eggs//bacon.txt'  

假定在C:/目录中已存在一个名为eggs的文件夹,这个shutil.move调用就是说,“将C:/bacon.txt移动到文件夹C:/eggs中。

如果在C:/eggs中原来已经存在一个文件bacon.txt,它就会被覆写。因为用这种方式很容易不小心覆写文件,所以在使用move时应该注意。

destination路径也可以指定一个文件名。在下面的例子中,source文件被移动并改名。

>>> shutil.move('C://bacon.txt', 'C://eggs//new_bacon.txt')'C://eggs//new_bacon.txt'  

这一行是说,“将C:/bacon.txt移动到文件夹C:/eggs,完成之后,将bacon.txt文件改名为new_bacon.txt。”

前面两个例子都假设在C:/目录下有一个文件夹eggs。但是如果没有eggs文件夹,move就会将bacon.txt改名,变成名为eggs的文件。

>>> shutil.move('C://bacon.txt', 'C://eggs')'C://eggs'  

这里,move在C:/目录下找不到名为eggs的文件夹,所以假定destination指的是一个文件,而非文件夹。所以bacon.txt文本文件被改名为eggs(没有.txt文件扩展名的文本文件),但这可能不是你所希望的!这可能是程序中很难发现的缺陷,因为move调用会很开心地做一些事情,但和你所期望的完全不同。这也是在使用move时要小心的另一个理由。

最后,构成目的地的文件夹必须已经存在,否则Python会抛出异常。在交互式环境中输入以下代码:

>>> shutil.move('spam.txt', 'c://does_not_exist//eggs//ham')Traceback (most recent call last):  File "C:/Python34/lib/shutil.py", line 521, in move    os.rename(src, real_dst)FileNotFoundError: [WinError 3] The system cannot find the path specified:'spam.txt' -> 'c://does_not_exist//eggs//ham'During handling of the above exception, another exception occurred:Traceback (most recent call last):  File "< pyshell#29>", line 1, in < module>    shutil.move('spam.txt', 'c://does_not_exist//eggs//ham')  File "C:/Python34/lib/shutil.py", line 533, in move    copy2(src, real_dst)  File "C:/Python34/lib/shutil.py", line 244, in copy2    copyfile(src, dst, follow_symlinks=follow_symlinks)  File "C:/Python34/lib/shutil.py", line 108, in copyfile    with open(dst, 'wb') as fdst:FileNotFoundError: [Errno 2] No such file or directory: 'c://does_not_exist//eggs//ham'  

Python在does_not_exist目录中寻找eggs和ham。它没有找到不存在的目录,所以不能将spam.txt移动到指定的路径。

9.1.3 永久删除文件和文件夹

利用os模块中的函数,可以删除一个文件或一个空文件夹。但利用shutil模块,可以删除一个文件夹及其所有的内容。

  • 用os.unlink(path)将删除path处的文件。
  • 调用os.rmdir(path)将删除path处的文件夹。该文件夹必须为空,其中没有任何文件和文件夹。
  • 调用shutil.rmtree(path)将删除path处的文件夹,它包含的所有文件和文件夹都会被删除。

在程序中使用这些函数时要小心!可以第一次运行程序时,注释掉这些调用,并且加上print调用,显示会被删除的文件。这样做是一个好主意。下面有一个Python程序,本来打算删除具有.txt扩展名的文件,但有一处录入错误(用粗体突出显示),结果导致它删除了.rxt文件。

import osfor filename in os.listdir:    if filename.endswith('.rxt'):os.unlink(filename)  

如果你有某些重要的文件以.rxt结尾,它们就会被不小心永久地删除。作为替代,你应该先运行像这样的程序:

import osfor filename in os.listdir:    if filename.endswith('.rxt'):#os.unlink(filename)print(filename)  

现在os.unlink调用被注释掉,所以Python会忽略它。作为替代,你会打印出将被删除的文件名。先运行这个版本的程序,你就会知道,你不小心告诉程序要删除.rxt文件,而不是.txt文件。

在确定程序按照你的意图工作后,删除print(filename)代码行,取消os.unlink(filename)代码行的注释。然后再次运行该程序,实际删除这些文件。

9.1.4 用send2trash模块安全地删除

因为Python内建的shutil.rmtree函数不可恢复地删除文件和文件夹,所以用起来可能有危险。删除文件和文件夹的更好方法,是使用第三方的send2trash模块。你可以在终端窗口中运行pip install send2trash,安装该模块(参见附录A,其中更详细地解释了如何安装第三方模块)。

利用send2trash,比Python常规的删除函数要安全得多,因为它会将文件夹和文件发送到计算机的垃圾箱或回收站,而不是永久删除它们。如果因程序缺陷而用send2trash删除了某些你不想删除的东西,稍后可以从垃圾箱恢复。

安装send2trash后,在交互式环境中输入以下代码:

>>> import send2trash>>> baconFile = open('bacon.txt', 'a') # creates the file>>> baconFile.write('Bacon is not a vegetable.')25>>> baconFile.close>>> send2trash.send2trash('bacon.txt')  

一般来说,总是应该使用send2trash.send2trash函数来删除文件和文件夹。虽然它将文件发送到垃圾箱,让你稍后能够恢复它们,但是这不像永久删除文件,不会释放磁盘空间。如果你希望程序释放磁盘空间,就要用os和shutil来删除文件和文件夹。请注意,send2trash函数只能将文件送到垃圾箱,不能从中恢复文件。

9.2 遍历目录树

假定你希望对某个文件夹中的所有文件改名,包括该文件夹中所有子文件夹中的所有文件。也就是说,你希望遍历目录树,处理遇到的每个文件。写程序完成这件事,可能需要一些技巧。好在,Python提供了一个函数,替你处理这个过程。

请看C:/delicious文件夹及其内容,如图9-1所示。

图9-1 一个示例文件夹,包含3个文件夹和4个文件

这里有一个例子程序,针对图9-1的目录树,使用了os.walk函数:

import osfor folderName, subfolders, filenames in os.walk('C://delicious'):    print('The current folder is ' + folderName)    for subfolder in subfolders:print('SUBFOLDER OF ' + folderName + ': ' + subfolder)    for filename in filenames:print('FILE INSIDE ' + folderName + ': '+ filename)    print('')  

os.walk函数被传入一个字符串值,即一个文件夹的路径。你可以在一个for循环语句中使用os.walk函数,遍历目录树,就像使用range函数遍历一个范围的数字一样。不像range,os.walk在循环的每次迭代中,返回3个值:

1.当前文件夹名称的字符串。

2.当前文件夹中子文件夹的字符串的列表。

3.当前文件夹中文件的字符串的列表。

所谓当前文件夹,是指for循环当前迭代的文件夹。程序的当前工作目录,不会因为os.walk而改变。

就像你可以在代码for i in range(10):中选择变量名称i一样,你也可以选择前面列出来的3个字的变量名称。我通常使用foldername、subfolders和filenames。

运行该程序,它的输出如下:

The current folder is C:/deliciousSUBFOLDER OF C:/delicious: catsSUBFOLDER OF C:/delicious: walnutFILE INSIDE C:/delicious: spam.txtThe current folder is C:/delicious/catsFILE INSIDE C:/delicious/cats: catnames.txtFILE INSIDE C:/delicious/cats: zophie.jpgThe current folder is C:/delicious/walnutSUBFOLDER OF C:/delicious/walnut: wafflesThe current folder is C:/delicious/walnut/wafflesFILE INSIDE C:/delicious/walnut/waffles: butter.txt.  

因为os.walk返回字符串的列表,保存在subfolder和filename变量中,所以你可以在它们自己的for循环中使用这些列表。用你自己定制的代码,取代print函数调用(或者如果不需要,就删除for循环)。

9.3 用zipfile模块压缩文件

你可能熟悉ZIP文件(带有.zip文件扩展名),它可以包含许多其他文件的压缩内容。压缩一个文件会减少它的大小,这在因特网上传输时很有用。因为一个ZIP文件可以包含多个文件和子文件夹,所以它是一种很方便的方式,将多个文件打包成一个文件。这个文件叫做“归档文件”,然后可以用作电子邮件的附件,或其他用途。

利用zipfile模块中的函数,Python程序可以创建和打开(或解压)ZIP文件。假定你有一个名为example.zip的zip文件,它的内容如图9-2所示。

图9-2 example.zip的内容

可以从http://nostarch.com/automatestuff/下载这个ZIP文件,或者利用计算机上已有的一个ZIP文件,接着完成下面的操作。

9.3.1 读取ZIP文件

要读取ZIP文件的内容,首先必须创建一个ZipFile对象(请注意大写首字母Z和F)。ZipFile对象在概念上与File对象相似,你在第8章中曾经看到open函数返回File对象:它们是一些值,程序通过它们与文件打交道。要创建一个ZipFile对象,就调用zipfile.ZipFile函数,向它传入一个字符串,表示.zip文件的文件名。请注意,zipfile是Python模块的名称,ZipFile是函数的名称。

例如,在交互式环境中输入以下代码:

 >>> import zipfile, os >>> os.chdir('C://') # move to the folder with example.zip >>> exampleZip = zipfile.ZipFile('example.zip') >>> exampleZip.namelist ['spam.txt', 'cats/', 'cats/catnames.txt', 'cats/zophie.jpg'] >>> spamInfo = exampleZip.getinfo('spam.txt') >>> spamInfo.file_size 13908 >>> spamInfo.compress_size 3828❶ >>> 'Compressed file is %sx smaller!' % (round(spamInfo.file_size / spamInfo .compress_size, 2)) 'Compressed file is 3.63x smaller!' >>> exampleZip.close  

ZipFile对象有一个namelist方法,返回ZIP文件中包含的所有文件和文件夹的字符串的列表。这些字符串可以传递给ZipFile对象的getinfo方法,返回一个关于特定文件的ZipInfo对象。ZipInfo对象有自己的属性,诸如表示字节数的file_size和compress_size,它们分别表示原来文件大小和压缩后文件大小。ZipFile对象表示整个归档文件,而ZipInfo对象则保存该归档文件中每个文件的有用信息。

❶处的命令计算出example.zip压缩的效率,用压缩后文件的大小除以原来文件的大小,并以%s字符串格式打印出这一信息。

9.3.2 从ZIP文件中解压缩

ZipFile对象的extractall方法从ZIP文件中解压缩所有文件和文件夹,放到当前工作目录中。

 >>> import zipfile, os >>> os.chdir('C://') # move to the folder with example.zip >>> exampleZip = zipfile.ZipFile('example.zip')❶ >>> exampleZip.extractall >>> exampleZip.close  

运行这段代码后,example.zip的内容将被解压缩到C:/。或者,你可以向extractall传递的一个文件夹名称,它将文件解压缩到那个文件夹,而不是当前工作目录。如果传递给extractall方法的文件夹不存在,它会被创建。例如,如果你用exampleZip.extractall('C:/ delicious')取代❶处的调用,代码就会从example.zip中解压缩文件,放到新创建的C:/delicious文件夹中。

ZipFile对象的extract方法从ZIP文件中解压缩单个文件。继续交互式环境中的例子:

>>> exampleZip.extract('spam.txt')'C://spam.txt'>>> exampleZip.extract('spam.txt', 'C://some//new//folders')'C://some//new//folders//spam.txt'>>> exampleZip.close  

传递给extract的字符串,必须匹配namelist返回的字符串列表中的一个。或者,你可以向extract传递第二个参数,将文件解压缩到指定的文件夹,而不是当前工作目录。如果第二个参数指定的文件夹不存在,Python就会创建它。extract的返回值是被压缩后文件的绝对路径。

9.3.3 创建和添加到ZIP文件

要创建你自己的压缩ZIP文件,必须以“写模式”打开ZipFile对象,即传入'w'作为第二个参数(这类似于向open函数传入'w',以写模式打开一个文本文件)。

如果向ZipFile对象的write方法传入一个路径,Python就会压缩该路径所指的文件,将它加到ZIP文件中。write方法的第一个参数是一个字符串,代表要添加的文件名。第二个参数是“压缩类型”参数,它告诉计算机使用怎样的算法来压缩文件。可以总是将这个值设置为zipfile.ZIP_DEFLATED(这指定了deflate压缩算法,它对各种类型的数据都很有效)。在交互式环境中输入以下代码:

>>> import zipfile>>> newZip = zipfile.ZipFile('new.zip', 'w')>>> newZip.write('spam.txt', compress_type=zipfile.ZIP_DEFLATED)>>> newZip.close  

这段代码将创建一个新的ZIP文件,名为new.zip,它包含spam.txt压缩后的内容。

要记住,就像写入文件一样,写模式将擦除ZIP文件中所有原有的内容。如果只是希望将文件添加到原有的ZIP文件中,就要向zipfile.ZipFile传入'a'作为第二个参数,以添加模式打开ZIP文件。

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

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

下面是程序要做的事:

  • 检查当前工作目录的所有文件名,寻找美国风格的日期。
  • 如果找到,将该文件改名,交换月份和日期的位置,使之成为欧洲风格。

这意味着代码需要做下面的事情:

  • 创建一个正则表达式,可以识别美国风格日期的文本模式。
  • 调用os.listdir,找出工作目录中的所有文件。
  • 循环遍历每个文件名,利用该正则表达式检查它是否包含日期。
  • 如果它包含日期,用shutil.move对该文件改名。

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

第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模块后,调用re.compile创建一个Regex对象❷。传入re.VERBOSE作为第二参数❸,这将在正则表达式字符串中允许空白字符和注释,让它更可读。

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

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

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

第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语句❷将跳过循环剩下的部分,转向下一个文件名。

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

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

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步:构成新文件名,并对文件改名

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

 #! python3 # renameDates.py - Renames filenames with American MM-DD-YYYY date format # to European DD-MM-YYYY. --snip--     # 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  

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

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

第4步:类似程序的想法

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

  • 为文件名添加前缀,诸如添加spam_,将eggs.txt改名为spam_eggs.txt。
  • 将欧洲风格日期的文件改名为美国风格日期。
  • 删除文件名中的0,诸如spam0042.txt。

9.5 项目:将一个文件夹备份到一个ZIP文件

假定你正在做一个项目,它的文件保存在C:/AlsPythonBook文件夹中。你担心工作会丢失,所以希望为整个文件夹创建一个ZIP文件,作为“快照”。你希望保存不同的版本,希望ZIP文件的文件名每次创建时都有所变化。例如AlsPythonBook_1.zip、AlsPythonBook_2.zip、AlsPythonBook_3.zip,等等。你可以手工完成,但这有点烦人,而且可能不小心弄错ZIP文件的编号。运行一个程序来完成这个烦人的任务会简单得多。

针对这个项目,打开一个新的文件编辑器窗口,将它保存为backupToZip.py。

第1步:弄清楚ZIP文件的名称

这个程序的代码将放在一个名为backupToZip的函数中。这样就更容易将该函数复制粘贴到其他需要这个功能的Python程序中。在这个程序的末尾,会调用这个函数进行备份。让你的程序看起来像这样:

 #! python3 # backupToZip.py - Copies an entire folder and its contents into # a ZIP file whose filename increments.❶ import zipfile, os def backupToZip(folder):     # Backup the entire contents of "folder" into a ZIP file.     folder = os.path.abspath(folder) # make sure folder is absolute     # Figure out the filename this code should use based on     # what files already exist.❷     number = 1❸     while True: zipFilename = os.path.basename(folder) + '_' + str(number) + '.zip' if not os.path.exists(zipFilename):     break number = number + 1❹     # TODO: Create the ZIP file.     # TODO: Walk the entire folder tree and compress the files in each folder.     print('Done.') backupToZip('C://delicious')  

先完成基本任务:添加#!行,描述该程序做什么,并导入zipfile和os模块❶。

定义backupToZip函数,它只接收一个参数,即folder。这个参数是一个字符串路径,指向需要备份的文件夹。该函数将决定它创建的ZIP文件使用什么文件名,然后创建该文件,遍历folder文件夹,将每个子文件夹和文件添加到ZIP文件中。在源代码中为这些步骤写下TODO注释,提醒你稍后来完成❹。

第一部分命名这个ZIP文件,使用folder的绝对路径的基本名称。如果要备份的文件夹是C:/delicious,ZIP文件的名称就应该是delicious_N.zip,第一次运行该程序时N=1,第二次运行时N=2,以此类推。

通过检查delicious_1.zip是否存在,然后检查delicious_2.zip是否存在,继续下去,可以确定N应该是什么。用一个名为number的变量表示N❷,在一个循环内不断增加它,并调用os.path.exists来检查该文件是否存在❸。第一个不存在的文件名将导致循环break,因此它就发现了新ZIP文件的文件名。

第2步:创建新ZIP文件

接下来让我们创建ZIP文件。让你的程序看起来像这样:

 #! python3 # backupToZip.py - Copies an entire folder and its contents into # a ZIP file whose filename increments. --snip--     while True:         zipFilename = os.path.basename(folder) + '_' + str(number) + '.zip' if not os.path.exists(zipFilename):     break number = number + 1     # Create the ZIP file.     print('Creating %s...' % (zipFilename))❶     backupZip = zipfile.ZipFile(zipFilename, 'w')      # TODO: Walk the entire folder tree and compress the files in each folder.     print('Done.') backupToZip('C://delicious')  

既然新ZIP文件的文件名保存在zipFilename变量中,你就可以调用zipfile.ZipFile,实际创建这个ZIP文件❶。确保传入'w'作为第二个参数,这样ZIP文件以写模式打开。

第3步:遍历目录树并添加到ZIP文件

现在需要使用os.walk函数,列出文件夹以及子文件夹中的每个文件。让你的程序看起来像这样:

 #! python3 # backupToZip.py - Copies an entire folder and its contents into # a ZIP file whose filename increments. --snip--     # Walk the entire folder tree and compress the files in each folder.❶     for foldername, subfolders, filenames in os.walk(folder):        print('Adding files in %s...' % (foldername))         # Add the current folder to the ZIP file.❷ backupZip.write(foldername)     # Add all the files in this folder to the ZIP file.❸     for filename in filenames: newBase / os.path.basename(folder) + '_' if filename.startswith(newBase) and filename.endswith('.zip')     continue # don't backup the backup ZIP files         backupZip.write(os.path.join(foldername, filename))     backupZip.close     print('Done.') backupToZip('C://delicious')  

可以在for循环中使用os.walk❶,在每次迭代中,它将返回这次迭代当前的文件夹名称、这个文件夹中的子文件夹,以及这个文件夹中的文件名。

在这个for循环中,该文件夹被添加到ZIP文件❷。嵌套的for循环将遍历filenames列表中的每个文件❸。每个文件都被添加到ZIP文件中,以前生成的备份ZIP文件除外。

如果运行该程序,它产生的输出看起来像这样:

Creating delicious_1.zip...Adding files in C:/delicious...Adding files in C:/delicious/cats...Adding files in C:/delicious/waffles...Adding files in C:/delicious/walnut...Adding files in C:/delicious/walnut/waffles...Done.  

第二次运行它时,它将C:/delicious中的所有文件放进一个ZIP文件,命名为delicious_2.zip,以此类推。

第4步:类似程序的想法

你可以在其他程序中遍历一个目录树,将文件添加到压缩的ZIP归档文件中。例如,你可以编程做下面的事情:

  • 遍历一个目录树,将特定扩展名的文件归档,诸如.txt或.py,并排除其他文件。
  • 遍历一个目录树,将除.txt和.py文件以外的其他文件归档。
  • 在一个目录树中查找文件夹,它包含的文件数最多,或者使用的磁盘空间最大。

9.6 小结

即使你是一个有经验的计算机用户,可能也会用鼠标和键盘手工处理文件。现在的文件浏览器使得处理少量文件的工作很容易。但有时候,如果用计算机的浏览器,你需要完成的任务可能要花几个小时。

os和shutil模块提供了一些函数,用于复制、移动、改名和删除文件。在删除文件时,你可能希望使用send2trash模块,将文件移动到回收站或垃圾箱,而不是永久地删除它们。在编程处理文件时,最好是先注释掉实际会复制/移动/改名/删除文件的代码,添加print调用,这样你就可以运行该程序,验证它实际会做什么。

通常,你不仅需要对一个文件夹中的文件执行这些操作,而是对所有下级子文件夹执行操作。os.walk函数将处理这个艰苦工作,遍历文件夹,这样你就可以专注于程序需要对其中的文件做什么。

zipfile模块提供了一种方法,用Python压缩和解压ZIP归档文件。和os和shutil模块中的文件处理函数一起使用,很容易将硬盘上任意位置的一些文件打包。和许多独立的文件相比,这些ZIP文件更容易上传到网站,或作为E-mail附件发送。

本书前面几章提供了源代码让你拷贝。但如果你编写自己的程序,可能在第一次编写时不会完美无缺。下一章将聚焦于一些Python模块,它们帮助你分析和调试程序,这样就能让程序很快正确运行。

9.7 习题

1.shutil.copy和shutil.copytree之间的区别是什么?

2.什么函数用于文件改名?

3.send2trash和shutil模块中的删除函数之间的区别是什么?

4.ZipFile对象有一个close方法,就像File对象的close方法。ZipFile对象的什么方法等价于File对象的open方法?

9.8 实践项目

作为实践,编程完成下面的任务。

9.8.1 选择性拷贝

编写一个程序,遍历一个目录树,查找特定扩展名的文件(诸如.pdf或.jpg)。不论这些文件的位置在哪里,将它们拷贝到一个新的文件夹中。

9.8.2 删除不需要的文件

一些不需要的、巨大的文件或文件夹占据了硬盘的空间,这并不少见。如果你试图释放计算机上的空间,那么删除不想要的巨大文件效果最好。但首先你必须找到它们。

编写一个程序,遍历一个目录树,查找特别大的文件或文件夹,比方说,超过100MB的文件(回忆一下,要获得文件的大小,可以使用 os 模块的os.path.getsize)。将这些文件的绝对路径打印到屏幕上。

9.8.3 消除缺失的编号

编写一个程序,在一个文件夹中,找到所有带指定前缀的文件,诸如spam001.txt, spam002.txt等,并定位缺失的编号(例如存在spam001.txt和spam003.txt,但不存在spam002.txt)。让该程序对所有后面的文件改名,消除缺失的编号。

作为附加的挑战,编写另一个程序,在一些连续编号的文件中,空出一些编号,以便加入新的文件。