Python-自动化指南-繁琐工作自动化-第三版-七-

Python 自动化指南(繁琐工作自动化)第三版(七)

原文:automatetheboringstuff.com/

译者:飞龙

协议:CC BY-NC-SA 4.0

11 文件组织

原文:automatetheboringstuff.com/3e/chapter11.html

除了创建和写入新文件外,你的程序还可以在硬盘上组织现有的文件。也许你有过在包含数十、数百甚至数千个文件的文件夹中翻找,并手动复制、重命名、移动或压缩它们的经历。或者考虑以下任务:

  • 复制文件夹中所有 PDF 文件(是 PDF 文件)的副本

  • 删除名为spam001.txtspam002.txtspam003.txt等数百个文件的文件名中的前导零

  • 将几个文件夹的内容压缩到一个 ZIP 文件中(这可以作为一个简单的备份系统)

所有这些无聊的事情都迫切需要在 Python 中自动化。通过编程你的电脑来完成这些任务,你可以将其变成一个快速工作的文件管理员,它永远不会出错。

当 Windows 使用反斜杠(\)来分隔文件路径中的文件夹时,本章中的 Python 代码将使用正斜杠(/)代替,因为它们在所有操作系统上都能工作。

shutil 模块

shutil模块提供了让你在 Python 程序中复制、移动、重命名和删除文件的功能。(模块的名称是 shell utilities 的缩写,其中shell是终端命令行的另一个术语。)要使用shutil函数,你首先需要运行import shutil

为了创建一个用于本章交互式 shell 示例的示例文件和文件夹,请在本章的交互式 shell 示例之前运行以下代码:

>>> from pathlib import Path
>>> h = Path.home()
>>> (h / 'spam').mkdir(exist_ok=True)
>>> with open(h / 'spam/file1.txt', 'w', encoding='utf-8') as file:
...     file.write('Hello')
... 

这将创建一个名为spam的文件夹,其中包含一个名为file1.txt的文本文件。本章中的示例将复制、移动、重命名和删除此文件和文件夹。所有shutil函数都可以接受字符串或Path对象作为文件路径参数。

复制文件和文件夹

shutil模块提供了用于复制文件以及整个文件夹的功能。调用shutil.copy(source, destination)会将位于路径 source 的文件复制到路径 destination 的文件夹中。源和目标都可以是字符串或Path对象。如果目标是一个文件名,它将被用作复制文件的新的名称。如果目标是一个文件夹,文件将以原始名称复制到该文件夹中。此函数返回复制文件的路径。

在交互式 shell 中输入以下内容,以查看shutil.copy()的工作方式:

>>> import shutil
>>> from pathlib import Path
>>> h = Path.home()
>>> shutil.copy(h / 'spam/file1.txt', h) # ❶
'C:\\Users\\Al\\file1.txt'
>>> shutil.copy(h / 'spam/file1.txt', h / 'spam/file2.txt') # ❷
WindowsPath('C:/Users/Al/spam/file2.txt') 

第一个shutil.copy()调用将位于C:\Users\Al\spam\file1.txt的文件复制到主文件夹C:\Users\Al。返回值是新复制文件的路径。请注意,由于我们指定了文件夹作为目标❶,新复制的文件将与原始file1.txt文件具有相同的文件名。第二个shutil.copy()调用❷将位于C:\Users\Al\spam\file1.txt的文件复制到C:\Users\Al\spam文件夹,但将复制的文件命名为file2.txt

虽然shutil.copy()可以复制单个文件,但调用shutil.copytree(source, destination)将复制路径为 source 的文件夹,包括其所有文件和子文件夹,到路径为 destination 的文件夹。该函数返回复制文件夹的路径。

在交互式 shell 中输入以下内容:

>>> import shutil
>>> from pathlib import Path
>>> h = Path.home()
>>> shutil.copytree(h / 'spam', h / 'spam_backup')
WindowsPath('C:/Users/Al/spam_backup') 

shutil.copytree()调用创建了一个名为*spam_backup*的新文件夹,其内容与原始*spam*文件夹相同。您现在已安全地备份了您宝贵的垃圾邮件。

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

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

如果目标指向一个文件夹,源文件将被移动到目标文件夹中,并保持其当前文件名。例如,在交互式 shell 中输入以下内容:

>>> import shutil
>>> from pathlib import Path
>>> h = Path.home()
>>> (h / 'spam2').mkdir()
>>> shutil.move(h / 'spam/file1.txt', h / 'spam2')
'C:\\Users\\Al\\spam2\\file1.txt' 

在主文件夹中创建*spam2*文件夹后,这个shutil.move()调用表示,“将*C:\Users\Al\spam\file1.txt*移动到文件夹*C:\Users\Al\spam2*。”如果*C:\Users\Al\spam2*中已经存在*file1.txt*文件,Python 将会覆盖它。

如果目标路径不是一个已存在的文件夹,shutil.move()将使用此路径来重命名文件。在以下示例中,源文件被移动并且被重命名:

>>> shutil.move(h / 'spam/file1.txt', h / 'spam2/new_name.txt')
'C:\\Users\\Al\\spam2\\new_name.txt' 

这行代码表示,“将*C:\Users\Al\spam\file1.txt*移动到文件夹*C:\Users\Al\spam2*,并且在此过程中,将那个*file1.txt*文件重命名为*new_name.txt*。”

永久删除文件和文件夹

您可以使用os模块中的函数删除单个文件或单个空文件夹,而要删除文件夹及其所有内容,则使用shutil模块:

  • 调用shutil.rmtree(path)将删除(即移除)路径中的整个文件夹树,包括它包含的所有文件和子文件夹。

  • 调用os.unlink(path)将删除路径中的单个文件。

  • 调用os.rmdir(path)将删除路径中的文件夹。此文件夹必须为空。

在您的程序中使用这些函数时要小心!通常,先运行程序并注释掉这些调用,并添加print()调用以显示将被删除的文件是一个好主意。这被称为“干运行”。以下是一个旨在删除具有.txt文件扩展名的文件的 Python 程序,但它有一个错误(以粗体显示),导致它删除.rxt文件:

import os
from pathlib import Path
for filename in Path.home().glob('*.rxt'):
    os.unlink(filename) 

如果您有任何以.rxt结尾的重要文件,它们将被意外地永久删除。相反,您应该首先以这种方式运行程序:

import os
from pathlib import Path
for filename in Path.home().glob('*.rxt'):
    #os.unlink(filename)
    print('Deleting', filename) 

os.unlink()调用现在被注释掉了,所以 Python 会忽略它。相反,您将打印出将被删除的文件名。首先运行这个版本的程序将显示您意外地告诉程序删除.rxt文件而不是.txt文件。

您还应该为复制、重命名或移动文件的程序进行试运行。最后,创建任何程序接触到的整个文件夹的备份副本可能是个好主意,以防您需要完全恢复原始文件。一旦您确定程序按预期工作,删除print(filename)行并取消注释os.unlink(filename)行。然后,再次运行程序以实际删除文件。

删除到回收站

Python 的内置shutil.rmtree()函数会不可逆地删除文件和文件夹。这使得该函数使用起来很危险,因为一个错误可能会删除您无意删除的文件。删除文件和文件夹的一个更好的方法是使用第三方send2trash模块。(有关如何安装第三方包的更深入解释,请参阅附录 A。)

使用send2trash模块的send2trash()函数比 Python 的常规删除函数更安全,因为它会将文件夹和文件发送到您的计算机的回收站或回收箱,而不是永久删除它们。如果您的程序中的错误使用send2trash删除了您无意删除的内容,您可以从回收站中稍后恢复它。

安装send2trash后,在交互式外壳中输入以下内容以将文件file1.txt发送到回收站:

>>> import send2trash
>>> send2trash.send2trash('file1.txt') 

通常,您应该使用send2trash.send2trash()函数来删除文件和文件夹。但是,虽然将文件发送到回收站允许您稍后恢复它们,但它不会像永久删除那样释放磁盘空间。请注意,send2trash()函数只能将文件发送到回收站;它不能从回收站中取出文件。

遍历目录树

如果您想要列出文件夹中的所有文件和子文件夹,请调用os.listdir()函数并传递文件夹名称:

>>> import os
>>> os.listdir(r'C:\Users\Al')
['.anaconda', '.android', '.cache', '.dotnet', '.eclipse', '.gitconfig',
# --snip--
'__pycache__'] 

您还可以通过调用iterdir()方法来获取文件夹中的Path对象列表:

>>> from pathlib import Path
>>> home = Path.home()
>>> list(home.iterdir())
[WindowsPath('C:/Users/Al/.anaconda'), WindowsPath('C:/Users/Al/.android'),
WindowsPath('C:/Users/Al/.cache'),
# --snip--
WindowsPath('C:/Users/Al/__pycache__')] 

假设您想要重命名某个文件夹中的每个文件,以及该文件夹中每个子文件夹中的每个文件。也就是说,您想要遍历目录树,在访问每个文件时进行操作。编写一个执行此操作的程序可能会变得复杂;幸运的是,Python 提供了os.walk()函数来为您处理这个过程。

让我们在交互式外壳中运行以下代码来创建一系列文件夹和文件:

>>> from pathlib import Path
>>> h = Path.home()
>>> (h / 'spam').mkdir(exist_ok=True)
>>> (h / 'spam/eggs').mkdir(exist_ok=True)
>>> (h / 'spam/eggs2').mkdir(exist_ok=True)
>>> (h / 'spam/eggs/bacon').mkdir(exist_ok=True)
>>> for f in ['spam/file1.txt', 'spam/eggs/file2.txt', 'spam/eggs/file3.txt',
'spam/eggs/bacon/file4.txt']:
...     with open(h / f, 'w', encoding='utf-8') as file:
...         file.write('Hello')
...
>>> # At this point, the folders and files now exist. 

此代码将在您的家目录中创建以下文件夹和文件:

  • spam 文件夹

  • spam/file1.txt 文件

  • spam/eggs 文件夹

  • spam/eggs/file2.txt 文件

  • spam/eggs/file3.txt 文件

  • spam/eggs2 文件夹

  • spam/eggs/bacon 文件夹

  • spam/eggs/bacon/file4.txt 文件

这里有一个示例程序,它使用os.walk()函数遍历此文件夹树,并将每个文件重命名为大写字母:

import os, shutil
from pathlib import Path
h = Path.home()

for folder_name, subfolders, filenames in os.walk(h / 'spam'):
    print('The current folder is ' + folder_name)

    for subfolder in subfolders:
        print('SUBFOLDER OF ' + folder_name + ': ' + subfolder)

    for filename in filenames:
        print('FILE INSIDE ' + folder_name + ': '+ filename)
        # Rename file to uppercase:
        p = Path(folder_name)
        shutil.move(p / filename, p / filename.upper())

    print('') 

os.walk() 函数传递一个单个字符串值:文件夹的路径。您可以在 for 循环中使用 os.walk() 来遍历目录树,就像您可以使用 range() 函数遍历数字范围一样。与 range() 不同,os.walk() 函数将在循环的每次迭代中返回三个值:

  • 当前文件夹名称的字符串

  • 当前文件夹中子文件夹的字符串列表

  • 当前文件夹中文件的字符串列表

这里的 当前文件夹 指的是在当前 for 循环迭代中访问的文件夹。os.walk() 函数不会改变程序的当前工作目录。就像您可以在代码 for i in range(10): 中选择变量名 i 一样,您也可以为前面列出的三个值选择变量名。我总是使用描述性的名称 folder_namesubfoldersfilenames

当我在我的计算机上运行此程序时,它给出了以下输出:

The current folder is C:\Users\Al\spam
SUBFOLDER OF C:\Users\Al\spam: eggs
SUBFOLDER OF C:\Users\Al\spam: eggs2
FILE INSIDE C:\Users\Al\spam: file1.txt

The current folder is C:\Users\Al\spam\eggs
SUBFOLDER OF C:\Users\Al\spam\eggs: bacon
FILE INSIDE C:\Users\Al\spam\eggs: file2.txt
FILE INSIDE C:\Users\Al\spam\eggs: file3.txt

The current folder is C:\Users\Al\spam\eggs\bacon
FILE INSIDE C:\Users\Al\spam\eggs\bacon: file4.txt

The current folder is C:\Users\Al\spam\eggs2 

因为 os.walk()subfolderfilename 变量返回字符串列表,所以您可以在自己的 for 循环中使用这些返回值。例如,您可以将文件夹和文件名传递给像 shutil.move() 这样的函数,就像示例中那样。

使用 zipfile 模块压缩文件

您可能熟悉 ZIP 文件(具有 .zip 文件扩展名),它可以包含许多其他文件的压缩内容。压缩文件可以减小其大小,这在通过互联网传输时非常有用。由于 ZIP 文件还可以包含多个文件和子文件夹,因此它是将多个文件打包成一个文件的便捷方式。这个单独的文件,称为 存档文件,然后可以被,比如说,附加到电子邮件中。

您的 Python 程序可以使用 zipfile 模块中的函数创建或从 ZIP 文件中提取。

创建和添加到 ZIP 文件

要创建自己的压缩 ZIP 文件,您必须通过将 'w' 作为第二个参数传递来以写入模式打开 ZipFile 对象。(注意对象名称中的大写字母 ZF,它与 zipfile 模块名称不同。)这个过程与通过将 'w' 传递给 open() 函数以写入模式打开文本文件类似。对于文件名,您可以传递字符串或 Path 对象。

当您将路径传递给 ZipFile 对象的 write() 方法时,Python 将压缩该路径上的文件并将其添加到 ZIP 文件中。write() 方法的第一个参数是要添加的文件名的字符串。第二个参数是 压缩类型 参数,它告诉计算机应该使用什么算法来压缩文件;您始终可以将此值设置为 zipfile.ZIP_DEFLATED 以指定 deflate 压缩算法,该算法适用于所有类型的数据。如果您不传递此值,write() 方法将使用常规、未压缩的大小将文件添加到 ZIP 文件中。在交互式外壳中输入以下内容:

>>> import zipfile
>>> with open('file1.txt', 'w', encoding='utf-8') as file_obj:
...     file_obj.write('Hello' * 10000)
...
>>> with zipfile.ZipFile('example.zip', 'w') as example_zip:
...     example_zip.write('file1.txt', compress_type=zipfile.ZIP_DEFLATED,
 compresslevel=9) 

此代码创建一个名为file1.txt的文本文件,并向其中写入 50,000 个字符的字符串'Hello' * 10000(约 49KB)。然后,它创建一个名为example.zip的新 ZIP 文件,其中包含file1.txt的压缩内容(约 213 字节;高度重复的数据也高度可压缩)。compresslevel关键字参数(从 Python 3.7 及以后版本添加)可以设置为从09的任何值,其中9是最慢但压缩程度最高的级别。如果您不指定此关键字参数,则默认为6

zipfile.ZipFile()函数以类似于open()函数打开文件的方式在with语句中打开 ZIP 文件。这确保了当执行离开with语句的代码块时,会自动调用close()方法。

请记住,就像写入文件一样,写入模式会删除 ZIP 文件中所有现有的内容。如果您只想向现有的 ZIP 文件中添加文件,请将'a'作为zipfile.ZipFile()的第二个参数传递,以以追加模式打开 ZIP 文件。

阅读 ZIP 文件

要读取 ZIP 文件的内容,您必须首先通过调用zipfile.ZipFile()函数并传递 ZIP 文件的文件名来创建一个ZipFile对象。请注意,zipfile是 Python 模块的名称,ZipFile()是函数的名称。

例如,在交互式 shell 中输入以下内容:

>>> import zipfile

>>> example_zip = zipfile.ZipFile('example.zip')
>>> example_zip.namelist()
['file1.txt']
>>> file1_info = example_zip.getinfo('file1.txt')
>>> file1_info.file_size
50000
>>> file1_info.compress_size
97
>>> f'Compressed file is {round(file1_info.file_size / file1_info # ❶
 .compress_size, 2)}x smaller!'

'Compressed file is 515.46x smaller!'
>>> example_zip.close() 

ZipFile对象有一个namelist()方法,它返回 ZIP 文件中包含的所有文件和文件夹的字符串列表。这些字符串可以传递给getinfo() ZipFile方法,以返回有关该特定文件的ZipInfo对象。ZipInfo对象有自己的属性,例如file_sizecompress_size,它们分别以字节为单位持有表示原始文件大小和压缩文件大小的整数。虽然ZipFile对象代表整个存档文件,但ZipInfo对象包含有关存档中单个文件的有用信息。

❶处的命令通过将原始文件大小除以压缩文件大小来计算example.zip的压缩效率,然后打印出此信息。

从 ZIP 文件中提取

ZipFile对象的extractall()方法可以从 ZIP 文件中提取所有文件和文件夹到当前工作目录。按照第 249 页“创建和添加到 ZIP 文件”中的说明创建一个名为example.zip的 ZIP 文件,然后输入以下内容到交互式 shell 中:

>>> import zipfile
>>> example_zip = zipfile.ZipFile('example.zip')
>>> example_zip.extractall() # ❶
>>> example_zip.close() 

运行此代码后,Python 会将example.zip的内容提取到当前工作目录。可选地,您可以将文件夹名称传递给extractall(),以便将其提取到当前工作目录以外的文件夹中。如果传递给extractall()方法的文件夹不存在,Python 将创建它。例如,如果您将❶处的调用替换为example_zip.extractall('C:\\spam'),则代码将把example.zip中的文件提取到新创建的C:\spam文件夹中。

ZipFile 对象的 extract() 方法可以从 ZIP 文件中提取单个文件。通过输入以下内容继续交互式 shell 示例:

>>> example_zip.extract('file1.txt')
'C:\\Users\\Al\\Desktop\\file1.txt'
>>> example_zip.extract('file1.txt', 'C:\\some\\new\\folders')
'C:\\some\\new\\folders\\file1.txt'
>>> example_zip.close() 

你传递给 extract() 的字符串必须与 namelist() 返回的列表中的某个字符串匹配。可选地,你可以传递第二个参数给 extract() 以将文件提取到当前工作目录之外的文件夹中。如果这个第二个参数是一个尚不存在的文件夹,Python 将创建该文件夹。

项目 5:将文件夹备份到 ZIP 文件中

假设你正在处理一个名为 C:\Users\Al\AlsPythonBook 的文件夹中的项目文件。你担心丢失你的工作,因此你想创建整个文件夹的 ZIP 文件“快照”。你还想保留这些快照的不同版本,因此你想 ZIP 文件的文件名在每次创建新版本时递增;例如,AlsPythonBook_1.zipAlsPythonBook_2.zipAlsPythonBook_3.zip,依此类推。你可以手动完成这项工作,但这会相当麻烦,你可能会不小心错误地编号 ZIP 文件的名称。运行一个为你完成这项无聊任务的程序会简单得多。

对于这个项目,打开一个新的文件编辑窗口,并将其保存为 backup_to_zip.py

第 1 步:确定 ZIP 文件的名称

我们将把这个程序的代码放入一个名为 backup_to_zip() 的函数中。这将使得将函数复制粘贴到需要此功能的其他 Python 程序变得容易。在程序末尾,将调用该函数以执行备份。使你的程序看起来如下:

# backup_to_zip.py - Copies an entire folder and its contents into
# a ZIP file whose filename increments

import zipfile, os
from pathlib import Path

def backup_to_zip(folder):
    # Back up the entire contents of "folder" into a ZIP file.
    folder = Path(folder)  # Make sure folder is a Path object, not string.

    # Figure out the ZIP filename this code should use, based on
    # what files already exist.
    number = 1 # ❶
    number = 1 # ❶

        zip_filename = Path(folder.parts[-1] + '_' + str(number) + '.zip')
        if not zip_filename.exists():
            break
        number = number + 1

    number = 1 # ❶

 # TODO: Walk the entire folder tree and compress the files in each folder.
    print('Done.')

backup_to_zip(Path.home() / 'spam') 

首先,导入 zipfileos 模块。接下来,定义一个名为 backup_to_zip() 的函数,它只接受一个参数 folder。该参数是一个字符串或 Path 对象,指向需要备份内容的文件夹。该函数将确定它将创建的 ZIP 文件的文件名。然后,它将创建文件,遍历 folder 文件夹,并将每个子文件夹和文件添加到 ZIP 文件中。在源代码中为这些步骤编写 TODO 注释,以提醒自己在以后完成它们 ❸。

第一个任务,为 ZIP 文件命名,使用 folder 的绝对路径的基本名称。如果正在备份的文件夹是 C:\Users\Al\spam,ZIP 文件的名称应该是 spam_N.zip,其中 N 是第一次运行程序时为 1,第二次为 2,依此类推。

你可以通过检查 spam_1.zip 是否已存在,然后检查 spam_2.zip 是否已存在,依此类推来确定 N 应该是多少。使用名为 number 的变量来表示 N ❶,并在调用 exists() 检查文件是否存在时在循环中持续增加它 ❷。找到的第一个不存在的文件名将导致循环 break,因为它将找到新 ZIP 文件的文件名。

第 2 步:创建新的 ZIP 文件

接下来,让我们创建 ZIP 文件。使你的程序看起来如下:

# backup_to_zip.py - Copies an entire folder and its contents into
# a ZIP file whose filename increments

# --snip--

    # Create the ZIP file.
 print(f'Creating {zip_filename}...')
 backup_zip = zipfile.ZipFile(zip_filename, 'w')

    # TODO: Walk the entire folder tree and compress the files in each folder.
    print('Done.')

backup_to_zip(Path.home() / 'spam') 

现在新的 ZIP 文件名已存储在 zip_filename 变量中,你可以调用 zipfile.ZipFile() 来实际创建 ZIP 文件。确保将 'w' 作为第二个参数传递以在写入模式下打开 ZIP 文件。我们还将从注释中删除 TODO,因为我们已经完成了该部分的代码编写。

第 3 步:遍历目录树

现在你需要使用 os.walk() 函数来完成列出文件夹及其子文件夹中每个文件的工作。让你的程序看起来像以下这样:

# backup_to_zip.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 folder_name, subfolders, filenames in os.walk(folder): # ❶
 folder_name = Path(folder_name)
 print(f'Adding files in folder {folder_name}...')

        # Add all the files in this folder to the ZIP file.
for filename in filenames: # ❷
 print(f'Adding file {filename}...')
 backup_zip.write(folder_name / filename)
 backup_zip.close()
    print('Done.')
backup_to_zip(Path.home() / 'spam') 

for 循环中使用 os.walk() ❶。在每次迭代中,该函数将返回当前迭代文件夹的名称、该文件夹中的子文件夹以及该文件夹中的文件名。嵌套的 for 循环可以遍历 filenames 列表中的每个文件名 ❷。这些文件中的每一个都将被添加到 ZIP 文件中。

当你运行这个程序时,它应该产生类似以下这样的输出:

Creating spam_1.zip...
Adding files in spam...
Adding file file1.txt...
Done. 

第二次运行它时,它将所有文件放入名为 spam_2.zip 的 ZIP 文件中,依此类推。

其他程序的想法

你可以在多个其他程序中遍历目录树并将文件添加到压缩的 ZIP 存档中。例如,你可以编写执行以下操作的程序:

  • 遍历目录树并仅存档具有特定扩展名的文件,例如 .txt.py,以及其他文件。

  • 遍历目录树并存档除了 .txt.py 之外的所有文件。

  • 仅存档目录树中使用最多磁盘空间或自上次存档以来已修改的文件夹。

摘要

即使你是一个经验丰富的计算机用户,你也可能使用鼠标和键盘手动处理文件。现代文件浏览器使得处理少量文件变得容易。但有时你可能需要执行一个任务,使用计算机的文件浏览器可能需要数小时。

osshutil 模块提供了复制、移动、重命名和删除文件的功能。在删除文件时,你可能想使用 send2trash 模块将文件移动到回收站或垃圾桶,而不是永久删除它们。并且当编写处理文件的程序时,进行干运行是一个好主意;注释掉实际执行复制、移动、重命名或删除的代码,并用 print() 调用替换。这样,你可以运行程序并验证它确切会做什么。

通常,你不仅需要在单个文件夹中的文件上执行这些操作,还需要在该文件夹的每个子文件夹中、这些子文件夹的每个子文件夹中,依此类推。os.walk() 函数为你处理跨文件夹的遍历,这样你就可以专注于你的程序需要对这些文件做什么。

zipfile 模块通过 Python 提供了一种在 .zip 归档中压缩和解压文件的方法。结合 osshutil 的文件处理函数,zipfile 使得从硬盘上的任何位置打包多个文件变得容易。这些 ZIP 文件比许多单独的文件更容易上传到网站或作为电子邮件附件发送。

练习问题

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

2.  用于重命名文件的函数是什么?

3.  send2trashshutil 模块中的删除函数有什么区别?

4.  ZipFile 对象有一个 close() 方法,就像 File 对象的 close() 方法一样。ZipFile 方法中哪个方法与 File 对象的 open() 方法等效?

练习程序

为了练习,编写程序来完成以下任务。

选择性复制

编写一个程序,遍历文件夹树并搜索具有特定文件扩展名(如 .pdf.jpg)的文件。将这些文件从当前位置复制到新文件夹。

删除不必要的文件

在你的硬盘上,一些不必要的但巨大的文件或文件夹占据大部分空间并不罕见。如果你试图释放电脑上的空间,首先识别最大的不必要的文件会更有效。

编写一个程序,遍历文件夹树并搜索异常大的文件或文件夹——比如说,文件大小超过 100MB 的文件。记住,为了获取文件的大小,你可以使用 os 模块的 os.path.getsize()。将这些文件及其绝对路径打印到屏幕上。

文件重命名

编写一个程序,在单个文件夹中查找所有具有给定前缀的文件,例如 spam001.txtspam002.txt 等,并定位任何编号中的间隔(例如,如果有一个 spam001.txt 和一个 spam003.txt 但没有 spam002.txt)。让程序将所有后续文件重命名以关闭这个间隔。

要创建这些示例文件(跳过 spam042.txtspam086.txtspam103.txt),请运行以下代码:

>>> for i in range(1, 121):
...     if i not in (42, 86, 103):
...         with open(f'spam{str(i).zfill(3)}.txt', 'w') as file:
...             pass
... 

作为额外的挑战,编写另一个程序,可以在编号文件中插入间隔(并在间隔后的文件名中提升数字)以便可以插入新文件。

将日期从美国风格转换为欧洲风格

假设你的老板通过电子邮件发送给你数千个文件,文件名中包含美国风格的日期(MM-DD-YYYY),并需要将它们重命名为欧洲风格的日期(DD-MM-YYYY)。这项无聊的任务可能需要一整天的时间手动完成!相反,编写一个程序来完成以下操作:

1.  在当前工作目录及其所有子目录中搜索所有文件名中的美国风格日期。使用 os.walk() 函数遍历子文件夹。

2.  使用正则表达式识别包含 MM-DD-YYYY 模式的文件名——例如,spam12-31-1900.txt。假设月份和日期总是使用两位数字,并且不存在非日期匹配的文件。(你不会找到名为 99-99-9999.txt 的文件。)

  1. 当找到文件名时,使用月份和日期交换来使其成为欧洲风格。使用shutil.move()函数进行重命名。

shutil模块

shutil模块提供了让你在 Python 程序中复制、移动、重命名和删除文件的功能。(该模块的名称是 shell utilities 的缩写,其中shell是终端命令行的另一种说法。)要使用shutil函数,你首先需要运行import shutil

为了创建一个示例文件和文件夹进行操作,在本章的交互式 shell 示例之前运行以下代码:

>>> from pathlib import Path
>>> h = Path.home()
>>> (h / 'spam').mkdir(exist_ok=True)
>>> with open(h / 'spam/file1.txt', 'w', encoding='utf-8') as file:
...     file.write('Hello')
... 

这将创建一个名为spam的文件夹,其中包含一个名为file1.txt的文本文件。本章中的示例将复制、移动、重命名和删除此文件和文件夹。所有shutil函数都可以接受字符串或Path对象作为文件路径参数。

复制文件和文件夹

shutil模块提供了复制文件以及整个文件夹的功能。调用shutil.copy(source, destination)将路径 source 处的文件复制到路径 destination 处的文件夹。源和目标都可以是字符串或Path对象。如果目标是一个文件名,它将被用作复制文件的新名称。如果目标是一个文件夹,文件将以原始名称复制到该文件夹。此函数返回复制文件的路径。

在交互式 shell 中输入以下内容以查看shutil.copy()的工作方式:

>>> import shutil
>>> from pathlib import Path
>>> h = Path.home()
>>> shutil.copy(h / 'spam/file1.txt', h) # ❶
'C:\\Users\\Al\\file1.txt'
>>> shutil.copy(h / 'spam/file1.txt', h / 'spam/file2.txt') # ❷
WindowsPath('C:/Users/Al/spam/file2.txt') 

