【python 让繁琐工作自动化】第10章 组织文件


Automate the Boring Stuff with Python: Practical Programming for Total Beginners (2nd Edition)
Copyright © 2020 by Al Sweigart.


考虑下面这样的任务:
① 在一个文件夹及其子文件夹中复制所有的 PDF 文件(且只复制 PDF 文件)。
② 文件夹中有数百个名为 spam001.txt,spam002.txt,spam003.txt 等的文件,删除每个文件名中的前导零。
③ 将多个文件夹的内容压缩到一个 ZIP 文件中(可能是一个简单的备份系统)。

在 OS X 和 Linux 上,文件浏览器很有可能自动显示扩展名。在 Windows 上,文件扩展名可能默认是隐藏的。要显示扩展名,点开 Start ▶ ControlPanel ▶ Appearance and Personalization ▶ Folder Options \text{Start} \blacktriangleright \text{ControlPanel} \blacktriangleright \text{Appearance and Personalization} \blacktriangleright \text{Folder Options} StartControlPanelAppearance and PersonalizationFolder Options。在 View(查看)选项卡中,Advanced Settings(高级设置) 框中,取消 Hide extensions for known file types(隐藏已知文件类型的扩展名)复选框。

10.1 shutil 模块

shutil(或 shell 工具:shell utilities)模块包含一些函数,允许在 Python 程序中复制、移动、重命名和删除文件。

复制文件和文件夹

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

>>> 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() 复制单个文件,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 文件夹一样。

移动与重命名文件和文件夹

调用 shutil.move(source, destination),将路径 source 处的文件或文件夹移动到路径 destination 处,返回一个字符串,表示新位置的绝对路径。
如果 destination 指向文件夹,那么 source 文件移动到 destination 中,并保持原来的名字。

>>> import shutil
>>> shutil.move('C:\\bacon.txt', 'C:\\eggs') # assuming a folder named eggs already exists in the C:\ directory
'C:\\eggs\\bacon.txt'

如果 C:\eggs 目录中已经有了一个 bacon.txt 文件,运行上面代码后,它将会被覆写。
使用 move() 时应该注意文件覆写的情况。

路径 destination 也可以指定为一个文件。这时,source 文件会移动并且重命名。

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

上面两个例子都假设 C:\ directory 目录下已存在 eggs 文件夹。如果不存在 eggs 文件夹,那么 move() 将 bacon.txt 重命名为一个 eggs 的文件。

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

这可能是程序中很难发现的缺陷,因为 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'
永久删除文件和文件夹

删除一个文件或者一个空文件夹,可以使用 os 模块中的函数。删除一个文件夹及其所有内容,使用 shutil 模块。
调用 os.unlink(path) 删除 path 处的文件。
调用 os.rmdir(path) 删除 path 处的文件夹。该文件夹必须为空,其中没有任何文件和文件夹。
调用 shutil.rmtree(path) 移除 path 处的文件夹,该文件夹内包含的所有文件和文件夹均被删除。

在程序中使用这些函数时要小心!可以第一次运行程序时,注释掉这些调用,并且加上 print() 调用,显示会被删除的文件。在确定程序按预期工作后, 删除 print(filename) 代码行, 取消 os.unlink(filename) 等类似代码行的注释。然后再次运行该程序,实际删除这些文件。

10.2 使用 send2trash 模块进行安全删除

因为 Python 的内置 shutil.rmtree() 函数不可逆地删除文件和文件夹,使用它可能很危险。删除文件和文件夹更好的方法,是使用第三方 send2trash 模块。在终端窗口运行 pip install send2trash 可以安装这个模块。
———————————————————————————————————————————————
如果用上面的命令安装失败。
可以在 https://pypi.python.org/ 网站搜索 send2trash。
也可以在网站 https://github.com/hsoft/send2trash 查看源代码。
下载 Send2Trash-1.5.0.tar.gz,运行命令 pip install Send2Trash-1.5.0.tar.gz 后,安装成功。
———————————————————————————————————————————————

使用 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() 函数来删除文件和文件夹。
如果想要程序释放磁盘空间,就要用 osshutil 来删除文件和文件夹。

10.3 遍历目录树

import os

for 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() 遍历一个目录树。os.walk() 在循环的每次迭代中返回 3 个值:
① 一个字符串,表示当前文件夹的名称;
② 一个字符串列表,表示当前文件夹内包含的文件夹;
③ 一个字符串列表,表示当前文件夹内包含的文件。
路径
运行程序,输出如下:

The current folder is C:\delicious
SUBFOLDER OF C:\delicious: cats
SUBFOLDER OF C:\delicious: walnut
FILE INSIDE C:\delicious: spam.txt

The current folder is C:\delicious\cats
FILE INSIDE C:\delicious\cats: catnames.txt
FILE INSIDE C:\delicious\cats: zophie.jpg

The current folder is C:\delicious\walnut
SUBFOLDER OF C:\delicious\walnut: waffles

The current folder is C:\delicious\walnut\waffles
FILE INSIDE C:\delicious\walnut\waffles: butter.txt.

