Skip to content

Latest commit

 

History

History
652 lines (446 loc) · 40.1 KB

File metadata and controls

652 lines (446 loc) · 40.1 KB

11 以编程方式检测英语

原文:https://inventwithpython.com/cracking/chapter11.html

老头子说了些更长更复杂的话。过了一会儿,Waterhouse(现在戴着他的密码分析师帽子,在明显的随机性中寻找意义,他的神经回路利用了信号中的冗余)意识到这个人说的是带有浓重口音的英语。 —尼尔·斯蒂芬森, Cryptonomicon

Images

以前,我们使用换位文件密码来加密和解密整个文件,但我们还没有尝试编写一个暴力破解程序来破解密码。用换位文件加密法加密的信息可以有成千上万个可能的密钥,你的计算机仍然可以很容易地对这些密钥进行暴力破解,但是你必须查看成千上万个解密文本才能找到一个正确的明文。你可以想象,这可能是一个大问题,但有一个解决办法。

当计算机使用错误的密钥解密消息时,得到的字符串是垃圾文本而不是英文文本。我们可以给计算机编程,让它识别解密后的信息是英语。这样,如果计算机使用错误的密钥解密,它知道继续尝试下一个可能的密钥。最终,当计算机尝试用一个密钥来解密英文文本时,它会停下来让你注意到这个密钥,让你不必查看成千上万个不正确的解密。

本章涵盖的主题

  • 字典数据类型

  • split()

  • None

  • 被零除误差

  • float()int()str()函数和整数除法

  • append()列表法

  • 默认参数

  • 计算百分比

计算机如何理解英语?

计算机不能理解英语,至少不能像人类理解英语那样理解。计算机不理解数学、象棋或人类的反叛,就像时钟不理解午餐时间一样。计算机只是一个接一个地执行指令。但这些指令可以模仿复杂的行为来解决数学问题,赢得象棋比赛,或追捕人类抵抗运动的未来领导人。

理想情况下,我们需要创建的是一个 Python 函数(姑且称之为isEnglish()函数),我们可以向它传递一个字符串,如果该字符串是英文文本,则返回值为True,如果是随机的乱码,则返回值为False。让我们看看一些英文文本和一些垃圾文本,看看它们可能有什么模式:

Robots are your friends. Except for RX-686\. She will try to eat you.
ai-pey  e. xrx ne augur iirl6 Rtiyt fhubE6d hrSei t8..ow eo.telyoosEs  t

请注意,英文文本是由您可以在字典中找到的单词组成的,但垃圾文本不是。因为单词通常由空格分隔,所以检查消息字符串是否是英语的一种方法是在每个空格处将消息分割成更小的字符串,并检查每个子字符串是否是字典中的单词。要将消息字符串分割成子字符串,我们可以使用名为split()的 Python字符串方法,该方法通过查找字符之间的空格来检查每个单词的开始和结束位置。(第 150 页上的split()方法对此有更详细的介绍。)然后,我们可以使用if语句将每个子串与字典中的每个单词进行比较,如下面的代码所示:

if word == 'aardvark' or word == 'abacus' or word == 'abandon' or word ==
'abandoned' or word == 'abbreviate' or word == 'abbreviation' or word ==
'abdomen' or ...

我们可以写出那样的代码,但我们可能不会,因为把它们都打出来会很乏味。幸运的是,我们可以使用英语字典文件,这是包含几乎每个英语单词的文本文件。我将为您提供一个字典文件来使用,所以我们只需要编写isEnglish()函数来检查消息中的子字符串是否在字典文件中。

不是每个单词都存在于我们的字典文件中。字典文件可能不完整;例如,它可能没有单词aardvark。也有非常好的解密,其中可能有非英语单词,如我们的英语句子示例中的RX-686。明文也可以是不同的语言,但是我们现在假设它是英语。

所以isEnglish()函数不会是万无一失的,但是如果字符串参数中的大多数单词是英语单词,那么很有可能该字符串是英语文本。用错误的密钥解密的密文解密成英文的概率非常低。

你可以从www.nostarch.com/crackingcodes下载我们将为这本书使用的字典文件(超过 45000 个单词)。字典文本文件以大写形式每行列出一个单词。打开它,你会看到这样的东西:

AARHUS
AARON
ABABA
ABACK
ABAFT
ABANDON
ABANDONED
ABANDONING
ABANDONMENT
ABANDONS
--snip--

我们的isEnglish()函数将一个解密的字符串分割成单独的子字符串,并检查每个子字符串是否作为一个单词存在于字典文件中。如果一定数量的子字符串是英语单词,我们会将该文本识别为英语。如果文本是英文的,我们很有可能用正确的密钥成功解密了密文。

