使用有限状态机原理实现英文分词

提出问题

使用Python开发一个英文句子分词程序,把一段英文句子切分为每个单词。不能导入任何官方的或者第三方的库,也不能使用字符串的split()方法。python

代码是如何一步一步恶化的

单词与空格

对于只有单词和空格,不含其余符号的英语句子,可使用空格来切分单词。因而对于句子I am kingname, 一个字符一个字符的进行遍历。首先遍历到I,发现它是一个字母,因而把它存到一个变量word中,而后遍历到空格,因而把变量word的值添加到变量word_list中,再把word清空。接下来遍历到字母a,又把a放到变量word中。再遍历到m,发现它仍是一个字母,因而把字母m拼接到变量word的末尾。此时变量word的值为am。再遍历到第二个空格,因而把word的值添加到word_list中,清空wordapp

最后,因为第三个单词kingname的末尾没有空格,因此须要手动把它添加到列表word_list中。编码

完整的代码以下:spa

def split(target):
    if not target:
        return []
    word_list = []
    word = ''
    for letter in target:
        if letter == ' ':
            word_list.append(word)
            word = ''
        else:
            word += letter
    return word_list


if __name__ == '__main__':
    sentence = 'I am kingname'
    result_word_list = split(sentence)
    print(result_word_list)
复制代码

运行效果以下图所示。3d

单词空格与逗号句号

如今不只仅只有单词和空格,还有逗号和句号。有这样一个句子:"I am kingname,you should remember me."若是使用上一小节的程序,那么代码就会出现问题,以下图所示。code

其中,"kingname,you"应该是两个单词,可是在这里变成了一个单词。因此如今不只遇到空格要进行切分,遇到逗号句号还须要进行切分。那么对代码作一些修改,变成以下代码:cdn

def split(target):
    if not target:
        return []
    word_list = []
    word = ''
    for letter in target:
        if letter in [' ', ',', '.']:
            word_list.append(word)
            word = ''
        else:
            word += letter
    if word:
        word_list.append(word)
    return word_list


if __name__ == '__main__':
    sentence = 'I am kingname,you should remember me.'
    result_word_list = split(sentence)
    print(result_word_list)
复制代码

如今运行起来看上去没有问题了,以下图所示。blog

然而,有些人写英文的时候喜欢在标点符号右侧加一个空格,例如:"I am kingname, you should remember me."这样小小的一修改,上面的代码又出问题了,以下图所示。开发

分词出来的结果里面凭空多出来一个空字符串。为了解决这个问题,再加一层判断,只有发现word不为空字符串的时候才把它加入到word_list中,代码继续修改:rem

def split(target):
    if not target:
        return []
    word_list = []
    word = ''
    for letter in target:
        if letter in [' ', ',', '.']:
            if not word:
                continue
            word_list.append(word)
            word = ''
        else:
            word += letter
    if word:
        word_list.append(word)
    return word_list


if __name__ == '__main__':
    sentence = 'I am kingname, you should remember me.'
    result_word_list = split(sentence)
    print(result_word_list)
复制代码

代码看起来又能够正常工做了。以下图所示。

单词空格与各类标点符号

标点符号可不只仅只有逗号句号。如今又出现了冒号分号双引号感叹号问号等等杂七杂八的符号。英文句子变为:"I am kingname, you should say: "Kingname Oba" to me, will you?"

使用上面的代码,发现运行起来又出问题了。以下图所示。

为了能覆盖到全部的标点符号,如今修改一下逻辑。原来是“遇到空格/逗号/句号”就把word放到word_list中。如今要改成“若是当前字符不是字母,就把word放到word_list中”。因而代码进一步作修改:

constant = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'


def split(target):
    if not target:
        return []
    word_list = []
    word = ''
    for letter in target:
        if letter not in constant:
            if not word:
                continue
            word_list.append(word)
            word = ''
        else:
            word += letter
    if word:
        word_list.append(word)
    return word_list


if __name__ == '__main__':
    sentence = 'I am kingname, you should say: "Kingname Oba" to me, will you?'
    result_word_list = split(sentence)
    print(result_word_list)
复制代码

代码修改之后又能够正常工做了,其运行效果以下图所示:

奇奇怪怪的单引号

若是双引号包含的句子里面还须要用到引号,那么就须要在内部使用单引号。例若有这样一个句子:“I am kingname, you should say: "Kingname Oba, I always remember your motto: 'kingname is genius'" to me, will you?”

使用前面的代码,运行起来彷佛没有问题,以下图所示。

可是,单引号还有其余用途——有人喜欢把两个单词合并成一个单词,例如:

  • "do not” == "don't"
  • "is not" == "isn't"
  • "I will" == "I'll"
  • "I have" == "I've"

