开启成长之旅!这是我参与「日新方案 12 月更文应战」的第6天,点击查看活动概况


本文主要以单词字母词频计算、常用词频计算为需求,逐渐打开叙说,作为了解python的练手项目。


单词计算

需求

用户需求: 英语的26 个字母的频率在一本小说中是怎么散布的?某类型文章中常呈现的单词是什么?某作家最常用的词汇是什么?《哈利波特》 中最常用的短语是什么?

下面将顺次分化用户的需求,逐渐进行求解:

字母频率计算

第1步

更明确的需求如下:

首要先输出一个英文文本文件中 26 字母呈现的频率,由高到低摆放,并显现字母呈现的百分比,准确到小数点后面两位。

如果两个字母呈现的频率一样,那么就依照字典序摆放。

最终程序的指令行参数 为 xxx.xxx -c。即输入该参数,就能够回来对应的字母频率。

你需求思考: 如果要处理一本大部头小说 (例如 Gone With The Wind), 你的程序功率怎么? 有没有什么能够优化的当地?

拿到此需求,简单!遍历求解即可

先界说一个读取英文文本,并回来全部小写字母的函数。

# 读取文本数据
def get_context(filename):
    try:
        context = open(filename, 'r').read()
        context = context.lower()  # 小写
        return context
    except:  # 文件名输入过错
        sys.exit()

字母频率直接经过字典存取呈现的次数即可,详细完成:

# 计算字母呈现的频率
def calculate_letter_times(filename):
    start_time = timer()
    context = get_context(filename)
    letter_dict = {}
    for letter in context:
        if 'a' <= letter <= 'z':
            letter_dict[letter] = letter_dict.get(letter, 0) + 1
    total = sum(letter_dict.values())  # 总数
    letter_dict.update(zip(letter_dict.keys(), [val / total for val in letter_dict.values()]))
    letter_list = sort_dict(letter_dict)
    print(f"文件{filename}中的各字母呈现频率如下:")
    for k, v in letter_list:
        print('{} : {:.2f}%'.format(k, v * 100))
    end_time = timer()
    spend_time = end_time - start_time
    return spend_time

字典中经过letter_dict[letter] = letter_dict.get(letter, 0) + 1即可获取到对应的字母的频率。

dict.values()回来对应字典的值。 要想字典的值更新为对应的字母占比,则需求把对应的键和值从头拼装一下,经过zip将键和值拼装在一起,详细代码如下: letter_dict.update(zip(letter_dict.keys(), [val / total for val in letter_dict.values()]))

最终需求回来字母频率最高的的, 即需求对字典进行排序,详细完成:

# 值从大到小,键依照字典序排序,并回来;list
def sort_dict(my_dict):
    my_list = list(my_dict.items())
    my_list.sort(key=lambda x: (-x[1], x[0]))
    return my_list

至于怎么优化呢?这儿值得思索….


常见单词的计算

第二步:

方针:输出单个文件中的前 N 个最常呈现的英语单词。

详细需求:

单词界说:以英文字母最初,由英文字母和字母数字符号组成的字符串视为一个单词。单词以分隔符切割且不区分大小写。在输出时,一切单词都用小写字符表明。

切割符:空格,非字母数字符号

例:good123是一个单词,123good不是一个单词。good,Good和 GOOD是同一个单词

要求完成的功用:

  • 功用1: xxx.xxx -f
    • 输出文件中一切不重复的单词,依照呈现次数由多到少摆放,呈现次数同样多的,以字典序摆放。
  • 功用2: xxx.xxx -d
    • 指定文件目录,对目录下每一个文件履行 main.exe -f 的操作。
    • xxx.xxx -d -s 同上, 可是会递归遍历目录下的一切子目录。
  • 功用3: xxx.xxx -n
    • 输出呈现次数最多的前 n 个单词。 当没有指明数量的时分,默许列出一切单词的频率。

首要,是否是单词的判别,先将一切的字符地替换成空格,最终先将字符串以空格切割,再使用.isalpha() 判别当时单词是否全部为字母,就能够达到方针了…