检测英语模块的源代码

选择文件 -> 新文件,打开新文件编辑器窗口。在文件编辑器中输入以下代码,然后保存为detectEnglish.py。确保dictionary.txtdetectEnglish.py在同一个目录下,否则这段代码不会工作。按F5运行程序。

detectEnglish.py

# Detect English module
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

# To use, type this code:
#   import detectEnglish
#   detectEnglish.isEnglish(someString) # Returns True or False
# (There must be a "dictionary.txt" file in this directory with all
# English words in it, one word per line. You can download this from
# https://www.nostarch.com/crackingcodes/.)
UPPERLETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
LETTERS_AND_SPACE = UPPERLETTERS + UPPERLETTERS.lower() + ' \t\n'

def loadDictionary():
    dictionaryFile = open('dictionary.txt')
    englishWords = {}
    for word in dictionaryFile.read().split('\n'):
        englishWords[word] = None
    dictionaryFile.close()
    return englishWords

ENGLISH_WORDS = loadDictionary()


def getEnglishCount(message):
    message = message.upper()
    message = removeNonLetters(message)
    possibleWords = message.split()

    if possibleWords == []:
        return 0.0 # No words at all, so return 0.0

    matches = 0
    for word in possibleWords:
        if word in ENGLISH_WORDS:
            matches += 1
    return float(matches) / len(possibleWords)


def removeNonLetters(message):
    lettersOnly = []
    for symbol in message:
        if symbol in LETTERS_AND_SPACE:
            lettersOnly.append(symbol)
    return ''.join(lettersOnly)


def isEnglish(message, wordPercentage=20, letterPercentage=85):
    # By default, 20% of the words must exist in the dictionary file, and
    # 85% of all the characters in the message must be letters or spaces
    # (not punctuation or numbers).
    wordsMatch = getEnglishCount(message) * 100 >= wordPercentage
    numLetters = len(removeNonLetters(message))
    messageLettersPercentage = float(numLetters) / len(message) * 100
    lettersMatch = messageLettersPercentage >= letterPercentage
    return wordsMatch and lettersMatch

检测英语模块的样本运行

我们将在本章中编写的detectEnglish.py程序不会自己运行。相反,其他加密程序将导入detectEnglish.py,以便它们可以调用detectEnglish.isEnglish()函数,当字符串被确定为英文时,该函数将返回True。这就是为什么我们没有给detectEnglish.py一个main()函数。detectEnglish.py中的其他函数是isEnglish()函数将调用的帮助函数。我们在这一章所做的所有工作将允许任何程序用一个import语句导入detectEnglish模块并使用其中的函数。

您还可以在交互式 shell 中使用该模块来检查单个字符串是否为英语,如下所示:

>>> import detectEnglish
>>> detectEnglish.isEnglish('Is this sentence English text?')
True

在本例中,该函数确定字符串'Is this sentence English text?'确实是英文,因此返回True

指令和设置常数

让我们看一下detectEnglish.py程序的第一部分。前九行代码是注释,给出了如何使用这个模块的说明。

# Detect English module
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

# To use, type this code:
#   import detectEnglish
#   detectEnglish.isEnglish(someString) # Returns True or False
# (There must be a "dictionary.txt" file in this directory with all
# English words in it, one word per line. You can download this from
# https://www.nostarch.com/crackingcodes/.)
UPPERLETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
LETTERS_AND_SPACE = UPPERLETTERS + UPPERLETTERS.lower() + ' \t\n'

前九行代码是注释,给出了如何使用这个模块的说明。他们提醒用户,除非名为dictionary.txt的文件与detectEnglish.py在同一个目录下,否则这个模块不会工作。

第 10 行和第 11 行将一些变量设置为常量,它们都是大写的。正如你在第 5 章中所学的,常量是变量,它们的值一旦设定就永远不能改变。UPPERLETTERS是一个包含 26 个大写字母的常量,我们设置它是为了方便和节省键入时间。我们使用UPPERLETTERS常量来设置LETTERS_AND_SPACE,它包含字母表中所有的大小写字母以及空格字符、制表符和换行符。我们没有输入所有的大写和小写字母,而是将UPPERLETTERS.lower()返回的小写字母和额外的非字母字符与UPPERLETTERS连接起来。制表符和换行符用转义符\t\n表示。

字典数据类型

