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

青南發表於2017-12-10

提出問題

使用Python開發一個英文句子分詞程式,把一段英文句子切分為每一個單詞。不能匯入任何官方的或者第三方的庫,也不能使用字串的split()方法。

程式碼是如何一步一步惡化的

單詞與空格

對於只有單詞和空格,不含其他符號的英語句子,可以使用空格來切分單詞。於是對於句子I am kingname, 一個字元一個字元的進行遍歷。首先遍歷到I,發現它是一個字母,於是把它存到一個變數word中,然後遍歷到空格,於是把變數word的值新增到變數word_list中,再把word清空。接下來遍歷到字母a,又把a放到變數word中。再遍歷到m,發現它還是一個字母,於是把字母m拼接到變數word的末尾。此時變數word的值為am。再遍歷到第二個空格,於是把word的值新增到word_list中,清空word

最後,由於第三個單詞kingname的末尾沒有空格,所以需要手動把它新增到列表word_list中。

完整的程式碼如下:

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)
複製程式碼

執行效果如下圖所示。

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

單詞空格與逗號句號

現在不僅僅只有單詞和空格,還有逗號和句號。有這樣一個句子:"I am kingname,you should remember me."如果使用上一小節的程式,那麼程式碼就會出現問題,如下圖所示。

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

其中,"kingname,you"應該是兩個單詞,但是在這裡變成了一個單詞。所以現在不僅遇到空格要進行切分,遇到逗號句號還需要進行切分。那麼對程式碼做一些修改,變成如下程式碼:

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)
複製程式碼

現在執行起來看上去沒有問題了,如下圖所示。

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

然而,有些人寫英文的時候喜歡在標點符號右側加一個空格,例如:"I am kingname, you should remember me."這樣小小的一修改,上面的程式碼又出問題了,如下圖所示。

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

分詞出來的結果裡面憑空多出來一個空字串。為了解決這個問題,再加一層判斷,只有發現word不為空字串的時候才把它加入到word_list中,程式碼繼續修改:

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

相關文章