开启掘金成长之旅!这是我参与「掘金日新计划 · 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时,分割文本的形式如下所示
再添加对应的停词表,总体代码:
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 --> 词频统计测试