10.4 使用 zipfile 模块压缩文件

一个 ZIP 文件(使用 .zip 文件扩展名)可以包含多个文件和子文件夹,这是将多个文件打包成一个文件的简便方法。这个文件称为归档文件(archive file),可以用作电子邮件的附件或其他用途。
使用 zipfile 模块中函数,Python 程序可以创建和打开(或提取:extract)ZIP 文件。

假设 C 盘有一个 ZIP 文件,名为 example.zip。其内容如下:
ZIP 文件内容
这个 ZIP 文件可以从 http://nostarch.com/automatestuff/ 下载。

读取 ZIP 文件

调用 zipfile.ZipFile() 函数,传入一个字符串表示 .zip 文件的文件名,可以创建一个 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_sizecompress_size(以字节为单位),它们分别表示原来文件大小和压缩文件大小。
ZipFile 对象表示整个归档文件,而 ZipInfo 对象则保存归档文件中单个文件的有用信息。

从 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() 方法的文件夹不存在,这个文件夹将被创建。

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() 返回的值是提取文件的绝对路径。

创建和添加到 ZIP 文件

要创建自己的压缩 ZIP 文件,必须以写模式打开 ZipFile 对象,将 'w' 作为第二个参数传入。

将路径传递给 ZipFile 对象的 write() 方法,Python 将会压缩该路径所指的文件,将其添加到 ZIP 文件中。write() 方法的第一个参数是一个字符串,表示要添加的文件名,第二个参数是压缩类型(compression type)参数,它告诉计算机应该使用什么算法来压缩文件,可以将这个值设置为 zipfile.ZIP_DEFLATED。(这指定了 deflate 压缩算法,适用于所有类型的数据。)

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

就像写入文件一样,写模式将擦除 ZIP 文件中所有原有的内容。如果想将文件添加到现有的 ZIP 文件中,那么将 'a' 作为第二个参数传递给 zipfile.ZipFile(),以添加模式下打开 ZIP 文件。

10.5 项目:将带有美式日期的文件重命名为欧式日期

任务:你的老板用电子邮件发给你上千个文件,文件名包含美国风格的日期(MM-DD-YYYY),需要将它们改名为欧洲风格的日期(DD-MM-YYYY)。

程序需要做的:
① 它搜索当前工作目录中的所有文件名,查找美国风格的日期。
② 当找到一个时,交换的月份和日期的位置,重命名文件,使其具有欧洲风格。

这意味着代码需要做:
① 创建一个正则表达式,能够识别美国风格日期的文本格式。
② 调用 os.listdir(),找出工作目录下的所有文件。
③ 循环遍历每个文件名,使用正则表达式检查它是否有日期。
④ 如果有日期,则使用 shutil.move() 重命名文件。

步骤 1:为美国风格的日期创建一个正则表达式
#! 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.
步骤 2:识别文件名中的日期部分
# 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)

为了使分组编号直观,请从头开始阅读正则表达式,在每次遇到左括号时进行计数加一。无需考虑代码,只需写下正则表达式的框架。

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)
步骤 3:形成新的文件名并重命名文件
	# 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
类似程序的想法

需要对大量的文件改名的情况。
① 在文件名的开头添加前缀,如添加 spam_ 将 eggs.txt 改为 spam_eggs.txt。
② 删除文件名中的 0,如 spam0042.txt。

10.6 项目:将文件夹备份到 ZIP 文件

假设你正在做一个项目,它的文件保存在 C:\AlsPythonBook 的文件夹中。你想为整个文件夹创建一个 ZIP 文件,作为“快照”。你希望保存不同的版本,希望 ZIP 文件的文件名每次创建时都有所变化。例如AlsPythonBook_1.zip、AlsPythonBook_2.zip、AlsPythonBook_3.zip,等等。

步骤 1:弄清楚 ZIP 文件的名称
#! 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')
步骤 2:创建新的 ZIP 文件
	# Create the ZIP file.
	print('Creating %s...' % (zipFilename))
	backupZip = zipfile.ZipFile(zipFilename, 'w')
步骤 3:遍历目录树并添加到 ZIP 文件
	# 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()

运行该程序,输出看起来像这样:

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.
类似程序的想法

① 遍历目录树,只归档具有特定扩展名(如 .txt 或 .py)的文件,而不归档其他文件。
② 遍历一个目录树并归档除 .txt 和 .py 之外的所有文件。
③ 查找目录树中文件数量最多的文件夹,或使用磁盘空间最多的文件夹。

10.7 小结

在编写处理文件的程序时,最好先注释掉执行实际复制/移动/重命名/删除操作的代码,并添加一个 print() 调用,这样就可以运行程序,确切地验证它将做什么。

10.8 实践项目

选择性复制

编写一个程序,遍历文件夹树,查找具有特定文件扩展名(如 .pdf 或 .jpg)的文件。将这些文件从它们所在的任何位置复制到一个新文件夹中。

#! python3
# selectiveCopy.py - Copy files with a certain extension (such as .pdf or .jpg) to a new folder.

