解决问题

我们已经探索了 Python 语言中的许多部分,现在我们将通过设计并编写一款程序来了解如何把这些部分组合到一起。这些程序一定是能做到一些有用的事情。这其中的方法就是去学习如何靠你自己来编写一份 Python 脚本。

问题

我们希望解决的问题如下:

我想要一款程序来备份我所有的重要文件。

虽然这是一个简单的问题,但是其中并没有足够的信息有助于让我们开始规划一份解决方案。我们需要进行一些分析(Analysis)。例如,我们应该如何指定哪些文件是我们需要备份的?它们应该如何进行备份?储存到哪里?

在正确地分析了这些问题过后,我们便开始设计(Design)我们的程序。我们将列出一份关于我们的程序应如何运转的清单。在这个案例中,我已经编写了如下清单来说明将如何工作。如果由你来设计程序,你可能不会做出同样的分析,因为每个人都有其自己的行事方式,所以出现不同是完全正常、且正确的。

  • 需要备份的文件与目录应在一份列表中予以指定。
  • 备份必须存储在一个主备份目录中。
  • 备份文件将打包压缩成 zip 文件。
  • zip 压缩文件的文件名由当前日期与时间构成。
  • 我们使用在任何 GNU/Linux 或 Unix 发行版中都会默认提供的标准 zip 命令进行打包。在这里你需要了解到只要有命令行界面,你就可以使用任何需要用到的压缩或归档命令。

针对 Windows 用户的提示

Windows 用户可以从 GnuWin32 项目页面 上下载并安装 zip 命令,并将 C:\Program Files\GnuWin32\bin 添加至你的系统的 PATH 环境变量中,这一操作过程与我们为使系统识别 Python 命令本身所做的事情相同。

解决方案

由于我们的程序设计方案现在已经相当稳定,我们便可以开始编写代码,这个过程我们称之为实现(Implementation)我们的解决方案。

将下述代码保存为 backup_ver1.py

import os
import time

# 1. 需要备份的文件与目录将被
# 指定在一个列表中。
# 例如在 Windows 下:
# source = ['"C:\\My Documents"', 'C:\\Code']
# 又例如在 Mac OS X 与 Linux 下:
source = ['/Users/swa/notes']
# 在这里要注意到我们必须在字符串中使用双引号
# 用以括起其中包含空格的名称。

#2. 备份文件必须存储在一个
#主备份目录中
#例如在 Windows 下:
# target_dir = 'E:\\Backup'
# 又例如在 Mac OS X 和 Linux 下:
target_dir = '/Users/swa/backup'
# 要记得将这里的目录地址修改至你将使用的路径

# 3. 备份文件将打包压缩成 zip 文件。
# 4. zip 压缩文件的文件名由当前日期与时间构成。
target = target_dir + os.sep + \
         time.strftime('%Y%m%d%H%M%S') + '.zip'

# 如果目标目录还不存在,则进行创建
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # 创建目录

# 5. 我们使用 zip 命令将文件打包成 zip 格式
zip_command = 'zip -r {0} {1}'.format(target,
                                      ' '.join(source))

# 运行备份
print('Zip command is:')
print(zip_command)
print('Running:')
if os.system(zip_command) == 0:
    print('Successful backup to', target)
else:
    print('Backup FAILED')

输出:

$ python backup_ver1.py
Zip command is:
zip -r /Users/swa/backup/20140328084844.zip /Users/swa/notes
Running:
  adding: Users/swa/notes/ (stored 0%)
  adding: Users/swa/notes/blah1.txt (stored 0%)
  adding: Users/swa/notes/blah2.txt (stored 0%)
  adding: Users/swa/notes/blah3.txt (stored 0%)
Successful backup to /Users/swa/backup/20140328084844.zip

现在,我们正处于测试(Testing)阶段,在这一阶段我们测试我们的程序是否能正常工作。如果其行为不符合我们的预期,那么我们需要对我们的程序进行 Debug 工作,也就是说,移除程序中的 Bug(错误)。

