“为什么治安警察抓人,刑讯逼供?来获取他们的信息。硬盘对酷刑毫无抵抗力。你需要给硬盘一个抵抗的方法。这就是密码学。” ——Patrick Ball,人权数据分析小组
在前面的章节中,我们的程序只处理一些小消息,这些小消息是我们作为字符串值直接输入到源代码中的。我们在这一章中制作的密码程序将允许你加密和解密整个文件,这些文件的大小可能有数百万个字符。
本章涵盖的主题
-
open()
功能 -
读取和写入文件
-
write()
、close()
和read()
文件对象方法 -
os.path.exists()
功能 -
upper()
、lower()
和title()
字符串方法 -
startswith()
和endswith()
字符串方法 -
time
模块和time.time()
功能
换位文件密码程序加密和解密纯(无格式)文本文件。这种文件只有文本数据,通常带有*。txt* 文件扩展名。可以用 Windows 上的记事本、macOS 上的 TextEdit、Linux 上的 gedit 等程序编写自己的文本文件。(文字处理程序也可以生成纯文本文件,但请记住,它们不会保存任何字体、大小、颜色或其他格式。)你甚至可以使用 IDLE 的文件编辑器,用.txt
扩展代替了通常的.py
扩展保存文件。
对于某些样本,您可以从www.nostarch.com/crackingcodes
下载文本文件。这些样本文本文件是现在公共领域的书籍,可以合法下载和使用。例如,玛丽·雪莱的经典小说《弗兰肯斯坦》在其文本文件中有超过 78000 个单词!把这本书输入加密程序要花很多时间,但通过使用下载的文件,程序可以在几秒钟内完成加密。
与转置密码测试程序一样,转置文件密码程序导入transpositonecrypt.py
和transpositonecrypt.py
文件,以便调用encryptMessage()
和decryptMessage()
函数。因此,您不必在新程序中重新键入这些功能的代码。
选择文件 -> 新文件,打开新文件编辑器窗口。在文件编辑器中输入以下代码,保存为transpositionfilecipher.py
。然后从www.nostarch.com/crackingcodes
下载frankenstein.txt
,并将该文件放在与transpositoinfilecipher.py
文件相同的文件夹中。按F5
运行程序。
换位 FileCipher.py
# Transposition Cipher Encrypt/Decrypt File
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)
import time, os, sys, transpositionEncrypt, transpositionDecrypt
def main():
inputFilename = 'frankenstein.txt'
# BE CAREFUL! If a file with the outputFilename name already exists,
# this program will overwrite that file:
outputFilename = 'frankenstein.encrypted.txt'
myKey = 10
myMode = 'encrypt' # Set to 'encrypt' or 'decrypt'.
# If the input file does not exist, the program terminates early:
if not os.path.exists(inputFilename):
print('The file %s does not exist. Quitting...' % (inputFilename))
sys.exit()
# If the output file already exists, give the user a chance to quit:
if os.path.exists(outputFilename):
print('This will overwrite the file %s. (C)ontinue or (Q)uit?' %
(outputFilename))
response = input('> ')
if not response.lower().startswith('c'):
sys.exit()
# Read in the message from the input file:
fileObj = open(inputFilename)
content = fileObj.read()
fileObj.close()
print('%sing...' % (myMode.title()))
# Measure how long the encryption/decryption takes:
startTime = time.time()
if myMode == 'encrypt':
translated = transpositionEncrypt.encryptMessage(myKey, content)
elif myMode == 'decrypt':
translated = transpositionDecrypt.decryptMessage(myKey, content)
totalTime = round(time.time() - startTime, 2)
print('%sion time: %s seconds' % (myMode.title(), totalTime))
# Write out the translated message to the output file:
outputFileObj = open(outputFilename, 'w')
outputFileObj.write(translated)
outputFileObj.close()
print('Done %sing %s (%s characters).' % (myMode, inputFilename,
len(content)))
print('%sed file is %s.' % (myMode.title(), outputFilename))
# If transpositionCipherFile.py is run (instead of imported as a module),
# call the main() function:
if __name__ == '__main__':
main()
当您运行transpositonfilecipher.py
程序时,它应该产生以下输出:
Encrypting...
Encryption time: 1.21 seconds
Done encrypting frankenstein.txt (441034 characters).
Encrypted file is frankenstein.encrypted.txt.
一个新的Frankenstein.encrypted.txt
文件被创建在与transpositoinfilecipher.py
相同的文件夹中。当你用 IDLE 的文件编辑器打开这个文件时,你会看到frankenstein.py
的加密内容。它应该是这样的:
PtFiyedleo a arnvmt eneeGLchongnes Mmuyedlsu0#uiSHTGA r sy,n t ys
s nuaoGeL
sc7s,
--snip--
一旦你有了一个加密的文本,你可以把它发送给别人解密。收件人还需要有换位文件密码程序。
要解密文本,请对源代码(粗体)进行以下更改,并再次运行换位文件密码程序:
inputFilename = 'frankenstein.encrypted.txt'
# BE CAREFUL! If a file with the outputFilename name already exists,
# this program will overwrite that file:
outputFilename = 'frankenstein.decrypted.txt'
myKey = 10
myMode = 'decrypt' # Set to 'encrypt' or 'decrypt'.
这一次当你运行程序时,一个名为Frankenstein.decrypted.txt
的新文件会出现在文件夹中,该文件与原来的frankenstein.txt
文件完全相同。
在我们深入研究transpositionfilecipher.py
的代码之前,让我们先来看看 Python 是如何处理文件的。读取文件内容的三个步骤是:打开文件,将文件内容读入变量,然后关闭文件。同样,要在文件中写入新内容,您必须打开(或创建)文件,写入新内容,然后关闭文件。
Python 可以使用open()
函数打开一个文件进行读写。open()
函数的第一个参数是要打开的文件名。如果文件与 Python 程序在同一个文件夹中,可以只使用文件名,比如'thetimemachine.txt'
。如果打开timemachine.txt
的命令与你的 Python 程序存在于同一个文件夹中,该命令如下所示:
fileObj = open('thetimemachine.txt')
一个文件对象存储在fileObj
变量中,该变量将用于读取或写入文件。
您还可以指定文件的绝对路径,包括文件所在的文件夹和父文件夹。比如'C:\\Users\\Al\\frankenstein.txt'
(Windows 上)和'/Users/Al/frankenstein.txt'
(MAC OS 和 Linux 上)就是绝对路径。请记住,在 Windows 上,必须通过在反斜杠(\
)前键入另一个反斜杠来对其进行转义。
例如,如果您想打开frankenstein.txt
文件,您可以将文件的路径作为字符串传递给open()
函数的第一个参数(并根据您的操作系统格式化绝对路径):
fileObj = open('C:\\Users\\Al\\frankenstein.txt')
file 对象有几种写入、读取和关闭文件的方法。
对于加密程序,在读入文本文件的内容后,您需要将加密(或解密)的内容写入一个新文件,这将通过使用write()
方法来完成。
要在文件对象上使用write()
,需要以写模式打开文件对象,这可以通过将字符串'w'
作为第二个参数传递给open()
来实现。(第二个参数是一个可选参数,因为open()
函数仍然可以在不传递两个参数的情况下使用。)例如,在交互式 shell 中输入以下代码行:
>>> fileObj = open('spam.txt', 'w')
这一行以写模式创建了一个名为spam.txt
的文件,这样您就可以编辑它了。如果在open()
函数创建新文件的地方存在同名文件,旧文件将被覆盖,因此在写入模式下使用open()
时要小心。
随着spam.txt
现在以写模式打开,您可以通过调用文件上的write()
方法写入文件。write()
方法有一个参数:要写入文件的文本字符串。在交互 shell 中输入以下内容将'Hello, world!'
写入spam.txt
:
>>> fileObj.write('Hello, world!')
13
将字符串'Hello, world!'
传递给write()
方法,将该字符串写入spam.txt
文件,然后打印13
,即写入文件的字符串中的字符数。
当您处理完一个文件时,您需要通过调用 file 对象上的close()
方法来告诉 Python 您已经处理完了这个文件:
>>> fileObj.close()
还有一种附加模式,它类似于写模式,只是附加模式不会覆盖文件。相反,字符串被写到文件中已有内容的末尾。虽然我们不会在这个程序中使用它,但是您可以通过将字符串'a'
作为第二个参数传递给open()
来以追加模式打开一个文件。
如果您在尝试调用 file 对象上的write()
时得到一个io.UnsupportedOperation: not readable
错误消息,您可能没有以写模式打开该文件。当您不包括open()
函数的可选参数时,它会自动以读取模式('r'
)打开文件对象,这允许您只对文件对象使用read()
方法。
read()
方法返回一个包含文件中所有文本的字符串。为了进行测试,我们将读取之前用write()
方法创建的spam.txt
文件。从交互式 shell 中运行以下代码:
>>> fileObj = open('spam.txt', 'r')
>>> content = fileObj.read()
>>> print(content)
Hello world!
>>> fileObj.close()
文件打开,创建的文件对象存储在fileObj
变量中。一旦有了 file 对象,就可以使用read()
方法读取文件,并将其存储在content
变量中,然后打印出来。当你处理完文件对象后,你需要用close()
关闭它。
如果您得到错误消息IOError: [Errno 2] No such file or directory
,请确保文件确实在您认为的位置,并再次检查您键入的文件名和文件夹名是否正确。(目录是文件夹的别称。)
我们将在transpositionfilecipher.py
中打开的文件上使用open()
、read()
、write()
和close()
进行加密或解密。
transpositonfilecipher.py
程序的第一部分应该看起来很熟悉。第 4 行是程序transpositionEncrypt.py
和transpositionencrypt.py
以及 Python 的time
、os
和sys
模块的import
语句。然后我们开始main()
,设置一些在程序中使用的变量。
# Transposition Cipher Encrypt/Decrypt File
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)
import time, os, sys, transpositionEncrypt, transpositionDecrypt
def main():
inputFilename = 'frankenstein.txt'
# BE CAREFUL! If a file with the outputFilename name already exists,
# this program will overwrite that file:
outputFilename = 'frankenstein.encrypted.txt'
myKey = 10
myMode = 'encrypt' # Set to 'encrypt' or 'decrypt'.
inputFilename
变量保存要读取的文件的字符串,加密(或解密)的文本被写入在outputFilename
中命名的文件。换位密码使用一个整数作为密钥,存储在myKey
中。程序期望myMode
存储'encrypt'
或'decrypt'
来告诉它加密或解密inputFilename
文件。但是在我们能够读取inputFilename
文件之前,我们需要使用os.path.exists()
来检查它是否存在。
读取文件总是无害的,但是写入文件时需要小心。在写入模式下对已经存在的文件名调用open()
函数会覆盖原始内容。使用os.path.exists()
函数,您的程序可以检查该文件是否已经存在。
os.path.exists()
函数采用单个字符串参数作为文件名或文件路径,如果文件已经存在,则返回True
,如果不存在,则返回False
。os.path.exists()
函数存在于path
模块中,后者存在于os
模块中,所以当我们导入os
模块时,path
模块也被导入。
在交互式 shell 中输入以下内容:
>>> 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()
函数确认calc.exe
文件存在于 Windows 中。当然,只有在 Windows 上运行 Python 时,才能得到这些结果。记住在 Windows 文件路径中转义反斜杠,方法是在它前面键入另一个反斜杠。如果你使用的是 macOS,只有 macOS 的例子会返回 T1,对于 Linux 只有最后一个例子会返回 T2。如果没有给出完整的文件路径 ➊,Python 将检查当前的工作目录。对于 IDLE 的交互式 shell,这是安装 Python 的文件夹。
我们使用os.path.exists()
函数来检查inputFilename
中的文件名是否存在。否则,我们没有要加密或解密的文件。我们在第 14 行到第 17 行这样做:
# If the input file does not exist, then the program terminates early:
if not os.path.exists(inputFilename):
print('The file %s does not exist. Quitting...' % (inputFilename))
sys.exit()
如果文件不存在,我们向用户显示一条消息,然后退出程序。
接下来,该程序检查是否存在与outputFilename
同名的文件,如果存在,它会要求用户键入C
以继续运行该程序,或者键入Q
以退出该程序。因为用户可能会输入各种响应,比如'c'
、'C'
,甚至是单词'Continue'
,我们希望确保程序会接受所有这些版本。为此,我们将使用更多的字符串方法。
upper()
和lower()
字符串方法将分别以全大写或全小写字母返回它们被调用的字符串。在交互式 shell 中输入以下内容,查看这些方法如何处理同一个字符串:
>>> 'Hello'.upper()
'HELLO'
>>> 'Hello'.lower()
'hello'
正如lower()
和upper()
字符串方法返回小写或大写的字符串一样,title()
字符串方法返回大写的字符串。标题大小写是每个单词的第一个字符大写,其余字符小写。在交互式 shell 中输入以下内容:
>>> 'hello'.title()
'Hello'
>>> 'HELLO'.title()
'Hello'
>>> 'extra! extra! man bites shark!'.title()
'Extra! Extra! Man Bites Shark!'
稍后我们将在程序中使用title()
来格式化我们为用户输出的消息。
如果在字符串的开头找到了它的字符串参数,startswith()
方法将返回True
。在交互式 shell 中输入以下内容:
>>> 'hello'.startswith('h')
True
>>> 'hello'.startswith('H')
False
>>> spam = 'Albert'
>>> spam.startswith('Al') # ➊
True
startswith()
方法区分大小写,也可以用于多字符字符串 ➊。
endswith()
字符串方法用于检查一个字符串值是否以另一个指定的字符串值结尾。在交互式 shell 中输入以下内容:
>>> 'Hello world!'.endswith('world!')
True
>>> 'Hello world!'.endswith('world') # ➋
False
字符串值必须完全匹配。注意,'world'
➋ 中缺少感叹号导致endswith()
返回False
。
如前所述,我们希望程序接受任何以C
开头的响应,而不考虑大小写。这意味着无论用户键入c
、continue
、C
还是另一个以C
开头的字符串,我们都希望文件被覆盖。我们将使用字符串方法lower()
和startswith()
使程序在接受用户输入时更加灵活:
# If the output file already exists, give the user a chance to quit:
if os.path.exists(outputFilename):
print('This will overwrite the file %s. (C)ontinue or (Q)uit?' %
(outputFilename))
response = input('> ')
if not response.lower().startswith('c'):
sys.exit()
在第 23 行,我们获取字符串的第一个字母,并使用startswith()
方法检查它是否是一个C
。我们使用的startswith()
方法区分大小写,并检查小写的'c'
,所以我们使用lower()
方法修改response
字符串的大写,使其总是小写。如果用户没有输入以C
开头的响应,那么startswith()
返回False
,使得if
语句评估为True
(因为if
语句中的not
),调用sys.exit()
结束程序。技术上,用户不必输入Q
退出;任何不以C
开头的字符串都会导致调用sys.exit()
函数来退出程序。
在第 27 行,我们开始使用本章开始时讨论的文件对象方法。
# Read in the message from the input file:
fileObj = open(inputFilename)
content = fileObj.read()
fileObj.close()
print('%sing...' % (myMode.title()))
第 27 到 29 行打开存储在inputFilename
中的文件,将其内容读入content
变量,然后关闭文件。读入文件后,第 31 行向用户输出一条消息,告诉他们加密或解密已经开始。因为myMode
应该包含字符串'encrypt'
或'decrypt'
,所以调用title()
字符串方法会将myMode
中字符串的第一个字母大写,并将字符串拼接到'%sing'
字符串中,所以它会显示'Encrypting...'
或'Decrypting...'
。
加密或解密整个文件可能比一个短字符串需要更长的时间。用户可能想知道处理一个文件需要多长时间。我们可以通过使用time
模块来测量加密或解密过程的长度。
time.time()
函数返回当前时间,作为自 1970 年 1 月 1 日以来的秒数的浮点值。这一时刻被称为 Unix 纪元。在交互式 shell 中输入以下内容,看看这个函数是如何工作的:
>>> import time
>>> time.time()
1540944000.7197928
>>> time.time()
1540944003.4817972
因为time.time()
返回一个浮点值,所以可以精确到一个毫秒(即 1/1000 秒)。当然,time.time()
显示的数字取决于调用这个函数的时间,可能很难解释。可能不清楚 1540944000.7197928 是 2018 年 10 月 30 日星期二,大约下午 5 点
。然而,time.time()
函数对于比较调用time.time()
之间的秒数很有用。我们可以用这个函数来确定一个程序已经运行了多长时间。
例如,如果您减去我之前在交互式 shell 中调用time.time()
时返回的浮点值,您将得到我键入时这些调用之间的时间:
>>> 1540944003.4817972 - 1540944000.7197928
2.7620043754577637
如果你需要编写处理日期和时间的代码,参见www.nostarch.com/crackingcodes
获取关于datetime
模块的信息。
在第 34 行,time.time()
返回当前时间,存储在名为startTime
的变量中。第 35 到 38 行调用encryptMessage()
或decryptMessage()
,这取决于'encrypt'
或'decrypt'
是否存储在myMode
变量中。
# Measure how long the encryption/decryption takes:
startTime = time.time()
if myMode == 'encrypt':
translated = transpositionEncrypt.encryptMessage(myKey, content)
elif myMode == 'decrypt':
translated = transpositionDecrypt.decryptMessage(myKey, content)
totalTime = round(time.time() - startTime, 2)
print('%sion time: %s seconds' % (myMode.title(), totalTime))
第 39 行在程序解密或加密并从当前时间中减去startTime
后再次调用time.time()
。结果是两次调用time.time()
之间的秒数。time.time() - startTime
表达式计算出一个传递给round()
函数的值,该值四舍五入到最接近的两位小数点,因为程序不需要毫秒精度。该值存储在totalTime
中。第 40 行使用字符串拼接来打印程序模式,并向用户显示程序加密或解密所花费的时间。
加密(或解密)的文件内容现在存储在translated
变量中。但是当程序终止时,这个字符串就被遗忘了,所以我们想把这个字符串存储在一个文件中,以便在程序结束运行后仍然存在。第 43 到 45 行的代码通过打开一个新文件(并将'w'
传递给open()
函数)然后调用write()
文件对象方法来实现这一点:
# Write out the translated message to the output file:
outputFileObj = open(outputFilename, 'w')
outputFileObj.write(translated)
outputFileObj.close()
然后,第 47 行和第 48 行向用户显示更多消息,表明该过程已经完成,并显示所写文件的名称:
print('Done %sing %s (%s characters).' % (myMode, inputFilename,
len(content)))
print('%sed file is %s.' % (myMode.title(), outputFilename))
第 48 行是main()
函数的最后一行。
第 53 行和第 54 行(在第 6 行的def
语句执行后执行)调用main()
函数,如果该程序正在运行而不是正在导入:
# If transpositionCipherFile.py is run (instead of imported as a module),
# call the main() function:
if __name__ == '__main__':
main()
这在第 95 页的变量__name__
T2 中有详细解释。
恭喜你!除了open()
、read()
、write()
和close()
函数之外,transpositonfilecipher.py
程序没什么特别的,这些函数让我们可以加密硬盘上的大型文本文件。您学习了如何使用os.path.exists()
函数来检查文件是否已经存在。如您所见,我们可以通过导入新程序中使用的功能来扩展程序的功能。这大大提高了我们使用计算机加密信息的能力。
您还学习了一些有用的字符串方法,使程序在接受用户输入时更加灵活,以及如何使用time
模块来测量程序运行的速度。
与凯撒密码程序不同,换位文件密码有太多可能的密钥,无法简单地使用蛮力进行攻击。但是,如果我们能编写一个识别英语的程序(而不是一连串的胡言乱语),计算机就能检查成千上万次解密尝试的结果,并确定哪个密钥能成功地将一条信息解密成英语。你将在第 11 章中学习如何做到这一点。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
os.exists()
和os.path.exists()
哪个正确?
-
Unix 纪元是什么时候?
-
下面的表达式表示什么?
'Foobar'.startswith('Foo') 'Foo'.startswith('Foobar') 'Foobar'.startswith('foo') 'bar'.endswith('Foobar') 'Foobar'.endswith('bar') 'The quick brown fox jumped over the yellow lazy dog.'.title() ```**