def count_frequent_word(filename, word_num=0):
    start_time = timer()
    context = get_context(filename)
    punctuation = '!"#$%^&*()_-=+`~,./;:’[]{\|<>'
    for ch in punctuation:
        context = context.replace(ch, " ")
    words = context.split()
    word_dict = {}
    for word in words:
        if word.isalpha():  # 全部是字母
            word_dict[word] = word_dict.get(word, 0) + 1
    # print(word_dict)
    word_list = sort_dict(word_dict)
    if word_num == 0:
        word_num = len(word_list)
    for item in range(min(word_num, len(word_list))):  # 取最小值,避免想要输出10个单词可是文本中没有10个
        k, v = word_list[item]
        print('{0:<20} : {1:>5}次'.format(k, v))
    end_time = timer()
    spend_time = end_time - start_time
    print('The time consumed is {}s \n'.format(spend_time))

非递归完成目录:

前置知识:

  • os.path.isdir()判别是否是文件夹
  • os.listdir()回来目录下的文件夹名以及文件称号。(依照字典序回来列表方法)

完成:

def judge_file_type(filename):  # 判别文本类型,txt回来true, 否则回来false
    try:
        suffix = filename.split('.')[1]
        return suffix in ['txt']
    except:
        pass
# 不递归遍历目录 即支撑参数-d
def traverse_dir(dir_name, word_num=0, stop_filename=None, verb_filename=None):
    if os.path.isdir(dir_name):
        for text in os.listdir(dir_name):
            if judge_file_type(text):
                print(f"在{text}文本中,单词呈现的频率如下:")
                query_filename = os.path.join(dir_name, text)  # 这儿需求把整个途径传进去
                count_frequent_word(filename=query_filename, word_num=word_num,
                                    stop_filename=stop_filename, verb_form_file=verb_filename)
                print("------------------切割线------------------\n")
    else:
        print(f"当时途径 {dir_name} 无文件")

递归目录,即当时文件夹目录下以及其子文件目录下文件都需求辨认。

  • os.walk() 目录遍历器
def walkFile(file):
    for root, dirs, files in os.walk(file):
        # root 表明当时正在访问的文件夹途径
        # dirs 表明该文件夹下的子目录名list
        # files 表明该文件夹下的文件list
        # 遍历文件
        for f in files:
            print(os.path.join(root, f))
        # 遍历一切的文件夹
        for d in dirs:
            print(os.path.join(root, d))

稍加修正一下,即可协助咱们完成所提的需求。详细完成:

# 递归遍历目录 即支撑参数-d
def recursive_traverse_dir(cur_dir_name, word_num=0, stop_filename=None, verb_filename=None):  # -s 递归遍历
    for cur_dir, dir_list, text_list in os.walk(cur_dir_name):
        for text_name in text_list:
            if judge_file_type(text_name):
                cur_path = os.path.join(cur_dir, text_name)
                print(f"在{cur_path}文本中,单词呈现的频率如下:")
                count_frequent_word(filename=cur_path, word_num=word_num,
                                    stop_filename=stop_filename, verb_form_file=verb_filename)
                print("------------------切割线------------------\n")

支撑停词表

从第一步的成果看出,在一本小说里, 频率呈现最高的单词一般都是 “a”, “it”, “the”,“and”, “this”, 这些词, 咱们并不感兴趣. 咱们能够做一个 stop word 文件 (停词表), 在计算词 汇的时分,越过这些词。 咱们把这个文件叫 “stopwords.txt” file.

方针:支撑 stop words

新增需求:

  • 功用4: 支撑新的指令行参数, 例如: xxx.exe -x -f

在上述的递归目录中,咱们已经悄悄的增加了停词表和动词表两个参数。 这儿加上停词表,在计算单词数量时不记录停词表中单词数量。

完成:

def count_frequent_word(filename, word_num=0, stop_filename=None):
    start_time = timer()
    context = get_context(filename)
    punctuation = '!"#$%^&*()_-=+`~,./;:’[]{\|<>'
    for ch in punctuation:
        context = context.replace(ch, " ")
    if stop_filename is not None:
        stop_list = get_context(stop_filename).split()  # stopwords list
    words = context.split()
    word_dict = {}
    for word in words:
        if word.isalpha():  # 全部是字母
            if stop_filename is not None and word in stop_list:  # 停词list
                continue
            else:
                word_dict[word] = word_dict.get(word, 0) + 1
    # print(word_dict)
    word_list = sort_dict(word_dict)
    if word_num == 0:
        word_num = len(word_list)
    for item in range(min(word_num, len(word_list))):  # 取最小值,避免想要输出10个单词可是文本中没有10个
        k, v = word_list[item]
        print('{0:<20} : {1:>5}次'.format(k, v))
    end_time = timer()
    spend_time = end_time - start_time
    print('The time consumed is {}s \n'.format(spend_time))