在我们继续剩余的detectEnglish.py代码之前,您需要了解更多关于字典数据类型的知识,以理解如何将文件中的文本转换成字符串值。字典数据类型(不要与字典文件混淆)存储值,它可以像列表一样包含多个其他值。在列表中,我们使用整数索引来检索列表中的项目,例如spam[42]。但是对于字典值中的每一项,我们使用一个键来检索值。虽然我们只能使用整数从列表中检索条目,但是字典值中的键可以是整数或字符串,比如spam['hello']spam[42]。字典让我们比列表更灵活地组织程序数据,并且不以任何特定的顺序存储项目。字典不像列表那样使用方括号,而是使用大括号。比如一个空字典长这样{}

请记住,字典文件和字典值是完全不同的概念,只是名称相似而已。一个 Python 字典值可以包含多个其他值。字典文件是包含英语单词的文本文件。

字典的条目被输入为键值对,其中键和值由冒号分隔。多个键值对用逗号分隔。要从字典中检索值,请使用方括号,方括号之间有关键字,类似于使用列表进行索引时的情况。要尝试使用键从字典中检索值,请在交互式 shell 中输入以下内容:

>>> spam = {'key1': 'This is a value', 'key2': 42}
>>> spam['key1']
'This is a value'

首先,我们用两个键值对建立了一个名为spam的字典。然后我们访问与'key1'字符串键相关的值,这是另一个字符串。与列表一样,您可以在字典中存储所有类型的数据。

注意,和列表一样,变量不存储字典值;相反,它们存储对字典的引用。以下示例代码显示了引用同一字典的两个变量:

>>> spam = {'hello': 42}
>>> eggs = spam
>>> eggs['hello'] = 99
>>> eggs
{'hello': 99}
>>> spam
{'hello': 99}

第一行代码建立了另一个名为spam的字典,这次只有一个键值对。您可以看到它存储了一个与'hello'字符串键相关联的整数值42。第二行将字典键值对分配给另一个名为eggs的变量。然后,您可以使用eggs将与'hello'字符串键相关联的原始字典值更改为99。现在,eggsspam这两个变量应该用更新后的值返回相同的字典键值对。

字典和列表的区别

字典在许多方面类似于列表,但也有一些重要的区别:

  • 字典项目没有任何顺序。字典中没有列表中的第一项或最后一项。

  • 不能用+运算符连接字典。如果要添加新项目,请使用新键索引。比如foo['a new key'] = 'a string'

  • 列表只有范围从0到列表长度减一的整数索引值,但是字典可以使用任何键。如果您在变量spam中存储了一个字典,那么您可以在spam[3]中存储一个值,而不需要spam[0]spam[1]spam[2]的值。

增加或改变字典中的条目

还可以通过使用字典键作为索引来添加或更改字典中的值。在交互式 shell 中输入以下内容,查看其工作原理:

>>> spam = {42: 'hello'}
>>> print(spam[42])
hello
>>> spam[42] = 'goodbye'
>>> print(spam[42])
goodbye

该字典有一个与关键字42相关联的现有字典字符串值'hello'。我们可以使用spam[42] = 'goodbye'给那个键重新分配一个新的字符串值'goodbye'。为现有字典键分配新值会覆盖与该键关联的原始值。例如,当我们试图用关键字42访问字典时,我们会得到与之相关的新值。

正如列表可以包含其他列表一样,字典也可以包含其他字典(或列表)。要查看示例,请在交互式 shell 中输入以下内容:

>>> foo = {'fizz': {'name': 'Al', 'age': 144}, 'moo':['a', 'brown', 'cow']}
>>> foo['fizz']
{'age': 144, 'name': 'Al'}
>>> foo['fizz']['name']
'Al'
>>> foo['moo']
['a', 'brown', 'cow']
>>> foo['moo'][1]
'brown'

这个示例代码展示了一个字典(名为foo),它包含两个键'fizz''moo',每个键对应一个不同的值和数据类型。'fizz'键保存另一个字典,'键保存一个列表。(请记住,字典值不会按顺序排列它们的项目。这就是为什么foo['fizz']以不同于您输入的顺序显示键值对。)要从嵌套在另一个字典中的字典中检索一个值,首先要使用方括号指定想要访问的更大数据集的键,在本例中是'fizz'。然后再次使用方括号,输入与想要检索的嵌套字符串值'Al'相对应的键'name'

使用len()函数配合字典使用

函数显示了列表中的项目数或字符串中的字符数。它还可以显示字典中条目的数量。在交互式 shell 中输入以下代码,看看如何使用len()函数来计算字典中的条目:

>>> spam = {}
>>> len(spam)
0
>>> spam['name'] = 'Al'
>>> spam['pet'] = 'Zophie the cat'
>>> spam['age'] = 89
>>> len(spam)
3

这个例子的第一行显示了一个名为spam的空字典。len()函数正确显示这个空字典的长度是0。然而,在您将以下三个值'Al''Zophie the cat'89引入字典后,len()函数现在为您刚刚分配给变量的三个键值对返回3

