对英文文档中的单词与词组进行频率统计

一、程序分析

 

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()得情况下修改广度优先遍历。尝试另一种数据结构,森林比图占的内存多。

  修改中...

posted @ 2018-10-05 19:45  &一弦一柱&  阅读(1630)  评论(1编辑  收藏  举报