如果上面的程序不能够正常工作,复制打印在 Zip command is 后面的命令,将其粘贴至 shell(在 GNU/Linux 与 Mac OS X 环境中)或 cmd(对于 Windows 环境),看看存在什么错误并尝试将其修复。同时你还需要检查 zip 命令手册来看看是不是哪里存在错误。如果这条命令成功运行,那么可能是错误可能存在在 Python 程序本身之中,因此你需要检查你的程序是否如上面所展示那番。

它是如何工作的

你会注意到我们是如何一步步将我们的设计转化为代码的。

我们首先导入 ostime 模块以准备使用它们。然后,我们在 source 列表中指定我们需要备份的文件与目录。我们需要存储我们所有备份文件的目标目录在 target_dir 变量中予以指定。我们将要创建的 zip 归档文件的名字由当前日期与时间构成,在这里通过 time.strftime() 函数来创建。文件名将以 .zip 作为扩展名,并存储在 target_dir 目录中。

在这里要注意 os.sep 变量的使用方式——它将根据你的操作系统给出相应的分隔符,在 GNU/Linux 与 Unix 中它会是 '/',在 Windows 中它会是 '\\',在 Mac OS 中它会是 ':'。使用 os.sep 而非直接使用这些字符有助于使我们的程序变得可移植,从而可以在上述这些系统中都能正常工作。

time.strftime() 函数会遵循某些格式(Specification),其中一种就如我们在上方程序中所使用的那样。%Y 将被替换成带有具体世纪的年份。%m 将会被替换成以 0112 的十进制数所表示的月份。有关这些格式的全部列表可以在 Python 参考手册中查询到。

我们使用连接(Concatenates)字符串的加法(+)运算符来创建目标 zip 文件的文件名,也就是说,它将两个字符串连接到一起并返回一个新的字符串。然后,我们创建了一串字符串 zip_command,其中包括了我们要执行的命令。如果这条命令不能正常工作,你可以把它拷贝到 Shell(GNU/Linux 终端或 DOS 提示符)中进行检查。

我们使用的 zip 命令会有一些选项与参数需要传递。-r 选项用以指定 zip 命令应该递归地Recursively)对目录进行工作,也就是说它应该包括所有的子文件夹与其中的文件。这两个选项结合到一起并可以指定一个快捷方式作 -qr。选项后面跟着的是将要创建的 zip 文件的名称,再往后是需要备份的文件与目录的列表。我们通过使用已经讨论过并已了解该如何运用的的字符串方法 join 来将列表 source 转换成字符串。

随后,我们终于可以运行这一使用了 os.system 函数的命令,这一函数可以使命令像是从系统中运行的。也就是说,从 shell 中运行的——如果运行成功,它将返回 0,如果运行失败,将返回一个错误代码。

根据命令运行的结果是成功还是失败,我们将打印出与之相应的信息来告诉你备份的结果究竟如何。

就是这样,我们便创建了一份用以备份我们的重要文件的脚本!

针对 Windows 用户的提示

除了使用双反斜杠转义序列,你还可以使用原始字符串。例如使用 'C:\\Documents'r'C:\Documents'。然而,要使用 'C:\Documents',因为它将被识别为你使用了一个未知的转义序列 \D 来结束路径的输入。

现在,我们已经拥有了一份可以正常工作的备份脚本,我们可以在任何我们需要备份文件的时候使用它。这被称作软件的操作(Operation)部署(Deployment)阶段。

上面所展示的程序能够正常工作,但是(通常)第一个程序都不会按照你所期望的进行工作。可能是因为你没有正确地设计程序,或如果你在输入代码时出现了错误。出现这些情况时,在恰当的时候,你需要回到设计阶段,或者你需要对你的程序进行 Debug 工作。

第二版

