问题解决

我们已经探索了 Python 语言的各个部分,现在我们将通过设计和编写一个真正有用的程序,来看看所有这些部分是如何组合在一起的。目的是学会如何独立编写 Python 脚本。

问题

我们要解决的问题如下:

我想要一个程序,能为我所有的重要文件创建备份。

虽然这是一个简单的问题,但信息还不够充分,无法直接开始解决。我们需要进行一些分析。例如,我们如何指定哪些文件需要备份?它们如何存储?存储在哪里

在对问题进行充分分析之后,我们设计程序。我们列出程序应该如何工作的要点。在这个例子中,我列出了希望它如何工作的清单。如果你来做设计,可能不会得出相同的分析结果,因为每个人的做事方式不同,这完全没问题。

  • 需要备份的文件和目录在一个列表中指定。
  • 备份必须存储在一个主备份目录中。
  • 文件被备份到一个 zip 文件中。
  • zip 压缩包的名称是当前的日期和时间。
  • 我们使用任何标准 GNU/Linux 或 Unix 发行版中默认可用的标准 zip 命令。请注意,你可以使用任何你想要的归档命令,只要它有命令行接口。

Windows 用户注意

Windows 用户可以从 GnuWin32 项目页面安装 zip 命令,并将 C:\Program Files\GnuWin32\bin 添加到系统的 PATH 环境变量中,类似于我们之前识别 python 命令时所做的那样

解决方案

由于我们的程序设计现在已经比较稳定,我们可以编写代码,即解决方案的实现

保存为 backup_ver1.py

import os
import time

# 1. The files and directories to be backed up are
# specified in a list.
# Example on Windows:
# source = ['"C:\\My Documents"']
# Example on Mac OS X and Linux:
source = ['/Users/swa/notes']
# Notice we have to use double quotes inside a string
# for names with spaces in it.  We could have also used
# a raw string by writing [r'C:\My Documents'].

# 2. The backup must be stored in a
# main backup directory
# Example on Windows:
# target_dir = 'E:\\Backup'
# Example on Mac OS X and Linux:
target_dir = '/Users/swa/backup'
# Remember to change this to which folder you will be using

# 3. The files are backed up into a zip file.
# 4. The name of the zip archive is the current date and time
target = target_dir + os.sep + \
         time.strftime('%Y%m%d%H%M%S') + '.zip'

# Create target directory if it is not present
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # make directory

# 5. We use the zip command to put the files in a zip archive
zip_command = 'zip -r {0} {1}'.format(target,
                                      ' '.join(source))

# Run the backup
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

现在,我们进入了测试阶段,测试程序是否正常工作。如果程序行为不符合预期,我们就需要调试程序,即从程序中去除 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、macOS 中是 '/',在 Windows 中是 '\\'。使用 os.sep 而不是直接使用这些字符,可以使我们的程序具有可移植性,在所有这些系统上都能正常工作。

time.strftime() 函数接受一个格式规范,就像我们在上面的程序中使用的那样。%Y 规范会被替换为带世纪的年份。%m 规范会被替换为 0112 之间的十进制月份数,以此类推。完整的规范列表可以在 Python 参考手册中找到。

我们使用加法运算符创建目标 zip 文件的名称,它会连接字符串,即将两个字符串连接在一起并返回一个新的字符串。然后,我们创建一个字符串 zip_command,其中包含我们要执行的命令。你可以在 shell(GNU/Linux 终端或 DOS 提示符)中运行这个命令来检查它是否有效。

我们使用的 zip 命令有一些可用选项,其中之一是 -r-r 选项指定 zip 命令应该对目录进行递归操作,即它应该包含所有子目录和文件。选项后面跟着要创建的 zip 压缩包的名称,然后是需要备份的文件和目录列表。我们使用字符串的 join 方法将 source 列表转换为字符串,这个方法我们之前已经学过如何使用了。

然后,我们最终使用 os.system 函数运行命令,该函数就像从系统中运行命令一样(即在 shell 中运行)——如果命令成功执行,它返回 0,否则返回一个错误编号。

根据命令的结果,我们打印相应的消息,表示备份失败或成功。

就是这样,我们已经创建了一个备份重要文件的脚本!

Windows 用户注意