第一个shutil.copy()调用将文件C:\Users\Al\spam\file1.txt复制到主文件夹C:\Users\Al。返回值是新复制文件的路径。请注意,由于我们指定了一个文件夹作为目标❶,所以新复制的文件将与原始file1.txt文件具有相同的文件名。第二个shutil.copy()调用❷将文件C:\Users\Al\spam\file1.txt复制到C:\Users\Al\spam文件夹,但将复制的文件命名为file2.txt

虽然shutil.copy()可以复制单个文件,但调用shutil.copytree(source, destination)将复制路径 source 处的文件夹,包括所有文件和子文件夹,到路径 destination 处的文件夹。该函数返回复制文件夹的路径。

在交互式 shell 中输入以下内容:

>>> import shutil
>>> from pathlib import Path
>>> h = Path.home()
>>> shutil.copytree(h / 'spam', h / 'spam_backup')
WindowsPath('C:/Users/Al/spam_backup') 

shutil.copytree()调用创建一个名为spam_backup的新文件夹,其内容与原始spam文件夹相同。你现在已经安全地备份了宝贵的垃圾邮件。

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

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

如果目标指向一个文件夹,源文件将被移动到目标文件夹,并保持其当前文件名。例如,在交互式 shell 中输入以下内容:

>>> import shutil
>>> from pathlib import Path
>>> h = Path.home()
>>> (h / 'spam2').mkdir()
>>> shutil.move(h / 'spam/file1.txt', h / 'spam2')
'C:\\Users\\Al\\spam2\\file1.txt' 

在主文件夹中创建spam2文件夹后,这个shutil.move()调用表示,“将C:\Users\Al\spam\file1.txt移动到文件夹C:\Users\Al\spam2。”如果C:\Users\Al\spam2中已经有一个file1.txt文件,Python 将会覆盖它。

如果目标路径不是一个现有的文件夹,shutil.move()将使用此路径来重命名文件。在以下示例中,源文件被移动并且重命名:

>>> shutil.move(h / 'spam/file1.txt', h / 'spam2/new_name.txt')
'C:\\Users\\Al\\spam2\\new_name.txt' 

这行代码表示,“将C:\Users\Al\spam\file1.txt移动到文件夹C:\Users\Al\spam2,并且在此过程中,将那个file1.txt文件重命名为new_name.txt。”

永久删除文件和文件夹

你可以使用os模块中的函数删除单个文件或单个空文件夹,而要删除文件夹及其所有内容,则使用shutil模块:

  • 调用shutil.rmtree(路径)将删除(即移除)路径处的整个文件夹树,包括它包含的所有文件和子文件夹。

  • 调用os.unlink(路径)将删除路径处的单个文件。

  • 调用os.rmdir(路径)将删除路径处的文件夹。这个文件夹必须是空的。

在你的程序中使用这些函数时要小心!通常,先注释掉这些调用并添加print()调用以显示将被删除的文件是一个好主意。这被称为干运行。以下是一个旨在删除具有.txt文件扩展名的文件的 Python 程序,但它有一个(粗体显示)错误,导致它删除.rxt文件:

import os
from pathlib import Path
for filename in Path.home().glob('*.rxt'):
    os.unlink(filename) 

如果你有任何以.rxt结尾的重要文件,它们可能已经被意外永久删除。相反,你应该首先以这种方式运行程序:

import os
from pathlib import Path
for filename in Path.home().glob('*.rxt'):
    #os.unlink(filename)
    print('Deleting', filename) 

os.unlink()调用现在被注释掉了,所以 Python 会忽略它。相反,你会打印出将被删除的文件名。首先运行这个版本的程序将显示你意外地告诉程序删除.rxt文件而不是.txt文件。

你还应该为复制、重命名或移动文件的程序进行干运行。最后,创建任何程序接触到的整个文件夹的备份副本可能是个好主意,以防你需要完全恢复原始文件。一旦你确定程序按预期工作,删除print(filename)行并取消注释os.unlink(filename)行。然后,再次运行程序以实际删除文件。

删除到回收站

Python 的内置shutil.rmtree()函数会不可逆地删除文件和文件夹。这使得该函数使用起来很危险,因为一个错误可能会导致删除你未打算删除的文件。删除文件和文件夹的一个更好的方法是使用第三方send2trash模块。(参见附录 A,以获取如何安装第三方包的更深入的解释。)

使用 send2trash 模块的 send2trash() 函数比 Python 的常规删除函数更安全,因为它会将文件夹和文件发送到你的计算机的垃圾桶或回收站,而不是永久删除它们。如果你的程序中的错误使用 send2trash 删除了你无意中删除的东西,你可以在以后从回收站中恢复它。

在安装 send2trash 之后,将以下内容输入到交互式 shell 中,以将文件 file1.txt 发送到回收站:

>>> import send2trash
>>> send2trash.send2trash('file1.txt') 

通常情况下,你应该使用 send2trash.send2trash() 函数来删除文件和文件夹。但是,虽然将文件发送到回收站允许你在以后恢复它们,但它并不会像永久删除那样释放磁盘空间。请注意,send2trash() 函数只能将文件发送到回收站;它不能从回收站中取出文件。

复制文件和文件夹

shutil 模块提供了用于复制文件以及整个文件夹的函数。调用 shutil.copy(source, destination) 将会将位于路径 source 的文件复制到位于路径 destination 的文件夹中。源和目标都可以是字符串或 Path 对象。如果目标是一个文件名,它将被用作复制文件的新的名称。如果目标是一个文件夹,文件将以原始名称复制到该文件夹中。此函数返回复制文件的路径。

将以下内容输入到交互式 shell 中,以查看 shutil.copy() 的作用:

>>> import shutil
>>> from pathlib import Path
>>> h = Path.home()
>>> shutil.copy(h / 'spam/file1.txt', h) # ❶
'C:\\Users\\Al\\file1.txt'
>>> shutil.copy(h / 'spam/file1.txt', h / 'spam/file2.txt') # ❷
WindowsPath('C:/Users/Al/spam/file2.txt') 

第一个 shutil.copy() 调用将 C:\Users\Al\spam\file1.txt 文件复制到主文件夹 C:\Users\Al。返回值是新复制文件的路径。请注意,由于我们指定了文件夹作为目标 ❶,新复制的文件将与原始 file1.txt 文件具有相同的文件名。第二个 shutil.copy() 调用 ❷ 将 C:\Users\Al\spam\file1.txt 文件复制到 C:\Users\Al\spam 文件夹,但将复制文件的名称改为 file2.txt

虽然 shutil.copy() 会复制单个文件,但调用 shutil.copytree(source, destination) 将会将位于路径 source 的文件夹及其所有文件和子文件夹复制到位于路径 destination 的文件夹中。该函数返回复制文件夹的路径。

将以下内容输入到交互式 shell 中:

>>> import shutil
>>> from pathlib import Path
>>> h = Path.home()
>>> shutil.copytree(h / 'spam', h / 'spam_backup')
WindowsPath('C:/Users/Al/spam_backup') 

shutil.copytree() 调用将创建一个名为 spam_backup 的新文件夹,其内容与原始 spam 文件夹相同。你现在已经安全地备份了宝贵的垃圾邮件。

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

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

如果目标指向一个文件夹,源文件将被移动到目标中,并保持其当前文件名。例如,将以下内容输入到交互式 shell 中:

>>> import shutil
>>> from pathlib import Path
>>> h = Path.home()
>>> (h / 'spam2').mkdir()
>>> shutil.move(h / 'spam/file1.txt', h / 'spam2')
'C:\\Users\\Al\\spam2\\file1.txt' 

在主文件夹中创建名为 spam2 的文件夹后,这个 shutil.move() 调用会表示,“将 C:\Users\Al\spam\file1.txt 文件移动到文件夹 C:\Users\Al\spam2。” 如果 C:\Users\Al\spam2 中已经存在一个 file1.txt 文件,Python 会覆盖它。

如果目标路径不是一个现有文件夹,shutil.move() 将使用此路径来重命名文件。在以下示例中,源文件被移动并且重命名:

>>> shutil.move(h / 'spam/file1.txt', h / 'spam2/new_name.txt')
'C:\\Users\\Al\\spam2\\new_name.txt' 

这行代码表示,“将 C:\Users\Al\spam\file1.txt 文件移动到文件夹 C:\Users\Al\spam2,同时,将那个 file1.txt 文件重命名为 new_name.txt。”

永久删除文件和文件夹

你可以使用 os 模块中的函数删除单个文件或单个空文件夹,而要删除文件夹及其所有内容,则使用 shutil 模块:

  • 调用 shutil.rmtree(path) 将会删除(即移除)路径处的整个文件夹树,包括它包含的所有文件和子文件夹。

  • 调用 os.unlink(path) 将会删除路径处的单个文件。

  • 调用 os.rmdir(path) 将会删除路径处的文件夹。这个文件夹必须是空的。

在你的程序中使用这些函数时要小心!通常,首先以注释掉这些调用并添加 print() 调用来显示将被删除的文件的方式运行你的程序是个好主意。这被称为 试运行。以下是一个旨在删除具有 .txt 文件扩展名的文件的 Python 程序,但它有一个错误(以粗体显示),导致它删除 .rxt 文件:

import os
from pathlib import Path
for filename in Path.home().glob('*.rxt'):
    os.unlink(filename) 

如果你有一些以 .rxt 结尾的重要文件,它们可能会被意外地永久删除。相反,你应该首先以这种方式运行程序:

import os
from pathlib import Path
for filename in Path.home().glob('*.rxt'):
    #os.unlink(filename)
    print('Deleting', filename) 

os.unlink() 调用现在被注释掉了,所以 Python 会忽略它。相反,你会打印出将被删除的文件名。首先运行这个版本的程序会显示你意外地告诉程序删除 .rxt 文件而不是 .txt 文件。

你还应该为复制、重命名或移动文件的程序进行试运行。最后,创建任何程序接触到的整个文件夹的备份副本可能是个好主意,以防你需要完全恢复原始文件。一旦你确定程序按预期工作,删除 print(filename) 行并取消注释 os.unlink(filename) 行。然后,再次运行程序以实际删除文件。

删除到回收站

Python 的内置 shutil.rmtree() 函数会不可逆地删除文件和文件夹。这使得该函数使用起来很危险,因为一个错误可能会导致你无意中删除文件。删除文件和文件夹的更好方法是使用第三方 send2trash 模块。(参见附录 A 以获取如何安装第三方包的更深入的解释。)

使用 send2trash 模块的 send2trash() 函数比 Python 的常规删除函数更安全,因为它会将文件夹和文件发送到你的电脑的回收站或回收箱,而不是永久删除它们。如果你的程序中的错误使用 send2trash 删除了你无意中删除的内容,你可以在稍后从回收站中恢复它。

在安装 send2trash 后,将以下内容输入到交互式外壳中,将文件 file1.txt 发送到回收站:

>>> import send2trash
>>> send2trash.send2trash('file1.txt') 

通常,你应该使用 send2trash.send2trash() 函数来删除文件和文件夹。但是,虽然将文件发送到回收站允许你在以后恢复它们,但它不会像永久删除那样释放磁盘空间。请注意,send2trash() 函数只能将文件发送到回收站;它不能从回收站中取出文件。

遍历目录树

如果你想列出文件夹中的所有文件和子文件夹,请调用 os.listdir() 函数并传递一个文件夹名称:

>>> import os
>>> os.listdir(r'C:\Users\Al')
['.anaconda', '.android', '.cache', '.dotnet', '.eclipse', '.gitconfig',
# --snip--
'__pycache__'] 

你也可以通过调用 iterdir() 方法来获取文件夹中 Path 对象的列表:

>>> from pathlib import Path
>>> home = Path.home()
>>> list(home.iterdir())
[WindowsPath('C:/Users/Al/.anaconda'), WindowsPath('C:/Users/Al/.android'),
WindowsPath('C:/Users/Al/.cache'),
# --snip--
WindowsPath('C:/Users/Al/__pycache__')] 

假设你想要重命名某个文件夹中的每个文件,以及该文件夹中每个子文件夹中的每个文件。也就是说,你想要遍历目录树,在访问每个文件时进行操作。编写一个执行此操作的程序可能会变得复杂;幸运的是,Python 提供了 os.walk() 函数来为你处理这个过程。

让我们通过在交互式外壳中运行以下代码来创建一系列文件夹和文件:

>>> from pathlib import Path
>>> h = Path.home()
>>> (h / 'spam').mkdir(exist_ok=True)
>>> (h / 'spam/eggs').mkdir(exist_ok=True)
>>> (h / 'spam/eggs2').mkdir(exist_ok=True)
>>> (h / 'spam/eggs/bacon').mkdir(exist_ok=True)
>>> for f in ['spam/file1.txt', 'spam/eggs/file2.txt', 'spam/eggs/file3.txt',
'spam/eggs/bacon/file4.txt']:
...     with open(h / f, 'w', encoding='utf-8') as file:
...         file.write('Hello')
...
>>> # At this point, the folders and files now exist. 

此代码将在你的主文件夹中创建以下文件夹和文件:

  • The spam 文件夹

  • The spam/file1.txt 文件

  • The spam/eggs 文件夹

  • The spam/eggs/file2.txt 文件

  • The spam/eggs/file3.txt 文件

  • The spam/eggs2 文件夹

  • The spam/eggs/bacon 文件夹

  • The spam/eggs/bacon/file4.txt 文件

这里有一个示例程序,它使用 os.walk() 函数遍历这个文件夹树,并将每个文件重命名为大写字母:

import os, shutil
from pathlib import Path
h = Path.home()

for folder_name, subfolders, filenames in os.walk(h / 'spam'):
    print('The current folder is ' + folder_name)

    for subfolder in subfolders:
        print('SUBFOLDER OF ' + folder_name + ': ' + subfolder)

    for filename in filenames:
        print('FILE INSIDE ' + folder_name + ': '+ filename)
        # Rename file to uppercase:
        p = Path(folder_name)
        shutil.move(p / filename, p / filename.upper())

    print('') 

os.walk() 函数传递一个单个字符串值:文件夹的路径。你可以在 for 循环中使用 os.walk() 来遍历目录树,就像你可以使用 range() 函数遍历一系列数字一样。与 range() 不同,os.walk() 函数在循环的每次迭代中都会返回三个值:

  • 当前文件夹名称的字符串

  • 当前文件夹中子文件夹的字符串列表

  • 当前文件夹中文件的字符串列表

这里的 当前文件夹 指的是 for 循环当前迭代中访问的文件夹。os.walk() 函数不会改变程序的当前工作目录。就像你可以在代码 for i in range(10): 中选择变量名 i 一样,你也可以为前面列出的三个值选择变量名。我总是使用描述性的名称 folder_namesubfoldersfilenames

当我在电脑上运行这个程序时,它给出了以下输出:

The current folder is C:\Users\Al\spam
SUBFOLDER OF C:\Users\Al\spam: eggs
SUBFOLDER OF C:\Users\Al\spam: eggs2
FILE INSIDE C:\Users\Al\spam: file1.txt

The current folder is C:\Users\Al\spam\eggs
SUBFOLDER OF C:\Users\Al\spam\eggs: bacon
FILE INSIDE C:\Users\Al\spam\eggs: file2.txt
FILE INSIDE C:\Users\Al\spam\eggs: file3.txt

The current folder is C:\Users\Al\spam\eggs\bacon
FILE INSIDE C:\Users\Al\spam\eggs\bacon: file4.txt

The current folder is C:\Users\Al\spam\eggs2 

由于 os.walk() 返回 subfolderfilename 变量的字符串列表,您可以使用返回值在自己的 for 循环中使用。例如,您可以将文件夹和文件名传递给像 shutil.move() 这样的函数,如下例所示。

使用 zipfile 模块压缩文件

您可能熟悉 ZIP 文件(具有 .zip 文件扩展名),它可以包含许多其他文件的压缩内容。压缩文件可以减小其大小,这在通过互联网传输时非常有用。由于 ZIP 文件还可以包含多个文件和子文件夹,因此它是将多个文件打包成一个文件的便捷方式。这个单独的文件,称为 存档文件,然后可以,比如说,附加到电子邮件中。

您的 Python 程序可以使用 zipfile 模块中的函数创建或从 ZIP 文件中提取内容。

创建和添加到 ZIP 文件

要创建您自己的压缩 ZIP 文件,您必须通过将 'w' 作为第二个参数传递来以写入模式打开 ZipFile 对象。(注意对象名称中的大写字母 ZF,它与 zipfile 模块名称不同。)此过程类似于通过将 'w' 传递给 open() 函数以在写入模式下打开文本文件。对于文件名,您可以传递字符串或 Path 对象。

当您将路径传递给 ZipFile 对象的 write() 方法时,Python 将压缩该路径上的文件并将其添加到 ZIP 文件中。write() 方法的第一个参数是要添加的文件名的字符串。第二个参数是 压缩类型 参数,它告诉计算机应该使用什么算法来压缩文件;您始终可以将此值设置为 zipfile.ZIP_DEFLATED 以指定 deflate 压缩算法,该算法对所有类型的数据都适用。如果您不传递此值,write() 方法将使用常规、未压缩的大小将文件添加到 ZIP 文件中。在交互式外壳中输入以下内容:

>>> import zipfile
>>> with open('file1.txt', 'w', encoding='utf-8') as file_obj:
...     file_obj.write('Hello' * 10000)
...
>>> with zipfile.ZipFile('example.zip', 'w') as example_zip:
...     example_zip.write('file1.txt', compress_type=zipfile.ZIP_DEFLATED,
 compresslevel=9) 

此代码创建一个名为 file1.txt 的文本文件,并向其中写入 50,000 个字符的字符串 'Hello' * 10000(约 49KB)。然后,它创建一个名为 example.zip 的新 ZIP 文件,其中包含 file1.txt 的压缩内容(约 213 字节;高度重复的数据也高度可压缩)。compresslevel 关键字参数(从 Python 3.7 及以后版本添加)可以设置为从 09 的任何值,其中 9 是最慢但压缩程度最高的级别。如果您不指定此关键字参数,则默认值为 6

zipfile.ZipFile() 函数在 with 语句中打开 ZIP 文件,其方式类似于 open() 函数打开文件。这确保了当执行离开 with 语句的代码块时,会自动调用 close() 方法。

请记住,就像写入文件一样,写入模式将擦除 ZIP 文件中所有现有的内容。如果您只想向现有的 ZIP 文件中添加文件,请将 'a' 作为 zipfile.ZipFile() 的第二个参数传递,以以 追加模式 打开 ZIP 文件。

读取 ZIP 文件

要读取 ZIP 文件的内容,你必须首先通过调用 zipfile.ZipFile() 函数并传递 ZIP 文件的名称来创建一个 ZipFile 对象。请注意,zipfile 是 Python 模块的名称,而 ZipFile() 是函数的名称。

例如,将以下内容输入到交互式 shell 中:

>>> import zipfile

>>> example_zip = zipfile.ZipFile('example.zip')
>>> example_zip.namelist()
['file1.txt']
>>> file1_info = example_zip.getinfo('file1.txt')
>>> file1_info.file_size
50000
>>> file1_info.compress_size
97
>>> f'Compressed file is {round(file1_info.file_size / file1_info # ❶
 .compress_size, 2)}x smaller!'

'Compressed file is 515.46x smaller!'
>>> example_zip.close() 

ZipFile 对象有一个 namelist() 方法,它返回 ZIP 文件中包含的所有文件和文件夹的字符串列表。这些字符串可以传递给 getinfo() 方法,以返回关于该特定文件的 ZipInfo 对象。ZipInfo 对象有自己的属性,例如 file_sizecompress_size,它们分别以字节为单位持有表示原始文件大小和压缩文件大小的整数。虽然 ZipFile 对象代表整个存档文件,但 ZipInfo 对象包含关于存档中单个文件的有用信息。

处的命令通过将原始文件大小除以压缩文件大小来计算 example.zip 的压缩效率,然后打印此信息。 # ❶

从 ZIP 文件中提取

ZipFile 对象的 extractall() 方法可以将 ZIP 文件中的所有文件和文件夹提取到当前工作目录。按照第 249 页“创建和添加到 ZIP 文件”中的说明创建一个名为 example.zip 的 ZIP 文件,然后进入交互式 shell 中输入以下命令:

>>> import zipfile
>>> example_zip = zipfile.ZipFile('example.zip')
>>> example_zip.extractall() # ❶
>>> example_zip.close() 

运行此代码后,Python 将将 example.zip 的内容提取到当前工作目录。可选地,你可以传递一个文件夹名称给 extractall(),以便将其提取到当前工作目录以外的文件夹中。如果传递给 extractall() 方法的文件夹不存在,Python 将创建它。例如,如果你将 ❶ 处的调用替换为 example_zip.extractall('C:\\spam'),代码将把 example.zip 中的文件提取到新创建的 C:\spam 文件夹中。

ZipFile 对象的 extract() 方法将从一个 ZIP 文件中提取单个文件。继续交互式 shell 示例,输入以下命令:

>>> example_zip.extract('file1.txt')
'C:\\Users\\Al\\Desktop\\file1.txt'
>>> example_zip.extract('file1.txt', 'C:\\some\\new\\folders')
'C:\\some\\new\\folders\\file1.txt'
>>> example_zip.close() 

传递给 extract() 的字符串必须与 namelist() 返回的列表中的某个字符串匹配。可选地,你可以传递第二个参数给 extract(),以将文件提取到当前工作目录以外的文件夹中。如果这个第二个参数是一个尚不存在的文件夹,Python 将创建该文件夹。

项目 5:将文件夹备份到 ZIP 文件中

假设你正在处理一个项目,你将文件保存在名为C:\Users\Al\AlsPythonBook的文件夹中。你担心丢失你的工作,因此你想创建整个文件夹的 ZIP 文件“快照”。你还想保留这些快照的不同版本,因此你希望 ZIP 文件的文件名在创建新版本时递增;例如,AlsPythonBook_1.zipAlsPythonBook_2.zipAlsPythonBook_3.zip,依此类推。你可以手动完成这项工作,但这会相当麻烦,你可能会不小心错误地编号 ZIP 文件的名称。运行一个为你完成这项无聊任务的程序会简单得多。

对于这个项目,打开一个新的文件编辑窗口,并将其保存为backup_to_zip.py

第 1 步:确定 ZIP 文件的名称

我们将把这个程序的代码放入一个名为backup_to_zip()的函数中。这将使得将函数复制粘贴到需要此功能的其他 Python 程序变得容易。在程序末尾,将调用该函数以执行备份。让你的程序看起来像这样:

# backup_to_zip.py - Copies an entire folder and its contents into
# a ZIP file whose filename increments

import zipfile, os
from pathlib import Path

def backup_to_zip(folder):
    # Back up the entire contents of "folder" into a ZIP file.
    folder = Path(folder)  # Make sure folder is a Path object, not string.

    # Figure out the ZIP filename this code should use, based on
    # what files already exist.
    number = 1 # ❶
    number = 1 # ❶

        zip_filename = Path(folder.parts[-1] + '_' + str(number) + '.zip')
        if not zip_filename.exists():
            break
        number = number + 1

    number = 1 # ❶

 # TODO: Walk the entire folder tree and compress the files in each folder.
    print('Done.')

backup_to_zip(Path.home() / 'spam') 

首先,导入zipfileos模块。接下来,定义一个名为backup_to_zip()的函数,该函数仅接受一个参数,即folder。此参数是一个字符串或Path对象,指向需要备份内容的文件夹。该函数将确定要为它创建的 ZIP 文件使用什么文件名。然后,它将创建文件,遍历folder文件夹,并将每个子文件夹和文件添加到 ZIP 文件中。在源代码中为这些步骤编写TODO注释,以提醒自己在以后完成它们 ❸。

第一个任务,命名 ZIP 文件,使用folder的绝对路径的基本名称。如果正在备份的文件夹是C:\Users\Al\spam,则 ZIP 文件的名称应该是spam_N.zip,其中N是第一次运行程序时为 1,第二次为 2,依此类推。

你可以通过检查spam_1.zip是否已经存在,然后检查spam_2.zip是否已经存在,依此类推来确定N应该是什么。使用名为number的变量N ❶,并在调用exists()检查文件是否存在的循环中递增它。找到的第一个不存在的文件名将导致循环break,因为它已经找到了新 ZIP 的文件名。

第 2 步:创建新的 ZIP 文件

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

# backup_to_zip.py - Copies an entire folder and its contents into
# a ZIP file whose filename increments

# --snip--

    # Create the ZIP file.
 print(f'Creating {zip_filename}...')
 backup_zip = zipfile.ZipFile(zip_filename, 'w')

    # TODO: Walk the entire folder tree and compress the files in each folder.
    print('Done.')

backup_to_zip(Path.home() / 'spam') 

现在新的 ZIP 文件名已存储在zip_filename变量中,你可以调用zipfile.ZipFile()来实际创建 ZIP 文件。确保将'w'作为第二个参数传递以在写入模式下打开 ZIP 文件。我们还将从注释中删除 TODO,因为我们已经完成了这一节代码的编写。

第 3 步:遍历目录树

现在你需要使用os.walk()函数来完成列出文件夹及其子文件夹中每个文件的工作。让你的程序看起来像以下这样:

# backup_to_zip.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 folder_name, subfolders, filenames in os.walk(folder): # ❶
 folder_name = Path(folder_name)
 print(f'Adding files in folder {folder_name}...')

        # Add all the files in this folder to the ZIP file.
for filename in filenames: # ❷
 print(f'Adding file {filename}...')
 backup_zip.write(folder_name / filename)
 backup_zip.close()
    print('Done.')
backup_to_zip(Path.home() / 'spam') 

for 循环中使用 os.walk() ❶。在每次迭代中,该函数将返回当前文件夹的名称、该文件夹中的子文件夹以及该文件夹中的文件名。嵌套的 for 循环可以遍历 filenames 列表中的每个文件名 ❷。这些文件中的每一个都将被添加到 ZIP 文件中。

当你运行这个程序时,它应该产生类似以下的输出:

Creating spam_1.zip...
Adding files in spam...
Adding file file1.txt...
Done. 

第二次运行时,它将所有文件放入 spam 文件夹中,并创建一个名为 spam_2.zip 的 ZIP 文件,依此类推。

其他程序的想法

你可以在其他几个程序中遍历目录树并将文件添加到压缩 ZIP 归档中。例如,你可以编写执行以下操作的程序:

  • 遍历目录树,仅归档具有特定扩展名的文件,例如 .txt.py,其他文件则不归档。

  • 遍历目录树并归档除 .txt.py 之外的所有文件。

  • 仅归档目录树中使用最多磁盘空间或自上次归档以来已修改的文件夹。

创建和添加到 ZIP 文件

要创建自己的压缩 ZIP 文件,你必须通过传递 'w' 作为第二个参数以写入模式打开 ZipFile 对象。(注意对象名称中的大写字母 ZF,这与 zipfile 模块名称不同。)这个过程类似于通过将 'w' 传递给 open() 函数以写入模式打开文本文件。对于文件名,你可以传递一个字符串或一个 Path 对象。

当你将路径传递给 ZipFile 对象的 write() 方法时,Python 将压缩该路径上的文件并将其添加到 ZIP 文件中。write() 方法的第一个参数是要添加的文件名字符串。第二个参数是 压缩类型 参数,它告诉计算机应使用什么算法来压缩文件;你可以始终将此值设置为 zipfile.ZIP_DEFLATED 以指定 deflate 压缩算法,该算法对所有类型的数据都适用。如果你不传递此值,则 write() 方法将文件以常规、未压缩的大小添加到 ZIP 文件中。在交互式外壳中输入以下内容:

>>> import zipfile
>>> with open('file1.txt', 'w', encoding='utf-8') as file_obj:
...     file_obj.write('Hello' * 10000)
...
>>> with zipfile.ZipFile('example.zip', 'w') as example_zip:
...     example_zip.write('file1.txt', compress_type=zipfile.ZIP_DEFLATED,
 compresslevel=9) 

此代码创建一个名为 file1.txt 的文本文件,并向其中写入 50,000 个字符的字符串 'Hello' * 10000(约 49KB)。然后,它创建一个名为 example.zip 的新 ZIP 文件,其中包含 file1.txt 的压缩内容(约 213 字节;高度重复的数据也高度可压缩)。compresslevel 关键字参数(自 Python 3.7 及以后版本添加)可以设置为从 09 的任何值,其中 9 是最慢但压缩程度最高的级别。如果你不指定此关键字参数,则默认为 6

zipfile.ZipFile() 函数以类似于 open() 函数打开文件的方式在 with 语句中打开 ZIP 文件。这确保了当执行离开 with 语句的代码块时,会自动调用 close() 方法。

请记住,就像写入文件一样,写入模式将擦除 ZIP 文件中所有现有的内容。如果你想简单地向现有的 ZIP 文件中添加文件,将 'a' 作为 zipfile.ZipFile() 的第二个参数传递,以以 追加模式 打开 ZIP 文件。