我们的第一版脚本已经能够工作了。然而,我们还可以对它作出一些改进,从而使它能够更好地在每一天都可以正常工作。我们将这一阶段称之为软件的维护(Maintenance)阶段。

我认为有一种颇为有用的改进是起用一种更好的文件命名机制——使用时间作为文件名,存储在以当前日期为名字的文件夹中,这一文件夹则照常存储在主备份目录下。这种机制的第一个有点在于你的备份会以分层的形式予以存储,从而使得它们能更易于管理。第二个优点是文件名能够更短。第三个优点在于由于只有当天进行了备份才会创建相应的目录,独立的目录能够帮助你快速地检查每天是否都进行了备份。

保存为 backup_ver2.py

import os
import time

# 1. 需要备份的文件与目录将被
# 指定在一个列表中。
# 例如在 Windows 下:
# source = ['"C:\\My Documents"', 'C:\\Code']
# 又例如在 Mac OS X 与 Linux 下:
source = ['/Users/swa/notes']
# 在这里要注意到我们必须在字符串中使用双引号
# 用以括起其中包含空格的名称。

# 2. 备份文件必须存储在一个
# 主备份目录中
# 例如在 Windows 下:
# target_dir = 'E:\\Backup'
# 又例如在 Mac OS X 和 Linux 下:
target_dir = '/Users/swa/backup'
# 要记得将这里的目录地址修改至你将使用的路径

# 如果目标目录不存在则创建目录
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # 创建目录

# 3. 备份文件将打包压缩成 zip 文件。
# 4. 将当前日期作为主备份目录下的子目录名称
today = target_dir + os.sep + time.strftime('%Y%m%d')
# 将当前时间作为 zip 文件的文件名
now = time.strftime('%H%M%S')

# zip 文件名称格式
target = today + os.sep + now + '.zip'

# 如果子目录尚不存在则创建一个
if not os.path.exists(today):
    os.mkdir(today)
    print('Successfully created directory', today)

# 5. 我们使用 zip 命令将文件打包成 zip 格式
zip_command = 'zip -r {0} {1}'.format(target,
                                      ' '.join(source))

# 运行备份
print('Zip command is:')
print(zip_command)
print('Running:')
if os.system(zip_command) == 0:
    print('Successful backup to', target)
else:
    print('Backup FAILED')

输出:

$ python backup_ver2.py
Successfully created directory /Users/swa/backup/20140329
Zip command is:
zip -r /Users/swa/backup/20140329/073201.zip /Users/swa/notes
Running:
  adding: Users/swa/notes/ (stored 0%)
  adding: Users/swa/notes/blah1.txt (stored 0%)
  adding: Users/swa/notes/blah2.txt (stored 0%)
  adding: Users/swa/notes/blah3.txt (stored 0%)
Successful backup to /Users/swa/backup/20140329/073201.zip

它是如何工作的

程序的大部分都保持不变。有所改变的部分是我们通过 os.path.exists 函数来检查主文件目录中是否已经存在了以当前日期作为名称的子目录。如果尚未存在,我们通过 os.mkdir 函数来创建一个。

第三版

第二版在我要制作多份备份时能够正常工作,但当备份数量过于庞大时,我便很难找出备份之间有什么区别了。例如,我可能对我的程序或者演示文稿做了重大修改,然后我想将这些修改与 zip 文件的文件名产生关联。这可以通过将用户提供的注释内容添加到文件名中来实现。

预先提醒:下面给出的程序将不会正常工作,所以不必惊慌,只需跟着案例去做因为你要在里面学上一课。

保存为 backup_ver3.py

import os
import time

# 1. 需要备份的文件与目录将被
# 指定在一个列表中。
# 例如在 Windows 下:
# source = ['"C:\\My Documents"', 'C:\\Code']
# 又例如在 Mac OS X 与 Linux 下:
source = ['/Users/swa/notes']
# 在这里要注意到我们必须在字符串中使用双引号
# 用以括起其中包含空格的名称。