除了使用双反斜杠转义序列外,你也可以使用原始字符串(raw string)。例如,使用 'C:\\Documents'r'C:\Documents'。但是,不要使用 'C:\Documents',因为你会使用到一个未知的转义序列 \D

现在我们有了一个可以工作的备份脚本,可以在我们想要备份文件时随时使用。这被称为软件的运行阶段或部署阶段。

上面的程序工作正常,但(通常)第一个程序不会完全按照你的预期工作。例如,如果你没有正确设计程序,或者在输入代码时犯了错误等,就可能出现问题。相应地,你需要回到设计阶段,或者需要调试你的程序。

第二版

我们的脚本第一版可以正常工作。但是,我们可以做一些改进,使它在日常使用中更加方便。这被称为软件的维护阶段。

我认为一个有用的改进是更好的文件命名机制——使用时间作为目录中的文件名,并以当前日期作为主备份目录中的子目录名。第一个好处是备份以分层方式存储,因此更容易管理。第二个好处是文件名更短。第三个好处是不同的目录可以帮助你检查是否每天都做了备份,因为只有在你当天做了备份时才会创建该目录。

保存为 backup_ver2.py

import os
import time

# 1. The files and directories to be backed up are
# specified in a list.
# Example on Windows:
# source = ['"C:\\My Documents"', 'C:\\Code']
# Example on Mac OS X and Linux:
source = ['/Users/swa/notes']
# Notice we had to use double quotes inside the string
# for names with spaces in it.

# 2. The backup must be stored in a
# main backup directory
# Example on Windows:
# target_dir = 'E:\\Backup'
# Example on Mac OS X and Linux:
target_dir = '/Users/swa/backup'
# Remember to change this to which folder you will be using

# Create target directory if it is not present
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # make directory

# 3. The files are backed up into a zip file.
# 4. The current day is the name of the subdirectory
# in the main directory.
today = target_dir + os.sep + time.strftime('%Y%m%d')
# The current time is the name of the zip archive.
now = time.strftime('%H%M%S')

# The name of the zip file
target = today + os.sep + now + '.zip'

# Create the subdirectory if it isn't already there
if not os.path.exists(today):
    os.mkdir(today)
    print('Successfully created directory', today)

# 5. We use the zip command to put the files in a zip archive
zip_command = 'zip -r {0} {1}'.format(target,
                                      ' '.join(source))

# Run the backup
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 压缩包的名称关联起来。这可以通过在 zip 压缩包的名称后面附加用户提供的注释来轻松实现。

警告:以下程序不能正常工作,所以不要惊慌,请继续阅读,因为这里有一个教训。

保存为 backup_ver3.py

import os
import time

# 1. The files and directories to be backed up are
# specified in a list.
# Example on Windows:
# source = ['"C:\\My Documents"', 'C:\\Code']
# Example on Mac OS X and Linux:
source = ['/Users/swa/notes']
# Notice we had to use double quotes inside the string
# for names with spaces in it.

# 2. The backup must be stored in a
# main backup directory
# Example on Windows:
# target_dir = 'E:\\Backup'
# Example on Mac OS X and Linux:
target_dir = '/Users/swa/backup'
# Remember to change this to which folder you will be using

# Create target directory if it is not present
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # make directory

# 3. The files are backed up into a zip file.
# 4. The current day is the name of the subdirectory
# in the main directory.
today = target_dir + os.sep + time.strftime('%Y%m%d')
# The current time is the name of the zip archive.
now = time.strftime('%H%M%S')

# Take a comment from the user to
# create the name of the zip file
comment = input('Enter a comment --> ')
# Check if a comment was entered
if len(comment) == 0:
    target = today + os.sep + now + '.zip'
else:
    target = today + os.sep + now + '_' + 
        comment.replace(' ', '_') + '.zip'

# Create the subdirectory if it isn't already there
if not os.path.exists(today):
    os.mkdir(today)
    print('Successfully created directory', today)

# 5. We use the zip command to put the files in a zip archive
zip_command = "zip -r {0} {1}".format(target,
                                      ' '.join(source))

# Run the backup
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 说有一个语法错误(syntax error),这意味着脚本不满足 Python 期望的结构。当我们观察 Python 给出的错误时,它也告诉我们检测到错误的位置。所以我们从那一行开始调试程序。