在字典中使用 in 运算符

您可以使用in操作符来查看字典中是否存在某个键。重要的是要记住in操作符检查的是键,而不是值。要查看该操作符的运行情况,请在交互式 shell 中输入以下内容:

>>> eggs = {'foo': 'milk', 'bar': 'bread'}
>>> 'foo' in eggs
True
>>> 'milk' in eggs
False
>>> 'blah blah blah' in eggs
False
>>> 'blah blah blah' not in eggs
True

我们用一些键值对建立了一个名为eggs的字典,然后使用in操作符检查字典中存在哪些键。钥匙'foo'eggs中的一个钥匙,所以True被返回。鉴于'milk'返回False是因为它是一个值,而不是一个键,'blah blah blah'的计算结果是False,因为这个字典中不存在这样的条目。not in操作符也处理字典值,这可以在最后一个命令中看到。

使用字典查找条目比使用列表更快

想象一下交互式 shell 中的以下列表和字典值:

>>> listVal = ['spam', 'eggs', 'bacon']
>>> dictionaryVal = {'spam':0, 'eggs':0, 'bacon':0}

Python 对dictionaryVal中的表达式'bacon'求值的速度比listVal中的'bacon'快一点。这是因为对于列表,Python 必须从列表的开头开始,然后按顺序遍历每个项目,直到找到搜索项目。如果列表非常大,Python 必须搜索大量条目,这个过程会花费很多时间。

但是字典,也称为哈希表,直接翻译计算机内存中存储键值对的位置,这就是为什么字典的条目没有顺序。不管字典有多大,查找任何条目总是要花同样多的时间。

当搜索短列表和字典时,这种速度上的差异几乎不明显。但是我们的detectEnglish模块将会有成千上万的条目,当isEnglish()函数被调用时,我们将在代码中使用的表达式word in ENGLISH_WORDS将会被多次求值。在处理大量项目时,使用字典值可以加快这个过程。

用字典为循环使用

您还可以使用for循环遍历字典中的键,就像您可以遍历列表中的条目一样。在交互式 shell 中输入以下内容:

>>> spam = {'name': 'Al', 'age': 99}
>>> for k in spam:
...   print(k, spam[k])
...
Age 99
name Al

要使用for语句迭代字典中的键,从for关键字开始。设置变量k,使用in关键字指定要循环遍历spam,并以冒号结束语句。如您所见,输入print(k, spam[k])将返回字典中的每个键及其对应的值。

实现字典文件

现在让我们返回到detectEnglish.py并设置字典文件。字典文件位于用户的硬盘上,但是除非我们将该文件中的文本作为字符串值加载,否则我们的 Python 代码无法使用它。我们将创建一个loadDictionary()助手函数来完成这项工作:

def loadDictionary():
    dictionaryFile = open('dictionary.txt')
    englishWords = {}

首先,我们通过调用open()并传递文件名的字符串'dictionary.txt'来获取字典的 file 对象。然后我们将字典变量命名为englishWords,并将其设置为一个空字典。

我们将把字典文件(存储英语单词的文件)中的所有单词存储在字典值(Python 数据类型)中。相似的名字很不幸,但两者完全不同。即使我们可以使用一个列表来存储字典文件中每个单词的字符串值,我们还是使用字典来代替,因为in操作符在字典上比在列表上工作得更快。

接下来,您将了解到split()字符串方法,我们将使用它将字典文件分割成子字符串。

的拆分()方法

*split()字符串方法接受一个字符串,并通过在每个空格处分割传递的字符串来返回几个字符串的列表。要查看如何工作的示例,请在交互式 shell 中输入以下内容:

>>> 'My very energetic mother    just served us Nutella.'.split()
['My', 'very', 'energetic', 'mother', 'just', 'served', 'us', 'Nutella.']

结果是一个包含八个字符串的列表,原始字符串中的每个单词对应一个字符串。即使列表中有多个空格,也会从列表项中删除空格。您可以向split()方法传递一个可选参数,告诉它在不同的字符串而不是空格上进行分割。在交互式 shell 中输入以下内容:

>>> 'helloXXXworldXXXhowXXXareXXyou?'.split('XXX')
['hello', 'world', 'how', 'areXXyou?']

注意,该字符串没有任何空格。使用split('XXX')'XXX'出现的地方分割原始字符串,产生一个四个字符串的列表。字符串的最后一部分'areXXyou?'没有被拆分,因为'XX''XXX'不同。

将字典文件拆分成单个单词