常用短语的计算

方针:查询常用的短语

先界说短语:”两个或多个英语单词, 它们之间只有空格分隔”. 请看下面的例子:

  hello world //这是一个短语

  hello, world //这不是一个短语

新增需求:

  • 功用5: 支撑新的指令行参数 -p
    •  参数阐明要输出多少个词的短语,并依照呈现频率摆放。同一频率的词组, 依照字典序来摆放。

完成办法:

本文采取正则方法完成,经过界说切割文本的单词数来确认短语的方法。(这种办法比较没有脑子) 当词的个数为3时,切割文本的方法如下所示

「Python」词频统计

再增加对应的停词表,总体代码:

def query_phrase(query_filename, length_of_phrase, stop_filename=None):  # -p
    context = get_context(query_filename)
    context = context.replace('\n', ' ')
    re_single_word = r'(([a-z]+ )+[a-z]+)'  # 将整个文本抽取成单个的语句
    pattern = re.compile(re_single_word)
    sentence = pattern.findall(context)  # list -> 包含一切的语句
    txt = ','.join(sentence[i][0] for i in range(len(sentence)))
    regex = "[a-z]+[0-9]*"  # 匹配每一个单词
    pattern = regex
    for i in range(length_of_phrase - 1):  # 匹配一句话中的length个单词
        pattern += "[\s|,][a-z]+[0-9]*"
    word_list = []
    for i in range(length_of_phrase):
        if i == 0:
            temp_list = re.findall(pattern, txt)
        else:
            word_pattern = regex
            txt = re.sub(word_pattern, '', txt, 1).strip()
            temp_list = re.findall(pattern, txt)
        word_list += temp_list
    temp_counter = Counter(word_list)
    dic_num = {}
    phrases = temp_counter.keys()
    stop_phrase_list = []
    if stop_filename is not None:
        stop_phrase_list = get_context(stop_filename).strip().split('\n')  # 得到无意义的词组list v
    total_num = 0
    for phrase in phrases:
        if ',' not in phrase:
            if len(stop_phrase_list) > 0 and phrase in stop_phrase_list:
                continue
            else:
                dic_num[phrase] = temp_counter[phrase]
                total_num += temp_counter[phrase]
    dic_list = sort_dict(dic_num)
    print(f'长度为{length_of_phrase}的常用短语呈现的频率如下:')
    for k, v in dic_list:
        if v > 1:
            print('{0:<20} : {1:>5}次'.format(k, v))

动词形状的统一

方针:把动词形状都统一之后再计数。

在英语中,动词经常有时态和语态的变化,

e.g. 动词 TAKE 有各种变形 take takes took taken taking

新增需求:

咱们希望在完成上面的各种功用的时分,有一个选项, 就是把动词的各种变形都归为它的原型来计算。

  • 功用6: 支撑动词形状的归一化,参数为 -v

停词表的方法如下: abandon -> attends,attending,attended 经过切割字符串,将同一形状的单词的键删除并将其对应次数增加到对应的单词原型上即可。

代码完成:

def count_frequent_word(filename, word_num=0, stop_filename=None, verb_form_file=None):
    start_time = timer()
    context = get_context(filename)
    punctuation = '!"#$%^&*()_-=+`~,./;:’[]{\|<>'
    for ch in punctuation:
        context = context.replace(ch, " ")
    if stop_filename is not None:
        stop_list = get_context(stop_filename).split()  # stopwords list
    words = context.split()
    word_dict = {}
    for word in words:
        if word.isalpha():  # 全部是字母
            if stop_filename is not None and word in stop_list:  # 停词list
                continue
            else:
                word_dict[word] = word_dict.get(word, 0) + 1
    # print(word_dict)
    if verb_form_file is not None:
        verb_file = get_context(verb_form_file).strip().split('\n')
        for verb_phrase in verb_file:
            # verb_phrase: abandon -> attends,attending,attended
            key, values = verb_phrase.split(" -> ")
            values_list = values.split(',')
            for key_word in values_list:
                if word_dict.get(key_word) is not None:
                    word_dict[key] += word_dict[key_word]  # 加上其变形的value
                    del word_dict[key_word]
    word_list = sort_dict(word_dict)
    if word_num == 0:
        word_num = len(word_list)
    for item in range(min(word_num, len(word_list))):  # 取最小值,避免想要输出10个单词可是文本中没有10个
        k, v = word_list[item]
        print('{0:<20} : {1:>5}次'.format(k, v))
    end_time = timer()
    spend_time = end_time - start_time
    print('The time consumed is {}s \n'.format(spend_time))

项目代码

值得一提的是argparse模块,

import argparse
parser = argparse.ArgumentParser()  # 实例化对象
# 界说获取的参数提示
parser.add_argument('-f', '--filename', help="需求处理的文本称号", default=None)  
if __name__ == '__main__':
    args = parser.parse_args()  # 获取控制台的输入参数
    if args.letter:
        calculate_letter_times(args.letter)

xx.exe -h / xx.exe --help 即可显现自己设置的一切功用选项

完好代码:

import argparse
import sys
import os
import re
from collections import Counter
from timeit import default_timer as timer
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--letter', help="计算字母呈现的频率")
parser.add_argument('-f', '--filename', help="需求处理的文本称号", default=None)
parser.add_argument('-n', '--nWords', help="输出的单词或短语个数", type=int)
parser.add_argument('-x', '--stopFile', help="停词表称号", default=None)
parser.add_argument('-v', '--verbFile', help="动词变化的文件称号", default=None)
parser.add_argument('-p', '--phraseLen', help="查询常用词组的长度", type=int)
parser.add_argument('-d', '--dirname', help="非递归遍历该目录", default=None)
parser.add_argument('-s', '--dir', help="递归遍历遍历目录", default=None)
# 读取文本数据
def get_context(filename):
    try:
        context = open(filename, 'r').read()
        context = context.lower()  # 小写
        return context
    except:  # 文件名输入过错
        sys.exit()
# 值从大到小,键依照字典序排序,并回来;list
def sort_dict(my_dict):
    my_list = list(my_dict.items())
    my_list.sort(key=lambda x: (-x[1], x[0]))
    return my_list
# 计算字母呈现的频率
def calculate_letter_times(filename):
    start_time = timer()
    context = get_context(filename)
    letter_dict = {}
    for letter in context:
        if 'a' <= letter <= 'z':
            letter_dict[letter] = letter_dict.get(letter, 0) + 1
    total = sum(letter_dict.values())  # 总数
    letter_dict.update(zip(letter_dict.keys(), [val / total for val in letter_dict.values()]))
    letter_list = sort_dict(letter_dict)
    print(f"文件{filename}中的各字母呈现频率如下:")
    for k, v in letter_list:
        print('{} : {:.2f}%'.format(k, v * 100))
    end_time = timer()
    spend_time = end_time - start_time
    return spend_time
#######################################################################################
# @author:xincheng-q
# time: 2022-09-15
# count_frequent_word()
# @filename:需求计算的文件名
# @word_num:需求展示的单词数量,即支撑参数-n
# @stop_filename:默许为None,支撑参数 -x xxx.txt
# @verb_form_file:默许为None,支撑参数-v时,将各词的形状统一。
#######################################################################################
def count_frequent_word(filename, word_num=0, stop_filename=None, verb_form_file=None):
    start_time = timer()
    context = get_context(filename)
    punctuation = '!"#$%^&*()_-=+`~,./;:’[]{\|<>'
    for ch in punctuation:
        context = context.replace(ch, " ")
    if stop_filename is not None:
        stop_list = get_context(stop_filename).split()  # stopwords list
    words = context.split()
    word_dict = {}
    for word in words:
        if word.isalpha():  # 全部是字母
            if stop_filename is not None and word in stop_list:  # 停词list
                continue
            else:
                word_dict[word] = word_dict.get(word, 0) + 1
    # print(word_dict)
    if verb_form_file is not None:
        verb_file = get_context(verb_form_file).strip().split('\n')
        for verb_phrase in verb_file:
            # verb_phrase: abandon -> attends,attending,attended
            key, values = verb_phrase.split(" -> ")
            values_list = values.split(',')
            for key_word in values_list:
                if word_dict.get(key_word) is not None:
                    word_dict[key] += word_dict[key_word]  # 加上其变形的value
                    del word_dict[key_word]
    word_list = sort_dict(word_dict)
    if word_num == 0:
        word_num = len(word_list)
    for item in range(min(word_num, len(word_list))):  # 取最小值,避免想要输出10个单词可是文本中没有10个
        k, v = word_list[item]
        print('{0:<20} : {1:>5}次'.format(k, v))
    end_time = timer()
    spend_time = end_time - start_time
    print('The time consumed is {}s \n'.format(spend_time))
