2.10. 解决问题的思路

解决问题的思路

我们已经探索了 Python 的多个部分,从现在开始我们要着眼于把它们糅合在一起工作,我们要用它们来设计并且写出可以很好的解决某些事的程序。也就是我们要学习如何以你自己的方式写 Python。

第一滴血

现在我们想要解决这个问题:

写一个可以备份我所有重要文件的程序。

尽管这个问题看起来简单,但仅仅是看叙述还不足以让我们开始解决它。我们需要一些小小的 分析。比如:我们如何界定 重要 文件的?怎么 进行存储啊? 我们要放到 哪里 呀?

适当的分析过后,我们就要开始设计我们的程序了。一般要列出我们要做的事情。要解决上面这个问题,我列了以下几个想要做的事。如果你做了同样的事,得出的东西可能不一样,因为我们每个人都有自己的思维方式,所以不同也完全 Ok:

  • 要备份的文件和目录放在一个列表中。
  • 备份必须存放在一个主要的备份目录中。
  • 备份的文件需要打包进 zip里。
  • zip 文件的名字是当前的日期和时间。
  • 使用标准的 zip 命令作为默认打包方式,这样可以运行在所有标准的 GNU/Linux 或 Unix 发行版上。(你可以使用任何提供了命令行接口的归档命令)

对于 Windows 用户的解决方案

Windows 用户可以从 GnuWin32 project page 处 安装 zip 命令行,之后把 C:\Program Files\GnuWin32\bin 放到系统 PATH 环境变量里即可,和 what we did for recognizing the python command itself 里讲的一样。

解决方案

随着程序的设计意图逐渐清晰合理,我们就可以写出我们的解决方案。

新建 backup_ver1.py:

import os
import time

# 1. 将要备份的文件和目录分配到一个列表中
# Windows 例子
# source = ['"C:\\My Documents"']
# Mac OS X 和 Linux 例子:
source = ['/Users/swa/notes']
# 注意如果名字中包含空格我们需要用复数引号
# 或者用 raw 字符串  [r'C:\My Documents'].

# 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

现在我们处在 测试 阶段,这个阶段我们要做的是测试我们的程序是否运行良好。如果程序不能如预期般运行,那我们需要进入 调试 阶段,需要修复一些 bugs (错误)。

如果上面的程序不能在你的电脑上运行,复制上面 Zip command is 后面那一行的输出,然后在你的 shell 中粘贴,看看是否有报错如果有请尝试解决它。同时也查看一下 zip 命令行手册看看可能是什么导致的出错。如果命令行运行正确,要注意上面的程序只能由 Python 运行,确认下是否准确的由 Python 执行了它。

我们是怎么编写出来的?

你注意到我们是怎么一步步从 设计 变成 代码 的了吗?

我们利用 ostime 模块,所以首先把它们导入。之后,我们把要备份的文件和目录指定到 source 列表中。我们存储备份的目标目录就放到 target_dir 变量里。我们要创建的 zip 归档文件的名字就用当前日期+时间,利用 time.strftime() 函数可以完成。同样我们也指定了 .zip 扩展名,之后会被放到 target_dir 目录中。

注意 os.sep 变量 - 它会根据你的操作系统变换目录分隔符,比如 GNU/Linux,Unix,macOS 下是 '/', Windows 下就会是 '\\'。使用 os.sep 代替直接用某个分割符可以让我们的程序具有移植性,也就是可能跨所有平台使用。

time.strftime() 函数需要占位符,比如我们上面程序中写的那些。 %Y 会替换为当前的年份。 %m 则是月份(01~12)。完整的占位符可以在这里找到 Python 参考手册.

我们使用可以 连接 字符串的额外操作符创建目标 zip 文件,它所起的作用也是连接这些字符串并且返回一个新的。之后我们创建 包含我们将要执行的命令的 zip_command 字符串。之后我们可以检测下这条命令是否可以在 shell 中运行(GNU/Linux 终端或 DOS 提示符)。

我们使用的 zip 命令其实有些额外的选项,其中一个选项是 -r-r 选项表示 zip 命令应该以递归的方式创建目录,也就会包含所有在给定目录中的子目录及文件。选项后面跟着的是我们要创建的归档文件名,在之后跟着的是要备份的列表。同样我们用字符串的 join 方法把 source 列表转换成字符串,这个我们之前用过了。