读取 ZIP 文件

要读取 ZIP 文件的内容,你必须首先通过调用 zipfile.ZipFile() 函数并传递 ZIP 文件的文件名来创建一个 ZipFile 对象。请注意,zipfile 是 Python 模块的名称,ZipFile() 是函数的名称。

例如,将以下内容输入到交互式 shell 中:

>>> import zipfile

>>> example_zip = zipfile.ZipFile('example.zip')
>>> example_zip.namelist()
['file1.txt']
>>> file1_info = example_zip.getinfo('file1.txt')
>>> file1_info.file_size
50000
>>> file1_info.compress_size
97
>>> f'Compressed file is {round(file1_info.file_size / file1_info # ❶
 .compress_size, 2)}x smaller!'

'Compressed file is 515.46x smaller!'
>>> example_zip.close() 

ZipFile 对象有一个 namelist() 方法,它返回 ZIP 文件中包含的所有文件和文件夹的字符串列表。这些字符串可以传递给 getinfo() ZipFile 方法,以返回有关该特定文件的 ZipInfo 对象。ZipInfo 对象有自己的属性,例如 file_sizecompress_size,它们分别以字节为单位持有表示原始文件大小和压缩文件大小的整数。虽然 ZipFile 对象代表整个存档文件,但 ZipInfo 对象包含有关存档中单个文件的有用信息。

❶处的命令通过将原始文件大小除以压缩文件大小来计算 example.zip 的压缩效率,然后打印此信息。

从 ZIP 文件中提取

ZipFile 对象的 extractall() 方法会将 ZIP 文件中的所有文件和文件夹提取到当前工作目录。按照第 249 页“创建和添加 ZIP 文件”中的说明创建一个名为 example.zip 的 ZIP 文件,然后进入交互式 shell 中输入以下内容:

>>> import zipfile
>>> example_zip = zipfile.ZipFile('example.zip')
>>> example_zip.extractall() # ❶
>>> example_zip.close() 

运行此代码后,Python 将将 example.zip 的内容提取到当前工作目录。可选地,你可以向 extractall() 方法传递一个文件夹名称,以便将其提取到当前工作目录以外的文件夹中。如果传递给 extractall() 方法的文件夹不存在,Python 将创建它。例如,如果你将❶处的调用替换为 example_zip.extractall('C:\\spam'),代码将把 example.zip 中的文件提取到新创建的 C:\spam 文件夹中。

ZipFile 对象的 extract() 方法将从 ZIP 文件中提取单个文件。继续交互式 shell 示例,输入以下内容:

>>> example_zip.extract('file1.txt')
'C:\\Users\\Al\\Desktop\\file1.txt'
>>> example_zip.extract('file1.txt', 'C:\\some\\new\\folders')
'C:\\some\\new\\folders\\file1.txt'
>>> example_zip.close() 

你传递给 extract() 的字符串必须与 namelist() 返回的列表中的某个字符串匹配。可选地,你可以向 extract() 传递第二个参数,以将文件提取到当前工作目录以外的文件夹中。如果这个第二个参数是一个尚不存在的文件夹,Python 将创建该文件夹。

项目 5:将文件夹备份到 ZIP 文件中

假设你正在处理一个项目,你将文件保存在名为 C:\Users\Al\AlsPythonBook 的文件夹中。你担心丢失你的工作,所以你想要创建整个文件夹的 ZIP 文件“快照”。你还想保留这些快照的不同版本,所以你希望 ZIP 文件的文件名在每次创建新版本时递增;例如,AlsPythonBook_1.zipAlsPythonBook_2.zipAlsPythonBook_3.zip,以此类推。你可以手动完成这项工作,但这会很烦人,你可能会不小心错误地编号 ZIP 文件的名称。运行一个为你完成这项无聊任务的程序会简单得多。

对于这个项目,打开一个新的文件编辑窗口并将其保存为 backup_to_zip.py

第 1 步:确定 ZIP 文件的名称

我们将把这个程序的代码放入一个名为 backup_to_zip() 的函数中。这将使得将函数复制粘贴到需要此功能的其他 Python 程序变得容易。在程序末尾,将调用该函数以执行备份。让你的程序看起来像这样:

# backup_to_zip.py - Copies an entire folder and its contents into
# a ZIP file whose filename increments

import zipfile, os
from pathlib import Path

def backup_to_zip(folder):
    # Back up the entire contents of "folder" into a ZIP file.
    folder = Path(folder)  # Make sure folder is a Path object, not string.

    # Figure out the ZIP filename this code should use, based on
    # what files already exist.
    number = 1 # ❶
    number = 1 # ❶

        zip_filename = Path(folder.parts[-1] + '_' + str(number) + '.zip')
        if not zip_filename.exists():
            break
        number = number + 1

    number = 1 # ❶

 # TODO: Walk the entire folder tree and compress the files in each folder.
    print('Done.')

backup_to_zip(Path.home() / 'spam') 

首先,导入 zipfileos 模块。接下来,定义一个名为 backup_to_zip() 的函数,它只接受一个参数 folder。这个参数是一个字符串或 Path 对象,指向需要备份内容的文件夹。该函数将确定要为它创建的 ZIP 文件使用什么文件名。然后,它将创建文件,遍历 folder 文件夹,并将每个子文件夹和文件添加到 ZIP 文件中。在源代码中为这些步骤写上 TODO 注释,以提醒自己在稍后执行它们 ❸。

命名 ZIP 文件的第一项任务使用 folder 的绝对路径的基本名称。如果正在备份的文件夹是 C:\Users\Al\spam,ZIP 文件的名称应该是 spam_N.zip,其中 N 是第一次运行程序时的 1,第二次运行时的 2,以此类推。

你可以通过检查 spam_1.zip 是否已存在,然后检查 spam_2.zip 是否已存在,以此类推来确定 N 应该是什么。为 N 使用一个名为 number 的变量 ❶,并在调用 exists() 函数检查文件是否存在时在循环中递增它 ❷。找到的第一个不存在的文件名将导致循环 break,因为它将找到新 ZIP 文件的文件名。

第 2 步:创建新的 ZIP 文件

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

# backup_to_zip.py - Copies an entire folder and its contents into
# a ZIP file whose filename increments

# --snip--

    # Create the ZIP file.
 print(f'Creating {zip_filename}...')
 backup_zip = zipfile.ZipFile(zip_filename, 'w')

    # TODO: Walk the entire folder tree and compress the files in each folder.
    print('Done.')

backup_to_zip(Path.home() / 'spam') 

现在新的 ZIP 文件名称已存储在 zip_filename 变量中,你可以调用 zipfile.ZipFile() 来实际创建 ZIP 文件。确保将 'w' 作为第二个参数传递以在写入模式下打开 ZIP 文件。我们还将从注释中删除 TODO,因为我们已经完成了这一节代码的编写。

第 3 步:遍历目录树

现在你需要使用 os.walk() 函数来完成列出文件夹及其子文件夹中每个文件的工作。让你的程序看起来像下面这样:

# backup_to_zip.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 folder_name, subfolders, filenames in os.walk(folder): # ❶
 folder_name = Path(folder_name)
 print(f'Adding files in folder {folder_name}...')

        # Add all the files in this folder to the ZIP file.
for filename in filenames: # ❷
 print(f'Adding file {filename}...')
 backup_zip.write(folder_name / filename)
 backup_zip.close()
    print('Done.')
backup_to_zip(Path.home() / 'spam') 

for循环中使用os.walk()❶。在每次迭代中,该函数将返回当前迭代文件夹的名称、该文件夹中的子文件夹以及该文件夹中的文件名。嵌套的for循环可以遍历filenames列表中的每个文件名❷。这些文件名中的每一个都将被添加到 ZIP 文件中。

当你运行这个程序时,它应该产生类似以下的输出:

Creating spam_1.zip...
Adding files in spam...
Adding file file1.txt...
Done. 

第二次运行时,它将所有文件放入名为spam_2.zip的 ZIP 文件中,依此类推。

其他程序的想法

你可以在其他几个程序中遍历目录树并将文件添加到压缩的 ZIP 存档中。例如,你可以编写执行以下操作的程序:

  • 遍历目录树并仅归档具有特定扩展名(如.txt.py)的文件,其他文件则不归档。

  • 遍历目录树并归档除.txt.py文件之外的所有文件。

  • 仅归档使用最多磁盘空间或自上次归档以来已修改的目录树中的文件夹。

总结

即使你是一个经验丰富的计算机用户,你也可能需要手动使用鼠标和键盘来处理文件。现代文件浏览器使得处理少量文件变得容易。但有时你需要执行一个任务,使用计算机的文件浏览器可能需要数小时。

osshutil模块提供了复制、移动、重命名和删除文件的功能。在删除文件时,你可能想使用send2trash模块将文件移动到回收站或垃圾桶,而不是永久删除它们。并且当编写处理文件的程序时,进行一个干运行是个好主意;注释掉实际执行复制、移动、重命名或删除的代码,并用print()调用替换。这样,你可以运行程序并验证它确切会做什么。

通常,你不仅需要在单个文件夹中的文件上执行这些操作,还需要在该文件夹中的每个子文件夹、这些子文件夹中的每个子文件夹,依此类推的每个子文件夹中执行这些操作。os.walk()函数会为你处理这些文件夹间的遍历,这样你就可以专注于你的程序需要对这些文件做什么。

zipfile模块通过 Python 提供了一种压缩和解压缩.zip存档文件的方法。结合osshutil的文件处理函数,zipfile使得从硬盘上的任何位置打包多个文件变得容易。这些 ZIP 文件比许多单独的文件更容易上传到网站或作为电子邮件附件发送。

实践问题

  1. shutil.copy()shutil.copytree()之间有什么区别?

  2. 用来重命名文件的功能是什么?

  3. send2trashshutil模块的删除函数之间有什么区别?

  4. ZipFile对象有一个close()方法,就像File对象的close()方法一样。哪个ZipFile方法相当于File对象的open()方法?

实践程序

为了练习,编写程序来完成以下任务。

选择性复制

编写一个程序,遍历文件夹树并搜索具有特定文件扩展名(如.pdf.jpg)的文件。将这些文件从当前位置复制到新文件夹中。

删除不需要的文件

几个不需要但体积庞大的文件或文件夹占据硬盘大部分空间的情况并不少见。如果你试图在电脑上释放空间,首先识别最大的不需要文件会更有效。

编写一个程序,遍历文件夹树并搜索异常大的文件或文件夹——比如说,文件大小超过 100MB 的文件。(记住,为了获取文件的大小,你可以使用os.path.getsize()函数,该函数来自os模块。)将这些文件及其绝对路径打印到屏幕上。

文件重命名

编写一个程序,在单个文件夹中查找所有具有给定前缀的文件,例如spam001.txtspam002.txt等,并定位任何编号中的间隔(例如,如果有一个spam001.txt和一个spam003.txt但没有spam002.txt)。让程序重命名所有后续文件以关闭这个间隔。

为了创建这些示例文件(跳过spam042.txtspam086.txtspam103.txt),运行以下代码:

>>> for i in range(1, 121):
...     if i not in (42, 86, 103):
...         with open(f'spam{str(i).zfill(3)}.txt', 'w') as file:
...             pass
... 

作为额外的挑战,编写另一个程序,可以在编号文件中插入间隔(并在间隔后的文件名中提升数字)以便可以插入新文件。

将日期从美国风格转换为欧洲风格

假设你的老板通过电子邮件给你发送了成千上万的文件,这些文件的名称中包含美国风格的日期(MM-DD-YYYY),并且需要将它们重命名为欧洲风格的日期(DD-MM-YYYY)。这个无聊的任务可能需要一整天的时间手动完成!相反,编写一个程序来完成以下操作:

  1. 在当前工作目录及其所有子目录中搜索所有文件名中的美国风格日期。使用os.walk()函数遍历子文件夹。

  2. 使用正则表达式识别包含 MM-DD-YYYY 模式的文件名——例如,spam12-31-1900.txt。假设月份和日期总是使用两位数字,并且不存在非日期匹配的文件。(你不会找到名为99-99-9999.txt的文件。)

  3. 当找到一个文件名时,将其重命名为月份和日期互换的欧洲风格格式。使用shutil.move()函数进行重命名。

选择性复制

编写一个程序,遍历文件夹树并搜索具有特定文件扩展名(如.pdf.jpg)的文件。将这些文件从当前位置复制到新文件夹中。

删除不需要的文件

几个不需要但体积庞大的文件或文件夹占据硬盘大部分空间的情况并不少见。如果你试图在电脑上释放空间,首先识别最大的不需要文件会更有效。

编写一个程序,遍历文件夹树并搜索异常大的文件或文件夹——比如说,文件大小超过 100MB 的文件。(记住,为了获取文件的大小,你可以使用os.path.getsize()函数,该函数来自os模块。)将这些文件及其绝对路径打印到屏幕上。

文件重命名

编写一个程序,查找单个文件夹中所有具有给定前缀的文件,例如spam001.txtspam002.txt等等,并定位编号中的任何间隔(例如,如果有一个spam001.txt和一个spam003.txt但没有spam002.txt)。让程序重命名所有后续文件以关闭这个间隔。

为了创建这些示例文件(跳过spam042.txtspam086.txtspam103.txt),请运行以下代码:

>>> for i in range(1, 121):
...     if i not in (42, 86, 103):
...         with open(f'spam{str(i).zfill(3)}.txt', 'w') as file:
...             pass
... 

作为额外的挑战,编写另一个程序,可以插入编号文件的间隔(并在间隔后的文件名中增加数字)以便插入新文件。

将日期从美国风格转换为欧洲风格

假设你的老板给你发送了数千个文件,这些文件的名称中包含美国风格的日期(MM-DD-YYYY),并且需要将它们重命名为欧洲风格的日期(DD-MM-YYYY)。这项无聊的任务如果手动完成可能需要一整天的时间!相反,编写一个程序来完成以下任务:

  1. 在当前工作目录及其所有子目录中搜索所有文件名,查找美国风格的日期。使用os.walk()函数遍历子文件夹。

  2. 使用正则表达式来识别包含 MM-DD-YYYY 模式的文件名——例如,spam12-31-1900.txt。假设月份和日期总是使用两位数字,并且不存在非日期匹配的文件。(你不会找到名为99-99-9999.txt的文件。)

  3. 当找到文件名时,使用shutil.move()函数将文件名中的月份和日期交换,使其成为欧洲风格。

12 设计和部署命令行程序

原文:automatetheboringstuff.com/3e/chapter12.html

到目前为止,我们一直专注于从 Mu(或从您正在使用的任何代码编辑器)运行程序。本章讨论了如何从命令行终端运行程序。命令行可能让人感到害怕,因为它有难以理解的命令和缺乏用户友好的界面,大多数用户都远离它。但熟悉它确实有一些真正的益处,而且它并不比您迄今为止所做的任何编程更具挑战性。

一旦您编写了一个 Python 程序来自动化某些任务,每次想要运行它时都不得不打开 Mu 可能会是一种负担。命令行是执行 Python 脚本(尤其是如果您与没有安装 Mu 或甚至 Python 的朋友或同事共享程序时)的一种更方便的方式。在软件开发中,部署 是使我们的软件在我们的代码编辑器之外可用的过程。

任何名字的程序

本章(以及编程本身)使用了大量意味着 程序 或该术语的轻微变体的术语。您可以准确地称以下所有项目为程序。但它们之间的名称含义存在细微差别:

程序 一个完整的软件,无论大小,包含计算机执行的操作指令。

脚本 解释器从其源代码形式而不是编译后的机器代码形式运行的程序。这是一个非常宽泛的术语。Python 程序通常被称为脚本,尽管 Python 代码可以像其他语言一样编译(您将在“使用 PyInstaller 编译 Python 程序”中了解到这一点,见第 285 页)。

命令 通常从基于文本的终端运行且没有图形用户界面(GUI)的程序。所有配置都是在运行命令之前通过指定命令行参数来预先完成的(尽管 交互式命令 有时可能会用“您确定吗?Y/N”的问题来中断其操作)。在“cd、pwd、dir 和 ls 命令”中解释的 dirls 都是命令的例子。

Shell 脚本 一个方便地运行多个捆绑终端命令的单个文本文件。这样,用户可以运行一个 shell 脚本而不是手动逐个输入几个命令。在 macOS 和 Linux 上,shell 脚本文件具有 .sh 文件扩展名(或没有扩展名),而 Windows 使用 批处理文件 术语来表示具有 .bat 文件扩展名的 shell 脚本。

应用程序 一个具有图形用户界面(GUI)并包含多个相关功能的程序。Excel 和 Firefox 是应用程序的例子。应用程序通常有几个文件,由 安装程序 在您的计算机上设置(并且由 卸载程序 移除),而不是仅由复制到计算机上的单个可执行文件组成。

应用 是指手机和平板电脑应用程序的常见名称,但这个术语也可以用于桌面应用程序。

Web 应用 指的是运行在 Web 服务器上的程序,用户可以通过 Web 浏览器通过互联网与之交互。

您可以对这些术语的精确定义吹毛求疵;这些解释仅仅应该给您一个这些术语用法的总体感觉。如果您想熟悉更多术语,我的书《Python 进阶》(No Starch Press,2020)在其“编程术语”章节中有额外的定义。

使用终端

直到 20 世纪 90 年代,当苹果公司和微软推广了可以同时运行多个程序的具有图形用户界面的计算机时,程序都是从命令行界面(CLI,发音为“see-el-eye”或与“fly”押韵的词)启动的,并且通常限于基于文本的输入和输出。您也可能听到 CLI 被称为命令提示符、终端、shell 或控制台。软件开发人员仍然使用 CLI,并且通常在任何给定时间都会在他们的计算机上打开几个终端窗口。虽然基于文本的终端可能没有 GUI 的图标、按钮和图形,但一旦您学会了几个命令,它就是一个有效使用计算机的方法。

要打开终端窗口,请执行以下操作:

  • 在 Windows 上,点击开始按钮(或按Windows 键)并输入命令提示符(或如果您已安装,则输入PowerShell终端)。

  • 在 macOS 上,点击右上角的 Spotlight 图标(或按-空格键)并输入终端

  • 在 Ubuntu Linux 上,按Windows 键调出 Dash,并输入终端。或者,使用快捷键 CTRL-ALT-T。

就像交互式 shell 有一个>>>提示符一样,终端会显示一个提示符供您输入命令。在 Windows 上,这将是你当前所在文件夹的完整路径,后面跟着一个尖括号(>):

C:\Users\al>your commands go here

在 macOS 上,提示符显示您的用户名、您的计算机名称和当前工作目录(您的家目录用~表示),后面跟着一个百分号(%):

al@Als-MacBook-Pro ~ % your commands go here

在 Ubuntu Linux 上,提示符与 macOS 上的提示符类似,但以用户名和一个@符号开头:

al@al-VirtualBox:~$ your commands go here

虽然从开始菜单(Windows)或 Spotlight(macOS)运行程序更容易,但也可以从终端启动它们。Python 解释器本身就是一个经常在终端中运行的程序。

在本章中,我们假设您要运行的 Python 程序名为yourScript.py,并且它位于您家目录下的Scripts文件夹中。您不需要打开 Mu 来访问 Python 交互式 shell。从终端窗口,您可以在 Windows 上输入python或在 macOS 和 Linux 上输入python3来启动它。(您应该会看到其熟悉的>>>提示符。)要从终端运行您的.py Python 文件,请在pythonpython3后输入其文件路径——无论是其绝对形式,如python C:\Users\al\Scripts\yourScript.py,还是相对形式,如python yourScript.py,如果当前工作目录设置为C:\Users\al\Scripts,即yourScript.py所在的文件夹。

cd、pwd、dir 和 ls 命令

正如所有运行中的程序都有一个当前工作目录(CWD)来附加相对文件路径一样,终端也有一个当前工作目录。您可以在终端提示符中看到这个 CWD,或者通过在 macOS 和 Linux 上运行pwd(打印工作目录)或在 Windows 上运行不带任何命令行参数的cd命令来查看它。

您的 Python 程序可以通过调用os.chdir()函数来更改 CWD。在终端中,您可以通过输入cd命令后跟要更改到的文件夹的相对或绝对文件路径来完成相同的事情:

C:\Users\al>cd Desktop
C:\Users\al\Desktop>cd ..
C:\Users\al>cd C:\Windows\System32
C:\Windows\System32> 

在 Windows 上,您可能还需要额外的步骤来切换驱动器字母。您不能使用cd命令更改您所在的驱动器。相反,输入驱动器字母后跟一个冒号,然后使用cd在该驱动器上更改目录:

C:\Windows\System32>D:
D:\>cd backup
D:\backup> 

Windows 上的dir命令和 macOS 及 Linux 上的ls命令会列出当前工作目录(CWD)下的文件和子文件夹内容:

C:\Users\al>dir
# --snip--
08/26/2036  06:42 PM           171,304 _recursive-centaur.png
08/18/2035  11:25 AM             1,278 _viminfo
08/13/2035  12:58 AM    <DIR>          __pycache__
              77 File(s)     83,805,114 bytes
             108 Dir(s)  149,225,267,200 bytes free 

当在终端中导航文件系统时,您通常会交替使用cd来更改目录和dir/ls来查看目录内容。您可以在 Windows 上通过运行dir *.exe和在 macOS 和 Linux 上通过运行file * | grep executable来列出 CWD 中的所有可执行文件。一旦您在包含程序的文件夹中,您可以以下列方式运行程序:

  • 在 Windows 上,输入程序名时可以带有或不带.exe扩展名:example.exe

  • 在 macOS 和 Linux 上,输入./后跟程序名:./example

当然,您始终可以输入程序的完整绝对路径:C:\full\path\to\example.exe/full/path/to/example

如果您想打开非程序文件,例如名为example.txt的文本文件,您可以在 Windows 上输入example.txt或在 macOS 和 Linux 上输入open example.txt来使用其关联的应用程序打开它。在终端中,这会做与在 GUI 中双击example.txt文件图标相同的事情。如果没有为.txt文件设置关联的应用程序,操作系统将提示用户选择一个并记住它以供将来使用。

PATH 环境变量

所有正在运行的程序,无论它们是用什么语言编写的,都有一组称为 环境变量 的字符串变量。其中之一是 PATH 环境变量,它包含当你在终端中输入程序名称时终端会检查的文件夹列表。例如,如果你在 Windows 上输入 python 或者在 macOS 和 Linux 上输入 python3,终端会在 PATH 中列出的文件夹中查找具有该名称的程序。操作系统在使用 PATH 方面有一些不同的规则:

  • Windows 首先在当前工作目录 (CWD) 中查找该名称的程序,然后是 PATH 中的文件夹。

  • Linux 和 macOS 只检查 PATH 中的文件夹,而不会检查当前工作目录 (CWD)。如果你想运行当前工作目录中名为 example 的程序,你必须输入 ./example 而不是 example

要查看 PATH 环境变量的内容,请在 Windows 上运行 echo %PATH% 或者在 macOS 和 Linux 上运行 echo $PATHPATH 中存储的值是一个由分号(在 Windows 上)或冒号(在 macOS 和 Linux 上)分隔的文件夹名称的长字符串。例如,在 Ubuntu Linux 上,PATH 环境变量可能看起来像以下这样:

al@al-virtual-machine:~$ echo $PATH
/home/al/.local/bin:/home/al/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/
usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin 

如果你将 python3 输入到带有此 PATH 的 Linux 终端中,Linux 会首先在 /home/al/.local/bin 文件夹中查找名为 python3 的程序,然后是 /home/al/bin 文件夹,依此类推。最终会在 /usr/bin 中找到 python3 并运行它。PATH 环境变量很方便,因为你可以将程序放入 PATH 上的一个文件夹中,这样每次运行它时就不需要使用 cd 命令切换到其文件夹。

注意,终端窗口不会在 PATH 文件夹下的子文件夹中进行搜索。如果 C:\Users\al\Scripts 被列入 PATH,运行 spam.exe 将会运行 C:\Users\al\Scripts\spam.exe 文件,但不会运行 C:\Users\al\Scripts\eggs\spam.exe 文件。

PATH 编辑

到目前为止,你可能一直将你的 .py 文件保存在 Mu 编辑器默认使用的 mu_code 文件夹中。然而,我建议在你的主文件夹下创建一个 Scripts 文件夹。如果你的用户名恰好是 al,这个文件夹将是:

  • Windows 上的 C:\Users\al\Scripts

  • macOS 上的 /Users/al/Scripts

  • Ubuntu Linux 上的 /home/al/Scripts

让我们将这个文件夹添加到 PATH 中。

Windows

Windows 有两组环境变量:系统环境变量(适用于所有用户)和用户环境变量(覆盖系统环境变量,但仅适用于当前用户)。要编辑它们,点击 开始 菜单,然后输入 编辑您的账户的环境变量,这应该会打开 环境变量 窗口。

从屏幕顶部的用户变量列表中选择 Path(而不是屏幕底部的系统变量列表),点击 编辑,在出现的文本字段中添加新的文件夹名称 C:\Users\al\Scripts,使用分号分隔,然后点击 确定

macOS 和 Linux

要将文件夹添加到PATH环境变量中,你需要编辑终端启动脚本。在 macOS 上是.zshrc文件,在 Linux 上是.bashrc文件。这两个文件都在你的主目录中,包含每次打开新终端窗口时运行的命令。在 macOS 上,将以下内容添加到.zshrc文件的底部:

export PATH=/Users/al/Scripts:$PATH

在 Linux 上,将以下内容添加到.bashrc文件的底部:

export PATH=/home/al/Scripts:$PATH

这行代码会修改你打开的所有未来终端窗口的PATH,因此这个更改不会影响当前已打开的终端窗口。

whichwhere命令

如果你想知道程序位于PATH环境变量的哪个文件夹中,你可以在 macOS 和 Linux 上运行which程序,在 Windows 上运行where程序。例如,将以下which命令输入到 macOS 终端中:

al@Als-MacBook-Pro ~ % which python3
/Library/Frameworks/Python.framework/Versions/3.13/bin/python3 

在 Windows 上,将以下where命令输入到终端中:

C:\Users\al>where python
C:\Users\al\AppData\Local\Programs\Python\Python313\python.exe
C:\Users\al\AppData\Local\Programs\Python\Python312\python.exe 

where命令显示了PATH中包含名为python程序的每个文件夹。位于最顶层文件夹中的是当你输入python时运行的版本。whichwhere命令在你不确定PATH如何配置并且需要找到特定程序的位置时非常有用。

虚拟环境

假设你有两个 Python 程序,一个使用该包的 1.0 版本,另一个使用该包的 2.0 版本。Python 不能同时安装同一包的两个版本。如果 2.0 版本与 1.0 版本不向后兼容,每次你想切换程序运行时,你都需要卸载一个版本并重新安装另一个版本。

Python 解决这个问题的方法是虚拟环境;拥有自己安装的第三方包集的独立 Python 安装。一般来说,你创建的每个 Python 应用程序都需要自己的虚拟环境。但在学习编程的过程中,你可以使用一个虚拟环境来运行所有的简单脚本。Python 可以使用其内置的venv模块创建虚拟环境。要创建虚拟环境,请切换到你的Scripts文件夹并运行python –m venv .venv(在 macOS 和 Linux 上使用python3):

C:\Users\al>
C:\Users\al>cd Scripts
C:\Users\al\Scripts>python -m venv .venv 

这将在名为.venv的新文件夹中创建虚拟环境的文件。你可以选择任何你想要的文件夹名,但.venv是传统做法。以点开头的文件和文件夹是隐藏的,尽管你可以按照本书引言中的步骤来让操作系统默认显示它们。

当你在终端中运行pythonpython3时,你仍然会运行原始 Python 安装的解释器。要使用虚拟环境的 Python 版本,你必须激活它。在 Windows 上,通过运行C:\Users\al\Scripts\.venv\Scripts\activate.bat脚本来实现:

C:\Users\al\Scripts>cd .venv\Scripts
C:\Users\al\Scripts\.venv\Scripts>activate.bat
(.venv) C:\Users\al\Scripts\.venv\Scripts>where python.exe
C:\Users\Al\Scripts\.venv\Scripts\python.exe
C:\Users\Al\AppData\Local\Programs\Python\Python313\python.exe
C:\Users\Al\AppData\Local\Programs\Python\Python312\python.exe 

在激活虚拟环境后运行where python.exe会显示,从终端运行python将运行在.venv\Scripts文件夹中的 Python 解释器,而不是系统 Python(稍后讨论)。

macOS 和 Linux 上的等效脚本为 ~/Scripts/.venv/bin/activate,但由于安全权限,您不能直接运行它。相反,运行命令 source activate