def judge_file_type(filename):  # 判别文本类型,txt回来true, 否则回来false
    try:
        suffix = filename.split('.')[1]
        return suffix in ['txt']
    except:
        pass
# 不递归遍历目录 即支撑参数-d
def traverse_dir(dir_name, word_num=0, stop_filename=None, verb_filename=None):
    if os.path.isdir(dir_name):
        for text in os.listdir(dir_name):
            if judge_file_type(text):
                print(f"在{text}文本中,单词呈现的频率如下:")
                query_filename = os.path.join(dir_name, text)  # 这儿需求把整个途径传进去
                count_frequent_word(filename=query_filename, word_num=word_num,
                                    stop_filename=stop_filename, verb_form_file=verb_filename)
                print("------------------切割线------------------\n")
    else:
        print(f"当时途径 {dir_name} 无文件")
# 递归遍历目录 即支撑参数-d
def recursive_traverse_dir(cur_dir_name, word_num=0, stop_filename=None, verb_filename=None):  # -s 递归遍历
    for cur_dir, dir_list, text_list in os.walk(cur_dir_name):
        for text_name in text_list:
            if judge_file_type(text_name):
                cur_path = os.path.join(cur_dir, text_name)
                print(f"在{cur_path}文本中,单词呈现的频率如下:")
                count_frequent_word(filename=cur_path, word_num=word_num,
                                    stop_filename=stop_filename, verb_form_file=verb_filename)
                print("------------------切割线------------------\n")
        # print(f'cur_dir =  {cur_dir}, dir_list = {dir_list}, text_list = {text_list}')
        # if len(dir_list) > 0:
        #     for dir_name in dir_list:  # 有重复遍历文件的问题,没有修正
        #         print(dir_name)
        #         traverse_dir(func=func, dir_name=os.path.join(cur_dir, dir_name))
        # else:
        #     traverse_dir(func=func, dir_name=cur_dir)
def query_phrase(query_filename, length_of_phrase, stop_filename=None):  # -p
    context = get_context(query_filename)
    context = context.replace('\n', ' ')
    re_single_word = r'(([a-z]+ )+[a-z]+)'  # 将整个文本抽取成单个的语句
    pattern = re.compile(re_single_word)
    sentence = pattern.findall(context)  # list -> 包含一切的语句
    txt = ','.join(sentence[i][0] for i in range(len(sentence)))
    regex = "[a-z]+[0-9]*"  # 匹配每一个单词
    pattern = regex
    for i in range(length_of_phrase - 1):  # 匹配一句话中的length个单词
        pattern += "[\s|,][a-z]+[0-9]*"
    word_list = []
    for i in range(length_of_phrase):
        if i == 0:
            temp_list = re.findall(pattern, txt)
        else:
            word_pattern = regex
            txt = re.sub(word_pattern, '', txt, 1).strip()
            temp_list = re.findall(pattern, txt)
        word_list += temp_list
    temp_counter = Counter(word_list)
    dic_num = {}
    phrases = temp_counter.keys()
    stop_phrase_list = []
    if stop_filename is not None:
        stop_phrase_list = get_context(stop_filename).strip().split('\n')  # 得到无意义的词组list v
    total_num = 0
    for phrase in phrases:
        if ',' not in phrase:
            if len(stop_phrase_list) > 0 and phrase in stop_phrase_list:
                continue
            else:
                dic_num[phrase] = temp_counter[phrase]
                total_num += temp_counter[phrase]
    dic_list = sort_dict(dic_num)
    print(f'长度为{length_of_phrase}的常用短语呈现的频率如下:')
    for k, v in dic_list:
        if v > 1:
            print('{0:<20} : {1:>5}次'.format(k, v))