让我们回到我们在detectEnglish.py中的源代码,看看我们如何在字典文件中分割字符串并将每个单词存储在一个键中。

    for word in dictionaryFile.read().split('\n'):
        englishWords[word] = None

让我们分解第 16 行。dictionaryFile变量存储打开文件的文件对象。dictionaryFile.read()方法调用读取整个文件,并将其作为一个大字符串值返回。然后,我们在这个长字符串上调用split()方法,并在换行符上拆分。因为字典文件每行有一个单词,所以按换行符拆分会返回一个由字典文件中的每个单词组成的列表值。

行首的for循环遍历每个单词,将每个单词存储在一个键中。但是我们不需要与键相关联的值,因为我们使用的是 dictionary 数据类型,所以我们将只存储每个键的None值。

None是一种值,可以分配给变量来表示缺少值。布尔数据类型只有两个值,而 NoneType 只有一个值None。它总是不带引号,大写字母N

例如,假设您有一个名为quizAnswer的变量,它保存了用户对一个是非题的回答。如果用户跳过一个问题而没有回答,那么将quizAnswer赋给None作为默认值,而不是赋给TrueFalse是最有意义的。否则,它可能看起来像用户回答了问题,而他们没有。同样,函数调用通过到达函数的末尾而不是从一个评估为Nonereturn语句退出,因为它们不返回任何东西。

第 17 行使用被迭代的单词作为englishWords中的键,并将None存储为该键的值。

返回字典数据

for循环结束后,englishWords字典中应该有数万个键。此时,我们关闭文件对象,因为我们已经完成了对它的读取,然后返回englishWords:

    dictionaryFile.close()
    return englishWords

然后我们调用loadDictionary()并将它返回的字典值存储在一个名为ENGLISH_WORDS的变量中:

ENGLISH_WORDS = loadDictionary()

我们想在detectEnglish模块中的其余代码之前调用loadDictionary(),但是 Python 必须在我们调用函数之前执行loadDictionary()def语句。这就是为什么ENGLISH_WORDS的赋值在loadDictionary()函数代码之后的原因。

统计消息中的英文单词数

程序代码的第 24 行到第 27 行定义了getEnglishCount()函数,该函数接受一个字符串参数并返回一个浮点值,该值指示识别的英语单词与总单词的比率。我们将把比率表示为0.01.0之间的一个值。值0.0意味着message中没有单词是英语单词,而1.0意味着message中的所有单词都是英语单词。最有可能的是,getEnglishCount()将返回一个介于0.01.0之间的浮点值。isEnglish()函数使用该返回值来确定是评估为True还是False

def getEnglishCount(message):
    message = message.upper()
    message = removeNonLetters(message)
    possibleWords = message.split()

为了编写这个函数,首先我们从message中的字符串创建一个单个单词字符串的列表。第 25 行将字符串转换成大写字母。然后第 26 行通过调用removeNonLetters()删除字符串中的非字母字符,比如数字和标点符号。(稍后您将看到这个函数是如何工作的。)最后,第 27 行的split()方法将字符串拆分成单个单词,并将它们存储在一个名为possibleWords的变量中。

例如,如果字符串' Hello there. How are you?'在调用getEnglishCount()之后被传递,那么在第 25 到 27 行执行之后存储在possibleWords中的值将是['HELLO', 'THERE', 'HOW', 'ARE', 'YOU']

如果message中的字符串是由整数组成的,比如'12345',那么对removeNonLetters()的调用将返回一个空字符串,而对split()的调用将返回一个空列表。在程序中,空列表相当于英语中的零单词,这可能会导致被零除的错误。

被零除误差

为了返回一个在0.01.0之间的浮点值,我们将possibleWords中被识别为英语的单词数除以possibleWords中的总单词数。尽管这很简单,但我们需要确保possibleWords不是一个空列表。如果possibleWords为空,则表示possibleWords中的总字数为0

因为在数学中被零除没有意义,所以在 Python 中被零除会导致被零除的错误。要查看此错误的示例,请在交互式 shell 中输入以下内容:

>>> 42 / 0
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    42 / 0
ZeroDivisionError: division by zero

您可以看到,42除以0得到一个ZeroDivisionError和一条解释错误的消息。为了避免被零除的错误,我们需要确保possibleWords列表不为空。

第 29 行检查possibleWords是否为空列表,如果列表中没有单词,第 30 行返回0.0

    if possibleWords == []:
        return 0.0 # No words at all, so return 0.0

这种检查是必要的,以避免被零除的错误。

统计英语单词匹配数