# 2. 备份文件必须存储在一个
# 主备份目录中
# 例如在 Windows 下:
# target_dir = 'E:\\Backup'
# 又例如在 Mac OS X 和 Linux 下:
target_dir = '/Users/swa/backup'
# 要记得将这里的目录地址修改至你将使用的路径

# 如果目标目录还不存在,则进行创建
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # 创建目录

# 3. 备份文件将打包压缩成 zip 文件。
# 4. 将当前日期作为主备份目录下的
# 子目录名称
today = target_dir + os.sep + time.strftime('%Y%m%d')
# 将当前时间作为 zip 文件的文件名
now = time.strftime('%H%M%S')

# 添加一条来自用户的注释以创建
# zip 文件的文件名
comment = input('Enter a comment --> ')
# 检查是否有评论键入
if len(comment) == 0:
    target = today + os.sep + now + '.zip'
else:
    target = today + os.sep + now + '_' +
        comment.replace(' ', '_') + '.zip'

# 如果子目录尚不存在则创建一个
if not os.path.exists(today):
    os.mkdir(today)
    print('Successfully created directory', today)

# 5. 我们使用 zip 命令将文件打包成 zip 格式
zip_command = "zip -r {0} {1}".format(target,
                                      ' '.join(source))

# 运行备份
print('Zip command is:')
print(zip_command)
print('Running:')
if os.system(zip_command) == 0:
    print('Successful backup to', target)
else:
    print('Backup FAILED')

输出:

$ python backup_ver3.py
  File "backup_ver3.py", line 39
    target = today + os.sep + now + '_' +
                                        ^
SyntaxError: invalid syntax

它是如何(不)工作的

这个程序它跑不起来!Python 会说程序之中存在着语法错误,这意味着脚本并未拥有 Python 期望看见的结构。当我们观察 Python 给出的错误时,会看见它同时也告诉我们它检测到错误的额地方。所以我们开始从那个地方开始对我们的程序进行 Debug 工作。

仔细观察,我们会发现有一独立的逻辑行被分成了两行物理行,但我们并未指定这两行物理行应该是一起的。基本上,Python 已经发现了该逻辑行中的加法运算符(+)没有任何操作数,因此它不知道接下来应当如何继续。因此,我们在程序中作出修正。当我们发现程序中的错误并对其进行修正时,我们称为“错误修复(Bug Fixing)”

第四版

保存为 backup_ver4.py

import os
import time

# 1. 需要备份的文件与目录将被
# 指定在一个列表中。
# 例如在 Windows 下:
# source = ['"C:\\My Documents"', 'C:\\Code']
# 又例如在 Mac OS X 与 Linux 下:
source = ['/Users/swa/notes']
# 在这里要注意到我们必须在字符串中使用双引号
# 用以括起其中包含空格的名称。

# 2. 备份文件必须存储在一个
# 主备份目录中
# 例如在 Windows 下:
# target_dir = 'E:\\Backup'
# 又例如在 Mac OS X 和 Linux 下:
target_dir = '/Users/swa/backup'
# 要记得将这里的目录地址修改至你将使用的路径

# 如果目标目录还不存在,则进行创建
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # 创建目录

# 3. 备份文件将打包压缩成 zip 文件。
# 4. 将当前日期作为主备份目录下的
# 子目录名称
today = target_dir + os.sep + time.strftime('%Y%m%d')
# 将当前时间作为 zip 文件的文件名
now = time.strftime('%H%M%S')

# 添加一条来自用户的注释以创建
# zip 文件的文件名
comment = input('Enter a comment --> ')
# 检查是否有评论键入
if len(comment) == 0:
    target = today + os.sep + now + '.zip'
else:
    target = today + os.sep + now + '_' + \
        comment.replace(' ', '_') + '.zip'

# 如果子目录尚不存在则创建一个
if not os.path.exists(today):
    os.mkdir(today)
    print('Successfully created directory', today)