在这种状况下,就应该把单引号链接的两部分看做是一个单词,不该该把它们切开。

若是句子变成:I'm kingname, you should say: "Kingname Oba, I always remember your motto: 'kingname's genius'" to me, won't you?继续使用上面的代码,就发现返回的单词列表又不对了。以下图所示。

要解决这个问题,就须要肯定单引号具体是作普通的引号来使用,仍是放在缩写里使用。

做为普通单引号使用的时候,若是是前单引号,那么它的左边一定不是字母,若是做为后单引号,那么它的右边一定不是字母。而缩写里面的单引号,它左右两侧一定都是字母。而且须要注意,若是是句子里面第一个符号就是单引号,那么此时它左边没有字符;若是句子里面最后一个符号是单引号,那么它右边没有字符,此时若是使用下标来查找,就须要小心下标越界。

对代码进一步修改:

constant = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'


def split(target):
    if not target:
        return []
    word_list = []
    word = ''
    for index, letter in enumerate(target):
        if letter not in constant and letter != "'":
            if not word:
                continue
            word_list.append(word)
            word = ''
        elif letter == "'":
            if 0 < index < len(target) - 1 \
                    and target[index - 1] in constant \
                    and target[index + 1] in constant:
                word += letter
        else:
            word += letter
    if word:
        word_list.append(word)
    return word_list


if __name__ == '__main__':
    sentence = '''I'm kingname, you should say: "Kingname Oba, I always remember your motto: 'kingname's genius'" to me, won't you?'''
    result_word_list = split(sentence)
    for word in result_word_list:
        print(word)
复制代码

如今代码又能够成功运行了,以下图所示。

可是请细看代码,如今已经混乱到难以阅读难以理解了。若是再增长一个连字符又怎么改?若是单词内部出现了两个单引号怎么改?这种为了增长一个功能,要把不少不相干代码也进行修改的编码方式,相信能够击中不少初学者甚至是很多自称为软件工程师的人。

状态转义图

根据分词逻辑,遇到各类符号应该怎么处理,画一个分词的状态转移图出来。

从这个图上能够看出来,其实程序只须要知道当前是什么状态,以及遇到什么字符须要转移到什么状态就能够了。没有必要知道本身是从哪一个状态转移过来的,也没有必要知道和本身不相干的其余状态。

举一个例子:I'm kingname, you should say: "Kingname Oba, I always remember your motto: 'kingname's genius'" to me, won't you?这个句子中,should这个单词就是处于“单词状态”。它不在单引号内部,它也不是一个缩写。当咱们对句子每一个字符进行遍历的时候,遍历到“should”的“s”时进入“单词状态”,在单词状态,只须要关心接下来过来的下一个字符是什么,若是是字母,那依然是单词状态,把字母直接拼接上来便可。若是是单引号,那么进入“单引号在单词中状态”。至于“单引号在单词中状态”有什么逻辑,单词状态的代码根本不须要知道。这就像是接力赛,我把棒交给下一我的,个人任务就作完了,下一我的是跑到终点仍是爬到终点,都和我没有关系。

这就是有限状态机FSM的原理。

使用状态机

根据这个原理,使用状态和转移关系来改写代码,就可让代码的逻辑变得很是清晰。改进之后的代码以下:

class Spliter(object):
    def __init__(self):
        self.constant = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
        self.state = '初始状态'
        self.word = ''
        self.word_list = []
        self.state_dict = {'初始状态': self.parse_init,
                           '单词状态': self.parse_word,
                           '单引号在单词中状态': self.parse_contraction}

    def parse_init(self, letter):
        if letter in self.constant:
            self.state = '单词状态'
            self.word += letter

    def parse_word(self, letter):
        if letter in self.constant:
            self.word += letter
        elif letter == "'":
            self.state = '单引号在单词中状态'
            self.word += "'"
        else:
            self.word_list.append(self.word)
            self.state = '初始状态'
            self.word = ''

    def parse_contraction(self, letter):
        if letter in self.constant:
            self.word += letter
            self.state = '单词状态'
        else:
            self.word_list.append(self.word[:-1])
            self.word = ''
            self.state = '初始状态'

    def split(self, target):
        for letter in target:
            self.state_dict[self.state](letter)

        return self.word_list

if __name__ == '__main__':
    spliter = Spliter()
    sentence = '''I'm kingname, you should say: "Kingname Oba, I always remember your motto: 'kingname's genius'" to me, won't you?'''
    print(spliter.split(sentence))
复制代码

代码运行效果以下图所示。

须要注意的是,图中的代码只是使用了有限状态机的原理,而并不是一个有限状态机。

关注公众号:未闻Code

个人公众号:未闻Code

相关文章
相关标签/搜索