在之前的章节中,编写的程序只能操作较少的信息,这些信息往往是以字符串的形式直接写在代码中的。但本章中的程序可以对整个文件进行加密和解密,文件的大小可以包括成千上万个字符。
对文件进行置换操作的程序只对纯文本(无格式文本)文件进行加/解密,这类文件指的是那些后缀名为 .txt 且文件中不包含除文本数据以外的内容的文件。要编写这类文件,可以选择在windows系统下使用Notepad、在macOS系统下使用TextEdit,或者在linux系统下使用gedit。(word这样的文本处理程序同样也可以生成纯文本文件,但记住这些文件不能保存字体样式、字体大小、颜色或其他任何格式。)除上述文本编辑软件外,读者甚至可以使用IDLE文本编辑器,只要将文件后缀保存为 .txt 而不是通常使用的 .py 即可。
如果需要纯文本文件的样例,则可以从网络上下载一些txt小说,要将纯文本手动输入程序中,可能要花费很多时间,但如果使用现成的txt文件,则程序在数秒内就可以完成加密操作。
在前两章置换密码测试程序的基础上,针对文件的置换密码程序引入了transposition Encrypt.py和transpositionDecrypt.py这两个文件,这样就可以调用encryptMessage()和decryptMessage() 这两个函数。因此,编写这个新程序将不用重新输入两个函数的代码。
选中 File▶New File,打开一个新的编辑窗口,将下列代码输入编辑窗口并将其存储为transpositionFileCipher.py。接下来,访问本书配套资源下载一个名为frankenstein.txt的文件,并将其放置在与py文件相同的路径之下,按下F5键运行这个程序。
transpositionFileCipher.py
1. # 置换密码加/解密文件
2. # https://www.nostarch.com/crackingcodes/ (BSD Licensed)
3.
4. import time, os, sys, transpositionEncrypt, transpositionDecrypt
5.
6. def main():
7. inputFilename = 'frankenstein.txt'
8. # 注意,如果具有outputFilename 名称的文件已存在,则此程序
9. # 覆盖该文件
10. outputFilename = 'frankenstein.encrypted.txt'
11. myKey = 10
12. myMode = 'encrypt' # 设置为'encrypt'或'decrypt'
13.
14. # 如果输入文件不存在,则程序提前终止
15. if not os.path.exists(inputFilename):
16. print('The file %s does not exist. Quitting...' % (inputFilename))
17. sys.exit()
18.
19. # 如果输出文件已存在,则给用户退出的机会
20. if os.path.exists(outputFilename):
21. print('This will overwrite the file %s. (C)ontinue or (Q)uit?' %
(outputFilename))
22. response = input('> ')
23. if not response.lower().startswith('c'):
24. sys.exit()
25.
26. # 从输入文件中读取消息
27. fileObj = open(inputFilename)
28. content = fileObj.read()
29. fileObj.close()
30.
31. print('%sing...' % (myMode.title()))
32.
33. # 测量加/解密所需时间
34. startTime = time.time()
35. if myMode == 'encrypt':
36. translated = transpositionEncrypt.encryptMessage(myKey, content)
37. elif myMode == 'decrypt':
38. translated = transpositionDecrypt.decryptMessage(myKey, content)
39. totalTime = round(time.time() - startTime, 2)
40. print('%sion time: %s seconds' % (myMode.title(), totalTime))
41.
42. # 将置换后的消息写入输出文件
43. outputFileObj = open(outputFilename, 'w')
44. outputFileObj.write(translated)
45. outputFileObj.close()
46.
47. print('Done %sing %s (%s characters).' % (myMode, inputFilename,
len(content)))
48. print('%sed file is %s.' % (myMode.title(), outputFilename))
49.
50.
51. # 如果运行 transpositionCipherFile.py (而不是作为模块引入),则
52. # 调用main() 函数
53. if __name__ == '__main__':
54. main()
运行transpositionFileCipher.py得到的输出如下。
Encrypting...
Encryption time: 1.21 seconds
Done encrypting frankenstein.txt (441034 characters).
Encrypted file is frankenstein.encrypted.txt.
这样就创建出了一个名为frankenstein.encrypted.txt 的新文件,该文件与 transposition FileCipher.py 在同一个路径下。使用IDLE文件编辑器打开这个新文件,就可以看到frankenstein.txt 中的文本内容被加密后的结果了。它应有的格式如下所示。
PtFiyedleo a arnvmt eneeGLchongnes Mmuyedlsu0#uiSHTGA r sy,n t ys
s nuaoGeL
sc7s,
--snip--
每次加密一个文件,都可以将加密的结果发送给另一个人去解密它,对方同样需要文件置换操作程序的源代码。
要解密密文,可以对源代码进行下述改变(粗体部分),随后再次运行这个程序。
7. inputFilename = 'frankenstein.encrypted.txt'
8. # 如果具有outputFilename 名称的文件已存在,则此程序
9. # 覆盖该文件
10. outputFilename = 'frankenstein.decrypted.txt'
11. myKey = 10
12. myMode = 'decrypt' # 设置为 'encrypt'或'decrypt'
这时候运行该程序,就会在当前文件夹下创建出一个名为 frankenstein.decrypted.txt 的新文件,此时这个新文件的内容和原始明文是一致的。
在深入研究 transpositionFileCipher.py 文件的源代码之前,首先要明白Python是如何对文件进行操作的。读取文件内容的3个步骤分别是打开文件、读取文件内容并将其存储到一个变量中、关闭文件。类似地,要将新内容写入文件中时,首先必须打开(或创建)一个文件,接着将新的内容写入其中,最后关闭这个文件。
Python可以通过open()方法打开一个文件以供读取、写入内容时使用,其第一个参数为文件名。当要打开的文件和Python程序处于同一个文件夹下时,可以直接使用文件名,例如“thetimemachine.txt”,如果当前文件夹存在这么一个文件,则打开它的Python指令如下所示。
fileObj = open('thetimemachine.txt')
这样,一个文件对象就被存储在变量 fileObj 中了,之后进行读写操作时使用这个变量即可。
还可以用文件的绝对路径(absolute path)作为第一个参数,这样引号内就需要包括文件所在的文件夹及其所有父文件夹的名称,举个例子,类似“C:\Users\Al\frankenstein.txt”(Windows系统下),或“/Users/Al/frankenstein.txt”(macOS及Linux系统下)格式的都是绝对路径。记住,Windows系统下,反斜线(/)前一定要多加一个反斜线用于转义。
举个例子,若想打开“frankenstein.txt”文件,则需要将其路径以字符串的形式作为open()方法的第一个参数(绝对路径的格式由使用的操作系统决定)。
fileObj = open('C:\Users\Al\frankenstein.txt')
文件对象有多种用于读取、写入和关闭文件的方法,下面将对这些方法进行详细介绍,为方便说明这里调换一下顺序。
对于文件的加密程序而言,在读取文本内容之后就需要将加密的数据写入一个新的文件中,这时用到的方法就是write()。
要想使用一个文件对象的write()方法,首先需要将文件以写模式打开,即将字符串 'w' 传入open()方法作为其二个参数。open()方法的第二个参数是一个可选参数(optional parameter),这意味着open()方法在没有第二个参数的情况下仍然能够被调用。例如,将下列代码输入交互式运行环境中。
>>> fileObj = open('spam.txt', 'w')
这一行以写模式创建了一个名为“spam.txt”的文件,则可以对其进行编辑。如果在open()方法创建新文件的路径下存在一个同名文件,则该同名文件将被重写,因此,以写模式使用opne()方法时需要万分小心。
spam.txt 以写模式打开后,就可以调用write()方法往其中写入内容了。write()方法有一个参数:存储在一个字符串中的、将要被写入文件的内容。将下列代码输入交互式运行环境,把字符串Hello, world!写入 spam.txt 中。
>>> fileObj.write('Hello, world!')
13
上述代码将字符串Hello, world!作为参数传入write()方法,把该字符串写入文件 spam.txt 中并打印出数字13,这个数字代表了写入文件中的字符数。
对文件的操作执行完成之后,需要通过调用文件对象的close()方法告知Python此事。
>>> fileObj.close()
除上述必定会覆盖原文件内容的写模式之外,还存在一个附加模式,在该模式下字符串会被添加到文件已有内容的末尾。尽管本章程序中没有用到这个模式,读者也可以自己尝试以附加模式打开文件,只需要将字符串 'a' 作为 open() 方法的第二个参数即可。
如果在调用文件对象的write()方法时,遇到了“io.UnsupportedOperation: not readable”的报错信息,则可能是因为没有以写模式打开文件。调用open()方法的过程中若没有包括可选参数,则其默认值将被自动设置为写模式('r'),该模式下只允许使用者调用文件对象的read()方法。
read()方法能够以字符串的形式返回文件中包含的所有内容,为验证其功能,本节将读取之前用wirte()方法创建的 spam.txt 文件。在交互式运行环境中运行如下代码。
>>> fileObj = open('spam.txt', 'r')
>>> content = fileObj.read()
>>> print(content)
Hello world!
>>> fileObj.close()
打开文件之后创建的文件对象存储在变量 fileObj 中,如果该对象存在,则可以使用read()方法读取文件的内容并将其存储在变量 content 中,随后打印该变量的值。执行完上述对文件对象的操作后,使用close()方法关闭该文件。
如果遇到“IOError: [Errno 2] No such file or directory”的报错信息,请确保想要打开的文件就在读者认为的路径下,并再次检查文件名和文件夹的名称是否正确输入。(文件夹即路径。)
在transpositionFileCipher.py程序中,对文件进行的加密和解密需要用到上文提到的所有open()、write() 及 close()方法。
transpositionFileCipher.py 程序的第一部分应该看起来十分眼熟,第4行是一个import 语句,引入了transpositionEncypt.py和transpositionDecrypt.py两个程序和Python库中的time、os及sys模块,接下来的部分即main()函数,其中创建了程序需要用到的变量。
1. # 置换密码加/解密文件
2. # https://www.nostarch.com/crackingcodes/ (BSD Licensed)
3.
4. import time, os, sys, transpositionEncrypt, transpositionDecrypt
5.
6. def main():
7. inputFilename = 'frankenstein.txt'
8. # 注意,如果具有outputFilename 名称的文件已存在,则此程序
9. # 覆盖该文件
10. outputFilename = 'frankenstein.encrypted.txt'
11. myKey = 10
12. myMode = 'encrypt' # 设置为 'encrypt'或'decrypt'
变量 inputFilename 存储了待读取文件名的字符串,而加密后(或解密后)的内容写入以变量 outputFilename 的值命名的文件内。程序涉及的置换密码使用一个整数作为密钥,并存储在myKey中,同时,程序需要一个变量 myMode 存储字符串encrypt或decrypt以决定对 inputFilename 存储的文件进行何种操作。在读取 inputFilename 文件之前,首先要使用 os.path.exists() 检查该文件是否存在。
读取文件往往不会存在什么危害,但往文件中写入内容时就需要多加小心了,这是因为以写模式调用open()方法时,若原文件已存在,会覆盖掉原文件中的内容。针对这个潜在问题,程序可以使用os.path.exists() 方法,检查要打开的文件是否已经存在。
os.path.exists()方法只有一个参数,即文件名或指向文件的文件路径,如果文件存在,则返回True;否则返回False。该方法包含在path模块内,而path模块包含在 os 模块中,因此引入 os 模块时,path模块一并被引入了。
将下列代码输入交互式运行环境。
>>> import os
❶ >>> os.path.exists('spam.txt')
False
>>> os.path.exists('C:\Windows\System32\calc.exe') # Windows
True
>>> os.path.exists('/usr/local/bin/idle3') # macOS
False
>>> os.path.exists('/usr/bin/idle3') # Linux
False
在本例中,os.path.exists()方法证实了Windows系统中存在calc.exe文件。当然,只有在Windows系统下运行Python的时候,才能得到上面的结果。记住,在Windows下输入文件路径时,要在反斜杠前再添加一个反斜杠进行转义。如果使用的是macOS,则上述代码中只有macOS的样例会返回True,同理在Linux系统下只有最后一个例子会返回True。如果没有给出完整的路径❶,则Python会检查当前的工作路径;对IDLE交互式运行环境而言,当前工作路径即安装了Python的文件夹。
本章程序的第14~17行使用了os.path.exists()检查 inputFilename 中的文件是否存在,如果没有这一步,就无法获得用于加解密的文件。
14. # 如果输入文件不存在,则程序提前终止
15. if not os.path.exists(inputFilename):
16. print('The file %s does not exist. Quitting...' % (inputFilename))
17. sys.exit()
若文件不存在,程序将为用户弹出提示并退出。
接下来,程序需要检查是否存在与 outputFilename 同名的文件,如果存在,则询问用户是输入c继续运行程序还是输入q退出程序。由于用户可能会输入多种回复,例如c、C,甚至是单词Continue,因此程序需要确保可以接收所有这些输入,要实现这一功能,必须使用更多字符串方法。
upper()和lower()方法能够分别以全大写和全小写返回它们所接收的字符串。将下列代码输入交互式运行环境中以分辨这两个方法是如何对同一个字符串进行操作的。
>>> 'Hello'.upper()
'HELLO'
>>> 'Hello'.lower()
'hello'
lower()、upper()方法以小写和大写的形式返回字符串,title()方法也和它们类似,然而该方法返回的是各单词首字母大写的字符串,这意味着字符串中的每个单词的首字母是大写,而其余所有字母都是小写。将下列代码输入交互式运行环境中。
>>> 'hello'.title()
'Hello'
>>> 'HELLO'.title()
'Hello'
>>> 'extra! extra! man bites shark!'.title()
'Extra! Extra! Man Bites Shark!'
本章程序会在稍后部分使用title()方法,来为输出的信息格式化。
若字符串以参数指定的字符串开头,则startwith()方法返回True。将下列代码输入交互式运行环境。
>>> 'hello'.startswith('h')
True
>>> 'hello'.startswith('H')
False
>>> spam = 'Albert'
❶ >>> spam.startswith('Al')
True
startswith()方法对大小写敏感,同时也可以接收多字符的字符串❶。
endswith()方法用于检查字符串是否以某一个特定字符串结尾。将下列代码输入交互式运行环境。
>>> 'Hello world!'.endswith('world!')
True
❷ >>> 'Hello world!'.endswith('world')
False
字符串的匹配必须一字不差,注意,由于❷中缺少感叹号,因此endswith()的返回结果为False。
之前提到过,程序需要能够接收所有以字母C开头的响应,无论大小写,这意味着不管用户输入的是C、continue、c还是其他以C开头的字符串,程序都需要对文件进行重写。使用lower()和upper()方法可以使程序在处理用户输入的字符串时更加灵活。
19. # 如果输出文件已存在,则给用户退出的机会
20. if os.path.exists(outputFilename):
21. print('This will overwrite the file %s. (C)ontinue or (Q)uit?' %
(outputFilename))
22. response = input('> ')
23. if not response.lower().startswith('c'):
24. sys.exit()
第23行,取字符串的首字母并使用startswith()方法来检查它是否为C。由于startswith()方法大小写敏感且检查的是小写的 'c',因此在调用它之前使用lower()方法改变response字符串的首字母,使其保持为小写的 'c'。如果用户没有输入以C开头的响应,则if的条件语句将得到True(因为其中包含一个not),于是sys.exit()语句被调用,程序终止。从技术上来说,用户不需要输入q来退出,任何不以C开头的字符串都会导致 sys.exit() 方法的调用,从而使程序退出。
第27行,程序开始使用本章开头讨论过的文件对象方法。
26. # 从输入文件中读取消息
27. fileObj = open(inputFilename)
28. content = fileObj.read()
29. fileObj.close()
30.
31. print('%sing...' % (myMode.title()))
第27~29行打开了与inputFilename同名的文件,读取它的内容并存储到变量 content 中,随后关闭了文件。读取完文件之后,第31行为用户输出了一行提示信息,告知他们加密或解密已经开始。由于变量 myMode 中存储着字符串encrypt或decrypt,调用title()字符串方法将它的首字母转换为大写,又在它之后添加了ing字符串,因此最终它显示的内容是 Encrypting...或者Decrypting...。
对一个文件进行全面的加/解密往往要比仅加/解密一个短短的字符串要耗时多,而用户可能会想要了解加/解密文件的过程具体需要多长时间。程序可以使用 time 模块计算加/解密过程所需的时间长度。
time.time()方法以浮点数的形式返回从1970年1月1日至当前时间的总秒数,这个数字被称为UNIX时间戳。将下列代码输入交互式运行环境,观察该方法的运行结果。
>>> import time
>>> time.time()
1540944000.7197928
>>> time.time()
1540944003.4817972
由于time.time()返回的是一个浮点数,因此它可以精确到毫秒。当然,time.time()显示的时间由程序员调用它的时间决定,并且要将它转化为正常的时间也有一定难度,比如很难看出 1540944000.7197928 就是2018年的10月30日(星期二)的下午5点左右。然而time.time()非常适合于比较两次调用time.time()之间相差的秒数,因此程序可以使用它计算运行时间。
举个例子,如果按照下述代码,把前一段代码中两次调用time.time()的时间相减,就可以得到两次调用中间经过的时间了。
>>> 1540944003.4817972 - 1540944000.7197928
2.7620043754577637
如果想要编写对日期和时间进行操作的代码,可以查阅 datetime 模块的相关资料。
第34行,time.time()方法返回了当前时间并将其存储到名为 startTime 的变量中;第35~38行根据变量 myMode 的值是encrypt还是decrpt来调用encryptMessage()或decryptMessage()。
33. # 测量加/解密所需时间
34. startTime = time.time()
35. if myMode == 'encrypt':
36. translated = transpositionEncrypt.encryptMessage(myKey, content)
37. elif myMode == 'decrypt':
38. translated = transpositionDecrypt.decryptMessage(myKey, content)
39. totalTime = round(time.time() - startTime, 2)
40. print('%sion time: %s seconds' % (myMode.title(), totalTime))
加/解密完成后,第39行再次调用了time.time()方法,并用这次调用的时间减去startTime,得到的结果是两次调用time.time()方法的间隔时间。time.time() - startTime表达式将所得结果传给round()方法,也就是将其取整,因为程序并不需要精确到毫秒。这个整数值赋值给了变量 totalTime。第40行使用了字符串连接,并为用户打印了程序所处的模式及用于加密或解密的时长。
加密后(或解密后)的文件内容现在存储在变量translated中,但这个变量在程序终止时就会被释放,因此需要一个文件来存储这个字符串,这样哪怕程序停止执行,结果仍能保存。第43~45行的代码进行了这部分操作,打开了一个新文件[将w传给open()方法]并调用文件对象的write()方法。
42. # 将置换后的消息写入输出文件
43. outputFileObj = open(outputFilename, 'w')
44. outputFileObj.write(translated)
45. outputFileObj.close()
接下来在第47行和第48行打印了更多信息,告知用户输出文件的名称及加/解密过程已经结束。
47. print('Done %sing %s (%s characters).' % (myMode, inputFilename,
len(content)))
48. print('%sed file is %s.' % (myMode.title(), outputFilename))
第48行是main()函数的最后一行。
第53行和第54行(在第6行def语句执行之后被执行的两行)调用了main()函数,前提是当前程序处于运行状态而非被引用的状态下。
51. # 如果运行 transpositionCipherFile.py (而不是作为模块引入),则
52. # 调用main() 函数
53. if __name__ == '__main__':
54. main()
7.12节对这部分进行了详细解释。
除了open()、read()、write()和close()这些帮助我们在硬盘上加密大文本文件的函数,transpositionFileCipher.py 程序中没有包含太多的新内容。读者学到了如何使用 os.path.exists()函数检查文件是否已经存在。同时如读者所见,编程时可以通过在新程序中引入之前所写程序的函数来拓展程序的能力,这大大增长了计算机加密信息的能力。
除此之外,读者也学习到了一些有用的字符串方法,它们使得程序在接收yoghurt输入时更加灵活;time模块也可以帮助计算程序运行的时间。
和实现凯撒密码的程序不同的是,如果想要使用暴力算法破解通过置换密码加密的文件,会出现太多可能的密钥。但如果编写一个能够识别英语(和其他无意义的乱码)的程序,计算机就可以检查成千上万种解密结果,并确定一把可以成功将密文解密为英语的密钥。这在第11章中会详细介绍。
本文摘自《Python密码学编程 第2版》,[美] 阿尔·斯维加特(Al Sweigart) 著,郑新芳,赵怡翾译。