接下来我们终于要 运行 命令了,os.system 函数可以让命令如在 系统 中运行一样。如果命令运行正常那么会返回 0,否则返回的是错误代码。

根据命令的执行结果,我们打印出合适的信息来查看备份成功了还是失败了。

这就是我们目前为止所做的事了,我们成功的创建了一个可以备份我们重要文件的脚本!

Windows 用户注意事项
除了使用双反斜杠转义分隔符,同样可以用 raw 字符串。比如 'C:\\Documents'r'C:\Documents' 都是可以的,但 不要C:\Documents 因为这样会变成你用了一个不知道是什么的转义字符 \D

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

上面的程序运行良好,但(通常)第一个程序并不能非常准确的契合预期。比如,如果没有做良好的设计可能会出问题,或在写代码时出了个错等等。遇到这种情况的话你就不得不重返 设计 阶段或不得不进入 调试 阶段。

修改版

第一版的脚本可以正常工作,但我们可以对它进行一些优化,让脚本能更好满足我们的日常需求。这称作软件的 维护 阶段。

我认为备份文件的命名规则是一处值得改进的地方——使用 时间 作为文件名,而将 日期 作为目录名,文件夹都放在主备份目录下。这样做的一个优点是备份文件分层存放,便于管理。第二个优点是备份文件的文件名更短了。第三个优点是每天独立的目录让你很容易就能知道今天是否进行了备份。因为只有完成了今天的备份,目录才会被建立。

另存为 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')
# 当前时间作为备份文件的文件名
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_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 的名字上体现出来。而且也很容易实现,只要把用于提供的名字也附到 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')

# 让用户输出一个用于创建 zip 文件的名字。
comment = input('Enter a comment --> ')
# 检查是否有 comment。
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 报告了一个语法错误,表示脚本中存在不能让 Python 满意的语法结构。当我们审视 Python 给出的错误时,就会发现它已经给出错误发生的地方。所以我们要在那一行开始 调试

仔细观察后,就可以发现原本是一整行的逻辑链被我们分成了两行但并没指定它们是一起的。本质上说,Python 发现了额外操作符 + 但并无任何可操作的逻辑行,所以就不知道该怎么继续了。记住,如果我们要指定一个逻辑行需要与下一行接壤,我们可以在最后的部分使用一个反斜杠。Ok,现在知道怎么修复了吧。当我们发现了错误并最终矫正程序的做法我们就称之为 修复 bug

第四版

另存为 backup_ver4.py

import os
import time

# 1. 将要备份的文件和目录分配到一个列表中
# Windows 例子
# source = ['"C:\\My Documents"']
# Mac OS X 和 Linux 例子:
source = ['/Users/swa/notes']
# 注意如果名字中包含空格我们需要用复数引号
# 或者用 raw 字符串  [r'C:\My Documents'].

# 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)  # make directory

# 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 选项使 zip 命令具有 交互 级别,让你的程序更有交互性;或者增加一个 -q 选项,让程序能静默运行。

另一个可能的改进是允许通过命令行指定额外需要备份的文件和目录。我们可以通过 sys.argv 列表获得这些名字,然后用 list 类的 extend 方法把他们加到 source 列表里。

最重要的改进可能是不使用 os.system 来创建备份,而用内置的 zipfile 和 tarfile 模块来创建备份。它们是标准库的一部分,你可以直接使用它们,而不需要额外的 zip 程序依赖。

我在上面的示例程序中使用 os.system 纯粹是出于教学需要,因为这样示例足够简单,每个人都能理解,同时也足够真实有用。

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

软件开发过程

现在我们已经经历了编写软件的不同 阶段。这些阶段可以概括成以下环节:

  1. 需求(分析需求)
  2. 规格(确定规格)
  3. 编写(编写代码)
  4. 测试(测试与调试)
  5. 使用(操作或部署)
  6. 维护(优化与重构)

上面编写备份脚本的过程是一种推荐的编写程序的流程:一开始进行需求分析和系统设计,然后实现一个简单的版本,对它进行测试,有 bug 就进行调试。接着使用它,确保它能像设计的那样正常工作。再增加你想要的新功能,并继续重复编写-测试-使用的循环,直到软件实现预期的功能。

记住:

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

小结

我们已经学会了如何编写自己的 Python 脚本程序,并知道了编写程序的不同阶段。你可能已经发现使用本章的方法编写程序非常方便,你也因此能更加熟练的使用 Python 去解决问题。

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

本文章首发在 LearnKu.com 网站上。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~