# 5. 我们使用 zip 命令将文件打包成 zip 格式
zip_command = 'zip -r {0} {1}'.format(target,
                                      ' '.join(source))

# 运行备份
print('Zip command is:')
print(zip_command)
print('Running:')
if os.system(zip_command) == 0:
    print('Successful backup to', target)
else:
    print('Backup FAILED')

输出:

$ python backup_ver4.py
Enter a comment --> added new examples
Zip command is:
zip -r /Users/swa/backup/20140329/074122_added_new_examples.zip /Users/swa/notes
Running:
  adding: Users/swa/notes/ (stored 0%)
  adding: Users/swa/notes/blah1.txt (stored 0%)
  adding: Users/swa/notes/blah2.txt (stored 0%)
  adding: Users/swa/notes/blah3.txt (stored 0%)
Successful backup to /Users/swa/backup/20140329/074122_added_new_examples.zip

它是如何工作的

现在程序可以正常工作了!让我们来回顾一下我们在第三版中所作出的实际的增强工作。我们使用 input 函数来接受用户的注释内容,并通过 len 函数来检查输入内容的长度,以检查用户是否确实输入了什么内容。如果用户未输入任何内容而直接敲下了 enter 键(也许这份备份只是一份例行备份而没作出什么特殊的修改),那么我们将继续我们以前所做的工作。

不过,如果用户输入了某些注释内容,那么它将会被附加进 zip 文件的文件名之中,处在 .zip 扩展名之前。在这里需要注意的是我们用下划线替换注释中的空格——这是因为管理没有空格的文件名总会容易得多。

继续改进

第四版程序已经是一份对大多数用户来说都能令人满意地工作运行的脚本了,不过总会有改进的余地在。例如,你可以在程序中添加 -v 选项来指定程序的显示信息的详尽1程度,从而使你的程序可以更具说服力,或者是添加 -q 选项使程序能静默(Quiet)运行。

另一个可以增强的方向是在命令行中允许额外的文件与目录传递到脚本中。我们可以从 sys.argv 列表中获得这些名称,然后我们可以通过list 类提供的 extend 方法把它们添加到我们的 source 列表中.

最重要的改进方向是不使用 os.system 方法来创建归档文件,而是使用 zipfiletarfile 内置的模块来创建它们的归档文件。这些都是标准库的一部分,随时供你在你的电脑上没有 zip 程序作为没有外部依赖的情况下使用这些功能。

不过,在上面的例子中,我一直都在使用 os.system 这种方式作为创建备份的手段,这样就能保证案例对于所有人来说都足够简单同时也确实有用。

你可以试试编写第五版脚本吗?在脚本中使用 zipfile 模块而非 os.system 调用。

软件开发流程

我们已经经历了开发一款软件的流程中的各个阶段(Phases)。现在可以将这些阶段总结如下:

  1. What/做什么(分析)
  2. How/怎么做(设计)
  3. Do It/开始做(执行)
  4. Test/测试(测试与修复错误)
  5. Use/使用(操作或开发)
  6. Maintain/维护(改进)

编写程序时推荐的一种方式是遵循我们在编写备份脚本时所经历的步骤:进行分析与设计;开始实现一个简单版本;测试并修复错误;开始使用以确保工作状况皆如期望那般。现在,你可以添加任何你所希望拥有的功能,并继续去重复这一“开始做—测试—使用”循环,需要做多少次就去做多少次。

要记住:

程序是成长起来的,不是搭建出来的。 (Software is grown, not built.) ——Bill de hÓra

总结

我们已经看到了如何创建我们自己的 Python 程序与脚本,也了解了编写这些程序需要经历的数个阶段。或许你会发现我们在本章中学习的内容对于编写你自己的程序很有帮助,这样你就能慢慢习惯 Python,同样包括它解决问题的方式。

接下来,我们将讨论面向对象编程。


1. 原文作 Verbosity,沈洁元译本译作“交互”。