为了得出英语单词与总单词的比率,我们将把possibleWords中被识别为英语的单词数除以possibleWords中的总单词数。为此,我们需要统计possibleWords中识别的英语单词的数量。第 32 行将变量matches设置为0。第 33 行使用for循环迭代possibleWords中的每个单词,并检查该单词是否存在于ENGLISH_WORDS字典中。如果该单词存在于字典中,则第 35 行的matches中的值递增。

    matches = 0
    for word in possibleWords:
        if word in ENGLISH_WORDS:
            matches += 1

for循环完成后,字符串中的英文单词数存储在matches变量中。请记住,我们依赖字典文件的准确性和完整性来使detectEnglish模块正确工作。如果一个单词不在字典文本文件中,即使它是一个真实的单词,也不会被算作英语。相反,如果一个单词在字典中拼写错误,非英语单词可能会意外地被算作真实单词。

现在,possibleWords中被识别为英语的单词数和possibleWords中的总单词数由整数表示。要通过将这两个整数相除来返回一个位于0.01.0之间的浮点值,我们需要将其中一个转换成浮点值。

float()int()str()函数和整数除法

让我们看看如何将一个整数转换成一个浮点数,因为我们需要除以的两个值都是整数。Python 3 总是执行常规除法,不管值类型如何,而 Python 2 在除法运算中两个值都是整数时执行整数除法。因为用户可能会使用 Python 2 来导入detectEnglish.py,所以我们需要向float()传递至少一个整数变量,以确保在进行除法运算时返回一个浮点数。这样做可以确保无论使用哪个版本的 Python,都将执行常规除法。这是一个让代码向后兼容以前版本的例子。

虽然我们不会在这个程序中使用它们,但是让我们回顾一下将值转换成其他数据类型的其他函数。int()函数返回其参数的整数版本,而str()函数返回一个字符串。要查看这些函数是如何工作的,请在交互式 shell 中输入以下内容:

>>> float(42)
42.0
>>> int(42.0)
42
>>> int(42.7)
42
>>> int('42')
42
>>> str(42)
'42'
>>> str(42.7)
'42.7'

可以看到float()函数将整数42变成了浮点值。int()函数可以通过截断浮点数的十进制值将浮点数42.042.7转换成整数,也可以将字符串值'42'转换成整数。str()函数将数值转换成字符串值。如果您需要一个值的等价物是不同的数据类型,这些函数会很有帮助。

查找消息中英文单词的比例

为了求出英语单词占总单词的比率,我们用找到的matches的数量除以possibleWords的总数。第 36 行使用/操作符将这两个数相除:

    return float(matches) / len(possibleWords)

在我们将整数matches传递给float()函数之后,它返回该数字的浮点版本,我们将该数字除以possibleWords列表的长度。

return float(matches) / len(possibleWords)导致被零除错误的唯一方式是如果len(possibleWords)评估为0。只有当possibleWords是一个空列表时,才有可能。然而,第 29 行和第 30 行专门检查这种情况,如果列表为空,则返回0.0。如果possibleWords被设置为空列表,程序执行将永远不会超过第 30 行,所以我们可以确信第 36 行不会导致ZeroDivisionError

删除非字母字符

某些字符,如数字或标点符号,会导致我们的单词检测失败,因为单词看起来不会与它们在字典文件中的拼写完全相同。例如,如果message中的最后一个单词是'you.',并且我们没有删除字符串末尾的句点,那么它就不会被算作一个英语单词,因为'you'在字典文件中不会用句点拼写。为了避免这种误解,需要删除数字和标点符号。

前面解释的getEnglishCount()函数调用字符串上的函数removeNonLetters()来删除其中的任何数字和标点符号。

def removeNonLetters(message):
    lettersOnly = []
    for symbol in message:
        if symbol in LETTERS_AND_SPACE:
            lettersOnly.append(symbol)

第 40 行创建一个名为lettersOnly的空白列表,第 41 行使用一个for循环遍历message参数中的每个字符。接下来,for循环检查字符串LETTERS_AND_SPACE中是否存在该字符。如果字符是数字或标点符号,它不会存在于LETTERS_AND_SPACE字符串中,也不会添加到列表中。如果字符串中确实存在这个字符,那么使用append()方法将它添加到列表的末尾,我们接下来会看到这个方法。

【追加()列表法

当我们把一个值加到一个列表的末尾时,我们说我们是在把这个值追加到列表中。Python 中经常对列表这样做,以至于有一个append()列表方法将一个参数附加到列表的末尾。在交互式 shell 中输入以下内容:

>>> eggs = []
>>> eggs.append('hovercraft')
>>> eggs
['hovercraft']
>>> eggs.append('eels')
>>> eggs
['hovercraft', 'eels']