al@al-virtual-machine:~/Scripts$ cd .venv/bin
al@al-virtual-machine:~/Scripts/.venv/bin$ source activate
(.venv) al@al-virtual-machine:~/Scripts/.venv/bin$ which python3
/home/al/Scripts/.venv/bin/python3 

激活会更改 PATH 环境变量,以便 pythonpython3 运行 .venv 文件夹内的 Python 解释器而不是原始的一个。它还会更改您的终端提示,以包含 (.venv),这样您就知道虚拟环境是激活的。在激活的虚拟环境中运行 which python3 会显示 python3 在新创建的 .venv/bin 文件夹中运行 Python 解释器。这些更改仅适用于当前终端窗口;任何现有或新的终端窗口都不会有这些环境变量或提示更改。这个新的 Python 安装只有默认的包,并且不包含您可能在原始 Python 安装中已经安装的任何包。您可以通过运行 python –m pip list 来确认,这将列出已安装的包:

(.venv) C:\Users\al\Scripts\.venv\Scripts>python -m pip list
Package    Version
---------- -------
pip        23.0
setuptools 65.5.0 

标准做法是为您正在工作的每个 Python 项目创建一个虚拟环境,因为每个项目都可能有自己的独特包依赖关系。然而,在 Windows 上,我们可以对我们 Scripts 文件夹中编写的随机小脚本稍微宽松一些:它们都可以共享这一个。

macOS 和 Linux 操作系统都有自己的程序,这些程序依赖于操作系统附带的 Python 安装。为这个称为 系统 Python 的原始 Python 安装安装或更新包,可能会引入导致这些程序失败的不兼容性。使用系统 Python 运行自己的脚本是可以的;将第三方包安装到系统 Python 上稍微有些风险,但在 Scripts 文件夹中创建虚拟环境是一种很好的预防措施,以避免安装不兼容的包。

请记住,Mu 有自己的虚拟环境。当您在 Mu 中按 F5 运行脚本时,它不会使用您安装到 Scripts.venv 文件夹虚拟环境中的包。随着您编程能力的提高,您可能会发现同时打开 Mu 窗口编辑代码并保持终端窗口打开运行它更容易。您可以使用 Windows 和 Linux 上的 ALT-TAB 键盘快捷键或 macOS 上的 -TAB 快捷键快速在窗口之间切换焦点。

要停用虚拟环境,请在与 activate 脚本相同的文件夹中运行 deactivate.bat(在 Windows 上)或 deactivate(在 macOS 和 Linux 上)。您也可以简单地关闭终端窗口并打开一个新的。如果您想永久删除虚拟环境及其安装的包,只需删除 .venv 文件夹及其内容。

以下部分将告诉您在设置虚拟环境并添加您的 Scripts 文件夹到 PATH 后,如何部署您的脚本。

使用 pip 安装 Python 包

Python 内置了一个名为 pip 的命令行包管理程序(除非它在句子的开头,否则全部小写)。Pip 是 pip installs package 的递归缩写。虽然 Python 的标准库包含 sysrandomos 等模块,但还有数十万个第三方包可以在 PyPI(发音为“pie-pee-eye”,而不是“pie-pie”)上找到,即 Python 包索引,网址为 pypi.org。在 Python 中,package 是一个在 PyPI 上提供的 Python 代码集合,而 module 是一个包含 Python 代码的单独的 .py 文件。您可以从 PyPI 安装包含模块的包,并通过 import 语句导入模块。

虽然 pip 是一个独立的程序,但通过在 Windows 上运行 python –m pip 或在 macOS 和 Linux 上运行 python3 –m pip 来通过 Python 解释器运行它更容易,而不是直接运行 pip(在 Windows 上)或 pip3(在 macOS 和 Linux 上)程序。这可以防止在罕见的情况下出现错误,例如,当您有多个 Python 安装、PATH 配置错误,并且 pip/pip3 正在安装到与您运行 python/python3 时运行的 Python 解释器不同的解释器时。

要从 PyPI 安装一个包,请在终端中输入以下命令:

C:\Users\al>python –m pip install `package_name`

请记住,在 macOS 或 Linux 上运行这些各种命令时,请使用 python3 而不是 python。另外,请注意,您需要从终端窗口运行这些命令,而不是从 Python 交互式 shell 中运行。

要列出你已安装的所有包及其版本号,请运行 python –m pip list

C:\Users\al>python -m pip list
Package                   Version     Editable project location
------------------------- ----------- -------------------------
altgraph                  0.17.3
argon2-cffi               21.3.0
argon2-cffi-bindings      21.2.0
async-generator           1.10
# --snip--
wsproto                   1.2.0 

您还可以通过运行 python –m pip install –U package_name 将包升级到 PyPI 上的最新版本,或者通过运行 python –m pip install package_name==1.17.4 安装特定版本(例如,1.17.4)。

要卸载一个包,请运行 python –m pip uninstall package_name。您可以通过运行 python –m pip --help 来获取有关 pip 的更多信息。

自我意识的 Python 程序

Python 的标准库没有包含任何赋予程序意识的模块。(目前还没有。)但是,有几个内置变量可以给 Python 程序提供关于自身、其所在操作系统以及运行它的 Python 解释器的有用信息。Python 解释器会自动设置这些变量。

__file__ 变量包含 .py 文件的路径作为字符串。例如,如果我在我的家目录中运行一个 yourScript.py 文件,它评估为 'C:\Users\al\yourScript.py'。导入 from pathlib import Path 并调用 Path(__file__) 返回该文件的 Path 对象。如果您需要定位存在于 Python 程序文件夹中的文件,这些信息很有用。当您运行 Python 交互式 shell 时,__file__ 变量不存在。

sys.executable 变量包含 Python 解释程序本身的完整路径和文件名,而 sys.version 变量包含显示在交互式 shell 顶部的字符串,其中包含有关 Python 解释程序版本的信息。

sys.version_info.majorsys.version_info.minor 变量包含 Python 解释程序的主版本号和次版本号的整数。在我的运行 Python 版本 3.13.1 的笔记本电脑上,这些分别是 313。您还可以将 sys.version_info 传递给 list() 函数以获取更具体的信息:在我的笔记本电脑上,list(sys.version_info) 返回 [3, 13, 1 'final', 0]。以这种形式拥有版本信息比试图从 sys.version 字符串中提取它要容易得多。

如果在 Windows 上运行,os.name 变量包含字符串 'nt';如果在 macOS 或 Linux 上运行,则包含 'posix'。这对于您的 Python 脚本需要根据其运行的操作系统执行不同代码时非常有用。

对于更具体的操作系统识别,sys.platform 变量在 Windows 上包含 'win32',在 macOS 上包含 'darwin',在 Linux 上包含 'linux'

如果您需要关于操作系统版本和 CPU 类型的非常具体的信息,内置的 platform 模块可以检索这些信息。该模块的文档可在网上找到,网址为 docs.python.org/3/library/platform.html

如果您需要检查模块是否已安装,请将 import 语句放在 try 块中,并捕获 ModuleNotFoundError 异常:

try:
    import nonexistentModule
except ModuleNotFoundError:
    print('This code runs if nonexistentModule was not found.') 

如果该模块对您的程序功能是必需的,您可以在此处放置一个描述性的错误消息,并调用 sys.exit() 来终止程序。这比一个通用的错误消息和回溯对用户更有帮助。

基于文本的程序设计

在支持 GUI 的操作系统变得普遍之前,所有程序都使用文本与用户进行通信。本书侧重于创建小型、有用的程序,而不是专业的软件应用程序,因此本书中的程序通过命令行界面使用 print()input(),而不是 GUI 提供的窗口、按钮和图形。

即使受到文本的限制,软件应用程序仍然可以提供类似于现代 GUI 的用户界面。图 12-1 显示了 Norton Commander,这是一个用于浏览文件系统的应用程序。这些类型的应用程序被追称为 TUI(发音为“two-ee”),或 基于文本的用户界面 应用程序。

在顶部,一个包含多个以“名称”、“大小”、“日期”和“时间”命名的基于文本的列的计算机界面,后面跟着一个文件名列表。在底部,是 Windows 文件资源管理器界面打开到桌面文件夹的截图,显示了文件的图标。

图 12-1:基于文本的 Norton Commander(顶部)与现代 GUI 应用程序(底部)并排显示

即使你不是专业的软件开发人员,基于文本的用户界面的优势在于它们的简单性。本节描述了你的程序可以采取的几种设计方法来构建用户界面。

简短的命令名称

用户通常从命令行而不是通过在桌面或开始菜单上点击图标来运行基于文本的程序。有时这些命令可能看起来难以理解。当我开始学习 Linux 操作系统时,我很惊讶地发现我熟悉的 Windows copy 命令在 Linux 上被命名为 cp。名称 copy 比名称 cp 更易读。简短而晦涩的名称真的值得节省两个字符的输入吗?

随着我越来越多地在命令行上积累经验,我意识到答案是坚定的肯定。我们阅读源代码的频率比编写它要高,所以为变量和函数使用详尽的名称是有帮助的。但我们将命令输入到命令行中的频率比阅读它们要高,所以在这种情况下,情况正好相反:简短的命令名称使命令行更容易使用,并且它们减轻了手腕的压力。

如果你的程序是一个你每天可能会输入十几次的命令,试着为它想一个简短的名称。你可以使用 whichwhere 命令来检查该名称是否已被另一个程序使用。你还可以通过该名称在网上搜索任何现有的命令。简短的名称在很大程度上有助于使其易于使用。

命令行参数

要从命令行运行程序,只需输入其名称。对于 .py Python 源代码文件,你必须运行 python(Windows)或 python3(macOS 和 Linux)程序,然后在其后提供 .py 文件名,如下所示:python yourScript.py

命令后面的文本称为 命令行参数。命令行参数以与函数调用参数相同的方式传递给命令。例如,单独的 ls 命令会列出当前工作目录(CWD)中的文件。但你也可以运行 ls exampleFolderexampleFolder 命令行参数将指示 ls 命令列出 exampleFolder 文件夹中的文件。命令行参数允许你配置命令的行为。

Python 脚本可以通过 sys.argv 列表访问传递给 Python 解释器的命令行脚本。例如,如果你输入了 python3 yourScript.py hello worldpython3 程序将接收命令行参数并将它们转发到你的 Python 脚本中的 sys.argv 变量。sys.argv 变量将包含 ['yourScript.py', 'hello', 'world']

注意,sys.argv 中的第一个项目是 Python 脚本的文件名。其余的参数由空格分隔。如果你需要在命令行参数中包含空格字符,请在运行命令时将其放在双引号内。例如,python3 yourScript.py "hello world" 将设置 sys.argv['yourScript.py', 'hello world']

命令行参数的主要用途是在程序开始之前指定各种配置。您无需通过配置菜单或多步骤过程。不幸的是,这种方法意味着命令行参数可能会变得极其复杂且难以阅读。如果您在 Windows 命令后传递 /? 或在 macOS 或 Linux 命令后传递 --help,您通常会找到一页又一页的命令行参数文档。

如果您的程序接受的命令行参数集很简单,那么直接让程序读取 sys.argv 列表是最简单的。然而,随着您添加更多的命令行参数,可能的组合可能会变得难以管理。python yourScript.py spam eggs 是否与 python yourScript.py eggs spam 做相同的事情?如果用户可以提供 cheese 参数或 bacon 参数,那么如果他们同时提供这两个参数会发生什么?这种复杂性将需要您编写大量代码来处理各种边缘情况。在这种情况下,您可能最好使用 Python 的内置 argparse 模块来处理这些复杂情况。argparse 模块超出了本书的范围,但您可以在网上阅读其文档,网址为 docs.python.org/3/library/argparse.html

剪贴板 I/O

您不需要依赖 input() 来从文件或键盘读取文本。您也可以使用剪贴板来为您的 Python 程序提供文本输入和输出。跨平台的 pyperclip 模块提供了一个 copy() 函数,用于将文本放置在剪贴板上,以及一个 paste() 函数,该函数返回剪贴板上的文本作为字符串。Pyperclip 是一个第三方包,您可以通过终端使用 pip 安装:python –m pip install pyperclip。在 Linux 上,您还必须运行 sudo apt install xclip 以使 Pyperclip 正常工作。有关完整说明,请参阅附录 A。

您的所有剪贴板 I/O 程序都将遵循这种基本设计:

  1. 导入 pyperclip 模块。

  2. 调用 pyperclip.paste() 以从剪贴板获取输入文本。

  3. 对文本进行一些处理。

  4. 通过将结果传递给 pyperclip.copy() 将结果复制到剪贴板。

第八章中的“向 Wiki 标记添加项目符号”项目是这类程序的例子。一旦您按照第 275 页上“部署 Python 程序”中的说明部署了该程序,这种程序的设计就特别有用。只需突出显示输入文本,按 CTRL-C 复制它,然后运行程序。结果将出现在剪贴板上,随时可以粘贴。

在本章的后面部分,我们将探讨两个项目,即 ccwd 命令和剪贴板记录器,它们都使用了剪贴板。

使用 Bext 的彩色文本

您可以使用基于 Jonathan Hartley 的 Colorama 包构建的第三方 Bext 包来打印彩色文本。按照附录 A 中的说明使用 pip 安装 Bext。Bext 只能在从终端窗口运行的程序中使用,而不能在 Mu 或大多数其他代码编辑器中使用。要使print()打印彩色文本,请调用fg()bg()函数,使用字符串参数如'black''red''green''yellow''blue''magenta''purple''cyan''white'来改变(前景)文本颜色或背景颜色。您还可以传递'reset'来将颜色改回终端窗口的默认颜色。例如,在交互式 shell 中输入以下内容:

>>> import bext
>>> bext.fg('red')
>>> print('This text is red.')
This text is red.
>>> bext.bg('blue')
>>> print('Red text on blue background is an ugly color scheme.')
Red text on blue background is an ugly color scheme.
>>> bext.fg('reset')
>>> bext.bg('reset')
>>> print('The text is normal again. Ah, much better.')
The text is normal again. Ah, much better. 

请记住,用户可能将他们的终端窗口设置为浅色模式或深色模式,因此无法确定终端的默认外观是黑色文字在白色背景上还是白色文字在黑色背景上。您在使用颜色时也应有限制:过多的颜色可能会使您的程序看起来俗气或难以阅读。

Bext 还有一些有限的 TUI(文本用户界面)功能,包括以下内容:

bext.clear() 清除屏幕

bext.width() bext.height() 分别返回终端窗口的当前宽度(以列为单位)和高度(以行为单位)

bext.hide() bext.show() 分别隐藏和显示光标

bext.title(text) 将终端窗口的标题栏更改为文本字符串

bext.goto(x, y) 将光标移动到终端中的列 x 和行 y,其中 0, 0 是左上角位置

bext.get_key() 等待用户按下任何键,然后返回一个描述该键的字符串

bext.get_key()函数视为input()的单键版本。返回的字符串包括'a''z''5',但也包括像'left''f1''esc'这样的键。TAB 和 ENTER 键分别返回'\t''\n'。在交互式 shell 中调用bext.get_key()来测试各种键并查看它们的返回值。

要演示 Bext 能做什么,请运行 ASCII Art 鱼缸程序的源代码,程序地址为inventwithpython.com/projects/fishtank.py。首先,这个程序使用bext.clear()清除终端窗口中的所有文本。接下来,程序调用bext.goto()定位光标,并使用bext.fg()改变文本颜色,然后打印出由文本字符如><)))*>组成的各种鱼。这个程序在我的书《Python 小项目大全书》(No Starch Press,2021 年)中有介绍。

终端清除

bext.clear() 函数在你希望你的程序删除在运行之前留下的任何文本时很有用。你还可以用它来做翻页风格的动画:调用 clear() 清除终端,然后使用 print() 调用来填充文本,使用 Python 的 time.sleep() 暂停片刻,然后重复。有一个 Python 单行代码(一行代码来完成一个特殊技巧)可以清除屏幕,你可以将其放置在自己的 clear() 函数中:

import os
def clear():
    os.system('cls' if os.name == 'nt' else 'clear') 

此代码允许你的程序清除终端屏幕,而无需安装 Bext 包,并且仅在从终端运行的 Python 脚本中有效,而不是在 Mu 或其他代码编辑器中。你将在第十一章中了解更多关于 os.system() 调用的信息,它运行 cls 程序(在 Windows 上)或 clear 程序(在 macOS 和 Linux 上)。这里的奇怪语法是 Python 的 条件表达式(在其他语言中也称为 三元运算符)的一个例子。语法是 value1 if condition else value2,如果条件为 True,则评估为 value1,如果条件为 False,则评估为 value2。在我们的情况下,条件表达式在条件 os.name == 'nt'True 时评估为 'cls';否则,它评估为 'clear'。条件表达式(以及通常的单行代码)往往会产生难以阅读的代码,通常最好避免使用,但这是一个足够简单的情况。

声音和文本通知

在今天计算机提供的丰富音频之前,终端程序就已经存在了。今天,你的基于文本的程序不必是静音的。然而,有很好的理由将声音保持在最低限度或完全排除。当用户忙于查看其他窗口时,声音可以提供任务完成或出现问题的通知。但是,就像彩色文本一样,过度使用声音很容易变得令人烦恼。用户可能已经在执行涉及播放音频的任务,或者他们可能在进行一个声音会无礼打断的在线会议。而且如果用户的计算机被静音,他们无论如何也听不到声音通知。

如果你只需要播放一个简单的音频文件,你可以使用 playsound3 第三方包。一旦安装,你可以通过调用 playsound3 模块的 playsound() 函数并传递 MP3 或 WAV 音频文件的路径来播放音频文件。从 autbor.com/hello.mp3 下载 hello.mp3 文件(或使用你自己的文件),并在交互式外壳中输入以下内容:

>>> import playsound3
>>> playsound3.playsound('hello.mp3') 

playsound() 函数将在音频文件播放完毕后返回;也就是说,该函数会阻塞直到音频播放完成。请记住,如果你给它一个较长的音频文件来播放,这将暂时停止你的程序。如果 playsound() 抛出异常(如果文件名包含像等号这样的奇字符时会发生这种情况),尝试传递音频文件的 Path 对象而不是字符串。

同样,你可能希望限制程序产生的文本。在 Unix 命令设计哲学中,如果命令只输出相关信息,那么将一个命令的文本输出管道传输到另一个命令会更容易,因为多余的文本输出将需要过滤。许多命令将它们的文本输出保持在最低限度,或者完全没有,并通过退出代码来传达成功或错误。(退出代码在第十九章中介绍。)然而,如果你不是将输出管道传输到另一个命令,而是作为一个人类用户想要看到更多信息,许多命令接受 -v--verbose 命令行参数来启用这种 详细模式。其他命令采取相反的方法,将输出信息淹没其中,但提供 -q--quiet 命令行参数来提供一个没有文本输出的 安静模式。(这也可以作为静音声音通知的方法。)或者更好的是,让沉默成为默认行为,并让 --verbose--beep 启用声音或警报声。

如果你的程序不需要这种复杂程度,你可以忽略这个考虑。然而,一旦你开始与他人分享你的程序,他们可能会以你未预见到的方式使用它,提供这些选项将大大提高程序的用户友好性。

短程序:暴风雪

让我们创建一个基于文本的暴风雪动画。我们的程序使用填充单个字符单元格上半部分、下半部分和整个单元格的块文本字符。这些文本字符分别通过 chr(9600)chr(9604)chr(9608) 返回为字符串,我们的程序将它们存储在常量 TOPBOTTOMFULL 中,使我们的代码更易于阅读。

将以下代码输入到名为 snowstorm.py 的文件中:

import os, random, time, sys

TOP    = chr(9600)  # Character 9600 is '▀'
BOTTOM = chr(9604)  # Character 9604 is '▄'
FULL   = chr(9608)  # Character 9608 is '█'

# Set the snowstorm density to the command line argument:
DENSITY = 4  # Default snow density is 4%
if len(sys.argv) > 1:
    DENSITY = int(sys.argv[1])

def clear():
    os.system('cls' if os.name == 'nt' else 'clear')

while True:
    clear()  # Clear the terminal window.

    # Loop over each row and column:
    for y in range(20):
        for x in range(40):
            if random.randint(0, 99) < DENSITY:
                # Print snow:
                print(random.choice([TOP, BOTTOM]), end='')
 else:
                # Print empty space:
                print(' ', end='')
        print()  # Print a newline.

    # Print the snow-covered ground:
    print(FULL * 40 + '\n' + FULL * 40)
    print('(Ctrl-C to stop.)')

    time.sleep(0.2)  # Pause for a bit. 

首先,程序导入 osrandomsystime 模块。这些模块在 Python 标准库中,不需要安装任何第三方包。然后,程序使用 chr() 的返回值设置常量 TOPBOTTOMFULL。程序使用这些常量名称,因为它们比数字 9600、9604 和 9608 更容易理解。

用户可以通过提供命令行参数来指定暴风雪的密度。如果没有提供命令行参数,则 sys.argv 被设置为 ['snowstorm.py'],程序将 DENSITY 留在 4。但如果用户用,比如说,python snowstorm.py 20 运行程序,那么 sys.argv 将被设置为 ['snowstorm.py', '20'],程序将更新 DENSITYint(sys.argv[1]),或 20。然后用户就可以修改暴风雪的行为,而无需更改源代码。

在一个无限 while 循环内部,这个程序首先使用 cls/clear 一行代码清除屏幕。接下来,它使用两个嵌套的 for 循环遍历终端上的 40×20 空间中的每一行和每一列。(你可以增加或减少这些数字来改变雪暴的大小。)在每一行和每一列,程序打印一个字符:要么是一个随机选择的 TOPBOTTOM 字符来表示雪,要么是一个空格字符。(默认情况下,只有四分之一的字符不是空格。)这些 print() 调用传递了 end='' 关键字参数,这样 print() 就不会在每次调用后自动打印换行符。程序通过调用不带参数的 print() 来自己打印换行符,完成一行后。

在嵌套的 for 循环之后,程序打印两行 40 个 FULL 字符来表示地面,并提醒用户可以按 CTRL-C 停止程序。所有这些代码生成雪暴动画的一个“帧”,然后 time.sleep(0.2) 短暂保持这个帧,在执行循环回退以清除终端并重新开始整个过程之前。

我选择雪暴动画是因为它很有趣,而不是实用,就像一个基于终端的雪花球。这种技术的更有用的应用是创建一个 仪表盘 应用程序:一个在终端窗口中运行的程序,你可以将其保持打开状态以快速传达信息。这个程序打印相关信息,然后清除屏幕并每秒、每分钟、每小时或任何其他间隔重新打印更新后的信息。

使用 PyMsgBox 的弹出消息框

虽然为你的程序设计完整的 GUI 需要学习整个代码库,如 Tkinter、wxPython 或 PyQt,但你可以使用 PyMsgBox 包向程序添加小的 GUI 消息框。这是一个第三方包,你可以通过在终端中运行 pip install pymsgbox 来安装。PyMsgBox 允许你使用 Tkinter 创建对话框,Tkinter 是 Python 在 Windows 和 macOS 上自带的一个库。在 Ubuntu Linux 上,你必须首先通过在终端中运行 sudo apt install python3-tk 来安装 Tkinter。附录 A 有完整的说明。

PyMsgBox 有与 JavaScript 的消息框功能名称相对应的函数:

pymsgbox.alert(text) 显示一条文本消息,直到用户点击确定,然后返回字符串 'OK'

pymsgbox.confirm(text) 显示一条文本消息,直到用户点击确定或取消,然后返回 'OK''Cancel'

pymsgbox.prompt(text) 显示一条文本消息以及一个文本字段,然后返回用户输入的文本作为字符串,或者如果用户点击取消则返回 None

pymsgbox.password(text) 与 pymsgbox.prompt() 相同,但用户输入的文本被星号屏蔽

这些函数不会返回,直到用户点击确定、取消或 X(关闭)。如果你的程序只需要偶尔的通知或用户输入,使用 PyMsgBox 的对话框可能是 print()input() 的合适替代品。

部署 Python 程序

当你的 Python 程序完成之后,你可能不想每次都运行 Mu,加载 .py 文件,然后点击运行按钮来执行它。本节将解释如何部署你的 Python 程序,以便你可以用尽可能少的按键来运行它。

一定要按照第 261 页“PATH 环境变量”中的步骤,将 Scripts 文件夹添加到你的 PATH 环境变量中。由于我的用户名是 al,在 Windows 上,该文件夹的路径是 C:\Users\al\Scripts,在 macOS 上是 /Users/al/Scripts,在 Linux 上是 /home/al/Scripts。此外,你还需要为你的 Python 脚本设置一个虚拟环境,如下所述。

Windows

在 Windows 上,你可以通过同时按下 Windows 键和 R 键(或右键单击开始菜单按钮并选择运行)来打开运行对话框。这打开了一个类似一次性终端的小窗口:在其中,你可以运行单个命令。要从这里运行你的 Python 脚本,你需要执行以下操作:

  1. yourScript.py Python 脚本放置在你的 Scripts 文件夹中。

  2. 在你的 Scripts 文件夹中创建一个 yourScript.bat 批处理文件来运行 Python 脚本。

批处理文件 包含可以一起运行的终端命令。它们具有 .bat 文件扩展名,类似于 Linux 上的 shell 脚本或 macOS 上的 .command 脚本。如果你将名为 yourScript.bat 的批处理文件放置在 PATH 文件夹中,你可以通过输入 yourScript 来从运行对话框中运行它。在 Windows 上,运行 .bat.exe 文件时不需要输入文件扩展名。

批处理文件的内容是纯文本,就像 .py 文件一样,因此你可以使用 Mu 或记事本等文本编辑器创建批处理文件。批处理文件每行包含一个命令。要运行位于文件夹 C:\Users\al\Scripts 中的 yourScript.py 文件,创建一个名为 yourScript.bat 的文件,并包含以下内容:

@call %HOMEDRIVE%%HOMEPATH%\Scripts\.venv\Scripts\activate.bat
@python %HOMEDRIVE%%HOMEPATH%\Scripts\yourScript.py %*
@pause
@deactivate 

批处理文件可以命名为任何名称,但如果它与 Python 脚本同名则更容易记住。此批处理文件运行三个命令。第一个命令激活了你为 Scripts 文件夹创建的虚拟环境。开头的 @ 符号使得命令本身不会在终端窗口中显示。%HOMEDRIVE% 环境变量是 'C:',而 %HOMEPATH% 环境变量是到你的家文件夹的路径,例如 '\Users\al'。(波浪号 ~ 在 Windows 上不代表家文件夹。)结合使用,这提供了无论用户名是什么的虚拟环境激活脚本的路径。(如果你与同事共享这些文件以便他们在自己的电脑上运行,这很有帮助。)请注意,call 是必要的;如果一个批处理文件(如 yourScript.bat)在没有 call 的情况下运行另一个批处理文件(如 activate.bat),则第一个批处理文件的其余命令将不会运行。

接下来,批处理文件运行 python.exe,然后运行 yourScript.py%* 使得批处理文件将接收到的任何命令行参数转发到您的 Python 程序。始终包含 %* 是一个好主意,以防您以后向 Python 程序添加命令行参数。

第三个命令运行 pause 命令,这将导致 Windows 显示 按任意键继续 并等待用户按下键。这可以防止在 Python 程序完成后立即关闭终端窗口,以便您可以看到任何剩余的打印输出。如果您的程序没有打印输出,您可以省略此行。最后,@deactivate 行在您从终端运行此批处理文件且 Python 程序完成后终端窗口仍然打开的情况下,将取消激活虚拟环境。

在设置好批处理文件后,现在您可以通过按下 Windows 键 + R 键组合来打开运行对话框,并输入 yourScript(后跟任何命令行参数)来运行 yourScript.bat 脚本。或者,如果您有一个打开的终端窗口,您可以直接在任何文件夹中终端中输入 yourScript。这比从 Mu 这样的代码编辑器中运行它要快得多。

如果您创建了其他 Python 脚本,您可以重用此批处理文件。只需将文件复制并重命名,并将 yourScript.py 文件名更改为新的 Python 脚本名称。其他所有内容都可以保持不变。

macOS

在 macOS 上,同时按下 COMMAND 键和空格键会打开 Spotlight,允许您输入要运行的程序名称。要将您自己的 Python 脚本添加到 Spotlight,您必须执行以下操作:

  1. yourScript.py Python 脚本放置在您的 Scripts 文件夹中。

  2. 创建一个名为 yourScript.command 的文本文件以运行 Python 脚本。

  3. 运行 chmod u+x yourScript.command 以向 yourScript.command 文件添加执行权限。

一旦您的 .py Python 脚本位于您的 Scripts 文件夹中,例如 /Users/al/Scripts,在 Scripts 文件夹中创建一个名为 yourScript.command 的文本文件,并包含以下内容:

source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/yourScript.py
deactivate 

~ 代表主目录,例如 /Users/al。第一行激活虚拟环境,第二行使用虚拟环境的 Python 安装运行 Python 脚本。