if __name__ == '__main__':
    args = parser.parse_args()
    if args.letter:
        calculate_letter_times(args.letter)
    elif args.filename:
        if args.nWords and args.stopFile and args.verbFile:
            count_frequent_word(filename=args.filename, word_num=args.nWords, stop_filename=args.stopFile,
                                verb_form_file=args.verbFile)
        if args.nWords and args.stopFile:  # -x xx.txt -n x -f xx.txt
            count_frequent_word(filename=args.filename, word_num=args.nWords, stop_filename=args.stopFile)
        if args.verbFile and args.stopFile:  # -v xx.txt  -x xx.txt -f xx.txt
            count_frequent_word(filename=args.filename, stop_filename=args.stopFile, verb_form_file=args.verbFile)
        if args.verbFile and args.nWords:  # -v xx.txt  -n x -f xx.txt
            count_frequent_word(filename=args.filename, word_num=args.nWords, verb_form_file=args.verbFile)
        if args.stopFile:  # -x xx.txt -f xx.txt
            count_frequent_word(filename=args.filename, stop_filename=args.stopFile)
        if args.verbFile:  # -v xx.txt -f xx.txt
            count_frequent_word(filename=args.filename, verb_form_file=args.verbFile)
        if args.nWords:     # -n x -f xx.txt
            count_frequent_word(filename=args.filename, word_num=args.nWords)
        if args.phraseLen:  # -p
            query_phrase(query_filename=args.filename, length_of_phrase=args.phraseLen, stop_filename='stoppharse.txt')
        if not args.nWords and not args.stopFile and not args.verbFile and not args.phraseLen:
            count_frequent_word(filename=args.filename)
    elif args.dirname:
        traverse_dir(dir_name=args.dirname)
    elif args.dir:
        recursive_traverse_dir(cur_dir_name=args.dir)

由于笔者本身水平有限,最终完成出来的效果并不满意。如有更好的办法完成,希望和我们一起探讨~


运行代码办法

  • 办法一:能够选择进入到Test02/dist/main/目录下,履行main.exe -f xx.txt指令,留意这儿文件的途径要相对完好。

  • 办法二:在Test02/目录下,履行python main.py -f xx.txt指令。

最终完成的功用列表参数如下

usage: main.exe [-h] [-c LETTER] [-f FILENAME] [-n NWORDS] [-x STOPFILE] [-v VERBFILE] [-p PHRASELEN] [-d DIRNAME] [-s DIR]
optional arguments:
  -h, --help            show this help message and exit
  -c LETTER, --letter LETTER
                        计算字母呈现的频率
  -f FILENAME, --filename FILENAME
                        需求处理的文本称号
  -n NWORDS, --nWords NWORDS
                        输出的单词或短语个数
  -x STOPFILE, --stopFile STOPFILE
                        停词表称号
  -v VERBFILE, --verbFile VERBFILE
                        动词变化的文件称号
  -p PHRASELEN, --phraseLen PHRASELEN
                        查询常用词组的长度
  -d DIRNAME, --dirname DIRNAME
                        非递归遍历该目录
  -s DIR, --dir DIR     递归遍历遍历目录

main.exe -c hamlet.txt 查询hamlet.txt文件中一切字母呈现的占比

main.exe -f hamlet.txt 查询hamlet.txt文件中一切单词的频率

main.exe -f hamlet.txt -n 5 只显现频率最高的前5个单词

main.exe -x stopwords.txt -f hamlet.txt 使用停词表

main.exe -v ver.txt -f hamlet.txt 支撑动词形状的归一化

main.exe -d ./ 查询当时目录下的一切文件单词呈现的频率

main.exe -s ./ 递归遍历当时目录下的一切文件单词呈现的频率

main.exe -p 3 -f hamlet.txt 查询hamlet.txt文件中长度为3的词组呈现的频率

最终:项目开源在gitee –> 词频计算测验