仔细观察后,我们看到一个逻辑行被拆分成了两个物理行,但我们没有指定这两个物理行属于同一个逻辑行。基本上,Python 在该逻辑行中发现了加法运算符(+)但没有任何操作数,因此它不知道如何继续。记住,我们可以通过在物理行末尾使用反斜杠来指定逻辑行在下一个物理行中继续。因此,我们对程序进行了这个修正。当我们发现错误时对程序进行的修正被称为修复 bug

第四版

保存为 backup_ver4.py

import os
import time

# 1. The files and directories to be backed up are
# specified in a list.
# Example on Windows:
# source = ['"C:\\My Documents"', 'C:\\Code']
# Example on Mac OS X and Linux:
source = ['/Users/swa/notes']
# Notice we had to use double quotes inside the string
# for names with spaces in it.

# 2. The backup must be stored in a
# main backup directory
# Example on Windows:
# target_dir = 'E:\\Backup'
# Example on Mac OS X and Linux:
target_dir = '/Users/swa/backup'
# Remember to change this to which folder you will be using

# Create target directory if it is not present
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # make directory

# 3. The files are backed up into a zip file.
# 4. The current day is the name of the subdirectory
# in the main directory.
today = target_dir + os.sep + time.strftime('%Y%m%d')
# The current time is the name of the zip archive.
now = time.strftime('%H%M%S')

# Take a comment from the user to
# create the name of the zip file
comment = input('Enter a comment --> ')
# Check if a comment was entered
if len(comment) == 0:
    target = today + os.sep + now + '.zip'
else:
    target = today + os.sep + now + '_' + \
        comment.replace(' ', '_') + '.zip'

# Create the subdirectory if it isn't already there
if not os.path.exists(today):
    os.mkdir(today)
    print('Successfully created directory', today)

# 5. We use the zip command to put the files in a zip archive
zip_command = 'zip -r {0} {1}'.format(target,
                                      ' '.join(source))

# Run the backup
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 函数检查用户是否确实输入了内容(通过检查输入的长度)。如果用户只是按了回车键而没有输入任何内容(可能只是例行备份或没有做特殊修改),那么我们按照之前的方式继续处理。

但是,如果提供了注释,那么它会附加到 zip 压缩包名称中,在 .zip 扩展名之前。注意我们将注释中的空格替换为下划线——这是因为管理不含空格的文件名要容易得多。

更多改进

第四版对大多数用户来说是一个令人满意的可用脚本,但总有改进的空间。例如,你可以通过指定 -v 选项为 zip 命令添加详细程度级别,使你的程序输出更多信息,或者使用 -q 选项使其安静

另一个可能的改进是允许在命令行中将额外的文件和目录传递给脚本。我们可以从 sys.argv 列表中获取这些名称,并使用 list 类提供的 extend 方法将它们添加到我们的 source 列表中。

最重要的改进是不使用 os.system 的方式来创建归档文件,而是使用内置的 zipfiletarfile 模块来创建这些归档文件。它们是标准库的一部分,已经可供你使用,无需依赖计算机上安装的 zip 程序。

不过,在上面的例子中,我一直使用 os.system 的方式来创建备份,纯粹是出于教学目的,这样例子足够简单,每个人都能理解,但又足够真实,具有实用价值。

你能尝试编写使用 zipfile 模块而不是 os.system 调用的第五版吗?

软件开发过程

我们现在已经经历了编写软件过程中的各个阶段。这些阶段可以总结如下:

  1. 是什么(分析)
  2. 怎么做(设计)
  3. 开始做(实现)
  4. 测试(测试和调试)
  5. 使用(运行或部署)
  6. 维护(改进)

推荐的编写程序的方法是我们在创建备份脚本时遵循的过程:先做分析和设计。从一个简单版本开始实现。测试并调试它。使用它以确保它按预期工作。然后,添加你想要的任何功能,并根据需要不断重复"做-测-用"循环。

请记住:

软件是生长出来的,不是建造出来的。 -- Bill de hÓra

小结

我们学习了如何创建自己的 Python 程序/脚本,以及编写此类程序所涉及的各个阶段。你可能会发现像我们在本章中所做的那样创建自己的程序很有用,这样你就能熟悉 Python 和问题解决。

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

results matching ""

    No results matching ""