最后,在终端中,使用 cd 命令进入 ~/Scripts 目录,然后运行命令 chmod u+x yourScript.command。这将添加执行权限,以便您可以从 Spotlight 运行脚本。现在,您可以通过按下 ⌘-空格键并输入 yourScript.command 来快速运行 Python 脚本。(在输入前几个字符后,Spotlight 应该会自动完成整个名称。)您还可以通过在终端中输入 yourScript.command 来从终端运行您的 Python 脚本。

需要 yourScript.command 文件,因为如果您尝试从 Spotlight 运行 yourScript.py 文件,Spotlight 会看到 .py 文件扩展名,并假设您想要在 Mu 或其他代码编辑器中打开此文件,而不是直接运行它。

注意,如果您使用 macOS 的 TextEdit 创建 yourScript.command 文件,请确保通过按 SHIFT-⌘-T(或点击“格式”和“制作纯文本”菜单项)将其设置为纯文本文件。TextEdit 还会尝试提供帮助,自动将 python3 转换为 Python3,这会导致 Spotlight 出错。

很遗憾,Spotlight 没有让用户传递命令行参数给 Python 脚本的方法。任何命令行参数都必须预先写入 .command 文件中。

Ubuntu Linux

按下 Windows 键并输入您想要运行的程序名称,可以调出 Ubuntu Linux Dash。要将您的 Python 脚本添加到 Dash,您必须执行以下操作:

  1. yourScript.py Python 脚本放置在您的 Scripts 文件夹中。

  2. 创建一个名为 yourScript 的 shell 脚本,用于激活虚拟环境并运行您的 Python 脚本。

  3. 运行 chmod u+x yourScript 命令以向 shell 脚本添加执行权限。

~/.local/share/applications 文件夹中创建一个 yourScript.desktop 文件,以便从 Dash 运行 shell 脚本。

一旦您在 Scripts 文件夹(例如 /home/al/Scripts)中有了 .py Python 脚本,请在此文件夹中创建一个名为 yourScript 的文本文件(不带文件扩展名),并包含以下内容:

#!/usr/bin/env bash
source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/yourScript.py
read -p "Press any key to continue..." -n1 –s
deactivate 

~ 代表主文件夹,例如 /Users/al。第一行将此文件标识为 shell 脚本。此文件不需要 .sh 扩展名。

第二行激活虚拟环境,第三行使用虚拟环境的 Python 安装运行 Python 脚本。read 命令会导致终端显示 按任意键继续 并等待用户按键。这可以防止在 Python 程序完成后立即关闭终端窗口,以便您可以看到任何剩余的打印输出。如果您的程序没有打印输出,您可以省略此行。

创建此 shell 脚本后,切换到 ~/Scripts 目录并运行命令 chmod u+x yourScript。这添加了执行权限,以便您可以运行它。此时,您还可以通过在终端中输入 yourScript 来从终端运行您的 Python 脚本。要从 Dash 运行您的 Python 脚本,您必须创建一个 yourScript.desktop 文件。

在 Mu 或其他文本编辑器(如 gedit)中,创建一个包含以下内容的 yourScript.desktop 文件:

[Desktop Entry]
Name=yourScript
Exec=gnome-terminal -- /home/al/Scripts/yourScript
Type=Application 

将此文件保存到 /home/al/.local/share/applications 文件夹(将 al 替换为您自己的用户名)作为 yourScript.desktop。请注意,Exec 字段要求您拼写 /home/al;您不能在此文件中将值替换为 ~。如果您的文本编辑器没有显示 .local 文件夹(因为以点开头的文件夹被认为是隐藏的),请在保存文件对话框中按 CTRL-H 以显示隐藏文件。

现在,您可以通过按下 Windows 键来调用 Dash 并输入 yourScript 来快速运行 Python 脚本。Dash 应该会为您自动完成完整名称。在 yourScript.desktopName 字段中的 yourScript 文本将出现在 Dash 中,可以是任何名称,但将其与 yourScript.py 的名称相同会更方便。

接下来,让我们根据本章中的原则创建两个程序,并部署它们以便于使用。

简短程序:复制当前工作目录

虽然 macOS 和 Linux 上的 pwd 命令会打印当前工作目录,但有时将此值复制到剪贴板以便在其他地方粘贴是有用的。例如,在 Windows 上,我经常发现自己需要从终端复制当前工作目录,以便将其粘贴到保存文件对话框中,以便在同一目录下保存文件。虽然我可以使用鼠标从 Windows 提示符中选择当前工作目录进行复制(或者在 macOS 和 Linux 上运行 pwd 命令以打印工作目录并选择要复制的文本),但这需要几个步骤,而这本可以是一个一步完成的过程。

我的想法是编写一个名为 ccwd(代表 copy current working directory)的程序。首先,我将在 Windows 上输入 where ccwd,在 macOS 和 Linux 上输入 which ccwd,以确保当前没有具有相同名称的命令,然后也许还会快速进行互联网搜索以确认。ccwd 这个名称足够短,易于输入,同时也足够独特。

作为一项附加功能,假设终端的当前工作目录设置为 C:\Users\al\Scripts,但我想复制到剪贴板的是 C:\Users\al。我可以简单地运行 cd .. 命令,然后运行 ccwd,然后运行 cd Scripts 命令返回到 C:\Users\al\Scripts。但如果我能将相对文件路径作为命令行参数传递给 ccwd,那就更方便了。例如,当当前工作目录为 C:\Users\al\Scripts 时,ccwd .. 会将 C:\Users\al 复制到剪贴板。您不必指定此命令行参数(如果没有提供,程序默认为当前工作目录),但如果用户需要,这个功能是可用的。这些微小的改进可能看起来微不足道,但在线零售商在其网站上提供“一键购买”功能,因为他们知道对便利性的微小改进可以产生重大影响。

我们将使用 Pyperclip 包来处理剪贴板,所以请确保将其安装到 Scripts 文件夹的虚拟环境中。在 Mu 中创建一个新文件,并输入以下内容:

import pyperclip, os, sys
if len(sys.argv) > 1:
    os.chdir(sys.argv[1])
pyperclip.copy(os.getcwd()) 

将此程序保存为ccwd.py在你的家目录下的Scripts文件夹中。

第一行导入程序需要的模块。第二行检查程序是否接收到了任何命令行参数。记住,sys.argv是一个列表,它始终包含至少一个字符串:脚本的名字'ccwd.py'。如果它包含多个字符串,我们知道用户向程序提供了命令行参数。在这种情况下,第三行会更改程序当前的工作目录。请注意,每个程序都有自己的当前工作目录设置,使用os.chdir()更改此设置不会改变运行程序的终端的当前工作目录。最后,第四行将当前工作目录复制到剪贴板。

程序已完成,但要从终端运行它,我们必须输入它的完整路径:类似于python C:\Users\al\Scripts\ccwd.py的东西。这要输入很多,而且违背了快速轻松地将当前工作目录复制到剪贴板的脚本的目的。为了改进这个过程,让我们回顾一下在每个操作系统上部署此程序的步骤。

Windows

将 Python 文件保存为C:\Users\al\Scripts\ccwd.py(将al改为你的用户名)。在同一个Scripts文件夹中,创建一个包含以下内容的ccwd.bat文件:

@call %HOMEDRIVE%%HOMEPATH%\Scripts\.venv\Scripts\activate.bat
@python %HOMEDRIVE%%HOMEPATH%\Scripts\ccwd.py %*
@deactivate 

这个批处理文件没有@pause行,因为它没有print()输出。你现在可以从任何文件夹的终端中运行此程序,只需运行ccwd

C:\Users\al>ccwd
C:\Users\al> 

到目前为止,'C:\Users\al'已经在剪贴板上了。

macOS

将 Python 文件保存为/Users/al/Scripts/ccwd.py(将al改为你的用户名)。在同一个Scripts文件夹中,创建一个名为ccwd.command的文本文件,并包含以下内容:

source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/ccwd.py
deactivate 

然后,在终端中切换到Scripts文件夹并运行 chmod u+x ccwd.command:

al@Als-MacBook-Pro ~ % cd ~/Scripts
al@Als-MacBook-Pro Scripts % chmod u+x ccwd.command 

你现在可以从任何文件夹的终端中运行此程序,只需运行 ccwd.command:

al@Als-MacBook-Pro ~ % ccwd.command
al@Als-MacBook-Pro ~ % 

到目前为止,'/Users/al'应该已经在剪贴板上了。

Ubuntu Linux

将 Python 文件保存为/home/al/Scripts/ccwd.py(将al改为你的用户名)。在同一个Scripts文件夹中,创建一个名为ccwd的文本文件,并包含以下内容:

#!/usr/bin/env bash
source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/ccwd.py
deactivate 

这个ccwd shell 脚本不需要read -p "Press any key to continue..." -n1 –s行,因为它将仅在终端中运行,而不是从 Dash 运行,并且运行 Python 脚本后终端窗口不会消失。

然后,在终端中切换到Scripts文件夹并运行 chmod u+x ccwd:

al@al-VirtualBox:~$ cd ~/Scripts
al@al-VirtualBox:~/Scripts$ chmod u+x ccwd 

你现在可以从任何文件夹的终端中运行此程序,只需运行 ccwd:

al@al-VirtualBox:~$ ccwd
al@al-VirtualBox:~$ 

到目前为止,'/home/al'应该已经在剪贴板上了。

一个简短的程序:剪贴板记录器

假设你的工作部分是复制网页上链接的 URL 并将它们粘贴到电子表格中。(在第十三章中,你将学习如何抓取网页 HTML 源代码中的所有链接。但假设你只需要复制其中的一些,并且需要根据具体情况由人工决定哪些。)你可以按照以下步骤操作:

在网页浏览器中右键点击一个链接。

2.  从上下文菜单中选择“复制链接”或“复制链接地址”项。

3.  切换到电子表格应用程序。

4.  按 CTRL-V 粘贴链接。

5.  切换回网页浏览器。

这是一个无聊的任务,尤其是如果页面有数十或数百个链接时。让我们创建一个小型的剪贴板记录程序来使其更快。我们将在这个计算机上部署这个程序,以便在需要时方便地运行它。我们的程序将监控剪贴板,查看是否有新文本被复制到其中,如果是的话,它将打印到终端屏幕上。这样,我们可以将我们的五步过程简化为两步过程:

1.  在网页浏览器中右键点击一个链接。

2.  从上下文菜单中选择“复制链接”或“复制链接地址”项。

然后,用户只需将剪贴板记录器终端窗口中的所有文本一次性复制并粘贴到电子表格中。将以下内容输入到文件编辑器中,并保存为cliprec.py

import pyperclip, time

print('Recording clipboard... (Ctrl-C to stop)')
previous_content = ''
try:
    while True:
        content = pyperclip.paste()  # Get clipboard contents.

        if content != previous_content:
            # If it's different from the previous, print it:
            print(content)
            previous_content = content

        time.sleep(0.01)  # Pause to avoid hogging the CPU.
except KeyboardInterrupt:
    pass 

让我们看看这个程序的每个部分,从开始部分开始:

import pyperclip, time

print('Recording clipboard... (Ctrl-C to stop)')
previous_content = '' 

此程序从剪贴板复制并粘贴文本,因此我们需要导入pyperclip模块。我们还将导入time模块以使用其sleep()函数。程序会显示一条快速消息,说明它正在运行,并提醒用户 CTRL-C 会导致程序停止。程序将通过跟踪一个名为previous_content的变量来记录剪贴板的内容,该变量最初被设置为空字符串,以了解剪贴板内容的变化。

try:
    while True:
        content = pyperclip.paste()  # Get clipboard contents. 

程序的主体存在于一个无限while循环中,而这个循环本身又位于一个try块内部。当用户按下 CTRL-C 时,Python 会抛出一个KeyboardInterrupt异常,导致执行跳转到源代码底部的except块。

此循环持续监控剪贴板的内容,并记录每次用户将新文本复制到剪贴板上的情况。在循环中,第一步是通过调用pyperclip.paste()来收集剪贴板上的文本。

 if content != previous_content:
            # If it's different from the previous, print it:
            print(content)
            previous_content = content 

如果剪贴板上的当前内容与之前的内容不同,则程序会打印当前内容并将previous_content更新为content。这样,程序就为下一次用户将新文本复制到剪贴板上的情况设置了循环。

 time.sleep(0.01)  # Pause to avoid hogging the CPU.

如果剪贴板内容与之前获取的剪贴板内容相同,我们的程序可以什么都不做。然而,这个程序可以轻松地每秒执行数万次这个循环,而且用户不太可能那么频繁地更新剪贴板。(如果他们尝试这样做,可能会把键盘上的 CTRL 和 C 键用坏。)为了防止程序通过尽可能快地运行这个无生产力的循环而占用 CPU,我们引入了 0.01 秒的延迟,这样循环每秒只检查剪贴板更新 100 次。

except KeyboardInterrupt:
    pass 

程序的最后部分是except子句,它使用了 Python 的pass语句。这个语句实际上什么也不做,但 Python 期望在except语句之后的块中至少有一行。这就是pass语句被创建的原因。当用户按下 CTRL-C 时,执行将移动到这个except子句,并继续到程序的末尾,然后终止。

使用此应用程序运行时,用户可以复制几项内容,而无需在应用程序之间来回切换。这样的小程序可以使你的工作流程变得更加容易,尤其是如果你每天都要做这项工作。现在让我们将这个程序部署到每个操作系统上。

Windows

将 Python 文件保存为C:\Users\al\Scripts\cliprec.py(将al替换为你的用户名)。在同一个Scripts文件夹中,创建一个cliprec.bat文件,并包含以下内容:

@call %HOMEDRIVE%%HOMEPATH%\Scripts\.venv\Scripts\activate.bat
@python %HOMEDRIVE%%HOMEPATH%\Scripts\cliprec.py %*
@pause
@deactivate 

你现在可以从终端运行这个程序,或者同时按 Windows 键和 R 键打开运行对话框,然后输入cliprec

macOS

将 Python 文件保存为/Users/al/Scripts/cliprec.py(将al替换为你的用户名)。在同一个Scripts文件夹中,创建一个名为cliprec .command的文本文件,并包含以下内容:

source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/cliprec.py
deactivate 

然后,在终端中,切换到Scripts文件夹并运行 chmod u+x cliprec.command:

al@Als-MacBook-Pro ~ % cd ~/Scripts
al@Als-MacBook-Pro Scripts % chmod u+x cliprec.command 

你现在可以通过按⌘-空格键打开 Spotlight 并输入cliprec.command来运行这个程序。

Ubuntu Linux

将 Python 文件保存为/home/al/Scripts/cliprec.py(将al替换为你的用户名)。在同一个Scripts文件夹中,创建一个名为cliprec的文本文件,并包含以下内容:

#!/usr/bin/env bash
source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/cliprec.py
read -p "Press any key to continue..." -n1 –s
deactivate 

然后,在终端中,切换到Scripts文件夹并运行 chmod u+x cliprec:

al@al-VirtualBox:~$ cd ~/Scripts
al@al-VirtualBox:~/Scripts$ chmod u+x cliprec 

最后,创建一个保存为~/.local/share/applications/cliprec.desktop的文本文件,并包含以下内容:

[Desktop Entry]
Name=Clipboard Recorder
Exec=gnome-terminal -- /home/al/cliprec
Type=Application 

你现在可以通过按 Windows 键打开 Dash 并输入Clipboard Recorder,或者输入名称的前几个字符并让自动完成帮你完成来运行这个程序。

使用 PyInstaller 编译 Python 程序

虽然编程语言本身既不是解释执行的也不是编译执行的,但人们通常称 Python 为解释型语言。你可以为任何语言创建解释器或编译器。相反,用 Python 编写的程序主要是由解释器运行的。但使用 PyInstaller 包也可以从 Python 代码创建可执行程序,该包生成可以在命令行中运行的程序。

PyInstaller 并非直接将 Python 程序编译成机器码;相反,它创建了一个包含 Python 解释器和您的脚本的可执行程序。因此,这些程序通常相当大。即使是使用 PyInstaller 编译的简单“Hello, world”程序,其大小也可能接近 8MB,实际上比用汇编语言编写的版本大一千倍。然而,编译您的 Python 程序的好处是,您可以与没有安装 Python 的其他人共享您的程序。您将能够发送给他们一个可执行文件。

您可以通过运行 pip install pyinstaller 来安装 PyInstaller。您必须在希望可执行程序运行的操作系统上运行 PyInstaller。也就是说,如果您在 Windows 上,PyInstaller 可以创建 Windows 可执行程序,但不能创建 macOS 或 Linux 程序,反之亦然。

从终端运行以下命令(在 macOS 和 Linux 上使用 python3 而不是 python)来编译名为 yourScript.py 的 Python 脚本:

C:\Users\al>python -m PyInstaller --onefile yourScript.py
378 INFO: PyInstaller: `X`.`X`.`X`
378 INFO: Python: 3.`XX`.`XX`
392 INFO: Platform: Windows-`XX`-`XX`.`X`.`XXXX`
393 INFO: wrote C:\Users\al\Desktop\hello-test\hello.spec
399 INFO: UPX is not available.
# --snip--
11940 INFO: Appending PKG archive to EXE
11950 INFO: Fixing EXE headers
13622 INFO: Building EXE from EXE-00.toc completed successfully. 

注意,您必须以大写 P 和大写 I 的形式输入 PyInstaller,否则您将收到“没有模块名为 pyinstaller”的错误信息。另外,请注意,--onefile 参数有两个连字符。

运行 PyInstaller 后,将会有一个 build 文件夹(您可以删除它)和一个 dist 文件夹。dist 文件夹包含可执行程序。您不需要为它创建虚拟环境。然后您可以将其程序复制到其他计算机或作为电子邮件附件发送。请注意,作为安全预防措施,许多电子邮件提供商可能会阻止包含可执行程序的电子邮件。

这里提供的说明适用于基本的 Python 程序。pyinstaller.org 上的在线文档包含更多详细信息。

摘要

在本章中,您学习了如何将您的程序从代码编辑器中提取出来并部署,以便用户可以快速方便地运行它们。您还学习了更多关于如何设计具有基于文本的用户界面而不是更现代的图形界面的程序的指南。虽然 GUI 更易于使用,但 TUI(文本用户界面)更容易编写。当您需要为自己自动化任务时,使您的程序看起来像专业应用程序并不值得额外的努力。您只需要一个能工作的东西即可。

话虽如此,您有几种方法可以设计您的程序使其易于使用。通常,这涉及到命令行终端,许多用户对此不太熟悉。学习命令行概念可能需要一段时间,例如使用 cddir/ls 导航文件系统,PATH 环境变量以及命令行参数。但终端允许您非常快速地发出命令并运行程序,尤其是在您完成编写程序并部署之后。

本章还介绍了几个第三方包。Bext 包允许你添加彩色文本、定位光标和清除屏幕。PyMsgBox 包创建用于警报或基本输入的 GUI 框,而不使用终端窗口。由于你可能会运行需要同一包不兼容版本的程序,最好在单独的虚拟环境中运行脚本。你可以使用 Python 附带的自带venv模块创建虚拟环境。虚拟环境从终端激活,可以防止你通过为你提供安装包的单独位置来破坏现有的程序。

最后,PyInstaller 包允许你将.py文件编译成可执行程序。这些程序可能大小为几个兆字节,但你可以将这些程序与可能没有 Python(以及你的程序使用的第三方包)的同事共享。

本章没有涵盖许多编程语言概念;相反,它介绍了如何使你的程序在日常使用中更加可用和方便。到目前为止,你已经掌握了足够的 Python 语法来创建基本程序(尽管总有更多东西可以学习!)。本书的第二部分,你将探索几个第三方包,这些包可以扩展你的 Python 程序的功能。

实践问题

  1. 在 Windows 上,哪个命令列出文件夹内容?在 macOS 和 Linux 上呢?

  2. PATH 环境变量包含什么内容?

  3. __file__变量包含什么内容?

  4. 在 Windows 上,哪个命令擦除终端窗口中的文本?在 macOS 和 Linux 上呢?

  5. 如何创建一个新的虚拟环境?

  6. 在编译程序时,你应该传递给 PyInstaller 的命令行参数是什么?

实践程序:使你的程序可部署

通过在PATH文件夹中创建执行它们的 shell 脚本或使用 PyInstaller 编译它们,使你的现有程序易于运行。为以下项目执行此操作:

  • “从第十一章备份文件夹到 ZIP 文件”

  • “从第九章提取大型文档中的联系人信息”

  • “从第八章添加项目符号到 Wiki 标记”

  • “第七章的交互式棋盘模拟器”

  • 你创建的任何其他程序,你想轻松启动或与他人共享

以任何其他名称命名的程序

本章(以及编程)使用了许多意味着“程序”或该术语的轻微变体的术语。你可以准确地称所有以下项目为程序。但它们之间的名称含义存在细微差别:

程序一个完整的软件,无论大小,包含计算机执行的指令。

脚本一个解释器从其源代码形式而不是从编译的机器代码形式运行的程序。这是一个非常宽泛的术语。尽管 Python 代码可以像其他语言一样编译(你将在第 285 页的“使用 PyInstaller 编译 Python 程序”中了解到这一点),Python 程序通常被称为脚本。

命令 通常从基于文本的终端运行,没有图形用户界面(GUI)的程序。所有配置都是在运行命令之前通过指定命令行参数来完成的(尽管 交互式命令 有时可能会用“您确定吗?Y/N”的问题来中断其操作)。在“第 260 页的 cd、pwd、dir 和 ls 命令”中解释的 dirls 都是命令的例子。

Shell 脚本 一个单一的文本文件,可以方便地一次性运行多个捆绑的终端命令。这样,用户只需运行一个 shell 脚本,而不是手动逐个输入多个命令。在 macOS 和 Linux 上,shell 脚本文件具有 .sh 文件扩展名(或没有扩展名),而 Windows 使用具有 .bat 文件扩展名的批处理文件来表示 shell 脚本。

应用程序 一个具有图形用户界面(GUI)并包含多个相关功能的程序。Excel 和 Firefox 都是应用程序的例子。应用程序通常包含多个文件,由 安装程序 在您的计算机上设置(并且由 卸载程序 可以删除),而不是仅由一个可执行文件复制到计算机上。

App 移动电话和平板电脑应用程序的常用名称,但该术语也可以用于桌面应用程序。

Web 应用 在 Web 服务器上运行的程序,用户通过互联网通过 Web 浏览器与之交互。

您可以对这些术语的精确定义吹毛求疵;这些解释仅仅是为了给您一个关于这些术语用法的总体感觉。如果您想熟悉更多术语,我的书《Python 基础之外》(No Starch Press,2020)在其“编程术语”章节中提供了额外的定义。

使用终端

直到 1990 年代,当苹果和微软推广了可以同时运行多个程序的具有 GUI 的计算机时,程序都是从 命令行界面(CLI,发音为“see-el-eye”或与“fly”押韵的词)启动的,并且通常限于基于文本的输入和输出。您也可能听到 CLIs 被称为 命令提示符终端shell控制台。软件开发人员仍然使用 CLIs,并且通常在任何给定时间都会在他们的计算机上打开几个终端窗口。虽然基于文本的终端可能没有 GUI 的图标、按钮和图形,但一旦您学会了几个命令,它就是使用计算机的有效方式。

要打开终端窗口,请执行以下操作:

  • 在 Windows 上,点击 开始 按钮(或按 Windows 键)并输入 命令提示符(或如果您已安装,则输入 PowerShellTerminal)。

  • 在 macOS 上,点击右上角的 Spotlight 图标(或按 -空格键)并输入 Terminal

  • 在 Ubuntu Linux 上,按 Windows 键 弹出 Dash,并输入 Terminal。或者,使用键盘快捷键 CTRL-ALT-T。

就像交互式 shell 有一个 >>> 提示符一样,终端也会为你显示一个提示符来输入命令。在 Windows 上,这将是当前所在文件夹的完整路径,后面跟着一个小于号 (>):

C:\Users\al>your commands go here

在 macOS 上,提示符会显示你的用户名、计算机名称以及当前工作目录(你的家目录用 ~ 表示),后面跟着一个百分号 (%):

al@Als-MacBook-Pro ~ % your commands go here

在 Ubuntu Linux 上,提示符与 macOS 上的提示符类似,但以用户名和一个 @ 符号开头:

al@al-VirtualBox:~$ your commands go here

虽然从开始菜单(Windows)或 Spotlight(macOS)运行程序更简单,但也可以从终端启动它们。Python 解释器本身就是一个经常在终端中运行的程序。

在本章中,我们假设你想要运行的 Python 程序名为 yourScript.py,并且它位于你家目录下的 Scripts 文件夹中。你不需要打开 Mu 来访问 Python 交互式 shell。从终端窗口,你可以在 Windows 上输入 python 或者在 macOS 和 Linux 上输入 python3 来启动它。(你应该会看到它熟悉的 >>> 提示符。)要从终端运行你的 .py Python 文件,在 pythonpython3 后面输入其文件路径——可以是绝对路径,如 python C:\Users\al\Scripts\yourScript.py,也可以是相对路径,如 python yourScript.py,如果当前工作目录设置为 C:\Users\al\Scripts,即 yourScript.py 所在的文件夹。

cdpwddirls 命令

正如所有运行中的程序都有一个当前工作目录 (CWD) 来附加相对文件路径一样,终端也有一个当前工作目录。你可以通过终端提示符看到这个 CWD,或者通过在 macOS 和 Linux 上运行 pwd(打印工作目录)命令,或者在 Windows 上不带任何命令行参数运行 cd 命令来查看它。

你的 Python 程序可以通过调用 os.chdir() 函数来更改当前工作目录 (CWD)。在终端中,你可以通过输入 cd 命令后跟要更改到的文件夹的相对或绝对文件路径来完成相同的事情:

C:\Users\al>cd Desktop
C:\Users\al\Desktop>cd ..
C:\Users\al>cd C:\Windows\System32
C:\Windows\System32> 

在 Windows 上,你可能还需要额外的步骤来切换驱动器字母。你不能使用 cd 命令更改你所在的驱动器。相反,输入驱动器字母后跟一个冒号,然后使用 cd 来更改驱动器上的目录:

C:\Windows\System32>D:
D:\>cd backup
D:\backup> 

Windows 上的 dir 命令和 macOS 以及 Linux 上的 ls 命令会列出当前工作目录 (CWD) 的文件和子目录内容:

C:\Users\al>dir
# --snip--
08/26/2036  06:42 PM           171,304 _recursive-centaur.png
08/18/2035  11:25 AM             1,278 _viminfo
08/13/2035  12:58 AM    <DIR>          __pycache__
              77 File(s)     83,805,114 bytes
             108 Dir(s)  149,225,267,200 bytes free 

在终端中导航文件系统时,你通常会在这几个命令之间切换:使用 cd 来更改目录,使用 dirls 来查看目录内容。在 Windows 上,你可以通过运行 dir *.exe 来列出当前工作目录 (CWD) 中的所有可执行文件,而在 macOS 和 Linux 上,你可以使用 file * | grep executable 来实现相同的功能。一旦你进入了包含程序的文件夹,你可以以下几种方式运行它:

  • 在 Windows 上,输入程序名称,可以带有或没有 .exe 扩展名:example.exe

  • 在 macOS 和 Linux 上,输入 ./ 后跟程序名称:./example

当然,你总是可以输入程序的完整绝对路径:C:\full\path\to\example.exe/full/path/to/example

如果你想要打开一个非程序文件,例如名为 example.txt 的文本文件,你可以在 Windows 上通过输入 example.txt 或者在 macOS 和 Linux 上输入 open example.txt 来使用其关联的应用程序打开它。在终端中,这和双击 GUI 中 example.txt 文件图标所做的相同。如果没有为 .txt 文件设置关联的应用程序,操作系统将提示用户选择一个,并记住它以供将来使用。

PATH 环境变量

所有正在运行的程序,无论它们是用什么语言编写的,都有一组称为 环境变量 的字符串变量。其中之一是 PATH 环境变量,它包含终端在输入程序名称时检查的文件夹列表。例如,如果你在 Windows 上输入 python 或者在 macOS 和 Linux 上输入 python3,终端会在 PATH 中列出的文件夹中查找具有该名称的程序。操作系统在使用 PATH 方面有一些不同的规则:

  • Windows 首先检查当前工作目录 (CWD) 中是否有同名程序,然后检查 PATH 中的文件夹。

  • Linux 和 macOS 只检查 PATH 中的文件夹,根本不会检查 CWD。如果你想在你所在的 CWD 中运行名为 example 的程序,你必须输入 ./example 而不是 example

要查看 PATH 环境变量的内容,请在 Windows 上运行 echo %PATH% 或者在 macOS 和 Linux 上运行 echo $PATHPATH 中存储的值是一个由分号(在 Windows 上)或冒号(在 macOS 和 Linux 上)分隔的文件夹名称的长字符串。例如,在 Ubuntu Linux 上,PATH 环境变量可能看起来像以下这样:

al@al-virtual-machine:~$ echo $PATH
/home/al/.local/bin:/home/al/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/
usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin 

如果你要在 Linux 终端中输入 python3 并使用这个 PATH,Linux 会首先在 /home/al/.local/bin 文件夹中查找名为 python3 的程序,然后是 /home/al/bin 文件夹,依此类推。它最终会在 /usr/bin 中找到 python3 并运行它。PATH 环境变量很方便,因为它允许你只需将程序放入 PATH 上的一个文件夹,就可以避免每次运行它时都要使用 cd 命令切换到其文件夹。

注意,终端窗口不会搜索 PATH 文件夹下的子文件夹。如果 C:\Users\al\Scripts 列在 PATH 中,运行 spam.exe 将会运行 C:\Users\al\Scripts\spam.exe 文件,但不会运行 C:\Users\al\Scripts\eggs\spam.exe 文件。

PATH 编辑

到目前为止,你可能一直将你的 .py 文件保存在 Mu 编辑器默认使用的 mu_code 文件夹中。然而,我建议在你的主目录下创建一个 Scripts 文件夹。如果你的用户名恰好是 al,这个文件夹将是:

  • 在 Windows 上 C:\Users\al\Scripts

  • 在 macOS 上 /Users/al/Scripts

  • 在 Ubuntu Linux 上 /home/al/Scripts

让我们把这个文件夹添加到 PATH 中。

Windows

Windows 有两组环境变量:系统环境变量(适用于所有用户)和用户环境变量(覆盖系统环境变量,但仅适用于当前用户)。要编辑它们,点击 开始 菜单,然后输入 编辑您的账户的环境变量,这应该会打开 环境变量 窗口。

从屏幕顶部的用户变量列表中选择 路径(不是屏幕底部的系统变量列表),点击 编辑,在出现的文本字段中添加新的文件夹名称 C:\Users\al\Scripts,并用分号分隔,然后点击 确定

macOS 和 Linux

要将文件夹添加到 PATH 环境变量中,您需要编辑终端启动脚本。在 macOS 上是 .zshrc 文件,在 Linux 上是 .bashrc 文件。这两个文件都在您的家目录中,包含每次打开新终端窗口时运行的命令。在 macOS 上,将以下内容添加到 .zshrc 文件的底部:

export PATH=/Users/al/Scripts:$PATH

在 Linux 上,将以下内容添加到 .bashrc 文件的底部:

export PATH=/home/al/Scripts:$PATH

这行代码会修改您打开的所有未来终端窗口的 PATH,因此更改不会影响当前打开的终端窗口。

which 和 where 命令

如果您想找出 PATH 环境变量中哪个文件夹包含程序,您可以在 macOS 和 Linux 上运行 which 程序,在 Windows 上运行 where 程序。例如,在 macOS 终端中输入以下 which 命令:

al@Als-MacBook-Pro ~ % which python3
/Library/Frameworks/Python.framework/Versions/3.13/bin/python3 

在 Windows 上,将以下 where 命令输入到终端中:

C:\Users\al>where python
C:\Users\al\AppData\Local\Programs\Python\Python313\python.exe
C:\Users\al\AppData\Local\Programs\Python\Python312\python.exe 

where 命令显示了 PATH 中每个包含名为 python 的程序的文件夹。位于最顶层文件夹中的是您输入 python 时运行的版本。whichwhere 命令在您不确定 PATH 的配置方式并需要找到特定程序的位置时非常有用。

cd、pwd、dir 和 ls 命令

正如所有正在运行的程序都有一个当前工作目录 (CWD),相对文件路径会附加到该目录一样,终端也有一个当前工作目录。您可以在终端提示符中看到这个 CWD,或者通过在 macOS 和 Linux 上运行 pwd(用于打印工作目录)或在 Windows 上不带任何命令行参数运行 cd 命令来查看它。

您的 Python 程序可以通过调用 os.chdir() 函数来更改当前工作目录 (CWD)。在终端中,您可以通过输入 cd 命令后跟要更改到的文件夹的相对或绝对路径来完成相同的事情:

C:\Users\al>cd Desktop
C:\Users\al\Desktop>cd ..
C:\Users\al>cd C:\Windows\System32
C:\Windows\System32> 

在 Windows 上,您可能还需要额外的步骤来切换驱动器字母。您不能使用 cd 命令更改当前所在的驱动器。相反,输入驱动器字母后跟一个冒号,然后使用 cd 在该驱动器上更改目录:

C:\Windows\System32>D:
D:\>cd backup
D:\backup> 

Windows 上的 dir 命令和 macOS 及 Linux 上的 ls 命令将列出当前工作目录 (CWD) 的文件和子文件夹内容:

C:\Users\al>dir
# --snip--
08/26/2036  06:42 PM           171,304 _recursive-centaur.png
08/18/2035  11:25 AM             1,278 _viminfo
08/13/2035  12:58 AM    <DIR>          __pycache__
              77 File(s)     83,805,114 bytes
             108 Dir(s)  149,225,267,200 bytes free 

在终端中导航文件系统时,您通常会交替使用 cd 来更改目录和 dir/ls 来查看目录内容。您可以在 Windows 上通过运行 dir *.exe 或者在 macOS 和 Linux 上运行 file * | grep executable 来列出 CWD 中的所有可执行文件。一旦您进入包含程序的文件夹,您可以通过以下方式运行它:

  • 在 Windows 上,输入程序名称,带或不带 .exe 扩展名:example.exe

  • 在 macOS 和 Linux 上,输入 ./ 后跟程序名称:./example

当然,您始终可以输入程序的完整绝对路径:C:\full\path\to\example.exe/full/path/to/example

如果您想打开非程序文件,例如名为 example.txt 的文本文件,您可以在 Windows 上输入 example.txt 或者在 macOS 和 Linux 上输入 open example.txt 来使用其关联的应用程序。在终端中,这和双击 GUI 中 example.txt 文件图标执行的操作相同。如果没有为 .txt 文件设置关联应用程序,操作系统将提示用户选择一个并记住它以供将来使用。

PATH 环境变量

所有正在运行的程序,无论它们是用什么语言编写的,都有一组称为 环境变量 的字符串变量。其中之一是 PATH 环境变量,它包含终端在您输入程序名称时检查的文件夹列表。例如,如果您在 Windows 上输入 python 或者在 macOS 和 Linux 上输入 python3,终端会在 PATH 中列出的文件夹中查找具有该名称的程序。操作系统在如何使用 PATH 方面略有不同:

  • Windows 首先在 CWD 中查找同名程序,然后是 PATH 中的文件夹。

  • Linux 和 macOS 只检查 PATH 中的文件夹,而根本不检查 CWD。如果您想在 CWD 中运行名为 example 的程序,您必须输入 ./example 而不是 example

要查看 PATH 环境变量的内容,请在 Windows 上运行 echo %PATH% 或者在 macOS 和 Linux 上运行 echo $PATH。存储在 PATH 中的值是一个由分号(在 Windows 上)或冒号(在 macOS 和 Linux 上)分隔的文件夹名称的长字符串。例如,在 Ubuntu Linux 上,PATH 环境变量可能看起来如下:

al@al-virtual-machine:~$ echo $PATH
/home/al/.local/bin:/home/al/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/
usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin 

如果您使用此 PATH 在 Linux 终端中输入 python3,Linux 会首先在 /home/al/.local/bin 文件夹中查找名为 python3 的程序,然后是 /home/al/bin 文件夹,依此类推。它最终会在 /usr/bin 中找到 python3 并运行它。PATH 环境变量很方便,因为您只需将程序放入 PATH 上的文件夹,就可以避免每次运行它时都要使用 cd 命令切换到其文件夹。

注意,终端窗口不会在 PATH 文件夹下的子文件夹中搜索。如果 C:\Users\al\Scripts 列在 PATH 中,运行 spam.exe 将会运行 C:\Users\al\Scripts\spam.exe 文件,但不会运行 C:\Users\al\Scripts\eggs\spam.exe 文件。

PATH 编辑

到目前为止,您可能一直将 .py 文件保存在 Mu 编辑器默认使用的 mu_code 文件夹中。然而,我建议在您的家目录下创建一个 Scripts 文件夹。如果您的用户名恰好是 al,则此文件夹将是:

  • C:\Users\al\Scripts 在 Windows 上

  • /Users/al/Scripts 在 macOS 上

  • /home/al/Scripts 在 Ubuntu Linux 上

让我们将这个文件夹添加到 PATH

Windows

Windows 有两组环境变量:系统环境变量(适用于所有用户)和用户环境变量(覆盖系统环境变量,但仅适用于当前用户)。要编辑它们,点击 开始 菜单,然后输入 编辑您的账户的环境变量,这将打开 环境变量 窗口。

从屏幕顶部的用户变量列表中选择 Path(不是屏幕底部的系统变量列表),点击 编辑,在出现的文本字段中(以分号分隔),添加新的文件夹名称 C:\Users\al\Scripts,然后点击 确定

macOS 和 Linux

要将文件夹添加到 PATH 环境变量中,您需要编辑终端启动脚本。在 macOS 上是 .zshrc 文件,在 Linux 上是 .bashrc 文件。这两个文件都在您的家目录中,并在每次打开新的终端窗口时运行命令。在 macOS 上,将以下内容添加到 .zshrc 文件的底部:

export PATH=/Users/al/Scripts:$PATH

在 Linux 上,将以下内容添加到 .bashrc 文件的底部:

export PATH=/home/al/Scripts:$PATH

这行代码将修改您打开的所有未来终端窗口的 PATH,因此更改不会影响当前打开的终端窗口。

Windows

Windows 有两组环境变量:系统环境变量(适用于所有用户)和用户环境变量(覆盖系统环境变量,但仅适用于当前用户)。要编辑它们,点击 开始 菜单,然后输入 编辑您的账户的环境变量,这将打开 环境变量 窗口。

从屏幕顶部的用户变量列表中选择 Path(不是屏幕底部的系统变量列表),点击 编辑,在出现的文本字段中(以分号分隔),添加新的文件夹名称 C:\Users\al\Scripts,然后点击 确定

macOS 和 Linux

要将文件夹添加到 PATH 环境变量中,您需要编辑终端启动脚本。这是 macOS 上的 .zshrc 文件和 Linux 上的 .bashrc 文件。这两个文件都在您的家目录中,并在每次打开新的终端窗口时运行命令。在 macOS 上,将以下内容添加到 .zshrc 文件的底部:

export PATH=/Users/al/Scripts:$PATH

在 Linux 上,将以下内容添加到 .bashrc 文件的底部:

export PATH=/home/al/Scripts:$PATH

这行代码将修改您打开的所有未来终端窗口的 PATH,因此更改不会影响当前打开的终端窗口。

whichwhere 命令

如果你想要找出 PATH 环境变量中哪个文件夹包含了一个程序,你可以在 macOS 和 Linux 上运行 which 程序,在 Windows 上运行 where 程序。例如,在 macOS 终端中输入以下 which 命令:

al@Als-MacBook-Pro ~ % which python3
/Library/Frameworks/Python.framework/Versions/3.13/bin/python3 

在 Windows 终端中输入以下 where 命令:

C:\Users\al>where python
C:\Users\al\AppData\Local\Programs\Python\Python313\python.exe
C:\Users\al\AppData\Local\Programs\Python\Python312\python.exe 

where 命令显示了 PATH 中包含名为 python 程序的每个文件夹。位于最顶层文件夹中的是当你输入 python 时运行的版本。whichwhere 命令在你不确定 PATH 的配置方式并需要找到特定程序的位置时非常有用。

虚拟环境

假设你有两个 Python 程序,一个使用该包的 1.0 版本,另一个使用该包的 2.0 版本。Python 不能同时安装同一包的两个版本。如果 2.0 版本与 1.0 版本不兼容,每次你想切换程序运行时,都需要卸载一个版本并重新安装另一个版本。

Python 解决这个问题的方案是 虚拟环境;拥有自己安装的第三方包的独立 Python 安装。一般来说,你创建的每个 Python 应用都需要自己的虚拟环境。但在学习编程的过程中,你可以使用一个虚拟环境来运行所有的简单脚本。Python 可以使用其内置的 venv 模块创建虚拟环境。要创建虚拟环境,请切换到你的 Scripts 文件夹并运行 python –m venv .venv(在 macOS 和 Linux 上使用 python3):

C:\Users\al>
C:\Users\al>cd Scripts
C:\Users\al\Scripts>python -m venv .venv 

这将在名为 .venv 的新文件夹中创建虚拟环境的文件。你可以选择任何你想要的文件夹名,但 .venv 是一个传统选择。以点开头的文件和文件夹是隐藏的,尽管你可以按照本书引言中的步骤来让操作系统默认显示它们。

当你在终端中运行 pythonpython3 时,你仍然会运行原始 Python 安装的解释器。要使用虚拟环境的 Python 版本,你必须激活它。在 Windows 上,通过运行 C:\Users\al\Scripts.venv\Scripts\activate.bat 脚本来完成此操作:

C:\Users\al\Scripts>cd .venv\Scripts
C:\Users\al\Scripts\.venv\Scripts>activate.bat
(.venv) C:\Users\al\Scripts\.venv\Scripts>where python.exe
C:\Users\Al\Scripts\.venv\Scripts\python.exe
C:\Users\Al\AppData\Local\Programs\Python\Python313\python.exe
C:\Users\Al\AppData\Local\Programs\Python\Python312\python.exe 

在激活虚拟环境后运行 where python.exe 可以显示,从终端运行 python 将会运行在 .venv\Scripts 文件夹中的 Python 解释器,而不是系统 Python(稍后讨论)。

在 macOS 和 Linux 上的等效脚本为 ~/Scripts/.venv/bin/activate,但由于安全权限,你无法直接运行它。相反,运行命令 source activate

al@al-virtual-machine:~/Scripts$ cd .venv/bin
al@al-virtual-machine:~/Scripts/.venv/bin$ source activate
(.venv) al@al-virtual-machine:~/Scripts/.venv/bin$ which python3
/home/al/Scripts/.venv/bin/python3 

激活会更改 PATH 环境变量,以便 pythonpython3 运行 .venv 文件夹内的 Python 解释器而不是原始解释器。它还会更改您的终端提示,以包含 (.venv),这样您就知道虚拟环境已激活。在激活的虚拟环境中运行 which python3 会显示 python3 在新创建的 .venv/bin 文件夹中运行 Python 解释器。这些更改仅适用于当前终端窗口;任何现有或新的终端窗口都不会有这些环境变量或提示更改。这个新的 Python 安装只有默认包,并且不包含您可能在原始 Python 安装中已经安装的任何包。您可以通过运行 python –m pip list 来确认,这将列出已安装的包:

(.venv) C:\Users\al\Scripts\.venv\Scripts>python -m pip list
Package    Version
---------- -------
pip        23.0
setuptools 65.5.0 

标准做法是为您正在工作的每个 Python 项目创建一个虚拟环境,因为每个项目都可能有自己的独特包依赖关系。然而,在 Windows 上,我们可以对我们 Scripts 文件夹中编写的随机小脚本稍微宽松一些:它们都可以共享这个环境。

macOS 和 Linux 操作系统都有自己的程序,这些程序依赖于操作系统附带的 Python 安装。为称为 system Python 的原始 Python 安装安装或更新包,可能会引入导致这些程序失败的不兼容性。使用系统 Python 运行自己的脚本是可以的;将第三方包安装到系统 Python 上稍微有些风险,但在 Scripts 文件夹中创建虚拟环境是防止安装不兼容包的良好预防措施。

请记住,Mu 有自己的虚拟环境。当您在 Mu 中按 F5 运行脚本时,它不会使用您安装到 Scripts.venv 文件夹虚拟环境的包。随着您编程能力的提高,您可能会发现同时打开 Mu 窗口编辑代码和打开终端窗口运行它更容易。您可以使用 Windows 和 Linux 上的 ALT-TAB 键盘快捷键以及 macOS 上的 -TAB 快捷键快速在窗口之间切换焦点。

要停用虚拟环境,请在与 activate 脚本相同的文件夹中运行 deactivate.bat(在 Windows 上)或 deactivate(在 macOS 和 Linux 上)。您也可以简单地关闭终端窗口并打开一个新的。如果您想永久删除虚拟环境及其安装的包,只需删除 .venv 文件夹及其内容。

以下部分将向您介绍在设置虚拟环境和将您的 Scripts 文件夹添加到 PATH 后,如何部署您的脚本。

使用 pip 安装 Python 包

Python 附带了一个名为 pip 的命令行包管理器程序(除非它在句子的开头,否则全部小写)。Pip 是 pip installs package 的递归缩写。虽然 Python 的标准库包含 sysrandomos 等模块,但还有数十万个第三方包可以在 PyPI(发音为“pie-pee-eye”,而不是“pie-pie”)上找到,即 Python 包索引,网址为 pypi.org。在 Python 中,package 是一个在 PyPI 上提供的 Python 代码集合,而 module 是包含 Python 代码的单独 .py 文件。你可以从 PyPI 安装包含模块的包,并通过 import 语句导入模块。

虽然 pip 是一个独立的程序,但通过在 Windows 上运行 python –m pip 或在 macOS 和 Linux 上运行 python3 –m pip 来通过 Python 解释器运行它更容易,而不是直接运行 pip(在 Windows 上)或 pip3(在 macOS 和 Linux 上)程序。这可以防止在罕见的情况下出现错误,比如你有多个 Python 安装,你的 PATH 配置错误,以及 pip/pip3 正在安装到与运行 python/python3 时不同的 Python 解释器。

要从 PyPI 安装一个包,请在终端中输入以下内容:

C:\Users\al>python –m pip install `package_name`

请记住,在 macOS 或 Linux 上运行这些各种命令时,使用 python3 而不是 python。另外,请注意,你需要从终端窗口运行这些命令,而不是从 Python 交互式 shell 运行。

要列出你安装的所有包及其版本号,请运行 python –m pip list

C:\Users\al>python -m pip list
Package                   Version     Editable project location
------------------------- ----------- -------------------------
altgraph                  0.17.3
argon2-cffi               21.3.0
argon2-cffi-bindings      21.2.0
async-generator           1.10
# --snip--
wsproto                   1.2.0 

你也可以通过运行 python –m pip install –U package_name 将包升级到 PyPI 上的最新版本,或者通过运行 python –m pip install package_name==1.17.4 安装特定版本(例如,1.17.4)。

要卸载一个包,请运行 python –m pip uninstall package_name。你可以通过运行 python –m pip --help 来获取有关 pip 的更多信息。

自我意识的 Python 程序

Python 的标准库没有包含任何赋予你的程序意识的模块。(目前还没有。)但是,有几个内置变量可以给你的 Python 程序提供关于自身、运行它的操作系统以及 Python 解释器的有用信息。Python 解释器会自动设置这些变量。

__file__ 变量包含 .py 文件的路径作为字符串。例如,如果我在我的主文件夹中运行一个 yourScript.py 文件,它将评估为 'C:\Users\al\yourScript.py'。导入 from pathlib import Path 并调用 Path(__file__) 返回该文件的 Path 对象。如果你需要定位存在于 Python 程序文件夹中的文件,这个信息很有用。当你运行 Python 交互式 shell 时,__file__ 变量不存在。

sys.executable 变量包含 Python 解释程序本身的完整路径和文件名,而 sys.version 变量包含在交互式外壳顶部出现的字符串,其中包含有关 Python 解释程序版本的信息。

sys.version_info.majorsys.version_info.minor 变量包含 Python 解释程序的主版本号和次版本号的整数。在我的运行 Python 版本 3.13.1 的笔记本电脑上,这些分别是 313。您还可以将 sys.version_info 传递给 list() 函数以获取更具体的信息:在我的笔记本电脑上,list(sys.version_info) 返回 [3, 13, 1 'final', 0]。以这种形式拥有版本信息比试图从 sys.version 字符串中提取它要容易得多。

如果在 Windows 上运行,os.name 变量包含字符串 'nt';如果在 macOS 或 Linux 上运行,则包含 'posix'。这对于您的 Python 脚本需要根据运行在哪个操作系统上执行不同代码时非常有用。

对于更具体的操作系统识别,sys.platform 变量在 Windows 上包含 'win32',在 macOS 上包含 'darwin',在 Linux 上包含 'linux'

如果您需要关于操作系统版本和 CPU 类型的详细信息,内置的 platform 模块可以检索这些信息。此模块的文档可在网上找到,网址为 docs.python.org/3/library/platform.html

如果您需要检查模块是否已安装,请将 import 语句放在 try 块中,并捕获 ModuleNotFoundError 异常:

try:
    import nonexistentModule
except ModuleNotFoundError:
    print('This code runs if nonexistentModule was not found.') 

如果模块对于您的程序功能是必要的,您可以在此处放置描述性错误消息并调用 sys.exit() 来终止程序。这比通用的错误消息和回溯对用户更有帮助。

基于文本的程序设计

在支持 GUI 的操作系统变得普遍之前,所有程序都使用文本与用户进行通信。本书侧重于创建小型、有用的程序,而不是专业软件应用程序,因此本书中的程序通过命令行界面使用 print()input(),而不是 GUI 提供的窗口、按钮和图形。

然而,即使限制在文本中,软件应用程序仍然可以提供类似于现代 GUI 的用户界面。图 12-1 展示了 Norton Commander,这是一个用于浏览文件系统的应用程序。这类应用程序被追称为 TUI(发音为“two-ee”),或 基于文本的用户界面 应用程序。

顶部,一个包含多个标记为“名称”、“大小”、“日期”和“时间”的基于文本的列的计算机界面,后面跟着一个文件名列表。底部,Windows 文件资源管理器界面截图,显示桌面文件夹中的文件图标。

图 12-1:基于文本的 Norton Commander(顶部)与现代 GUI 应用程序(底部)并排

即使你不是专业的软件开发者,基于文本的用户界面的优势在于其简单性。本节描述了你的程序可以采用的一些设计方法来构建用户界面。

短命令名

用户通常从命令行而不是通过点击桌面或开始菜单上的图标来运行基于文本的程序。这些命令有时可能难以理解。当我开始学习 Linux 操作系统时,我惊讶地发现我熟悉的 Windows copy 命令在 Linux 上被命名为 cpcopy 这个名字比 cp 更易读。简短而晦涩的名字真的值得节省两个字符的输入吗?

随着我越来越多地使用命令行,我意识到答案是肯定的。我们阅读源代码的频率比我们编写的频率高,所以为变量和函数使用详尽的名称有帮助。但我们将命令输入到命令行中的频率比我们阅读的频率高,所以在这种情况下,情况正好相反:简短的命令名使命令行更容易使用,并减轻了手腕的压力。

如果你的程序是一个你每天可能会输入十几次的命令,试着给它想一个简短的名字。你可以使用 whichwhere 命令来检查该名字是否已被其他程序使用。你还可以通过互联网搜索以该名字命名的任何现有命令。简短的名字在很大程度上有助于使其易于使用。

命令行参数

要从命令行运行程序,只需输入其名称。对于 .py Python 源代码文件,你必须运行 python(Windows)或 python3(macOS 和 Linux)程序,然后在其后提供 .py 文件名,如下所示:python yourScript.py

命令后面的文本称为 命令行参数。命令行参数以与函数调用参数相同的方式传递给命令。例如,单独的 ls 命令会列出当前工作目录 (CWD) 中的文件。但你也可以运行 ls exampleFolderexampleFolder 命令行参数将指示 ls 命令列出 exampleFolder 文件夹中的文件。命令行参数允许你配置命令的行为。

Python 脚本可以通过 sys.argv 列表访问传递给 Python 解释器的命令行脚本。例如,如果你输入了 python3 yourScript.py hello worldpython3 程序将接收命令行参数并将它们转发到你的 Python 脚本中的 sys.argv 变量。sys.argv 变量将包含 ['yourScript.py', 'hello', 'world']

注意,sys.argv 中的第一个项目是 Python 脚本的文件名。其余的参数由空格分隔。如果你需要在命令行参数中包含空格字符,请在运行命令时将它们放在双引号内。例如,python3 yourScript.py "hello world" 将设置 sys.argv['yourScript.py', 'hello world']

命令行参数的主要用途是在程序开始之前指定各种配置。无需通过配置菜单或多步骤过程。不幸的是,这种方法意味着命令行参数可能会变得极其复杂且难以阅读。如果您在 Windows 命令后传递 /? 或者在 macOS 或 Linux 命令后传递 --help,您通常会找到一页又一页的命令行参数文档。

如果您的程序接受的命令行参数集很简单,那么直接让程序读取 sys.argv 列表是最简单的。然而,随着您添加更多的命令行参数,可能的组合可能会变得难以管理。如果 python yourScript.py spam eggspython yourScript.py eggs spam 做同样的事情,那么如果用户可以提供 cheese 参数或 bacon 参数,他们同时提供这两个参数会发生什么?这种复杂性将需要您编写大量代码来处理各种边缘情况。在这种情况下,您可能最好使用 Python 的内置 argparse 模块来处理这些复杂的情况。argparse 模块超出了本书的范围,但您可以在网上阅读其文档,网址为 docs.python.org/3/library/argparse.html

剪贴板 I/O

您不需要依赖 input() 从文件或键盘读取文本。您也可以使用剪贴板为您的 Python 程序提供文本输入和输出。跨平台的 pyperclip 模块有一个 copy() 函数用于将文本放置在剪贴板上,以及一个 paste() 函数,该函数返回剪贴板上的文本作为字符串。Pyperclip 是一个第三方包,您可以通过终端使用 pip 安装:python –m pip install pyperclip。在 Linux 上,您还必须运行 sudo apt install xclip 以使 Pyperclip 正常工作。有关完整说明,请参阅附录 A。

您的所有剪贴板 I/O 程序都将遵循以下基本设计:

  1. 导入 pyperclip 模块。

  2. 调用 pyperclip.paste() 从剪贴板获取输入文本。

  3. 在文本上执行一些操作。

  4. 通过传递给 pyperclip.copy() 将结果复制到剪贴板。

第八章中“向 Wiki 标记添加项目符号”的项目是这类程序的例子。一旦您按照第 275 页上“部署 Python 程序”中的说明部署了该程序,这种程序的设计就变得特别有用。只需突出显示输入文本,按 CTRL-C 复制它,然后运行程序。结果将出现在剪贴板上,随时可以粘贴到所需位置。

在本章的后面部分,我们将探讨两个项目,即 ccwd 命令和剪贴板记录器,它们都使用了剪贴板。

使用 Bext 的彩色文本

您可以使用基于 Jonathan Hartley 的 Colorama 包构建的第三方 Bext 包来打印彩色文本。按照附录 A 中的说明使用 pip 安装 Bext。Bext 只能在从终端窗口运行的程序中使用,而不能在 Mu 或大多数其他代码编辑器中使用。要使print()打印彩色文本,请调用fg()bg()函数,使用字符串参数如'black''red''green''yellow''blue''magenta''purple''cyan''white'来改变(前景)文本颜色或背景颜色。您还可以传递'reset'来将颜色改回终端窗口的默认颜色。例如,在交互式 shell 中输入以下内容:

>>> import bext
>>> bext.fg('red')
>>> print('This text is red.')
This text is red.
>>> bext.bg('blue')
>>> print('Red text on blue background is an ugly color scheme.')
Red text on blue background is an ugly color scheme.
>>> bext.fg('reset')
>>> bext.bg('reset')
>>> print('The text is normal again. Ah, much better.')
The text is normal again. Ah, much better. 

请记住,用户可能已将他们的终端窗口设置为浅色模式或深色模式,因此无法确定终端的默认外观是黑色文字在白色背景上还是白色文字在黑色背景上。您还应该限制对颜色的使用:过多的颜色可能会使程序看起来很俗气或难以阅读。

Bext 还有一些有限的 TUI(文本用户界面)功能,包括以下内容:

bext.clear() 清除屏幕

bext.width() bext.height() 分别返回终端窗口的当前宽度(以列为单位)和高度(以行为单位)

bext.hide() bext.show() 分别隐藏和显示光标

bext.title(text) 将终端窗口的标题栏更改为文本字符串

bext.goto(x, y) 将光标移动到终端中的列 x 和行 y,其中 0, 0 是左上角位置

bext.get_key() 等待用户按下任意键,然后返回一个描述该键的字符串

bext.get_key()函数视为input()的单键版本。返回的字符串包括'a''z''5',但也包括'left''f1''esc'等键。TAB 键和 ENTER 键分别返回'\t''\n'。在交互式 shell 中调用bext.get_key()以测试各种键并查看它们的返回值。

