一、程序分析
1、以只讀模式讀取文件到字符串
def process_file(path): try: with open(path, 'r') as file: text = file.read() except IOError: print("Read File Error!") return None return text
2、對字符串進行數據清洗,返回一個字典
import re word_list = re.sub('[^a-zA-Z0-9n]', ' ', textString).lower().split()
使用正則表達式過濾掉文檔中的特殊字符,把它們全部替換為空格,方便后續的分隔操作。(忽略大小寫,所以全部使用小寫字母)
2.1、只考慮單詞頻率統計
for word in word_list: if word in word_freq: word_freq[word] += 1 else: word_freq[word] = 1
判斷單詞列表中的單詞是否在單詞頻率字典中。
如果這個單詞在字典中,則該單詞的個數加1;
如果這個單詞不在字典中,則以這個單詞為鍵,賦值為1,表示這個單詞第一次出現。
2.2、考慮單詞和詞組的頻率
2.2.1、數據結構
詞組是由單詞連接構成的,一個單詞既可以與前面的單詞構成詞組,也可以與后面的單詞構成詞組。
同時,一個單詞可能在文章中多次出現,並且和前后的單詞構成多種不同的詞組。
這也就表示幾乎每一個單詞可以一前一后發散出去,這與圖狀結構頗為類似。(適用於兩個單詞構成的詞組)
選擇圖狀結構與當前的問題頗為契合。但是,目前python沒有一種已知的圖類來供我們操作。(自定義類暫不作考慮)
於是我想到了樹狀結構(兩個以上單詞構成的詞組也適用),砍去了單詞向前的發散,只保留向后的。至此,問題就變成了構建森林。
而在python中,森林是比較容易表示的。用一個字典放置每一棵樹的根節點,字典套字典,就形成了森林。
可能你們要問單詞和詞組的統計個數放在哪。我使用“Value”作鍵,鍵值number這樣一種結構放在該單詞的字典中。
具體的字典示例樣本參見sample.json
2.2.2、具體流程
以索引停在50,詞組限制單詞個數為3為例:
(1)在樹的根節點中尋找mother節點,找不到就創建mother節點。
"mother":{ "Value":1 }
(2)在樹的根節點中尋找your節點,找到了繼續尋找mother節點,找不到就創建mother節點。(不可能找不到,因為索引經過49之后肯定存在your根節點)
(3)以此類推,在樹的根節點中尋找did節點,找到了繼續尋找your節點,,找到了繼續尋找mother節點,找不到就創建mother節點......
具體實現:
count = len(word_list)
i = 0
while i < count: # 因為需要用到索引遍歷列表,只能使用while來遍歷列表
finish = i
start = i - num + 1 # num表示詞組的單詞個數限制,start表示以該單詞作為詞組結尾的第一個單詞的索引
if start < 0:
start = 0 # 處理開始時索引前面沒有單詞的特殊情況
index = i
while index >= start: # 做num次建立節點
if word_list[i] in get_dict_value(word_freq, word_list[index: finish]).keys():
get_dict_value(word_freq, word_list[index: finish])[word_list[i]]['Value'] += 1
else:
get_dict_value(word_freq, word_list[index: finish]).update({word_list[i]: {'Value': 1}})
index -= 1
i += 1
get_dict_value函數
def get_dict_value(word_freq={}, keys=[]): """如果keys為字符串,返回word_freq字典中以keys為鍵的值。 如果keys為列表,則使用eval()函數進行字符串拼接,深度查找word_freq字典中以keys為鍵的值。""" if type(keys).__name__ == 'str': return word_freq[keys] else: count = len(keys) if count == 0: return word_freq elif count == 1: return word_freq[keys[0]] elif count == 2: return word_freq[keys[0]][keys[1]] elif count == 3: return word_freq[keys[0]][keys[1]][keys[2]] #對尋找三個單詞以下的詞組進行特化 else: string = "word_freq['" string += "']['".join(keys) string += "']" return eval(string) #動態尋找字典的值的一般版本
2.2.3、字典格式化
數據已經存儲到了森林中,接下來就是如何把森林格式化成普通的字典。
相比深度優先遍歷,我選擇的是更易理解與實現的廣度優先遍歷。
廣度優先遍歷的原理我不再贅述。
def format_dict(word_freq={}): """對統計短語的情況生成的復雜字典進行格式化,格式化后的形式為<str,int>""" formated_word_freq = {} phrases = [] for word in word_freq.keys(): #將所有根節點放入隊列中 phrases.append(word) while len(phrases) > 0: #只要隊列還有元素,就表明還沒有遍歷結束 phrase = phrases[0] if len(get_dict_value(word_freq, phrase)) == 1 and type(phrase).__name__ == 'list': formated_word_freq[' '.join(phrase)] = get_dict_value(word_freq, phrase)['Value'] #搜索到葉子節點了,這個節點存入格式化好的字典中 else: for next_word in get_dict_value(word_freq, phrase): #除"Value"鍵值對以外的鍵都入隊 temp = [] if type(phrase).__name__ == 'str': temp.append(phrase) else: temp.extend(phrase) if next_word != 'Value': temp.append(next_word) phrases.append(temp) phrases.pop(0) #搜索完隊列的第一個節點的子節點,這個節點出隊 # print(formated_word_freq) return formated_word_freq
3、輸出字典(沒有改動)
def output_result(word_freq): if word_freq: sorted_word_freq = sorted(word_freq.items(), key=lambda v: v[1], reverse=True) for item in sorted_word_freq[:10]: print(item)
4、主函數
if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument('path') #路徑 parser.add_argument('num') #詞組單詞個數 args = parser.parse_args() path = args.path num = int(args.num) buffer = process_file(path) if buffer: word_freq = process_buffer(buffer, num) if num != 1: word_freq = format_dict(word_freq) output_result(word_freq)
命令行接受兩個參數,一個是英文文檔的路徑,還有一個是構成一個詞組的最大單詞個數(為1時即單詞統計)。沒有這個限制,森林里就會出現一棵從第一個單詞到最后一個單詞的樹。
二、代碼風格說明
基本遵從PEP8,PEP8 涵蓋了諸如空格、函數/類/方法之間的換行、import
、對已棄用功能的警告之類的尋常東西,是一個不錯的准則。
def get_dict_value(word_freq={}, keys=[]): """如果keys為字符串,返回word_freq字典中以keys為鍵的值。 如果keys為列表,則使用eval()函數進行字符串拼接,深度查找word_freq字典中以keys為鍵的值。""" if type(keys).__name__ == 'str': return word_freq[keys] else: count = len(keys) if count == 0: return word_freq elif count == 1: return word_freq[keys[0]] elif count == 2: return word_freq[keys[0]][keys[1]] elif count == 3: return word_freq[keys[0]][keys[1]][keys[2]] #對尋找三個單詞以下的詞組進行特化 else: string = "word_freq['" string += "']['".join(keys) string += "']" return eval(string) #動態尋找字典的值的一般版本
注釋基本也只在關鍵地方才有,不必面面俱到。
三、程序運行命令、運行結果截圖
運行命令:
python statistic.py Gone_with_the_wind.txt 1
python statistic.py Gone_with_the_wind.txt 2
四、簡單性能分析
運行命令:
python -m cProfile -o result.out -s cumulative statistic.py Gone_with_the_wind.txt 2
python gprof2dot.py -f pstats result.out | dot -Tpng -o result.png
單詞統計性能分析:
詞組統計性能分析:
在進行單詞統計時,時間有五成花費在過濾特殊字符上,有三成花費在創建字典上。
而在詞組統計中,過濾特殊字符只占了一成,兩層時間在創建森林,六成時間在格式化森林。其中,尤其以list的pop()函數占用時間最多,將近一半的時間用來出隊。
程序優化:嘗試在不pop()得情況下修改廣度優先遍歷。嘗試另一種數據結構,森林比圖占的內存多。
修改中...