使用有限狀態機原理實現英文分詞

提出問題

使用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

相關文章
相關標籤/搜索