为了展示 Bext 能做什么,运行 ASCII Art 鱼缸程序的源代码,程序地址为inventwithpython.com/projects/fishtank.py。首先,这个程序使用bext.clear()清除终端窗口中的所有文本。接下来,程序调用bext.goto()定位光标,并使用bext.fg()改变文本颜色,然后打印出由文本字符如><)))*>组成的各种鱼。这个程序在我的书《Python 小项目大全书》(No Starch Press,2021 年)中有介绍。

终端清除

如果你希望你的程序在运行前删除任何遗留的文本,bext.clear() 函数非常有用。你还可以用它来实现翻页式动画:调用 clear() 清除终端,然后使用 print() 调用来填充文本,使用 Python 的 time.sleep() 暂停片刻,然后重复。有一个 Python 的单行代码(用于执行特殊技巧的单行代码)可以清除屏幕,你可以将其放置在自己的 clear() 函数中:

import os
def clear():
    os.system('cls' if os.name == 'nt' else 'clear') 

这段代码可以让你的程序清除终端屏幕,而无需安装 Bext 包,并且仅在从终端运行的 Python 脚本中有效,而不是在 Mu 或其他代码编辑器中。你将在第十一章中了解更多关于 os.system() 调用的信息,它运行 cls 程序(在 Windows 上)或 clear 程序(在 macOS 和 Linux 上)。这里的奇怪语法是 Python 的条件表达式(在其他语言中也称为三元运算符)的例子。语法是 value1 if condition else value2,如果条件为 True,则评估为 value1,如果条件为 False,则评估为 value2。在我们的情况下,条件表达式在条件 os.name == 'nt'True 时评估为 'cls';否则,它评估为 'clear'。条件表达式(以及一般的一行代码)通常会产生难以阅读的代码,通常最好避免使用,但这是一个足够简单的情况。

声音和文本通知

终端程序在当今计算机提供的丰富音频出现之前就已经存在。如今,你的基于文本的程序不必保持沉默。然而,有充分的理由将声音降至最低或完全排除。当用户忙于查看其他窗口时,声音可以提供任务完成或出现问题的通知。但就像彩色文本一样,过度使用声音很容易变得令人烦恼。用户可能已经在执行涉及播放音频的任务,或者他们可能正在进行一个声音会无礼打断的在线会议。而且如果用户的计算机被静音,他们无论如何也听不到声音通知。

如果你只需要播放一个简单的音频文件,你可以使用 playsound3 第三方包。安装后,你可以通过调用 playsound3 模块的 playsound() 函数并传递 MP3 或 WAV 音频文件的文件路径来播放音频文件。从 autbor.com/hello.mp3 下载 hello.mp3 文件(或使用你自己的文件)并将以下内容输入到交互式 shell 中:

>>> import playsound3
>>> playsound3.playsound('hello.mp3') 

playsound() 函数将在音频文件播放完毕之前不会返回;也就是说,函数会 阻塞 直到音频播放完毕。请注意,如果你给它一个较长的音频文件来播放,这将暂时停止你的程序。如果 playsound() 抛出异常(如果文件名包含像等号这样的特殊字符),尝试传递音频文件的 Path 对象而不是字符串。

类似地,你可能希望限制程序产生的文本。在 Unix 命令设计哲学下,如果命令只输出相关信息,那么将一个命令的文本输出管道传输到另一个命令会更容易,因为多余的文本输出需要过滤。许多命令将它们的文本输出保持在最低限度,或者根本不输出,并通过退出码来传达成功或错误信息。(退出码在第十九章中介绍。)然而,如果你不是将输出管道传输到另一个命令,而是作为一个人类用户想要看到更多信息,许多命令接受 -v--verbose 命令行参数来启用这种 详细模式。其他命令则采取相反的方法,将输出信息淹没其中,但提供 -q--quiet 命令行参数来提供一个 静默模式,没有文本输出。(这也可以作为关闭声音通知的方法。)或者更好的做法是,将静默设置为默认行为,并让 --verbose--beep 启用声音或警报声。

如果你的程序不需要这种复杂程度,你可以忽略这个考虑。然而,一旦你开始与他人分享你的程序,他们可能会以你没有预见的方式使用它,提供这些选项将大大提高你的程序的用户友好性。

简短的命令名称

用户通常从命令行而不是通过在桌面或开始菜单上点击图标来运行基于文本的程序。这些命令有时可能难以理解。当我开始学习 Linux 操作系统时,我惊讶地发现我熟悉的 Windows copy 命令在 Linux 上被命名为 cp。名称 copy 比名称 cp 更易读。简短而晦涩的名称真的值得节省两个字符的输入吗?

随着我越来越多地使用命令行,我意识到答案是坚定的肯定。我们阅读源代码的频率比编写源代码的频率高,所以为变量和函数使用详细名称有帮助。但我们将命令输入到命令行中的频率比阅读命令的频率高,所以在这种情况下,情况正好相反:简短的命令名称使命令行更容易使用,并减轻手腕的压力。

如果你的程序是一个你每天可能会输入十几次的命令,试着为它想一个简短的名字。你可以使用 whichwhere 命令来检查该名称是否已被另一个程序使用。你还可以通过互联网搜索以该名称存在的任何现有命令。简短的名字在很大程度上有助于使其易于使用。

命令行参数

要从命令行运行程序,只需输入其名称。对于 .py Python 源代码文件,你必须运行 python(Windows)或 python3(macOS 和 Linux)程序,然后在其后提供 .py 文件名,如下所示:python yourScript.py

命令之后提供的文本片段被称为命令行参数。命令行参数以与函数调用参数相同的方式传递给命令。例如,单独的ls命令会列出当前工作目录(CWD)中的文件。但您也可以运行ls exampleFolder,此时exampleFolder命令行参数会指示ls命令列出exampleFolder文件夹中的文件。命令行参数允许您配置命令的行为。

Python 脚本可以通过sys.argv列表访问传递给 Python 解释器的命令行脚本。例如,如果您输入了python3 yourScript.py hello worldpython3程序会接收命令行参数并将它们转发到您的 Python 脚本中的sys.argv变量。sys.argv变量将包含['yourScript.py', 'hello', 'world']

注意,sys.argv中的第一个项目是 Python 脚本的文件名。其余参数由空格分隔。如果您需要在命令行参数中包含空格字符,请在运行命令时将其放在双引号内。例如,python3 yourScript.py "hello world"会将sys.argv设置为['yourScript.py', 'hello world']

命令行参数的主要用途是在启动程序之前指定各种配置。无需通过配置菜单或多步骤过程。不幸的是,这种方法意味着命令行参数可能会变得极其复杂且难以阅读。如果您在 Windows 命令后跟/?或在 macOS 或 Linux 命令后跟--help,您通常会找到一页又一页的命令行参数文档。

如果您的程序接受的命令行参数集很简单,那么直接读取sys.argv列表是最容易的。然而,随着您添加更多的命令行参数,可能的组合可能会变得难以管理。python yourScript.py spam eggs是否与python yourScript.py eggs spam做相同的事情?如果用户可以提供cheese参数或bacon参数,那么如果他们同时提供这两个参数会发生什么?这种复杂性将需要您编写大量代码来处理各种边缘情况。在这种情况下,您可能最好使用 Python 的内置argparse模块来处理这些复杂情况。argparse模块超出了本书的范围,但您可以在网上阅读其文档,网址为docs.python.org/3/library/argparse.html

剪贴板输入/输出

您不需要依赖 input() 从文件或键盘读取文本。您还可以使用剪贴板作为您 Python 程序的文本输入和输出。跨平台的 pyperclip 模块有一个 copy() 函数用于将文本放置在剪贴板上,以及一个 paste() 函数,它返回剪贴板上的文本作为字符串。Pyperclip 是一个第三方包,可以通过 pip 在终端中安装:python –m pip install pyperclip。在 Linux 上,您还必须运行 sudo apt install xclip 以使 Pyperclip 能够工作。有关完整说明,请参阅附录 A。

所有的剪贴板 I/O 程序都将遵循这种基本设计:

1.  导入 pyperclip 模块。

2.  调用 pyperclip.paste() 以从剪贴板获取输入文本。

3.  对文本执行一些操作。

4.  通过传递给 pyperclip.copy() 来将结果复制到剪贴板。

第八章中“向 Wiki 标记添加项目符号”的项目是这类程序的例子。一旦您按照第 275 页“部署 Python 程序”中的说明部署了该程序,这种程序的设计就变得特别有用。只需突出显示输入文本,按 CTRL-C 复制它,然后运行程序。结果将出现在剪贴板上,随时可以粘贴。

在本章的后面部分,我们将探讨两个项目,即 ccwd 命令和剪贴板记录器,它们都使用了剪贴板。

使用 Bext 添加彩色文本

您可以使用基于 Jonathan Hartley 的 Colorama 包构建的第三方 Bext 包来打印彩色文本。按照附录 A 中的说明使用 pip 安装 Bext。Bext 只在从终端窗口运行的程序中工作,而不是在 Mu 或大多数其他代码编辑器中。要使 print() 产生彩色文本,请调用 fg()bg() 函数,使用字符串参数(如 'black''red''green''yellow''blue''magenta''purple''cyan''white')来更改(前景)文本颜色或背景颜色。您还可以传递 'reset' 来将颜色改回终端窗口的默认颜色。例如,将以下内容输入到交互式 shell 中:

>>> import bext
>>> bext.fg('red')
>>> print('This text is red.')
This text is red.
>>> bext.bg('blue')
>>> print('Red text on blue background is an ugly color scheme.')
Red text on blue background is an ugly color scheme.
>>> bext.fg('reset')
>>> bext.bg('reset')
>>> print('The text is normal again. Ah, much better.')
The text is normal again. Ah, much better. 

请记住,用户可能已将他们的终端窗口设置为浅色模式或深色模式,因此无法确定终端的默认外观是黑色文字在白色背景上还是白色文字在黑色背景上。您在使用颜色时也应有限制:过多的颜色可能会使您的程序看起来俗气或难以阅读。

Bext 也具备一些有限的 TUI(文本用户界面)类似功能,包括以下内容:

bext.clear() 清除屏幕

bext.width() bext.height() 分别返回终端窗口的当前宽度(以列为单位)和高度(以行为单位)

bext.hide() bext.show() 分别隐藏和显示光标

bext.title(text) 将终端窗口的标题栏更改为文本字符串

bext.goto(x, y) 将光标移动到终端中的列 x 和行 y,其中 0, 0 是左上角位置

bext.get_key() 等待用户按下任意键,然后返回描述该键的字符串

bext.get_key()函数视为input()的单键版本。返回的字符串包括'a''z''5',但也包括像'left''f1''esc'这样的键。TAB 键和 ENTER 键分别返回'\t''\n'。在交互式 shell 中调用bext.get_key()来测试各种键并查看它们的返回值。

要演示 Bext 能做什么,请运行 ASCII Art 鱼缸程序的源代码,inventwithpython.com/projects/fishtank.py。首先,这个程序使用bext.clear()清除终端窗口中的所有文本。接下来,程序调用bext.goto()定位光标,并使用bext.fg()更改文本颜色,然后打印出由文本字符如><)))*>组成的各种鱼。这个程序在我的书《Python 小项目大全书》(No Starch Press,2021 年)中有介绍。

终端清除

如果你想让你的程序移除运行之前留下的任何文本,bext.clear()函数非常有用。你还可以用它来做翻页风格的动画:调用clear()清除终端,然后使用print()调用填充文本,使用 Python 的time.sleep()暂停片刻,然后重复。有一个 Python 的一行代码(用于执行特殊技巧的单行代码)可以清除屏幕,你可以将其放置在自己的clear()函数中:

import os
def clear():
    os.system('cls' if os.name == 'nt' else 'clear') 

这段代码允许你的程序清除终端屏幕,无需安装 Bext 包,并且仅在从终端运行的 Python 脚本中有效,不适用于 Mu 或其他代码编辑器。os.system()调用,你将在第十一章中了解更多,运行cls程序(在 Windows 上)或clear程序(在 macOS 和 Linux 上)。这里奇怪的语法是 Python 的条件表达式(在其他语言中也称为三元运算符)的一个例子。语法是value1 if condition else value2,如果条件为True则返回value1,如果条件为False则返回value2。在我们的例子中,条件表达式在条件os.name == 'nt'True时返回'cls';否则返回'clear'。条件表达式(以及一般的一行代码)通常会产生难以阅读的代码,通常最好避免,但这个例子足够简单。

声音和文本通知

在今天计算机提供的丰富音频之前,就已经存在了终端程序。今天,你的基于文本的程序不必是静音的。然而,有很好的理由将声音保持在最低限度或完全排除。当用户忙于查看其他窗口时,声音可以提供任务完成或发生问题的通知。但是,就像彩色文本一样,过度使用声音很容易变得令人烦恼。用户可能已经在执行涉及播放音频的任务,或者他们可能在进行一个声音会无礼打断的在线会议。而且如果用户的计算机被静音,他们无论如何也听不到声音通知。

如果你只需要播放一个简单的音频文件,你可以使用 playsound3 第三方包。一旦安装,你可以通过调用 playsound3 模块的 playsound() 函数并传递 MP3 或 WAV 音频文件的文件路径来播放音频文件。从 autbor.com/hello.mp3 下载 hello.mp3 文件(或使用你自己的文件)并将以下内容输入到交互式 shell 中:

>>> import playsound3
>>> playsound3.playsound('hello.mp3') 

playsound() 函数将在音频文件播放完毕后返回;也就是说,该函数会阻塞直到音频播放完成。请记住,如果你给它一个较长的音频文件来播放,这将暂时停止你的程序。如果 playsound() 抛出异常(如果文件名包含像等号这样的奇字符时会发生这种情况),尝试传递音频文件的 Path 对象而不是字符串。

同样,你可能希望限制你的程序产生的文本。在 Unix 命令设计哲学下,如果命令只输出相关信息,那么将一个命令的文本输出管道传输到另一个命令会更容易,因为多余的文本输出将不得不被过滤。许多命令将它们的文本输出保持在最低限度,或者完全没有,并通过退出代码来传达成功或错误。(退出代码在第十九章中介绍。)然而,如果你不是将输出管道传输到另一个命令,而是作为一个人类用户想要看到更多信息,许多命令接受 -v--verbose 命令行参数来启用这种详细模式。其他命令采取相反的方法,将输出信息淹没,但提供 -q--quiet 命令行参数来提供一个静音模式,没有文本输出。(这也可以作为静音声音通知的方法。)或者更好的是,让静音成为默认行为,并让 --verbose--beep 启用声音或警报声。

如果你的程序不需要这种复杂程度,你可以忽略这个考虑。然而,一旦你开始与他人分享你的程序,他们可能会以你没有预见的方式使用它,提供这些选项将大大提高你的程序的用户友好性。

短程序:暴风雪

让我们创建一个基于文本的雪花动画。我们的程序使用填充单个字符单元格上半部分、下半部分和整个字符的块文本字符。这些文本字符分别通过chr(9600)chr(9604)chr(9608)作为字符串返回,我们的程序将它们存储在常量TOPBOTTOMFULL中,使我们的代码更易于阅读。

将以下代码输入名为snowstorm.py的文件中:

import os, random, time, sys

TOP    = chr(9600)  # Character 9600 is '▀'
BOTTOM = chr(9604)  # Character 9604 is '▄'
FULL   = chr(9608)  # Character 9608 is '█'

# Set the snowstorm density to the command line argument:
DENSITY = 4  # Default snow density is 4%
if len(sys.argv) > 1:
    DENSITY = int(sys.argv[1])

def clear():
    os.system('cls' if os.name == 'nt' else 'clear')

while True:
    clear()  # Clear the terminal window.

    # Loop over each row and column:
    for y in range(20):
        for x in range(40):
            if random.randint(0, 99) < DENSITY:
                # Print snow:
                print(random.choice([TOP, BOTTOM]), end='')
 else:
                # Print empty space:
                print(' ', end='')
        print()  # Print a newline.

    # Print the snow-covered ground:
    print(FULL * 40 + '\n' + FULL * 40)
    print('(Ctrl-C to stop.)')

    time.sleep(0.2)  # Pause for a bit. 

首先,程序导入osrandomsystime模块。这些模块在 Python 标准库中,不需要安装任何第三方包。然后,程序使用chr()的返回值设置常量TOPBOTTOMFULL。程序使用这些常量名称是因为它们比数字 9600、9604 和 9608 更容易理解。

用户可以通过提供命令行参数来指定雪暴的密度。如果没有提供命令行参数,则sys.argv被设置为['snowstorm.py'],程序将DENSITY保留为4。但如果用户用,比如说,python snowstorm.py 20运行程序,那么sys.argv将被设置为['snowstorm.py', '20'],程序将更新DENSITYint(sys.argv[1]),即20。用户将能够修改雪暴的行为,而无需更改源代码。

在一个无限while循环内部,这个程序首先使用cls/clear单行命令清除屏幕。接下来,它使用两个嵌套的for循环遍历终端上的 40×20 空间中的每一行和每一列。(你可以增加或减少这些数字来改变雪花的大小。)在每一行和每一列,程序打印一个字符:要么是一个随机选择的TOPBOTTOM字符来表示雪花,要么是一个空格字符。(默认情况下,只有四分之一的字符不是空格。)这些print()调用传递了end=''关键字参数,这样print()就不会在每次调用后自动打印换行符。程序通过在完成一行后调用不带参数的print()来打印这个换行符。

在嵌套的for循环之后,程序打印两行 40 个FULL字符来表示地面,并提醒用户可以按 CTRL-C 停止程序。所有这些代码产生一个雪花动画的“帧”,然后time.sleep(0.2)短暂地保持这个帧,在执行循环回清除终端并从头开始整个过程之前。

我选择雪花动画是因为它有趣而不是实用,就像一个基于终端的雪花球。这种技术的更有用的应用是创建一个仪表盘应用程序:一个在终端窗口中运行的程序,你将其保持打开状态以便一目了然地传达信息。这个程序打印相关信息,然后清除屏幕并每秒、每分钟、每小时或其他间隔重新打印更新后的信息。

使用 PyMsgBox 弹出消息框

当设计你程序的完整 GUI 需要学习整个代码库,如 Tkinter、wxPython 或 PyQt 时,你可以使用 PyMsgBox 包给你的程序添加小的 GUI 消息框。这是一个第三方包,你可以通过在终端运行 pip install pymsgbox 来安装。PyMsgBox 允许你使用 Tkinter 创建对话框,Tkinter 是 Python 在 Windows 和 macOS 上自带的一个库。在 Ubuntu Linux 上,你必须首先通过在终端运行 sudo apt install python3-tk 来安装 Tkinter。附录 A 有完整的说明。

PyMsgBox 有与 JavaScript 的消息框功能名称相似的函数:

pymsgbox.alert(text) 显示一个文本消息,直到用户点击确定,然后返回字符串 'OK'

pymsgbox.confirm(text) 显示一个文本消息,直到用户点击确定或取消,然后返回 'OK''Cancel'

pymsgbox.prompt(text) 显示一个文本消息以及一个文本字段,然后返回用户输入的文本作为字符串或如果他们点击取消则返回None

pymsgbox.password(text) 与 pymsgbox.prompt() 相同,但用户输入的文本被星号掩盖

这些函数只有在用户点击确定、取消或 X(关闭)时才会返回。如果你的程序只需要偶尔的通知或用户输入,使用 PyMsgBox 的对话框可能是print()input()的合适替代。

部署 Python 程序

当你的 Python 程序完成后,你可能不想每次执行它时都运行 Mu,加载.py文件,然后点击运行按钮。本节解释了如何部署你的 Python 程序,以便你可以尽可能少地使用按键来运行它。

确保遵循第 261 页上的“PATH 环境变量”步骤,将 Scripts 文件夹添加到你的 PATH 环境变量中。由于我的用户名是 al,在 Windows 上的路径是 C:\Users\al\Scripts,在 macOS 上是 /Users/al/Scripts,在 Linux 上是 /home/al/Scripts。此外,你还需要为你的 Python 脚本设置一个虚拟环境,如下所述。

Windows

在 Windows 上,你可以通过同时按下 Windows 键和 R 键(或者右键点击开始菜单按钮并选择运行)来打开运行对话框。这会打开一个类似一次性终端的小窗口:在其中,你可以运行单个命令。要从这里运行你的 Python 脚本,你需要执行以下操作:

  1. yourScript.py Python 脚本放在你的 Scripts 文件夹中。

  2. 在你的 Scripts 文件夹中创建一个 yourScript.bat 批处理文件来运行 Python 脚本。

批处理文件包含可以一起运行的终端命令,通过运行批处理文件来执行。它们有一个.bat文件扩展名,类似于 Linux 上的 shell 脚本或 macOS 上的.command脚本。如果你在PATH文件夹中放置一个名为 yourScript.bat 的批处理文件,你可以通过输入 yourScript 来从运行对话框中运行它。在 Windows 上,运行.bat.exe文件时不需要输入文件扩展名。

批处理文件的内容是纯文本,就像 .py 文件一样,所以你可以使用 Mu 或记事本这样的文本编辑器来创建批处理文件。批处理文件每行包含一个命令。要将位于文件夹 C:\Users\al\Scripts 中的 yourScript.py 文件运行,创建一个名为 yourScript.bat 的文件,并包含以下内容:

@call %HOMEDRIVE%%HOMEPATH%\Scripts\.venv\Scripts\activate.bat
@python %HOMEDRIVE%%HOMEPATH%\Scripts\yourScript.py %*
@pause
@deactivate 

批处理文件可以命名为任何名字,但如果它与 Python 脚本同名,则更容易记住。这个批处理文件运行三个命令。第一个命令激活了你为 Scripts 文件夹创建的虚拟环境。开头的 @ 符号使得命令本身不会在终端窗口中显示。%HOMEDRIVE% 环境变量是 'C:',而 %HOMEPATH% 环境变量是你家文件夹的路径,例如 '\Users\al'。(在 Windows 上,波浪号 ~ 不表示家文件夹。)当结合使用时,无论用户名是什么,都提供了虚拟环境激活脚本的路径。(如果你需要与同事共享这些文件以便他们在自己的电脑上运行,这很有帮助。)请注意,call 是必要的;如果批处理文件(如 yourScript.bat)在没有 call 的情况下运行另一个批处理文件(如 activate.bat),则第一个批处理文件的其余命令将不会运行。

接下来,批处理文件运行 python.exe,然后运行 yourScript.py%* 使得批处理文件将接收到的任何命令行参数转发到你的 Python 程序。始终包含 %* 是一个好主意,以防你以后要向 Python 程序添加命令行参数。

第三个命令运行 pause 命令,这会导致 Windows 显示 按任意键继续 并等待用户按下键。这防止了在 Python 程序完成后终端窗口立即关闭,以便你可以看到任何剩余的打印输出。如果你的程序没有打印输出,你可以省略这一行。最后,@deactivate 行在从终端运行此批处理文件且在 Python 程序完成后终端窗口仍然打开的情况下,会关闭虚拟环境。

在设置好批处理文件后,现在你可以通过按下 Windows 键 + R 键组合来打开运行对话框,并输入 yourScript(后跟任何命令行参数)来运行 yourScript.bat 脚本。或者,如果你已经打开了一个终端窗口,你可以在任何文件夹中直接在终端中输入 yourScript。这比从 Mu 这样的代码编辑器中运行它要快得多。

如果你创建了其他 Python 脚本,你可以重用这个批处理文件。只需复制文件并更改 yourScript.py 文件名为新 Python 脚本的名字。其他所有内容都可以保持不变。

macOS

在 macOS 上,同时按下 COMMAND 键和空格键会打开 Spotlight,允许你输入要运行的程序名称。要将你自己的 Python 脚本添加到 Spotlight,你必须执行以下操作:

  1. yourScript.py Python 脚本放置在你的 Scripts 文件夹中。

  2. 创建一个名为 yourScript.command 的文本文件来运行 Python 脚本。

  3. 运行 chmod u+x yourScript.commandyourScript.command 文件添加执行权限。

一旦你的 .py Python 脚本位于你的 Scripts 文件夹中,例如 /Users/al/Scripts,在 Scripts 文件夹中创建一个名为 yourScript.command 的文本文件,内容如下:

source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/yourScript.py
deactivate 

~ 代表主文件夹,例如 /Users/al。第一行激活虚拟环境,第二行使用虚拟环境的 Python 安装运行 Python 脚本。

最后,在终端中,cd~/Scripts 并运行命令 chmod u+x yourScript.command。这添加了执行权限,这样你就可以从 Spotlight 运行脚本。现在,你可以通过按 ⌘-空格键并输入 yourScript.command 来快速运行 Python 脚本。(Spotlight 应该在你输入前几个字符后自动完成全名。)你还可以通过在终端中输入 yourScript.command 来从终端运行你的 Python 脚本。

yourScript.command 文件是必需的,因为如果你尝试从 Spotlight 运行 yourScript.py 文件,Spotlight 会看到 .py 文件扩展名,并假设你想要在 Mu 或其他代码编辑器中打开此文件,而不是直接运行它。

注意,如果你使用 macOS 的 TextEdit 创建 yourScript.command 文件,请确保通过按 SHIFT-⌘-T(或点击“格式”和“制作纯文本”菜单项)将其设置为纯文本文件。TextEdit 还会试图提供帮助,自动将 python3 转换为 Python3,这会导致 Spotlight 出错。

不幸的是,Spotlight 没有让用户将命令行参数传递给 Python 脚本的方法。任何命令行参数都必须预先写入 .command 文件中。

Ubuntu Linux

Ubuntu Linux Dash 可以通过按 Windows 键并输入你想要运行的程序名称来调出。要将你的 Python 脚本添加到 Dash,你必须执行以下操作:

  1. yourScript.py Python 脚本放置在你的 Scripts 文件夹中。

  2. 创建一个名为 yourScript 的 shell 脚本来激活虚拟环境并运行你的 Python 脚本。

  3. 运行 chmod u+x yourScript 命令为 shell 脚本添加执行权限。

  4. ~/.local/share/applications 文件夹中创建一个 yourScript.desktop 文件,以便从 Dash 运行 shell 脚本。

一旦你的 .py Python 脚本位于你的 Scripts 文件夹中,例如 /home/al/Scripts,在 Scripts 文件夹中创建一个名为 yourScript(无文件扩展名)的文本文件,内容如下:

#!/usr/bin/env bash
source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/yourScript.py
read -p "Press any key to continue..." -n1 –s
deactivate 

~ 代表主文件夹,例如 /Users/al。第一行将此文件标识为 shell 脚本。此文件不需要 .sh 扩展名。

第二行激活虚拟环境,第三行使用虚拟环境的 Python 安装运行 Python 脚本。read命令会导致终端显示按任意键继续并等待用户按下一个键。这可以防止 Python 程序完成后终端窗口立即关闭,以便您可以看到任何剩余的打印输出。如果您的程序没有打印输出,您可以省略此行。

创建此 shell 脚本后,使用cd命令进入*~/Scripts*目录,并运行命令chmod u+x yourScript。这将添加执行权限,以便您可以运行它。此时,您也可以通过在终端中输入yourScript来从终端运行您的 Python 脚本。要从 Dash 运行 Python 脚本,您必须创建一个*yourScript.desktop*文件。

在 Mu 或 gedit 等其他文本编辑器中,创建一个包含以下内容的*yourScript.desktop*文件:

[Desktop Entry]
Name=yourScript
Exec=gnome-terminal -- /home/al/Scripts/yourScript
Type=Application 

将此文件保存到*/home/al/.local/share/applications*文件夹中(将*al*替换为您自己的用户名)作为*yourScript.desktop*。请注意,Exec字段要求您输入/home/al;在此文件中不能使用~来替换该值。如果您的文本编辑器没有显示.local文件夹(因为以点开头的文件夹被视为隐藏),请在“保存文件”对话框中按 CTRL-H 来显示隐藏文件。

现在,您可以通过按下 Windows 键调出 Dash 并输入*yourScript*来快速运行 Python 脚本。Dash 应该会为您自动完成完整名称。*yourScript.desktop*中的Name字段中的*yourScript*文本将在 Dash 中显示,可以是任何名称,但将其与*yourScript.py*相同会更方便。

接下来,让我们利用本章中的原则创建两个程序,并部署它们以便于使用。

Windows

在 Windows 上,您可以通过同时按下 Windows 键和 R 键(或右键单击开始菜单按钮并选择运行)来调出运行对话框。这会打开一个类似一次性终端的小窗口:在其中,您可以运行单个命令。要从这里运行 Python 脚本,您需要执行以下操作:

  1. *yourScript.py* Python 脚本放置在您的*Scripts*文件夹中。

  2. 在您的*Scripts*文件夹中创建一个*yourScript.bat*批处理文件来运行 Python 脚本。

批处理文件包含可以一起运行的终端命令。它们具有.bat文件扩展名,类似于 Linux 上的 shell 脚本或 macOS 上的.command脚本。如果您将名为*yourScript.bat*的批处理文件放置在PATH文件夹中,您可以通过在运行对话框中输入yourScript来运行它。在 Windows 上,您不需要输入文件扩展名来运行.bat.exe文件。