创建一个名为eggs的空列表后,我们可以输入eggs.append ('hovercraft')将字符串值'hovercraft'添加到这个列表中。然后当我们输入eggs时,它返回这个列表中存储的唯一值,就是' hovercraft '。如果您再次使用append()'eels'添加到列表的末尾,eggs现在返回'hovercraft'后跟'eels'。类似地,我们可以使用append()列表方法将项目添加到我们之前在代码中创建的lettersOnly列表中。这就是第 43 行的lettersOnly.append(symbol)for循环中的作用。

创建一串字母

在完成for循环后,lettersOnly应该是来自原始message字符串的每个字母和空格字符的列表。因为单个字符串的列表对于查找英语单词没有用,所以第 44 行将lettersOnly列表中的字符串连接成一个字符串并返回它:

    return ''.join(lettersOnly)

为了将lettersOnly中的列表元素连接成一个大字符串,我们在空白字符串''上调用join()字符串方法。这将连接lettersOnly中的字符串,它们之间有一个空白字符串。然后这个字符串值作为removeNonLetters()函数的返回值返回。

检测英文单词

当用错误的密钥解密消息时,它通常会产生比典型的英语消息中多得多的非字母和非空格字符。此外,它产生的单词通常是随机的,在英语字典中是找不到的。isEnglish()函数可以在给定的字符串中检查这两个问题。

def isEnglish(message, wordPercentage=20, letterPercentage=85):
    # By default, 20% of the words must exist in the dictionary file, and
    # 85% of all the characters in the message must be letters or spaces
    # (not punctuation or numbers).

第 47 行设置了isEnglish()函数来接受一个字符串参数,当字符串是英文文本时返回一个布尔值True,否则返回False。该功能有三个参数:messagewordPercentage=20letterPercentage=85。第一个参数包含要检查的字符串,第二个和第三个参数设置单词和字母的默认百分比,字符串必须包含这些百分比才能被确认为英语。(百分比是一个介于 0 和 100 之间的数字,表示某样东西与这些东西的总数成比例。)我们将在下面几节中探讨如何使用默认参数和计算百分比。

使用默认参数

有时一个函数在被调用时几乎总是有相同的值传递给它。您可以在函数的def语句中指定一个默认参数,而不是在每个函数调用中都包含这些参数。

第 47 行的def语句有三个参数,分别为wordPercentageletterPercentage提供了默认参数 20 和 85。可以用一到三个参数调用isEnglish()函数。如果没有为wordPercentageletterPercentage传递参数,那么分配给这些参数的值将是它们的默认参数。

默认参数定义了message字符串中需要由真实英文单词组成的百分比,以便isEnglish()确定message是英文字符串,以及message中需要由字母或空格而不是数字或标点符号组成的百分比。例如,如果调用isEnglish()时只有一个参数,默认参数用于wordPercentage(整数20)和letterPercentage(整数85)参数,这意味着字符串的 20%需要由英语单词组成,85%需要由字母组成。这些百分比在大多数情况下适用于检测英语,但是在特定情况下,当isEnglish()需要更宽松或更严格的阈值时,您可能想要尝试其他参数组合。在这些情况下,程序可以只传递参数wordPercentageletterPercentage,而不使用默认参数。表 11-1 显示了对isEnglish()的函数调用以及它们的等价关系。

表 11-1: 有无默认参数的函数调用

函数调用 相当于
isEnglish('Hello') isEnglish('Hello', 20, 85)
isEnglish('Hello', 50) isEnglish('Hello', 50, 85)
isEnglish('Hello', 50, 60) isEnglish('Hello', 50, 60)
isEnglish('Hello', letterPercentage=60) isEnglish('Hello', 20, 60)

例如,表 11-1 中的第三个例子表明,当调用函数时指定了第二个和第三个参数,程序将使用这些参数,而不是默认参数。

计算百分比

一旦我们知道我们的程序将使用的百分比,我们将需要计算message字符串的百分比。例如,字符串值'Hello cat MOOSE fsdkl ewpin'有五个“单词”,但只有三个是英语。要计算英语单词在该字符串中所占的百分比,请将英语单词数除以总单词数,然后将结果乘以 100。'Hello cat MOOSE fsdkl ewpin'中英文单词的百分比是3 / 5 * 100,百分之六十。表 11-2 显示了一些计算百分比的例子。

表 11-2: 计算英语单词的百分比

英文单词数量 总字数 英语单词比例 × 100 = 百分比
3 5 0.6 × 100 =   60
6 10 0.6 × 100 =   60
300 500 0.6 × 100 = 60
32 87 0.3678 × 100 = 36.78
87 87 1 × 100 = 100
0 10 0 × 100 = 0

