译者:飞龙
本文来自【OpenDocCN 饱和式翻译计划】,采用译后编辑(MTPE)流程来尽可能提升效率。
收割 SB 的人会被 SB 们封神,试图唤醒 SB 的人是 SB 眼中的 SB。——SB 第三定律
十八、编程实现维吉尼亚密码
“我当时相信,现在仍然相信,广泛普及的加密技术给我们的安全和自由带来的好处远远超过犯罪分子和恐怖分子使用它所带来的不可避免的损害。”
——马特·布雷泽,AT&T 实验室,2001 年 9 月
意大利密码学家吉奥万·巴蒂斯塔·贝拉索是第一个在 1553 年描述维吉尼亚尔密码的人,但它最终以法国外交官布莱斯·德·维吉尼亚尔的名字命名,他是后来几年重新发明密码的许多人之一。它被称为“le chiffre indéchiffrable”,意思是“无法破译的密码”,直到 19 世纪英国学者查尔斯·巴贝奇破解了它,它才被破解。
因为维吉尼亚密码有太多可能被暴力破解的密钥,即使使用我们的英语检测模块,它也是本书迄今为止讨论的最强的密码之一。甚至对你在第十七章中学到的单词模式攻击无敌。
本章涵盖的主题
-
子项
-
使用列表-追加-连接过程构建字符串
在维吉尼亚密码中使用多个字母密钥
与凯撒密码不同,维吉尼亚尔密码有多个密钥。因为它使用了多组替换,所以维吉尼亚密码是一种多字母替换密码。与简单的替换密码不同,仅靠频率分析无法击败维吉尼亚密码。我们没有像凯撒密码那样使用 0 到 25 之间的数字密钥,而是使用字母密钥来表示维吉尼亚。
维吉尼亚密钥是一系列字母,例如一个英语单词,它被分成多个单字母子密钥,这些子密钥对明文中的字母进行加密。例如,如果我们使用PIZZA的维吉尼亚密钥,第一个子密钥是P,第二个子密钥是I,第三和第四个子密钥都是Z,第五个子密钥是A,第一个子密钥加密明文的第一个字母,第二个子密钥加密第二个字母,依此类推。当我们到达明文的第六个字母时,我们返回到第一个子密钥。
使用维吉尼亚密码和使用多个凯撒密码是一样的,如图 18-1 所示。我们对明文的每个字母应用不同的凯撒密码,而不是用一个凯撒密码加密整个明文。
图 18-1:多个凯撒密码组合成维吉尼亚密码
每个子密钥被转换成一个整数,并作为凯撒加密密钥。例如,字母A对应于凯撒密码密钥 0。字母B对应于密钥 1,以此类推直到密钥 25 的Z,如图 18-2 所示。
图 18-2:凯撒密钥及其对应字母
让我们来看一个例子。以下是显示在维根耶尔披萨旁边的信息“常识并不常见”。明文显示有相应的子密钥,该子密钥对其下的每个字母进行加密。
COMMONSENSEISNOTSOCOMMON
PIZZAPIZZAPIZZAPIZZAPIZZ
为了用子密钥P加密明文中的第一个C,使用子密钥的相应数字密钥 15 用凯撒密码加密它,这产生密码字母R,并通过循环子密钥对明文的每个字母重复该过程。表 18-1 显示了这一过程。明文字母的整数和子密钥(在括号中给出)相加,得到密文字母的整数。
表 18-1: 用维吉尼亚子密钥加密字母
| 明文字母 | 子密钥 | 密文字母 |
|---|---|---|
C (2) | P (15) | R (17) |
O (14) | I (8) | W (22) |
M (12) | Z (25) | L (11) |
M (12) | Z (25) | L (11) |
O (14) | A (0) | O (14) |
N (13) | P (15) | C (2) |
S (18) | I (8) | A (0) |
E (4) | Z (25) | D (3) |
N (13) | Z (25) | M (12) |
S (18) | A (0) | S (18) |
E (4) | P (15) | T (19) |
I (8) | I (8) | Q (16) |
S (18) | Z (25) | R (17) |
N (13) | Z (25) | M (12) |
O (14) | A (0) | O (14) |
T (19) | P (15) | I (8) |
S (18) | I (8) | A (0) |
O (14) | Z (25) | N (13) |
C (2) | Z (25) | B (1) |
O (14) | A (0) | O (14) |
M (12) | P (15) | B (1) |
M (12) | I (8) | U (20) |
O (14) | Z (25) | N (13) |
N (13) | Z (25) | M (12) |
使用带有密钥PIZZA(由子密钥 15,8,25,25,0 组成)的维吉尼亚密码将普通意义上的明文加密为密文RWLLOC ADMST QR MOI AN BOBUNM。
密钥越长越安全
维吉尼亚密钥中的字母越多,加密信息抵御暴力攻击的能力就越强。PIZZA 不是维吉尼亚关键字的好选择,因为它只有五个字母。一个五个字母的密钥有 11881376 种可能的组合(因为 26 个字母的 5 次方是26 ** 5 = 26×26×26×26×26 = 11881376)。一千一百万个密钥对于一个人来说太多了,无法用暴力破解,但是一台计算机可以在几个小时内尝试所有的密钥。它将首先尝试使用密钥AAAAA对消息进行解密,并检查得到的解密结果是否是英文。然后它可以尝试AAAAB,然后AAAAC,等等,直到它到达比萨饼。
好消息是,该密钥每多一个字母,可能的密钥数就会乘以 26。一旦有千万亿个可能的密钥,计算机需要很多年才能破解这个密码。表 18-2 显示了每种密钥长度有多少种可能的密钥。
表 18-2: 基于维吉尼亚密钥长度的可能密钥数
| 密钥长度 | 等式 | 可能的密钥 |
| --- | --- | --- |
| 1 | 26 | = 26 |
| 2 | 26 × 26 | = 676 |
| 3 | 676 × 26 | = 17,576 |
| 4 | 17,576 × 26 | = 456,976 |
| 5 | 456,976 × 26 | = 11,881,376 |
| 6 | 11,881,376 × 26 | = 308,915,776 |
| 7 | 308,915,776 × 26 | = 8,031,810,176 |
| 8 | 8,031,810,176 × 26 | = 208,827,064,576 |
| 9 | 208,827,064,576 × 26 | = 5,429,503,678,976 |
| 10 | 5,429,503,678,976 × 26 | = 141,167,095,653,376 |
| 11 | 141,167,095,653,376 × 26 | = 3,670,344,486,987,776 |
| 12 | 3,670,344,486,987,776 × 26 | = 95,428,956,661,682,176 |
| 13 | 95,428,956,661,682,176 × 26 | = 2,481,152,873,203,736,576 |
| 14 | 2,481,152,873,203,736,576 × 26 | = 64,509,974,703,297,150,976 |
对于 12 个或更多字母长的密钥,一台笔记本电脑不可能在合理的时间内破解它们。
选择防止字典攻击的密钥
维吉尼亚密钥不一定是像PIZZA这样的真实单词。它可以是任意长度的字母的任意组合,例如十二字母键DURIWKNMFICK。其实不用一个能在字典里找到的词是最好的。尽管单词RADIOLOGISTS也是一个比DURIWKNMFICK更容易记住的 12 个字母的密钥,但是密码分析人员可能会预料到密码学家正在使用一个英语单词作为密钥。
试图使用字典中的每个英语单词进行暴力攻击被称为字典攻击。有 95,428,956,661,682,176 个可能的十二个字母的密钥,但是在我们的字典文件中只有大约 1800 个十二个字母的单词。如果我们使用字典中的 12 个字母的单词作为密钥,这将比随机的 3 个字母的密钥(有 17,576 个可能的密钥)更容易被暴力破解。
当然,密码学家的优势在于密码分析者不知道维吉尼亚密钥的长度。但是密码分析者可以尝试所有的单字母密钥,然后所有的双字母密钥,等等,这将仍然允许他们非常快速地找到字典单词密钥。
维吉尼亚密码程序的源代码
选择文件 -> 新文件,打开新文件编辑器窗口。在文件编辑器中输入以下代码,保存为vigenereCipher.py,确保pyperclip.py在同一个目录下。按F5运行程序。
vigenereCipher.py
# Vigenere Cipher (Polyalphabetic Substitution Cipher)
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)
import pyperclip
LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
def main():
# This text can be downloaded from https://www.nostarch.com/
crackingcodes/:
myMessage = """Alan Mathison Turing was a British mathematician,
logician, cryptanalyst, and computer scientist."""
myKey = 'ASIMOV'
myMode = 'encrypt' # Set to either 'encrypt' or 'decrypt'.
if myMode == 'encrypt':
translated = encryptMessage(myKey, myMessage)
elif myMode == 'decrypt':
translated = decryptMessage(myKey, myMessage)
print('%sed message:' % (myMode.title()))
print(translated)
pyperclip.copy(translated)
print()
print('The message has been copied to the clipboard.')
def encryptMessage(key, message):
return translateMessage(key, message, 'encrypt')
def decryptMessage(key, message):
return translateMessage(key, message, 'decrypt')
def translateMessage(key, message, mode):
translated = [] # Stores the encrypted/decrypted message string.
keyIndex = 0
key = key.upper()
for symbol in message: # Loop through each symbol in message.
num = LETTERS.find(symbol.upper())
if num != -1: # -1 means symbol.upper() was not found in LETTERS.
if mode == 'encrypt':
num += LETTERS.find(key[keyIndex]) # Add if encrypting.
elif mode == 'decrypt':
num -= LETTERS.find(key[keyIndex]) # Subtract if
decrypting.
num %= len(LETTERS) # Handle any wraparound.
# Add the encrypted/decrypted symbol to the end of translated:
if symbol.isupper():
translated.append(LETTERS[num])
elif symbol.islower():
translated.append(LETTERS[num].lower())
keyIndex += 1 # Move to the next letter in the key.
if keyIndex == len(key):
keyIndex = 0
else:
# Append the symbol without encrypting/decrypting:
translated.append(symbol)
return ''.join(translated)
# If vigenereCipher.py is run (instead of imported as a module), call
# the main() function:
if __name__ == '__main__':
main()
维吉尼亚密码程序的运行示例
当您运行该程序时,其输出将如下所示:
Encrypted message:
Adiz Avtzqeci Tmzubb wsa m Pmilqev halpqavtakuoi, lgouqdaf, kdmktsvmztsl, izr
xoexghzr kkusitaaf.
The message has been copied to the clipboard.
该程序打印加密的邮件,并将加密的文本复制到剪贴板。
设置模块、常量和main()函数
程序的开头有描述程序的普通注释、pyperclip模块的一个import语句,以及一个名为LETTERS的变量,该变量包含每个大写字母的字符串。维吉尼亚密码的main()函数类似于本书中的其他main()函数:它从定义变量message、key和mode开始。
# Vigenere Cipher (Polyalphabetic Substitution Cipher)
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)
import pyperclip
LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
def main():
# This text can be downloaded from https://www.nostarch.com/
crackingcodes/:
myMessage = """Alan Mathison Turing was a British mathematician,
logician, cryptanalyst, and computer scientist."""
myKey = 'ASIMOV'
myMode = 'encrypt' # Set to either 'encrypt' or 'decrypt'.
if myMode == 'encrypt':
translated = encryptMessage(myKey, myMessage)
elif myMode == 'decrypt':
translated = decryptMessage(myKey, myMessage)
print('%sed message:' % (myMode.title()))
print(translated)
pyperclip.copy(translated)
print()
print('The message has been copied to the clipboard.')
在运行程序之前,用户在第 10、11 和 12 行设置这些变量。加密或解密的消息(取决于myMode的设置)存储在一个名为translated的变量中,因此它可以打印到屏幕上(第 20 行)并复制到剪贴板上(第 21 行)。
用列表-追加-连接过程构建字符串
这本书里几乎所有的程序都用某种形式的代码构建了一个字符串。也就是说,程序创建一个变量,该变量以空白字符串开始,然后使用字符串连接添加字符。这就是以前的密码程序对translated变量所做的。打开交互式 shell 并输入以下代码:
>>> building = ''
>>> for c in 'Hello world!':
>>> building += c
>>> print(building)
这段代码遍历字符串'Hello world!'中的每个字符,并将其连接到存储在building中的字符串的末尾。在循环的末尾,building保存着完整的字符串。
尽管字符串连接看起来是一种简单的技术,但在 Python 中却非常低效。从空白列表开始,然后使用append()列表方法会快得多。当您构建完字符串列表后,您可以使用join()方法将该列表转换为单个字符串值。下面的代码与前面的例子做同样的事情,但是速度更快。在交互式 shell 中输入代码:
>>> building = []
>>> for c in 'Hello world!':
>>> building.append(c)
>>> building = ''.join(building)
>>> print(building)
使用这种方法来构建字符串而不是修改字符串会使程序运行得更快。您可以通过使用time.time()对这两种方法进行计时来看出不同之处。打开一个新的文件编辑器窗口,输入以下代码:
stringTest.py
import time
startTime = time.time()
for trial in range(10000):
building = ''
for i in range(10000):
building += 'x'
print('String concatenation: ', (time.time() - startTime))
startTime = time.time()
for trial in range(10000):
building = []
for i in range(10000):
building.append('x')
building = ''.join(building)
print('List appending: ', (time.time() - startTime))
将该程序另存为stringTest.py并运行。输出将如下所示:
String concatenation: 40.317070960998535
List appending: 10.488219022750854
程序stringTest.py将变量startTime设置为当前时间,运行代码使用连接将 10,000 个字符追加到字符串中,然后打印完成连接所用的时间。然后程序将startTime重置为当前时间,运行代码来使用列表追加方法构建一个相同长度的字符串,然后打印完成所用的总时间。在我的电脑上,使用字符串连接来构建 10,000 个字符串,每个字符串包含 10,000 个字符,大约需要 40 秒,但使用列表-追加-连接过程来完成同样的任务只需要 10 秒。如果你的程序构建了很多字符串,使用列表可以让你的程序运行得更快。
我们将使用列表-追加-连接过程为本书中剩余的程序构建字符串。
加密和解密消息
因为加密和解密代码基本相同,我们将为函数translateMessage()创建两个名为encryptMessage()和decryptMessage()的包装函数,它们将保存要加密和解密的实际代码。
def encryptMessage(key, message):
return translateMessage(key, message, 'encrypt')
def decryptMessage(key, message):
return translateMessage(key, message, 'decrypt')
translateMessage()函数一次一个字符地构建加密(或解密)的字符串。translated中的列表存储了这些字符,以便在字符串构建完成时可以将它们连接起来。
def translateMessage(key, message, mode):
translated = [] # Stores the encrypted/decrypted message string.
keyIndex = 0
key = key.upper()
请记住,维吉尼亚密码只是凯撒密码,只是根据字母在消息中的位置使用不同的密钥。跟踪使用哪个子密钥的keyIndex变量从0开始,因为用于加密或解密消息第一个字符的字母是key[0]。
该程序假定密钥全部是大写字母。为了确保密钥有效,第 38 行在key上调用upper()。
translateMessage()中的其余代码类似于凯撒密码:
for symbol in message: # Loop through each symbol in message.
num = LETTERS.find(symbol.upper())
if num != -1: # -1 means symbol.upper() was not found in LETTERS.
if mode == 'encrypt':
num += LETTERS.find(key[keyIndex]) # Add if encrypting.
elif mode == 'decrypt':
num -= LETTERS.find(key[keyIndex]) # Subtract if
decrypting.
第 40 行的for循环在循环的每次迭代中将message中的字符设置为变量symbol。第 41 行找到了LETTERS中symbol大写版本的索引,这就是我们如何将一个字母翻译成一个数字。
如果第 41 行的num没有设置为-1,那么在LETTERS中找到了symbol的大写版本(意味着symbol是一个字母)。变量keyIndex跟踪使用哪个子密钥,子密钥总是key[keyIndex]求值的值。
当然,这只是一个单字母字符串。我们需要在LETTERS中找到这个字母的索引,将子密钥转换成整数。然后,这个整数被加到(如果加密的话)第 44 行的符号数上,或者被减到(如果解密的话)第 46 行的符号数上。
在凯撒密码中,我们检查了num的新值是否小于0(在这种情况下,我们给它加上了len(LETTERS))或者num的新值是否大于len(LETTERS)(在这种情况下,我们从中减去len(LETTERS))。这些检查处理环绕的情况。
然而,有一种更简单的方法来处理这两种情况。如果我们用len(LETTERS)对存储在num中的整数取模,我们可以用一行代码完成同样的计算:
num %= len(LETTERS) # Handle any wraparound.
例如,如果num是-8,我们想在它上面加上26(即len(LETTERS))得到18,这可以表示为-8 % 26,它的值是18。或者如果num是31,我们想要减去26得到5,并且31 % 26计算为5。第 48 行的模运算处理两种环绕情况。
加密(或解密)字符存在于LETTERS[num]。然而,我们希望加密(或解密)字符的大小写与symbol的原始大小写相匹配。
# Add the encrypted/decrypted symbol to the end of translated:
if symbol.isupper():
translated.append(LETTERS[num])
elif symbol.islower():
translated.append(LETTERS[num].lower())
所以如果symbol是大写字母,那么第 51 行的条件是True,第 52 行将LETTERS[num]处的字符追加到translated处,因为LETTERS中的所有字符都已经是大写的了。
但是,如果symbol是小写字母,则第 53 行的条件改为True,第 54 行将小写形式的LETTERS[num]附加到translated。这就是我们如何使加密(或解密)的消息与原始消息大小写相匹配。
现在我们已经翻译了这个符号,我们想要确保在下一次循环中使用 next 子密钥。第 56 行将keyIndex递增 1,因此下一次迭代使用下一个子密钥的索引:
keyIndex += 1 # Move to the next letter in the key.
if keyIndex == len(key):
keyIndex = 0
然而,如果我们在密钥的最后一个子密钥上,keyIndex将等于key的长度。第 57 行检查这种情况,如果是这种情况,将第 58 行上的keyIndex重置回0,以便key[keyIndex]指向第一个子密钥。
缩进表示第 59 行的else语句与第 42 行的if语句成对出现:
else:
# Append the symbol without encrypting/decrypting:
translated.append(symbol)
如果在LETTERS字符串中没有找到该符号,则执行第 61 行的代码。如果symbol是一个数字或标点符号,比如'5'或'?',就会出现这种情况。在这种情况下,第 61 行将未修改的符号附加到translated。
现在我们已经完成了在translated中构建字符串,我们在空白字符串上调用join()方法:
return ''.join(translated)
这一行使函数在被调用时返回整个加密或解密的消息。
调用main()函数
第 68 和 69 行结束了程序的代码:
if __name__ == '__main__':
main()
如果程序是自己运行的,而不是由另一个想要使用其encryptMessage ()和decryptMessage()函数的程序导入的,这些行将调用main()函数。
总结
你已经接近这本书的结尾了,但是请注意,维吉尼亚密码并不比凯撒密码复杂多少,凯撒密码是你学习的第一批密码程序之一。只需对凯撒密码稍加修改,我们就创造出了一种密码,它拥有的可能密钥比暴力破解的多得多。
维吉尼亚密码不容易受到简单替换破解程序使用的字典单词模式攻击。数百年来,“无法破译”的维吉尼亚密码将信息保密,但这种密码最终也变得脆弱。在第 19 章和第 20 章中,你将学习频率分析技术,这将使你能够破解维吉尼亚密码。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes找到。
-
除了维吉尼亚密码使用多个密钥而不是一个密钥之外,维吉尼亚密码与哪种密码相似?
-
一个密钥长为 10 的维吉尼亚密钥有多少种可能的密钥?
-
数百
-
数千
-
数百万
-
超过一万亿
-
-
维吉尼亚密码是什么样的密码?
十九、频率分析
“在混乱中寻找模式的不可言喻的天赋无法完成它的任务,除非他首先把自己沉浸在混乱中。如果它们确实包含模式,他现在没有以任何理性的方式看到它们。但是他头脑中的某些次理性部分可能会起作用。”
——尼尔·斯蒂芬森, Cryptonomicon
在本章中,你将学习如何确定每个英文字母在特定文本中的出现频率。然后,您将这些频率与您的密文的字母频率进行比较,以获得有关原始明文的信息,这将有助于您破解加密。这个确定一个字母在明文和密文中出现频率的过程被称为频率分析。理解频率分析是破解维吉尼亚密码的重要一步。我们将使用字母频率分析来破解第 20 章中的维吉尼亚密码。
本章涵盖的主题
-
字母频率和符号
-
sort()方法的key和reverse关键字参数 -
将函数作为值传递,而不是调用函数
-
使用
keys()、values()和items()方法将字典转换成列表
分析文本中字母的频率
当你掷硬币时,大约一半的时间是正面,一半的时间是反面。也就是头尾的频率应该差不多。我们可以用百分比来表示频率,方法是将一个事件发生的总次数(例如,我们抛了多少次头)除以一个事件的总尝试次数(即我们抛硬币的总次数),然后将商乘以 100。我们可以通过硬币正面或反面的频率来了解它:硬币的重量是公平的还是不公平的,甚至是双面硬币。
我们还可以从密文的字母频率中了解更多信息。英语字母表中有些字母比其他字母用得更频繁。例如,字母E、T、A和O在英语单词中出现频率最高,而字母J、X、Q和Z在英语中出现频率较低。我们将利用英语中字母频率的差异来破解维根加密的信息。
图 19-1 显示了标准英语中的字母频率。这张图表是利用书籍、报纸和其他来源的文字编辑而成的。
当我们将这些字母频率按频率从高到低排序时,E是最频繁的字母,其次是T,然后是A,依此类推,如图 19-2 所示。
英语中最常见的六个字母是ETAOIN。按频率排序的字母完整列表为ETAOINSHRDLCUMWFGYPBVKJXQZ。
回想一下,换位密码通过以不同的顺序排列原始英文明文的字母来加密消息。这意味着密文中的字母频率与原始明文中的字母频率没有区别。例如,在换位密文中,E、T和A应该比Q和Z出现得更频繁。
同样,在凯撒密文和简单替换密文中最常出现的字母更有可能是从最常见的英文字母(如E、T或A)加密而来的。同样,在密文中最不常出现的字母更有可能是从明文中的X、Q 和Z加密而来的。
图 19-1:典型英文文本中每个字母的频率分析
图 19-2:典型英文文本中出现频率最高和最低的字母
在破解维吉尼亚密码时,频率分析非常有用,因为它可以让我们一次一个地暴力破解每个子密钥。例如,如果一条消息是用密钥PIZZA加密的,我们需要用26 ** 5或 11,881,376 个密钥来一次找到整个密钥。然而,为了只暴力破解五个子密钥中的一个,我们只需要尝试 26 种可能性。对五个子密钥中的每一个都这样做意味着我们只需要暴力破解26 × 5或 130 个子密钥。
使用密钥PIZZA,消息中从第一个字母开始的每第五个字母用P加密,从第二个字母开始的每第五个字母用I加密,依此类推。我们可以通过用所有 26 个可能的子密钥解密密文中的每五个字母来暴力破解第一个子密钥。对于第一个子密钥,我们会发现P产生的解密字母比其他 25 个可能的子密钥更匹配英语的字母频率。这将是P是第一个子密钥的一个强有力的指示。然后,我们可以对其他子项重复此操作,直到获得整个项。
匹配字母频率
为了找到消息中的字母频率,我们将使用一种算法,简单地将字符串中的字母从最高频率到最低频率排序。然后算法使用这个有序的字符串来计算这本书所说的频率匹配分数,我们将使用它来确定一个字符串的字母频率与标准英语的字母频率有多相似。
为了计算密文的频率匹配分数,我们从 0 开始,然后每次在密文的六个最频繁的字母中出现一个最频繁的英文字母(E,T,A,O,I,N)时加一个点。在密文的六个最不常用的字母中,每次出现一个最不常用的字母(V、K、J、X、Q 或 Z ),我们都会给分数加一分。
字符串的频率匹配分数可以从 0(字符串的字母频率完全不同于英语字母频率)到 12(字符串的字母频率与常规英语的字母频率相同)。知道密文的频率匹配分数可以揭示关于原始明文的重要信息。
计算简单替换密码的频率匹配分数
我们将使用以下密文来计算使用简单替换密码加密的消息的频率匹配分数:
Sy l nlx sr pyyacao l ylwj eiswi upar lulsxrj isr sxrjsxwjr, ia esmm
rwctjsxsza sj wmpramh, lxo txmarr jia aqsoaxwa sr pqaceiamnsxu, ia esmm caytra
jp famsaqa sj. Sy, px jia pjiac ilxo, ia sr pyyacao rpnajisxu eiswi lyypcor
l calrpx ypc lwjsxu sx lwwpcolxwa jp isr sxrjsxwjr, ia esmm lwwabj sj aqax
px jia rmsuijarj aqsoaxwa. Jia pcsusx py nhjir sr agbmlsxao sx jisr elh.
-Facjclxo Ctrramm
当我们统计这段密文中每个字母出现的频率,从最高频率到最低频率排序,结果是ASRXJILPWMCYOUEQNTHBFZGKVD。A是出现频率最高的字母,S是第二高的字母,以此类推,字母D出现频率最低。
在本例中出现频率最高的六个字母(A、S、R、X、J和I)中,有两个字母(A和I)也是英语中出现频率最高的六个字母之一,它们是E、T、A、O、I和N。因此,我们在频率匹配分数上加 2 分。
密文中最不频繁出现的六个字母是F、Z、G、K、V和D。其中三个字母(Z、K和V)出现在最不频繁出现的字母集中,它们是V、K、J、X、Q和Z。因此我们在分数上再加三分。基于从该密文导出的频率排序 ASRXJILPWMCYOUEQNTHBFZGKVD,频率匹配分数为 5,如图 19-3 所示。
图 19-3:计算简单替换密码的频率匹配分数
使用简单替换密码加密的密文不会有很高的频率匹配分数。简单替换密文的字母频率与常规英语的字母频率不匹配,因为明文字母被密码字母一一替换。例如,如果字母T被加密成字母J,那么J更有可能在密文中频繁出现,尽管它是英语中出现频率最低的字母之一。
计算换位密码的频率匹配分数
这次,让我们计算使用换位密码加密的密文的频率匹配分数:
"I rc ascwuiluhnviwuetnh,osgaa ice tipeeeee slnatsfietgi tittynecenisl. e
fo f fnc isltn sn o a yrs sd onisli ,l erglei trhfmwfrogotn,l stcofiit.
aea wesn,lnc ee w,l eIh eeehoer ros iol er snh nl oahsts ilasvih tvfeh
rtira id thatnie.im ei-dlmf i thszonsisehroe, aiehcdsanahiec gv gyedsB
affcahiecesd d lee onsdihsoc nin cethiTitx eRneahgin r e teom fbiotd n
ntacscwevhtdhnhpiwru"
该密文中最频繁到最不频繁的字母是EISNTHAOCLRFDGWVMUYBPZXQJK。E是最常用的字母,I是第二常用的字母,依此类推。
这份密文中出现频率最高的四个字母(E、I、N和T)恰好也是标准英语(ETAOIN)中出现频率最高的字母。同样,密文中出现频率最低的五个字母(Z、X、Q、J、K)也出现在 VKJXQZ 中,总频率匹配得分为 9,如图 19-4 所示。
图 19-4:计算换位密码的频率匹配分数
使用换位密码加密的密文应该具有比简单替换密文高得多的频率匹配分数。原因是,与简单的替换密码不同,换位密码使用在原始明文中找到的相同字母,但排列顺序不同。因此,每个字母的频率保持不变。
对维吉尼亚密码使用频率分析
要破解维吉尼亚密码,我们需要单独解密子密钥。这意味着我们不能依靠使用英语单词检测,因为我们不能只用一个子密钥来解密足够多的信息。
相反,我们将解密用一个子密钥加密的字母,并执行频率分析,以确定哪个解密的密文产生最接近常规英语的字母频率。换句话说,我们需要找到哪个解密具有最高的频率匹配分数,这很好地表明我们已经找到了正确的子密钥。
我们对第二、第三、第四和第五个子密钥也重复这个过程。现在,我们只是猜测密钥长度是五个字母。(在第 20 章中,您将学习如何使用卡西斯基检查来确定密钥长度。)因为在维吉尼亚密码中每个子密钥(字母表中字母的总数)有 26 个解密,所以计算机只需对一个五个字母的密钥执行26 + 26 + 26 + 26或 156 次解密。这比对每个可能的子密钥组合执行解密要容易得多,总共需要 11,881,376 次解密(26 × 26 × 26 × 26 × 26)!
破解维吉尼亚密码还有更多的步骤,当我们编写破解程序时,你会在第 20 章中了解到。现在,让我们编写一个使用以下有用函数执行频率分析的模块:
getLetterCount()接受一个字符串参数,并返回一个字典,其中包含每个字母在字符串中出现的频率
getFrequencyOrder()获取一个字符串参数,并返回一个由 26 个字母组成的字符串,在该字符串参数中从最频繁到最不频繁排序
englishFreqMatchScore()接受一个字符串参数并返回一个从 0 到 12 的整数,表示一个字母的频率匹配分数
匹配字母频率的源代码
选择文件 -> 新文件,打开新文件编辑器窗口。在文件编辑器中输入以下代码,保存为freqAnalysis.py,确保pyperclip.py在同一个目录下。按F5运行程序。
freqAnalysis.py
# Frequency Finder
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)
ETAOIN = 'ETAOINSHRDLCUMWFGYPBVKJXQZ'
LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
def getLetterCount(message):
# Returns a dictionary with keys of single letters and values of the
# count of how many times they appear in the message parameter:
letterCount = {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0,
'G': 0, 'H': 0, 'I': 0, 'J': 0, 'K': 0, 'L': 0, 'M': 0, 'N': 0,
'O': 0, 'P': 0, 'Q': 0, 'R': 0, 'S': 0, 'T': 0, 'U': 0, 'V': 0,
'W': 0, 'X': 0, 'Y': 0, 'Z': 0}
for letter in message.upper():
if letter in LETTERS:
letterCount[letter] += 1
return letterCount
def getItemAtIndexZero(items):
return items[0]
def getFrequencyOrder(message):
# Returns a string of the alphabet letters arranged in order of most
# frequently occurring in the message parameter.
# First, get a dictionary of each letter and its frequency count:
letterToFreq = getLetterCount(message)
# Second, make a dictionary of each frequency count to the letter(s)
# with that frequency:
freqToLetter = {}
for letter in LETTERS:
if letterToFreq[letter] not in freqToLetter:
freqToLetter[letterToFreq[letter]] = [letter]
else:
freqToLetter[letterToFreq[letter]].append(letter)
# Third, put each list of letters in reverse "ETAOIN" order, and then
# convert it to a string:
for freq in freqToLetter:
freqToLetter[freq].sort(key=ETAOIN.find, reverse=True)
freqToLetter[freq] = ''.join(freqToLetter[freq])
# Fourth, convert the freqToLetter dictionary to a list of
# tuple pairs (key, value), and then sort them:
freqPairs = list(freqToLetter.items())
freqPairs.sort(key=getItemAtIndexZero, reverse=True)
# Fifth, now that the letters are ordered by frequency, extract all
# the letters for the final string:
freqOrder = []
for freqPair in freqPairs:
freqOrder.append(freqPair[1])
return ''.join(freqOrder)
def englishFreqMatchScore(message):
# Return the number of matches that the string in the message
# parameter has when its letter frequency is compared to English
# letter frequency. A "match" is how many of its six most frequent
# and six least frequent letters are among the six most frequent and
# six least frequent letters for English.
freqOrder = getFrequencyOrder(message)
matchScore = 0
# Find how many matches for the six most common letters there are:
for commonLetter in ETAOIN[:6]:
if commonLetter in freqOrder[:6]:
matchScore += 1
# Find how many matches for the six least common letters there are:
for uncommonLetter in ETAOIN[-6:]:
if uncommonLetter in freqOrder[-6:]:
matchScore += 1
return matchScore
按字母顺序存储字母
第 4 行创建了一个名为ETAOIN的变量,它存储字母表中从最频繁到最不频繁排列的 26 个字母:
# Frequency Finder
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)
ETAOIN = 'ETAOINSHRDLCUMWFGYPBVKJXQZ'
当然,并不是所有的英语文本都反映了这种精确的频率排序。你可以很容易地找到一本有一组字母频率的书,其中 Z 比 q 使用得更频繁。例如,欧内斯特·文森特·赖特的小说Gadsby从未使用字母 E,这给了它一组奇怪的字母频率。但是在大多数情况下,包括在我们的模块中,ETAOIN 顺序应该足够准确。
对于一些不同的函数,该模块还需要一个按字母顺序排列的所有大写字母的字符串,所以我们在第 5 行设置了LETTERS常量变量。
LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
LETTERS的作用与前面程序中的SYMBOLS变量相同:提供字符串和整数索引之间的映射。
接下来,我们将看看getLettersCount()函数如何计算存储在message字符串中的每个字母的频率。
计算邮件中的字母数
getLetterCount()函数接受message字符串并返回一个字典值,其键是单个大写字母字符串,其值是存储该字母在message参数中出现的次数的整数。
第 10 行通过给变量分配一个字典来创建变量letterCount,该字典将所有键设置为初始值0:
def getLetterCount(message):
# Returns a dictionary with keys of single letters and values of the
# count of how many times they appear in the message parameter:
letterCount = {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0,
'G': 0, 'H': 0, 'I': 0, 'J': 0, 'K': 0, 'L': 0, 'M': 0, 'N': 0,
'O': 0, 'P': 0, 'Q': 0, 'R': 0, 'S': 0, 'T': 0, 'U': 0, 'V': 0,
'W': 0, 'X': 0, 'Y': 0, 'Z': 0}
我们通过在第 12 行使用一个for循环检查message中的每个字符,增加与键相关的值,直到它们代表每个字母的计数。
for letter in message.upper():
if letter in LETTERS:
letterCount[letter] += 1
for循环遍历大写版本的message中的每个字符,并将该字符赋给letter变量。在第 13 行,我们检查字符是否存在于LETTERS字符串中,因为我们不想计算message中的非字母字符。当letter是LETTERS串的一部分时,第 14 行增加letterCount[letter]处的值。
在第 12 行的for循环结束后,第 16 行的letterCount字典应该有一个计数,显示每个字母在message中出现的频率。本字典从getLetterCount()返回:
return letterCount
例如,在本章中我们将使用下面的字符串(来自en.wikipedia.org/wiki/Alan_Turing):
"""Alan Mathison Turing was a British mathematician, logician, cryptanalyst, and computer
scientist. He was highly influential in the development of computer science, providing a
formalisation of the concepts of "algorithm" and "computation" with the Turing machine. Turing
is widely considered to be the father of computer science and artificial intelligence. During
World War II, Turing worked for the Government Code and Cypher School (GCCS) at Bletchley Park,
Britain's codebreaking centre. For a time he was head of Hut 8, the section responsible for
German naval cryptanalysis. He devised a number of techniques for breaking German ciphers,
including the method of the bombe, an electromechanical machine that could find settings
for the Enigma machine. After the war he worked at the National Physical Laboratory, where
he created one of the first designs for a stored-program computer, the ACE. In 1948 Turing
joined Max Newman's Computing Laboratory at Manchester University, where he assisted in the
development of the Manchester computers and became interested in mathematical biology. He wrote
a paper on the chemical basis of morphogenesis, and predicted oscillating chemical reactions
such as the Belousov-Zhabotinsky reaction, which were first observed in the 1960s. Turing's
homosexuality resulted in a criminal prosecution in 1952, when homosexual acts were still
illegal in the United Kingdom. He accepted treatment with female hormones (chemical castration)
as an alternative to prison. Turing died in 1954, just over two weeks before his 42nd birthday,
from cyanide poisoning. An inquest determined that his death was suicide; his mother and some
others believed his death was accidental. On 10 September 2009, following an Internet campaign,
British Prime Minister Gordon Brown made an official public apology on behalf of the British
government for "the appalling way he was treated." As of May 2012 a private member's bill was
before the House of Lords which would grant Turing a statutory pardon if enacted."""
对于这个字符串值,它有 135 个 A 实例,30 个 B 实例,依此类推,getLetterCount()将返回如下所示的字典:
{'A': 135, 'B': 30, 'C': 74, 'D': 58, 'E': 196, 'F': 37, 'G': 39, 'H': 87,
'I': 139, 'J': 2, 'K': 8, 'L': 62, 'M': 58, 'N': 122, 'O': 113, 'P': 36,
'Q': 2, 'R': 106, 'S': 89, 'T': 140, 'U': 37, 'V': 14, 'W': 30, 'X': 3,
'Y': 21, 'Z': 1}
获取元组的第一个成员
第 19 行的getItemAtIndexZero()函数在向其传递一个元组时返回索引0处的项目:
def getItemAtIndexZero(items):
return items[0]
在程序的后面,我们将把这个函数传递给sort()方法,将字母的频率按数字顺序排序。我们将在第 275 页的上的“将字典条目转换为可排序列表”中详细了解这一点。
按频率排序邮件中的字母
getFrequencyOrder()函数将一个message字符串作为参数,并返回一个包含字母表中 26 个大写字母的字符串,按照它们在message参数中出现的频率排列。如果message是可读的英语而不是随机的胡言乱语,那么这个字符串很可能与ETAOIN常量中的字符串相似,如果不是完全相同的话。getFrequencyOrder()函数中的代码完成了计算字符串频率匹配分数的大部分工作,我们将在第 20 章的维吉尼亚 hacking 程序中使用它。
例如,如果我们将"""Alan Mathison Turing..."""字符串传递给getFrequencyOrder (),该函数将返回字符串' ETIANORSHCLMDGFUPBWYVKXQJZ',因为 E 是该字符串中最常见的字母,接下来是 T、I、A 等等。
getFrequencyOrder()函数由五个步骤组成:
-
计数字符串中的字母
-
创建频率计数和字母列表的字典
-
按相反的顺序排列字母列表
-
将该数据转换成元组列表
-
将列表转换成函数
getFrequencyOrder()返回的最终字符串
让我们依次看看每一步。
用getLetterCount()计数字母
getFrequencyOrder()的第一步用message参数调用第 28 行的getLetterCount()来获得一个名为letterToFreq的字典,包含message中每个字母的计数:
def getFrequencyOrder(message):
# Returns a string of the alphabet letters arranged in order of most
# frequently occurring in the message parameter.
# First, get a dictionary of each letter and its frequency count:
letterToFreq = getLetterCount(message)
如果我们将"""Alan Mathison Turing..."""字符串作为message参数传递,第 28 行给letterToFreq分配如下字典值:
{'A': 135, 'C': 74, 'B': 30, 'E': 196, 'D': 58, 'G': 39, 'F': 37, 'I': 139,
'H': 87, 'K': 8, 'J': 2, 'M': 58, 'L': 62, 'O': 113, 'N': 122, 'Q': 2,
'P': 36, 'S': 89, 'R': 106, 'U': 37, 'T': 140, 'W': 30, 'V': 14, 'Y': 21,
'X': 3, 'Z': 1}
创建频率计数和字母列表的字典
getFrequencyOrder()的第二步是创建一个字典freqToLetter,它的键是频率计数,它的值是包含这些频率计数的字母列表。鉴于letterToFreq字典将字母键映射到频率值,而freqToLetter字典将频率键映射到字母值列表,因此我们需要翻转letterToFreq字典中的键和值。我们翻转键和值,因为多个字母可能具有相同的频率计数:'B'和'W'在我们的示例中都具有频率计数30,所以我们需要将它们放在类似于{30: ['B', 'W']}的字典中,因为字典键必须是惟一的。否则,类似于{30: 'B', 30: 'W'}的字典值将简单地用另一个键值对覆盖其中一个。
为了创建freqToLetter字典,第 32 行首先创建一个空白字典:
# Second, make a dictionary of each frequency count to the letter(s)
# with that frequency:
freqToLetter = {}
for letter in LETTERS:
if letterToFreq[letter] not in freqToLetter:
freqToLetter[letterToFreq[letter]] = [letter]
else:
freqToLetter[letterToFreq[letter]].append(letter)
第 33 行循环遍历LETTERS中的所有字母,第 34 行的if语句检查字母的频率或letterToFreq[letter]是否已经作为关键字存在于freqToLetter中。如果没有,那么第 35 行添加这个键,并以字母列表作为值。如果字母的频率已经作为关键字存在于freqToLetter中,第 37 行简单地将该字母附加到已经在letterToFreq[letter]中的列表的末尾。
使用使用"""Alan Mathison Turing..."""字符串创建的示例值letterToFreq,freqToLetter现在应该返回如下内容:
{1: ['Z'], 2: ['J', 'Q'], 3: ['X'], 135: ['A'], 8: ['K'], 139: ['I'],
140: ['T'], 14: ['V'], 21: ['Y'], 30: ['B', 'W'], 36: ['P'], 37: ['F', 'U'],
39: ['G'], 58: ['D', 'M'], 62: ['L'], 196: ['E'], 74: ['C'], 87: ['H'],
89: ['S'], 106: ['R'], 113: ['O'], 122: ['N']}
请注意,字典的键现在包含频率计数,其值包含具有这些频率的字母列表。
getFrequencyOrder()的第三步涉及到对freqToLetter的每个列表中的字母串进行排序。回想一下,freqToLetter[freq]计算出字母的列表*,其频率计数为freq。我们使用列表是因为两个或更多的字母可能具有相同的频率计数,在这种情况下,列表将具有由两个或更多字母组成的字符串。
当多个字母具有相同的频率计数时,我们希望按照与它们在ETAOIN字符串中出现的顺序相反的顺序对这些字母进行排序。这使得排序一致,并最小化偶然增加频率匹配分数的可能性。
例如,假设字母 V、I、N 和 K 的频率计数对于我们试图评分的字符串都是相同的。我们还假设字符串中的四个字母比 V、I、N 和 K 具有更高的频率计数,而十八个字母具有更低的频率计数。在这个例子中,我将使用x作为这些字母的占位符。图 19-5 显示了将这四个字母按顺序排列的样子。
图 19-5:如果四个字母按ETAOIN顺序排列,频率匹配得分将获得两分。
在这种情况下,I 和 N 给频率匹配分数增加了两分,因为 I 和 N 是前六个最频繁出现的字母,即使它们在这个示例字符串中出现的频率没有 V 和 K 高。因为频率匹配分数的范围只有 0 到 12,所以这两点可以产生很大的影响!但是通过将相同频率的字母以相反的顺序排列,我们可以将一个字母得分过高的可能性降到最低。图 19-6 以相反的顺序显示了这四个字母。
图 19-6:如果四个字母顺序相反,频率匹配分数不会增加。
通过以相反的顺序排列字母,我们避免了通过 I、N、V 和 k 的随机排序来人为增加频率匹配分数。如果有 18 个字母具有较高的频率计数,4 个字母具有较低的频率计数,也是如此,如图 19-7 所示。
图 19-7:对不太频繁的字母颠倒ETAOIN顺序也避免了增加匹配分数。
反向排序顺序确保 K 和 V 不匹配英语中六个最不频繁的字母中的任何一个,并且再次避免将频率匹配分数增加两分。
为了对freqToLetter字典中的每个列表值进行逆序排序,我们需要向 Python 的sort()函数传递一个方法。让我们看看如何将一个函数或方法传递给另一个函数。
将函数按值传递
在第 42 行,我们没有调用find()方法,而是将find作为一个值传递给sort()方法调用:
freqToLetter[freq].sort(key=ETAOIN.find, reverse=True)
我们可以这样做,因为在 Python 中,函数可以被视为值。事实上,定义一个名为spam的函数与将函数定义存储在名为spam的变量中是一样的。要查看示例,请在交互式 shell 中输入以下代码:
>>> def spam():
... print('Hello!')
...
>>> spam()
Hello!
>>> eggs = spam
>>> eggs()
Hello!
在这个示例代码中,我们定义了一个名为spam()的函数来打印字符串'Hello!'。这也意味着变量spam持有函数定义。然后我们将变量spam中的函数复制到变量eggs中。这样做了之后,我们就可以像调用spam()一样调用eggs()了!请注意,赋值语句在spam后不包含圆括号。如果是的话,它将调用spam()函数并将变量eggs设置为从spam()函数得到的返回值。
因为函数是值,所以我们可以在函数调用中将它们作为参数传递。在交互式 shell 中输入以下内容以查看示例:
>>> def doMath(func):
... return func(10, 5)
...
>>> def adding(a, b):
... return a + b
...
>>> def subtracting(a, b):
... return a - b
...
>>> doMath(adding) # ➊
15
>>> doMath(subtracting)
5
这里我们定义了三个函数:doMath()、adding()和subtracting()。当我们将adding中的函数传递给doMath()调用 ➊ 时,我们正在将adding赋给变量func,而func(10, 5)正在调用adding()并将10和5传递给它。因此调用func(10, 5)实际上与调用adding和(10, 5)相同。这就是doMath(adding)返回15的原因。同样,当我们将subtracting传递给doMath()调用时,doMath(subtracting)返回5,因为func(10, 5)与subtracting(10, 5)相同。
向sort()方法传递函数
将函数或方法传递给sort()方法让我们实现不同的排序行为。通常,sort()按字母顺序对列表中的值进行排序:
>>> spam = ['C', 'B', 'A']
>>> spam.sort()
>>> spam
['A', 'B', 'C']
但是,如果我们为关键字参数key传递一个函数(或方法),当列表中的每个值被传递给那个函数时,列表中的值就按照函数的返回值排序。例如,我们也可以将ETAOIN.find()字符串方法作为key传递给sort()调用,如下所示:
>>> ETAOIN = 'ETAOINSHRDLCUMWFGYPBVKJXQZ'
>>> spam.sort(key=ETAOIN.find)
>>> spam
['A', 'C', 'B']
当我们将ETAOIN.find传递给sort()方法时,sort()方法首先对每个字符串调用find()方法,以便ETAOIN.find('A')、ETAOIN.find('B')和ETAOIN.find('C')分别返回索引2、19和11——每个字符串在ETAOIN字符串中的位置。然后sort()使用这些返回的索引,而不是原来的'A'、'B'和'C'字符串,对spam列表中的项目进行排序。这就是为什么'A'、'B'和'C'字符串被排序为'A'、'C'和'B',反映它们在ETAOIN中出现的顺序。
用sort()方法反转字母列表
为了以相反的顺序对字母进行排序,我们首先需要通过将ETAOIN.find分配给key来基于ETAOIN字符串对它们进行排序。在对所有字母调用该方法使它们都成为索引后,sort()方法根据字母的数字索引对它们进行排序。
通常,sort()函数按字母或数字顺序对它所调用的任何列表进行排序,这被称为升序。为了以降序、反向字母顺序或反向数字顺序对项目进行排序,我们将True传递给sort()方法的reverse关键字参数。
我们在第 42 行做了所有这些:
# Third, put each list of letters in reverse "ETAOIN" order, and then
# convert it to a string:
for freq in freqToLetter:
freqToLetter[freq].sort(key=ETAOIN.find, reverse=True)
freqToLetter[freq] = ''.join(freqToLetter[freq])
回想一下,在这一点上,freqToLetter是一个字典,它将整数频率计数存储为它的键,将字母字符串列表存储为它的值。键freq处的字母串被排序,而不是freqToLetter字典本身。字典无法排序,因为它们没有顺序:不像列表项那样有“第一个”或“最后一个”键值对。
再次使用freqToLetter的"""Alan Mathison Turing..."""示例值,当循环结束时,这将是存储在freqToLetter中的值:
{1: 'Z', 2: 'QJ', 3: 'X', 135: 'A', 8: 'K', 139: 'I', 140: 'T', 14: 'V',
21: 'Y', 30: 'BW', 36: 'P', 37: 'FU', 39: 'G', 58: 'MD', 62: 'L', 196: 'E',
74: 'C', 87: 'H', 89: 'S', 106: 'R', 113: 'O', 122: 'N'}
注意,30、37和58键的字符串都是以相反的顺序排序的。在循环执行之前,键值对如下所示:{30: ['B', 'W'], 37: ['F', 'U'], 58: ['D', 'M'], ...}。循环之后,它们应该是这样的:{30: 'BW', 37: 'FU', 58: 'MD', ...}。
第 43 行的join()方法调用将字符串列表变成一个单独的字符串。例如,freqToLetter[30]中的值是['B', 'W'],被联接为'BW'。
按频率排序字典列表
getFrequencyOrder()的第四步是按照频率计数对freqToLetter字典中的字符串进行排序,并将字符串转换成一个列表。请记住,因为字典中的键值对是无序的,所以字典中所有键或值的列表值将是一个随机顺序的项目列表。这意味着我们还需要对这个列表进行排序。
使用key()、values()和items()字典方法
keys()、values()和items()字典方法都将字典的一部分转换成非字典数据类型。将字典转换成另一种数据类型后,可以使用list()函数将其转换成列表。
在交互式 shell 中输入以下内容以查看示例:
>>> spam = {'cats': 10, 'dogs': 3, 'mice': 3}
>>> spam.keys()
dict_keys(['mice', 'cats', 'dogs'])
>>> list(spam.keys())
['mice', 'cats', 'dogs']
>>> list(spam.values())
[3, 10, 3]
为了获得字典中所有键的列表值,我们可以使用keys()方法返回一个dict_keys对象,然后我们可以将该对象传递给list()函数。一个类似的名为values()的字典方法返回一个dict_values对象。这些例子分别给出了字典的键列表和值列表。
为了同时获得键和值,我们可以使用items() dictionary 方法返回一个dict_items对象,这使得键值对成为元组。然后我们可以将元组传递给list()。在交互式 shell 中输入以下内容以查看实际效果:
>>> spam = {'cats': 10, 'dogs': 3, 'mice': 3}
>>> list(spam.items())
[('mice', 3), ('cats', 10), ('dogs', 3)]
通过调用items()和list(),我们将spam字典的键值对转换成元组列表。这正是我们需要用freqToLetter字典做的事情,这样我们就可以按频率按数字顺序对字母串进行排序。
将字典条目转换为可排序列表
freqToLetter字典将整数频率计数作为键,将单字母字符串列表作为值。为了按频率顺序对字符串进行排序,我们调用items()方法和list()函数来创建字典的键值对的元组列表。然后,我们将这个元组列表存储在第 47 行名为freqPairs的变量中:
# Fourth, convert the freqToLetter dictionary to a list of
# tuple pairs (key, value), and then sort them:
freqPairs = list(freqToLetter.items())
在第 48 行,我们将之前在程序中定义的getItemAtIndexZero函数值传递给sort()方法调用:
freqPairs.sort(key=getItemAtIndexZero, reverse=True)
getItemAtIndexZero()函数获取元组中的第一项,在本例中是频率计数整数。这意味着freqPairs中的项目按照频率计数整数的数字顺序排序。第 48 行还为reverse关键字参数传递了True,因此元组从最大频率计数到最小频率计数反向排序。
继续" ""Alan Mathison Turing..."""的例子,在第 48 行执行之后,这将是freqPairs的值:
[(196, 'E'), (140, 'T'), (139, 'I'), (135, 'A'), (122, 'N'), (113, 'O'),
(106, 'R'), (89, 'S'), (87, 'H'), (74, 'C'), (62, 'L'), (58, 'MD'), (39, 'G'),
(37, 'FU'), (36, 'P'), (30, 'BW'), (21, 'Y'), (14, 'V'), (8, 'K'), (3, 'X'),
(2, 'QJ'), (1, 'Z')]
freqPairs变量现在是从最频繁到最不频繁字母排序的元组列表:每个元组中的第一个值是表示频率计数的整数,第二个值是包含与频率计数相关的字母的字符串。
创建排序后的字母列表
getFrequencyOrder()的第五步是从freqPairs中的排序列表中创建所有字符串的列表。我们希望得到一个字符串值,它的字母按照出现的频率排序,所以我们不需要freqPairs中的整数值。变量freqOrder从第 52 行的空白列表开始,第 53 行的for循环将freqPairs中每个元组的索引1处的字符串追加到freqOrder的末尾:
# Fifth, now that the letters are ordered by frequency, extract all
# the letters for the final string:
freqOrder = []
for freqPair in freqPairs:
freqOrder.append(freqPair[1])
继续这个例子,在第 53 行的循环结束后,freqOrder应该包含['E',``'T',``'``I',``'A',``'N',``'O',``'R',``'S',``'H',``'C',``'L',``'MD',``'``G',``'FU',``'P',``'BW',``'Y',``'V',``'K',``'X',``'QJ',``'Z']作为它的值。
第 56 行通过使用join()方法将字符串连接起来,从freqOrder中的字符串列表创建一个字符串:
return ''.join(freqOrder)
对于"""Alan Mathison Turing..."""示例,getFrequencyOrder()返回字符串'ETIANORSHCLMDGFUPBWYVKXQJZ'。根据这种排序,E 是示例字符串中最频繁出现的字母,T 是第二频繁出现的字母,I 是第三频繁出现的字母,依此类推。
既然我们已经将消息的字母频率作为一个字符串值,我们可以将它与英语的字母频率('ETAOINSHRDLCUMWFGYPBVKJXQZ')的字符串值进行比较,以查看它们的匹配程度。
计算消息的频率匹配分数
englishFreqMatchScore()函数为message获取一个字符串,然后返回一个介于0和12之间的整数,表示该字符串的频率匹配分数。分数越高,message中的字母频率越接近正常英语文本的频率。
def englishFreqMatchScore(message):
# Return the number of matches that the string in the message
# parameter has when its letter frequency is compared to English
# letter frequency. A "match" is how many of its six most frequent
# and six least frequent letters are among the six most frequent and
# six least frequent letters for English.
freqOrder = getFrequencyOrder(message)
计算频率匹配分数的第一步是通过调用getFrequencyOrder()函数得到message的字母频率排序,我们在第 65 行做了这个。我们将有序的字符串存储在变量freqOrder中。
matchScore变量从第 67 行的0开始,并由从第 69 行开始的for循环递增,该循环比较ETAOIN字符串的前六个字母和freqOrder的前六个字母,为它们共有的每个字母给出一个点:
matchScore = 0
# Find how many matches for the six most common letters there are:
for commonLetter in ETAOIN[:6]:
if commonLetter in freqOrder[:6]:
matchScore += 1
回想一下,[:6]片段与[0:6]相同,所以第 69 行和第 70 行分别对ETAOIN和freqOrder字符串的前六个字母进行了切片。如果字母 E、T、A、O、I 或 N 中的任何一个也在freqOrder字符串的前六个字母中,则第 70 行的条件为True,第 71 行递增matchScore。
第 73 到 75 行类似于第 69 到 71 行,除了在这种情况下,它们检查ETAOIN字符串中的最后六个字母(V、K、J、X、Q和Z)是否在freqOrder字符串中的最后六个字母中。如果是,则matchScore递增。
# Find how many matches for the six least common letters there are:
for uncommonLetter in ETAOIN[-6:]:
if uncommonLetter in freqOrder[-6:]:
matchScore += 1
第 77 行返回matchScore中的整数:
return matchScore
在计算频率匹配分数时,我们忽略频率顺序中间的 14 个字母。这些中间字母的频率彼此过于相似,无法给出有意义的信息。
总结
在本章中,您学习了如何使用sort()函数按字母或数字顺序对列表值进行排序,以及如何使用reverse和key关键字参数以不同方式对列表值进行排序。您学习了如何使用keys()、values()和items()字典方法将字典转换成列表。您还了解了可以在函数调用中将函数作为值传递。
在第 20 章中,我们将使用我们在本章中编写的频率分析模块来破解维吉尼亚密码!
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes找到。
-
什么是频率分析?
-
英语中最常用的六个字母是什么?
-
运行以下代码后,
spam变量包含什么?spam = [4, 6, 2, 8] spam.sort(reverse=True) -
如果
spam变量包含一个字典,如何获取字典中键的列表值?