批处理文件的内容是纯文本,就像 .py 文件一样,因此你可以使用 Mu 或记事本等文本编辑器创建批处理文件。批处理文件每行包含一个命令。要将位于文件夹 C:\Users\al\Scripts 中的 yourScript.py 文件运行,创建一个名为 yourScript.bat 的文件,并包含以下内容:

@call %HOMEDRIVE%%HOMEPATH%\Scripts\.venv\Scripts\activate.bat
@python %HOMEDRIVE%%HOMEPATH%\Scripts\yourScript.py %*
@pause
@deactivate 

批处理文件可以命名为任何名字,但如果它与 Python 脚本同名则更容易记住。这个批处理文件运行三个命令。第一个命令激活了你为 Scripts 文件夹创建的虚拟环境。开头的 @ 符号使得命令本身不会在终端窗口中显示。%HOMEDRIVE% 环境变量是 'C:',而 %HOMEPATH% 环境变量是你家文件夹的路径,例如 '\Users\al'。(在 Windows 上,波浪号 ~ 不表示家文件夹。)结合使用,这提供了无论用户名是什么的虚拟环境激活脚本的路径。(如果你与同事共享这些文件以便他们在自己的电脑上运行,这很有帮助。)请注意,call 是必要的;如果一个批处理文件(如 yourScript.bat)在没有 call 的情况下运行另一个批处理文件(如 activate.bat),则第一个批处理文件的其余命令将不会运行。

接下来,批处理文件运行 python.exe,然后运行 yourScript.py%* 使得批处理文件将接收到的任何命令行参数转发到你的 Python 程序。始终包含 %* 是一个好主意,以防你以后要向 Python 程序添加命令行参数。

第三个命令运行 pause 命令,这会导致 Windows 显示 按任意键继续 并等待用户按下键。这防止了在 Python 程序完成后终端窗口立即关闭,以便你可以看到任何剩余的打印输出。如果你的程序没有打印输出,你可以省略这一行。最后,@deactivate 行在从终端运行此批处理文件且在 Python 程序完成后终端窗口仍然打开的情况下,将取消虚拟环境的激活。

设置好批处理文件后,现在你可以通过按下 Windows 键 + R 键组合来打开运行对话框,并输入 yourScript(后跟任何命令行参数)来运行 yourScript.bat 脚本。或者,如果你有一个终端窗口已打开,你可以在任何文件夹中直接在终端中输入 yourScript。这比从 Mu 等代码编辑器中运行它要快得多。

如果你创建了其他 Python 脚本,你可以重用这个批处理文件。只需复制文件并更改 yourScript.py 文件名为新 Python 脚本的名字。其他内容可以保持不变。

macOS

在 macOS 上,同时按下 COMMAND 键和空格键会打开 Spotlight,允许你输入要运行的程序名称。要将你自己的 Python 脚本添加到 Spotlight,你必须执行以下操作:

  1. yourScript.py Python 脚本放置在你的 Scripts 文件夹中。

  2. 创建一个名为 yourScript.command 的文本文件来运行 Python 脚本。

  3. 运行 chmod u+x yourScript.commandyourScript.command 文件添加执行权限。

一旦你的 .py Python 脚本位于你的 Scripts 文件夹中,例如 /Users/al/Scripts,在 Scripts 文件夹中创建一个名为 yourScript.command 的文本文件,并包含以下内容:

source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/yourScript.py
deactivate 

~ 代表主文件夹,例如 /Users/al。第一行激活虚拟环境,第二行使用虚拟环境的 Python 安装运行 Python 脚本。

最后,在终端中,使用 cd 命令进入 ~/Scripts 并运行命令 chmod u+x yourScript.command。这添加了执行权限,以便你可以从 Spotlight 运行脚本。现在,你可以通过按 ⌘-空格键并输入 yourScript.command 来快速运行 Python 脚本。(Spotlight 应该在你输入前几个字符后自动完成全名。)你还可以通过在终端中输入 yourScript.command 来从终端运行你的 Python 脚本。

需要 yourScript.command 文件,因为如果你尝试从 Spotlight 运行 yourScript.py 文件,Spotlight 会看到 .py 文件扩展名,并假设你想在 Mu 或其他代码编辑器中打开此文件,而不是直接运行它。

注意,如果你使用 macOS 的 TextEdit 创建 yourScript.command 文件,请确保通过按 SHIFT-⌘-T(或点击“格式”和“制作纯文本”菜单项)将其设置为纯文本文件。TextEdit 还会尝试提供帮助,自动将 python3 转换为 Python3,这会导致 Spotlight 出错。

不幸的是,Spotlight 没有让用户将命令行参数传递给 Python 脚本的方法。任何命令行参数都必须预先写入 .command 文件中。

Ubuntu Linux

通过按 Windows 键并输入你想运行的程序名称,可以调出 Ubuntu Linux Dash。要将你的 Python 脚本添加到 Dash,你必须执行以下操作:

  1. yourScript.py Python 脚本放入你的 Scripts 文件夹。

  2. 创建一个名为 yourScript 的 shell 脚本,用于激活虚拟环境并运行你的 Python 脚本。

  3. 运行 chmod u+x yourScript 命令为 shell 脚本添加执行权限。

  4. ~/.local/share/applications 文件夹中创建一个 yourScript.desktop 文件,以便从 Dash 运行 shell 脚本。

一旦你的 .py Python 脚本位于你的 Scripts 文件夹中,例如 /home/al/Scripts,在 Scripts 文件夹中创建一个名为 yourScript 的文本文件(不带文件扩展名),并包含以下内容:

#!/usr/bin/env bash
source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/yourScript.py
read -p "Press any key to continue..." -n1 –s
deactivate 

~ 代表主文件夹,例如 /Users/al。第一行将此文件标识为 shell 脚本。此文件不需要 .sh 文件扩展名。

第二行激活虚拟环境,第三行使用虚拟环境的 Python 安装运行 Python 脚本。read命令会使终端显示按任意键继续并等待用户按下键。这可以防止 Python 程序执行完毕后终端窗口立即关闭,以便您可以看到任何剩余的打印输出。如果您的程序没有打印输出,您可以省略此行。

创建此 shell 脚本后,使用cd命令进入*~/Scripts*并运行命令chmod u+x yourScript。这添加了执行权限,以便您可以运行它。此时,您也可以通过在终端中输入yourScript来从终端运行您的 Python 脚本。要从 Dash 运行您的 Python 脚本,您必须创建一个*yourScript.desktop*文件。

在 Mu 或 gedit 等其他文本编辑器中,创建一个包含以下内容的*yourScript.desktop*文件:

[Desktop Entry]
Name=yourScript
Exec=gnome-terminal -- /home/al/Scripts/yourScript
Type=Application 

将此文件保存到*/home/al/.local/share/applications*文件夹(将al替换为您自己的用户名)作为*yourScript.desktop*。请注意,Exec字段要求您拼写/home/al;您不能在此文件中将值替换为~。如果您的文本编辑器没有显示.local文件夹(因为以点开头的文件夹被视为隐藏文件夹),请在保存文件对话框中按 CTRL-H 以显示隐藏文件。

现在,您可以通过按下 Windows 键调出 Dash 并输入*yourScript*来快速运行 Python 脚本。Dash 应该会为您自动完成全名。*yourScript*文本将出现在*yourScript.desktop*Name字段中,可以是任何名称,但将其与*yourScript.py*相同会更方便。

接下来,让我们根据本章的原则创建两个程序,并部署它们以便于使用。

简短程序:复制当前工作目录

虽然pwd命令在 macOS 和 Linux 上会打印当前工作目录,但有时将此值复制到剪贴板以便粘贴到其他地方很有用。例如,在 Windows 上,我经常发现自己从终端复制当前工作目录,以便可以在保存文件对话框中粘贴它以在该目录中保存文件。虽然我可以使用鼠标从 Windows 提示符中选择当前工作目录进行复制(或者在 macOS 和 Linux 上运行pwd命令以打印工作目录并选择要复制的文本),但这需要几个步骤,而这本可以是一个一步完成的过程。

我的想法是编写一个名为*ccwd*(代表*copy current working directory*)的程序。首先,我将在 Windows 上输入where ccwd,在 macOS 和 Linux 上输入which ccwd,以确保当前没有具有相同名称的命令,然后也许还会快速进行互联网搜索以确认。ccwd这个名字足够短,易于输入,同时也足够独特。

作为额外功能,假设终端的当前工作目录设置为 C:\Users\al\Scripts,但我想复制到剪贴板的是 C:\Users\al。我可以简单地运行 cd .. 命令,然后运行 ccwd,然后运行 cd Scripts 返回到 C:\Users\al\Scripts。但如果有能力将相对文件路径作为命令行参数传递给 ccwd 会更方便。例如,ccwd .. 当当前工作目录为 C:\Users\al\Scripts 时,会将 C:\Users\al 复制到剪贴板。您不必指定此命令行参数(如果没有提供,程序默认为当前工作目录),但如果用户需要,它作为功能是可用的。这些微小的改进可能看起来微不足道,但在线零售商在其网站上提供“一键购买”功能,因为他们知道对便利性的微小改进可以产生重大影响。

我们将使用 Pyperclip 包来处理剪贴板,所以请确保将其安装到 Scripts 文件夹的虚拟环境中。在 Mu 中创建一个新文件,并输入以下内容:

import pyperclip, os, sys
if len(sys.argv) > 1:
    os.chdir(sys.argv[1])
pyperclip.copy(os.getcwd()) 

将此程序保存为 ccwd.py 到您家目录下的 Scripts 文件夹中。

第一行导入程序所需的模块。第二行检查程序是否接收到了任何命令行参数。记住,sys.argv 是一个列表,它始终至少包含一个字符串:脚本名称 'ccwd.py'。如果它包含多个字符串,我们知道用户向程序提供了命令行参数。在这种情况下,第三行会更改程序当前的工作目录。请注意,每个程序都有自己的当前工作目录设置,使用 os.chdir() 更改此设置不会改变运行程序的终端的当前工作目录。最后,第四行将当前工作目录复制到剪贴板。

程序已结束,但要从终端运行它,我们必须输入其完整路径:例如 python C:\Users\al\Scripts\ccwd.py。这需要输入很多内容,并且违背了快速轻松地将当前工作目录复制到剪贴板的脚本的目的。为了改进这个过程,让我们回顾一下在每个操作系统上部署此程序的步骤。

Windows

将 Python 文件保存为 C:\Users\al\Scripts\ccwd.py(将 al 替换为您的用户名)。在相同的 Scripts 文件夹中,创建一个包含以下内容的 ccwd.bat 文件:

@call %HOMEDRIVE%%HOMEPATH%\Scripts\.venv\Scripts\activate.bat
@python %HOMEDRIVE%%HOMEPATH%\Scripts\ccwd.py %*
@deactivate 

这个批处理文件没有 @pause 行,因为它没有 print() 输出。现在您可以从任何文件夹的终端中运行此程序,只需运行 ccwd

C:\Users\al>ccwd
C:\Users\al> 

到目前为止,'C:\Users\al' 已被复制到剪贴板中。

macOS

将 Python 文件保存为 /Users/al/Scripts/ccwd.py(将 al 替换为您的用户名)。在相同的 Scripts 文件夹中,创建一个名为 ccwd.command 的文本文件,并包含以下内容:

source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/ccwd.py
deactivate 

然后,在终端中切换到 Scripts 文件夹,并运行 chmod u+x ccwd.command:

al@Als-MacBook-Pro ~ % cd ~/Scripts
al@Als-MacBook-Pro Scripts % chmod u+x ccwd.command 

现在,您可以从任何文件夹的终端中运行此程序,只需运行 ccwd.command:

al@Als-MacBook-Pro ~ % ccwd.command
al@Als-MacBook-Pro ~ % 

此时,'/Users/al' 应该在剪贴板上。

Ubuntu Linux

将 Python 文件保存为 /home/al/Scripts/ccwd.py(将 al 替换为你的用户名)。在相同的 Scripts 文件夹中,创建一个名为 ccwd 的文本文件,内容如下:

#!/usr/bin/env bash
source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/ccwd.py
deactivate 

这个 ccwd 脚本不需要 read -p "Press any key to continue..." -n1 –s 这一行,因为它只会在终端中运行,而不是从 Dash 中运行,并且在运行 Python 脚本后终端窗口不会消失。

然后,在终端中切换到 Scripts 文件夹并运行 chmod u+x ccwd

al@al-VirtualBox:~$ cd ~/Scripts
al@al-VirtualBox:~/Scripts$ chmod u+x ccwd 

你现在可以从任何文件夹的终端中运行此程序,只需运行 ccwd

al@al-VirtualBox:~$ ccwd
al@al-VirtualBox:~$ 

此时,'/home/al' 应该在剪贴板上。

Windows

将 Python 文件保存为 C:\Users\al\Scripts\ccwd.py(将 al 替换为你的用户名)。在相同的 Scripts 文件夹中,创建一个 ccwd.bat 文件,内容如下:

@call %HOMEDRIVE%%HOMEPATH%\Scripts\.venv\Scripts\activate.bat
@python %HOMEDRIVE%%HOMEPATH%\Scripts\ccwd.py %*
@deactivate 

这个批处理文件没有 @pause 行,因为它没有 print() 输出。你现在可以从任何文件夹的终端中运行此程序,只需运行 ccwd

C:\Users\al>ccwd
C:\Users\al> 

此时,'C:\Users\al' 在剪贴板上。

macOS

将 Python 文件保存为 /Users/al/Scripts/ccwd.py(将 al 替换为你的用户名)。在相同的 Scripts 文件夹中,创建一个名为 ccwd.command 的文本文件,内容如下:

source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/ccwd.py
deactivate 

然后,在终端中切换到 Scripts 文件夹并运行 chmod u+x ccwd.command

al@Als-MacBook-Pro ~ % cd ~/Scripts
al@Als-MacBook-Pro Scripts % chmod u+x ccwd.command 

你现在可以从任何文件夹的终端中运行此程序,只需运行 ccwd.command

al@Als-MacBook-Pro ~ % ccwd.command
al@Als-MacBook-Pro ~ % 

此时,'/Users/al' 应该在剪贴板上。

Ubuntu Linux

将 Python 文件保存为 /home/al/Scripts/ccwd.py(将 al 替换为你的用户名)。在相同的 Scripts 文件夹中,创建一个名为 ccwd 的文本文件,内容如下:

#!/usr/bin/env bash
source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/ccwd.py
deactivate 

这个 ccwd 脚本不需要 read -p "Press any key to continue..." -n1 –s 这一行,因为它只会在终端中运行,而不是从 Dash 中运行,并且在运行 Python 脚本后终端窗口不会消失。

然后,在终端中切换到 Scripts 文件夹并运行 chmod u+x ccwd

al@al-VirtualBox:~$ cd ~/Scripts
al@al-VirtualBox:~/Scripts$ chmod u+x ccwd 

你现在可以从任何文件夹的终端中运行此程序,只需运行 ccwd

al@al-VirtualBox:~$ ccwd
al@al-VirtualBox:~$ 

此时,'/home/al' 应该在剪贴板上。

短小程序:剪贴板记录器

假设你的部分工作是复制网页上链接的 URL 并将其粘贴到电子表格中。(在第十三章中,你将学习如何抓取网页 HTML 源代码中的所有链接。但假设你只需要复制其中的一些,并且必须根据具体情况决定哪些链接。)你可以按照以下步骤操作:

  1. 在网页浏览器中右键点击一个链接。

  2. 从上下文菜单中选择复制链接或复制链接地址项。

  3. 切换到电子表格应用程序。

  4. 按下 CTRL-V 粘贴链接。

  5. 切换回网页浏览器。

这是一个无聊的任务,尤其是如果页面有几十或几百个链接时。让我们创建一个小型的剪贴板记录程序来加快这个过程。我们将在这个计算机上部署这个程序,以便在需要时方便地运行它。我们的程序将监控剪贴板,查看是否有新文本被复制到其中,如果是的话,它将打印到终端屏幕上。这样,我们可以将我们的五步过程简化为两步过程:

  1. 在网页浏览器中右键单击一个链接。

  2. 从上下文菜单中选择“复制链接”或“复制链接地址”项。

然后,用户可以直接从剪贴板记录器的终端窗口复制所有文本,并一次性粘贴到电子表格中。将以下内容输入到文件编辑器中,并保存为 cliprec.py:

import pyperclip, time

print('Recording clipboard... (Ctrl-C to stop)')
previous_content = ''
try:
    while True:
        content = pyperclip.paste()  # Get clipboard contents.

        if content != previous_content:
            # If it's different from the previous, print it:
            print(content)
            previous_content = content

        time.sleep(0.01)  # Pause to avoid hogging the CPU.
except KeyboardInterrupt:
    pass 

让我们看看这个程序的每个部分,从开始部分开始:

import pyperclip, time

print('Recording clipboard... (Ctrl-C to stop)')
previous_content = '' 

这个程序会从剪贴板复制和粘贴文本,因此我们需要导入 pyperclip 模块。我们还将导入 time 模块以使用其 sleep() 函数。程序会显示一条快速消息,说明它正在运行,并提醒用户按 CTRL-C 会停止程序。程序将通过跟踪一个名为 previous_content 的变量来了解剪贴板内容的变化,该变量最初被设置为空字符串。

try:
    while True:
        content = pyperclip.paste()  # Get clipboard contents. 

程序的大部分代码存在于一个无限 while 循环中,而这个循环本身又位于一个 try 块内。当用户按下 CTRL-C 时,Python 会引发一个 KeyboardInterrupt 异常,导致执行跳转到源代码底部的 except 块。

这个循环持续监控剪贴板的内容,并记录每次用户将新文本复制到剪贴板上的情况。在循环中,第一步是通过调用 pyperclip.paste() 来收集剪贴板上的文本。

 if content != previous_content:
            # If it's different from the previous, print it:
            print(content)
            previous_content = content 

如果剪贴板上的当前内容与之前的内容不同,则程序会打印当前内容并将 previous_content 更新为 content。这样为下一次用户将新文本复制到剪贴板上的情况设置了循环。

 time.sleep(0.01)  # Pause to avoid hogging the CPU.

如果剪贴板内容与之前获取的剪贴板内容相同,我们的程序可以什么都不做。然而,这个程序可以轻松地每秒执行这个循环数万次,而且用户不太可能那么频繁地更新剪贴板。(如果他们尝试这样做,可能会把键盘上的 CTRL 和 C 键用坏。)为了防止程序通过尽可能快地运行这个无生产力的循环而占用 CPU,我们引入了 0.01 秒的延迟,这样循环每秒只检查剪贴板更新 100 次。

except KeyboardInterrupt:
    pass 

程序的最后部分是 except 子句,它使用了 Python 的 pass 语句。这个语句实际上什么也不做,但 Python 预期在 except 语句之后的块中至少有一行。这就是 pass 语句被创建的原因。当用户按下 CTRL-C 时,执行将移动到这个 except 子句,并继续到程序的末尾,然后终止。

使用此应用程序运行时,用户可以复制多个内容,而无需在应用程序之间来回切换。像这样的小程序可以使您的工作流程更加便捷,尤其是如果您每天都要做这项工作。现在让我们在每个操作系统上部署这个程序。

Windows

将 Python 文件保存为 C:\Users\al\Scripts\cliprec.py(将 al 替换为您的用户名)。在同一个 Scripts 文件夹中,创建一个 cliprec.bat 文件,内容如下:

@call %HOMEDRIVE%%HOMEPATH%\Scripts\.venv\Scripts\activate.bat
@python %HOMEDRIVE%%HOMEPATH%\Scripts\cliprec.py %*
@pause
@deactivate 

您现在可以从终端运行此程序,或者同时按 Windows 键和 R 键打开运行对话框,然后输入 cliprec

macOS

将 Python 文件保存为 /Users/al/Scripts/cliprec.py(将 al 替换为您的用户名)。在同一个 Scripts 文件夹中,创建一个名为 cliprec .command 的文本文件,内容如下:

source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/cliprec.py
deactivate 

然后,在终端中,切换到 Scripts 文件夹并运行 chmod u+x cliprec.command:

al@Als-MacBook-Pro ~ % cd ~/Scripts
al@Als-MacBook-Pro Scripts % chmod u+x cliprec.command 

您现在可以通过按 ⌘-空格键调出 Spotlight 并输入 cliprec.command 来运行此程序。

Ubuntu Linux

将 Python 文件保存为 /home/al/Scripts/cliprec.py(将 al 替换为您的用户名)。在同一个 Scripts 文件夹中,创建一个名为 cliprec 的文本文件,内容如下:

#!/usr/bin/env bash
source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/cliprec.py
read -p "Press any key to continue..." -n1 –s
deactivate 

然后,在终端中,切换到 Scripts 文件夹并运行 chmod u+x cliprec:

al@al-VirtualBox:~$ cd ~/Scripts
al@al-VirtualBox:~/Scripts$ chmod u+x cliprec 

最后,创建一个保存为 ~/.local/share/applications/cliprec.desktop 的文本文件,内容如下:

[Desktop Entry]
Name=Clipboard Recorder
Exec=gnome-terminal -- /home/al/cliprec
Type=Application 

您现在可以通过按 Windows 键调出 Dash 并输入 Clipboard Recorder,或者输入名称的前几个字符,让自动完成帮您完成。

Windows

将 Python 文件保存为 C:\Users\al\Scripts\cliprec.py(将 al 替换为您的用户名)。在同一个 Scripts 文件夹中,创建一个 cliprec.bat 文件,内容如下:

@call %HOMEDRIVE%%HOMEPATH%\Scripts\.venv\Scripts\activate.bat
@python %HOMEDRIVE%%HOMEPATH%\Scripts\cliprec.py %*
@pause
@deactivate 

您现在可以从终端运行此程序,或者同时按 Windows 键和 R 键打开运行对话框,然后输入 cliprec

macOS

将 Python 文件保存为 /Users/al/Scripts/cliprec.py(将 al 替换为您的用户名)。在同一个 Scripts 文件夹中,创建一个名为 cliprec .command 的文本文件,内容如下:

source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/cliprec.py
deactivate 

然后,在终端中,切换到 Scripts 文件夹并运行 chmod u+x cliprec.command:

al@Als-MacBook-Pro ~ % cd ~/Scripts
al@Als-MacBook-Pro Scripts % chmod u+x cliprec.command 

您现在可以通过按 ⌘-空格键调出 Spotlight 并输入 cliprec.command 来运行此程序。

Ubuntu Linux

将 Python 文件保存为 /home/al/Scripts/cliprec.py(将 al 替换为您的用户名)。在同一个 Scripts 文件夹中,创建一个名为 cliprec 的文本文件,内容如下:

#!/usr/bin/env bash
source ~/Scripts/.venv/bin/activate
python3 ~/Scripts/cliprec.py
read -p "Press any key to continue..." -n1 –s
deactivate 

然后,在终端中,切换到 Scripts 文件夹并运行 chmod u+x cliprec:

al@al-VirtualBox:~$ cd ~/Scripts
al@al-VirtualBox:~/Scripts$ chmod u+x cliprec 

最后,创建一个保存为 ~/.local/share/applications/cliprec.desktop 的文本文件,内容如下:

[Desktop Entry]
Name=Clipboard Recorder
Exec=gnome-terminal -- /home/al/cliprec
Type=Application 

你现在可以通过按 Windows 键打开 Dash 并输入 Clipboard Recorder 来运行此程序,或者输入名称的前几个字符,让自动完成帮你完成它。

使用 PyInstaller 编译 Python 程序

Python 通常被称为解释型语言,尽管编程语言本身既不是解释型也不是编译型。你可以为任何语言创建一个解释器或编译器。相反,用 Python 编写的程序主要是通过解释器来运行的。但使用 PyInstaller 包也可以从 Python 代码创建可执行程序,该包生成可以在命令行中运行的程序。

PyInstaller 并非直接将 Python 程序编译成机器码;相反,它创建了一个包含 Python 解释器和你的脚本的可执行程序。因此,这些程序通常相当大。即使是使用 PyInstaller 编译的简单“Hello, world”程序,其大小也可能接近 8MB,实际上比汇编语言编写的版本大一千倍。然而,编译你的 Python 程序的好处是,你可以与他人分享你的程序,即使他们没有安装 Python。你将能够发送给他们一个可执行文件。

你可以通过运行 pip install pyinstaller 来安装 PyInstaller。你必须在你希望可执行程序运行的操作系统上运行 PyInstaller。也就是说,如果你在 Windows 上,PyInstaller 可以创建一个 Windows 可执行程序,但不能创建 macOS 或 Linux 程序,反之亦然。

从终端运行以下命令(在 macOS 和 Linux 上使用 python3 而不是 python)来编译名为 yourScript.py 的 Python 脚本:

C:\Users\al>python -m PyInstaller --onefile yourScript.py
378 INFO: PyInstaller: `X`.`X`.`X`
378 INFO: Python: 3.`XX`.`XX`
392 INFO: Platform: Windows-`XX`-`XX`.`X`.`XXXX`
393 INFO: wrote C:\Users\al\Desktop\hello-test\hello.spec
399 INFO: UPX is not available.
# --snip--
11940 INFO: Appending PKG archive to EXE
11950 INFO: Fixing EXE headers
13622 INFO: Building EXE from EXE-00.toc completed successfully. 

注意,你必须用大写的 P 和大写的 I 输入 PyInstaller,否则你会得到“没有模块名为 pyinstaller”的错误信息。另外,请注意,--onefile 参数有两个短横线。

运行 PyInstaller 后,将会有一个 build 文件夹(你可以删除它)和一个 dist 文件夹。dist 文件夹包含可执行程序。你不需要为它创建虚拟环境。然后你可以将这个程序复制到其他计算机上,或者将其作为电子邮件附件发送。请注意,作为安全预防措施,许多电子邮件提供商可能会阻止包含可执行程序的电子邮件。

这里提供的说明适用于基本的 Python 程序。pyinstaller.org 上的在线文档包含更多详细信息。

摘要

在本章中,您学习了如何将程序从代码编辑器中取出并部署,以便用户可以快速方便地运行它们。您还学习了更多关于如何设计具有基于文本的用户界面而不是更现代的图形界面的程序的指南。虽然 GUI 用户友好,但 TUIs(文本用户界面)更容易编写。当您需要为自己自动化任务时,使程序看起来像专业应用程序并不值得额外的努力。您只需要一个能工作的东西。

话虽如此,您有几种方法可以使程序易于使用。通常,这涉及到命令行终端,许多用户对此不太熟悉。学习命令行概念可能需要一段时间,例如使用cddir/ls导航文件系统,PATH环境变量和命令行参数。但终端允许您非常快速地发出命令和运行程序,尤其是在您完成编写程序并部署它们之后。

本章还介绍了几个第三方包。Bext 包允许您添加彩色文本,定位光标,并清除屏幕。PyMsgBox 包创建用于警报或基本输入的 GUI 框,而不使用终端窗口。由于您可能有一天会运行需要同一包不兼容版本的程序,因此最好在单独的虚拟环境中运行脚本。您可以使用 Python 附带的venv模块创建虚拟环境。虚拟环境从终端激活,并可以通过为您提供一个单独的安装包的地方来防止您破坏现有的程序。

最后,PyInstaller 包允许您将.py文件编译成可执行程序。这些程序可能大小为几个兆字节,但您可以与可能没有 Python(以及程序使用的第三方包)的同事共享这些程序。

本章没有涵盖许多编程语言概念;相反,它涵盖了如何使程序在日常使用中易于使用和方便。到目前为止,您已经掌握了足够的 Python 语法来创建基本程序(尽管总有更多东西要学习!)。在本书的第二部分,您将探索几个扩展 Python 程序功能的第三方包。

实践问题

1.  在 Windows 上,哪个命令可以列出文件夹内容?在 macOS 和 Linux 上又是哪个命令?

2.  PATH环境变量包含什么内容?

3.  __file__变量包含什么内容?

4.  在 Windows 上,哪个命令可以清除终端窗口中的文本?在 macOS 和 Linux 上又是哪个命令?

5.  您如何创建一个新的虚拟环境?

6.  在编译程序时,您应该传递给 PyInstaller 的命令行参数是什么?

实践程序:使您的程序可部署

通过在PATH文件夹中创建执行它们的 shell 脚本,或者使用 PyInstaller 编译它们,使现有的程序易于运行。为以下项目执行此操作:

  • “将文件夹备份到 ZIP 文件”来自第十一章

  • “从大型文档中提取联系信息”来自第九章

  • “在维基标记中添加项目符号”来自第八章

  • “交互式棋盘模拟器”来自第七章

  • 你创建的任何其他程序,想要轻松启动或与他人分享

posted @ 2026-02-06 10:27  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报