import os, shutil

def selectiveCopy(searchDir, toDir, extension):
	# walks through a folder tree named "searchDir",
	# and searches for files with a certain file "extension" (such as .pdf or .jpg). 
	# copys these files to a new folder named "toDir".
	
	if not os.path.isdir(toDir):
		os.makedirs(toDir)
		
	for folderName, subfolders, filenames in os.walk(searchDir):
	    for filename in filenames:
	    	if filename.endswith(extension):
	    		filePath = os.path.join(folderName, filename)
	    		print('copy ' + filePath + ' to '+ toDir + '......')
	    		shutil.copy(filePath, toDir)
	print('Done.')
	
selectiveCopy('C:\\delicious', 'C:\\new_dir', '.txt')
删除不需要的文件

在硬盘上占用大量空间的文件或文件夹并不少见。如果想在电脑上腾出空间,可以删除占用大量空间且不需要的文件。但首先得找到它们。
编写一个程序,遍历一个文件夹树,查找异常大的文件或文件夹,比如文件大小超过 100MB 的文件。(要获得文件的大小,可以使用 os 模块中的 os.path.getsize()。)将这些文件及其绝对路径打印到屏幕上。

#! python3
# searchLargeFiles.py - Walks through a folder tree and searches for exceptionally large files 
# or folders—say, ones that have a file size of more than 100MB.

import os

def searchLargeFiles(folder, size):
	# walks through a "folder" tree,
	# and searches for large files which size is larger than "size" bytes.
	
	for folderName, subfolders, filenames in os.walk(folder):
	    for filename in filenames:
	    	filePath = os.path.join(folderName, filename)
	    	filesize = os.path.getsize(filePath)
	    	if filesize > size:
	    		print('The size of ' + filePath + ' is ' + str(filesize) + ' bytes.')

searchLargeFiles("C:\\users", 100 * 1024 * 1024)
填充间隔

编写一个程序,在一个文件夹中,查找所有带有给定前缀的文件,如 spam001.txt,spam002.txt 等等,并定位缺失的编号(例如,如果存在 spam001.txt 和 spam003.txt,但不存在 spam002.txt)。让程序重命名所有后来的文件,以填补这个缺失。
作为一个附加的挑战,编写另一个程序,在一些连续编号的文件中,空出一些编号,以便加入新的文件。

#! python3
# gapsOperation.py - Finds all files with a given prefix in a single folder, 
#                    handles with the gaps in the numbering.

import os, shutil, re

def closeGaps(folder, prefix, extension):
	# Finds all files with a given "prefix" in a single "folder", 
	# these files have the same file "extension", 
	# such as spam001.txt, spam002.txt, and so on,
	# locates any gaps in the numbering 
	# (such as if there is a spam001.txt and spam003.txt but no spam002.txt).
	# Renames all the later files to close this gap.
	
	# creates filename regex
	fileRegex = '^' + prefix + '([0-9]+)' + extension + '$'
	regexObj = re.compile(fileRegex)
	
	namelist = [] 	# save filename which match the regex
	numlist = [] 	# save file number
	for filename in os.listdir(folder):
		mo = regexObj.search(filename)
		if mo != None:
			namelist.append(filename)
			numlist.append(mo.group(1))
		
	for i in range(len(namelist)):
		if int(numlist[i]) != i + 1:
			newIndex = str(i+1).rjust(len(numlist[i]), '0')
			newFilename = prefix + newIndex + extension
			oldpath = os.path.join(folder, namelist[i])
			newpath = os.path.join(folder, newFilename)
			print('Rename ' + oldpath + ' to ' + newpath)
			# shutil.move(oldpath, newpath)   # uncomment after testing

def insertGaps(folder, prefix, extension):
	# Finds all files with a given "prefix" in a single "folder", 
	# these files have the same file "extension", 
	# inserts gaps into numbered files.

	# creates filename regex
	fileRegex = '^' + prefix + '([0-9]+)' + extension + '$'
	regexObj = re.compile(fileRegex)
	
	namelist = [] 	# save filename which match the regex
	numlist = [] 	# save file number
	for filename in os.listdir(folder):
		mo = regexObj.search(filename)
		if mo != None:
			namelist.append(filename)
			numlist.append(mo.group(1))
	
	if len(namelist) < 2:
		return
	
	num = len(namelist) * 2 - 1
	for i in range(len(namelist)-1, 0, -1):
		if int(numlist[i]) < num:
			newIndex = str(num).rjust(len(numlist[i]), '0')
			newFilename = prefix + newIndex + extension
			oldpath = os.path.join(folder, namelist[i])
			newpath = os.path.join(folder, newFilename)
			print('Rename ' + oldpath + ' to ' + newpath)
			# shutil.move(oldpath, newpath)   # uncomment after testing
		num = num - 2

closeGaps('C:\\Example', 'spam', '.txt')
insertGaps('C:\\Example', 'spam', '.txt')

学习网站:
https://automatetheboringstuff.com/chapter9/
https://automatetheboringstuff.com/2e/chapter10/

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值