该百分比将始终介于 0 %(表示没有单词是英语)和 100 %(表示所有单词都是英语)之间。如果字典文件中至少有 20%的单词,并且字符串中有 85%的字符是字母或空格,那么我们的isEnglish()函数会将字符串视为英语。这意味着,即使字典文件不完善,或者邮件中的某些单词不是我们定义的英语单词,邮件仍会被检测为英语。

第 51 行通过将message传递给getEnglishCount()来计算message中已识别英语单词的百分比,后者执行除法并返回一个介于0.01.0之间的浮点数:

    wordsMatch = getEnglishCount(message) * 100 >= wordPercentage

要从这个浮点数中得到一个百分比,用它乘以100。如果结果数大于或等于wordPercentage参数,True被存储在wordsMatch中。(回想一下,>=比较运算符将表达式计算为布尔值。)否则,False存储在wordsMatch中。

第 52 到 54 行通过将字母字符数除以message中的字符总数来计算字母字符在message字符串中的百分比。

    numLetters = len(removeNonLetters(message))
    messageLettersPercentage = float(numLetters) / len(message) * 100
    lettersMatch = messageLettersPercentage >= letterPercentage

在代码的前面,我们编写了removeNonLetters()函数来查找字符串中的所有字母和空格字符,所以我们可以重用它。第 52 行调用removeNonLetters(message)来获取一个字符串,该字符串只包含message中的字母和空格字符。将这个字符串传递给len()应该会返回message中字母和空格字符的总数,我们将它作为一个整数存储在numLetters变量中。

第 53 行通过获取numLetters中整数的浮点版本并除以len(message)来确定字母的百分比。len(message)的返回值将是message中的字符总数。正如前面讨论的,调用float()是为了确保第 53 行执行常规除法而不是整数除法,以防导入detectEnglish模块的程序员运行 Python 2。

第 54 行检查messageLettersPercentage中的百分比是否大于或等于letterPercentage参数。该表达式计算出一个存储在lettersMatch中的布尔值。

只有当wordsMatchlettersMatch变量都包含True时,我们才希望isEnglish()返回True。第 55 行使用and操作符将这些值组合成一个表达式:

    return wordsMatch and lettersMatch

如果wordsMatchlettersMatch变量都是TrueisEnglish()会声明message是英语并返回True。否则,isEnglish()将返回False

总结

换位文件密码是对凯撒密码的改进,因为它可以有数百或数千个可能的消息密钥,而不是只有 26 个不同的密钥。尽管计算机可以用成千上万的潜在密钥解密一条消息,但我们需要编写代码来确定解密后的字符串是否是有效的英语,从而确定原始消息。

在这一章中,我们创建了一个英语检测程序,它使用一个字典文本文件来创建字典数据类型。dictionary 数据类型非常有用,因为它可以像 list 一样包含多个值。然而,与列表不同的是,您可以使用字符串值而不仅仅是整数作为键来索引字典中的值。你可以用列表完成的大多数任务也可以用字典来完成,比如把它传递给len()或者对它使用innot in操作符。然而,in操作符在一个非常大的字典值上的执行速度要比在一个非常大的列表上快得多。这被证明对我们特别有用,因为我们的字典数据包含成千上万的值,我们需要快速筛选。

本章还介绍了split()方法,它可以将字符串拆分成一系列字符串,还介绍了 NoneType 数据类型,它只有一个值:None。该值对于表示缺少值很有用。

您学习了如何在使用/运算符时避免被零除的错误;使用int()float()str()函数将值转换成其他数据类型;并使用append()列表方法在列表末尾添加一个值。

定义函数时,可以给一些参数指定默认参数。如果在调用函数时没有为这些参数传递参数,程序将使用默认的参数值,这在您的程序中可能是一个有用的快捷方式。在第 12 章中,你将学会使用英文检测码破解换位密码!

练习题

练习题的答案可以在本书的网站www.nostarch.com/crackingcodes找到。

  1. 下面的代码打印了什么?

    spam = {'name': 'Al'}
    print(spam['name'])
  2. 这段代码打印了什么?

    spam = {'eggs': 'bacon'}
    print('bacon' in spam)
  3. 什么for循环代码会打印下面的spam字典中的值?

    spam = {'name': 'Zophie', 'species':'cat', 'age':8}
  4. 下面一行打印了什么?

    print('Hello, world!'.split())
  5. 下面的代码会输出什么?

    def spam(eggs=42):
        print(eggs)
    spam()
    spam('Hello')
  6. 这句话中有效英语单词的百分比是多少?

    "Whether it's flobulllar in the mind to quarfalog the slings and
    arrows of outrageous guuuuuuuuur."
    ```*