提出問題
使用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