news 2026/9/5 14:58:51

第11章 案例研究: 文本统计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第11章 案例研究: 文本统计

到目前为止,你见到的代码片段大多只有几行,旨在演示某项Python功能。编程新手会很快发现,将小小的代码片段组织成完整的程序是一大步。要编写规模较大的程序,必须更详尽地规划,还需对如何以最佳方式结合使用各项Python功能有所了解。刚开始编写较大的程序时,可能需要反复试验。

本章将循序渐进地开发一个较大的 Python程序。首先对要解决的问题进行描述,然后创建一个解决问题的Python程序,并对其进行测试。

程序编写工作很棘手,但要演示这一点很难。看起来有了清晰的问题描述后,我们就找到了简洁的解决方案;但实际上绝不可能如此简单:需要反复试验,开始会遭遇失败,还常常需要推倒重来。通过编写程序,你将逐渐学会合并使用各种技术的最佳方式,并了解哪些解决方案通常对解决哪些问题行之有效。

11.1 问题描述

受邀为解决重要问题而编写程序时,编程新手常常不知道从何处着手。至少从笼统的角度说,答案很简单:编写大型程序时,先得明白要解决的问题。这看似简单,但未能正确认识要解决的问题是极其常见的编程错误。有时候,编写程序之所以很难,是因为你没有真正明白自已要做什么。

本章要解决的问题是,计算并打印有关文本文件内容的统计数据。我们想知道给定文本文件包含多少个字符、行和单词。除单词外,我们还想知道在文件中出现次数最多的前10个单词,并按出现次数排列它们。

来看一个包含一小段文本的例子。

A long time ago, in a galaxy far away...

它包含如下内容:

一行文本。我们假定换行字符(\n)被用于标识行尾,且不为空的文本文件至少包含一行。

46个字符,包括空格和标点在内。

总共10个单词。然而,不同的单词只有8个,因为far和A都出现了两次

在Python解释器中进行试验很有帮助,例如:

正如你看到的,函数len指出这个字符串包含46个字符;函数split将字符串划分为单词——如果忽略末尾...,字符串s总共包含10个单词。

如果仔细查看split返回的单词列表,将发现单词far出现了两次,但split将它们视为两个不同的字符串:"far,"(末尾有逗号)和"far"(没有逗号)。同样,A和a也是相同的单词,只是大小写不同。

为处理上述细节,我们将精确地定义字符串为单词的含义:单词是包含一个或多个字符的字符串,其中每个字符都必须是小写字母a~z。我们将忽略非字母字符(如数字和标点),并将大写字母转换为小写。因此前面的示例如下。

原始文本:A long time ago, in a galaxy far, far away ...

修改后的文本:a long time ago, in a galaxy far, far away

通过将修改后的句子划分为单词,得到的结果更准确:

要计算不同的单词数,可将列表转换为集合(本书前面介绍过,集合不存储重复的值:)

删除非字母字符存在一些缺点。首先,字符数不对,因为有些字符被删除。但我们可采取这样应对的措施,即在修改前计算字符数。其次,对于有些单词,没有将其标点符号删除的好办法,例如,该如何 处理I‘d中的撇号呢?如果将其删除(并将I转换为小写),如果将为id,与原来的单词不同。如果将撇号替换为空格,结果将为I和d——一个是单词,一个不是。为解决这个问题,我们将撇号(还有连字符)视为字母。第三,改变大小定可能改变单词的含义。例如,将有些名字的首字母小定后,它将变成单词,如Polish(polish)和Bonnie(bonnie)。我们不考虑这个问题,因为它看起来并非什么大问题。

11.2 保留想要的字母

接下来,考虑如何自动将字符串转换成所需的格式。将字符串转换为小写很容易,如下所示。

删除不想要的字符有点棘手,一种办法是使用字符串函数replace将不要的字符替换为空字符,例如:

这种做法的问题在于,需要调用replace很多次:每种不需要的字符一次。相比于要保留的字符,要删除的字符多得多,因此这种做法的效率极低。

一种更佳的方法是保留想要的字母,例如:

#keep.py #包含所有要保留的字符的集合 keep = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ' ', '-', "'"} def normalize(s): """Convert s to a normalized string. """ result = '' for c in s.lower(): if c in keep: result +=c return result

运行:

这个函数以每次一个字符的方式遍历字符串s,仅当字符包含在要保留的字符集合中时,才将其附加到 result末尾。

11.3 使用大型数据文件测试代码

我们编写的代码不多,但足够做一些有用的实验。在下面的示例中,我们将使用文件PythonStandardLibraryCoreModules1999.txt作为测试文件,要处理文本文件,方式之一是将整个文件作为一个字符串读取到内存中。下面在解释器中手工完成这项任务。

从输出可知,这个文件包含82651个字符, 2210行, 12224个单词

下面将所有代码放在一个函数中,以自动完成这项任务

#filecount.py #包含所有要保留的字符的集合 keep = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ' ', '-', "'"} def normalize(s): """Convert s to a normalized string. """ result = '' for c in s.lower(): if c in keep: result +=c return result def file_stats(fname): """ Print statistics for the given file. """ s = open(fname, 'r').read() num_chars = len(s) num_lines = s.count('\n') num_words = len(normalize(s).split()) print("The file '%s' has: " % fname) print(" %s characters" % num_chars) print(" %s lines" % num_lines) print(" %s words" % num_words)

运行如下:

11.4 找出出现次数较多的单词

来考虑如下问题:找出文本文件中出现次数较多的单词。这里的解决方案是,创建一个字典,其中的键为单词,值为单词在文件中出现的次数。

例如:对于前面的示例文本(经过规范化):

a long time ago in a galaxy far far away

各个单词出现的次如下:

a: 2 long: 1 time: 1 ago: 1 in: 1 galaxy: 1 far: 2 away: 1

如果我们将上述内容转换为一个Python字典,结果钭类似于下面这样:

d = { 'a': 2, 'long': 1, 'time': 1, 'ago': 1, 'in': 1, 'galaxy': 1, 'far': 2, 'away': 1 }


可从这个字典提取很多有用的信息。

d.keys()是一个列表,包含文件中所有不同的单词

len(d.keys())是文件中不同的单词数

sum(d[k] for k in d)是d中所有值之和,即文件包含的单词总数(包括重复的单词)。sum是一个Python内置函数,返回序列的总和。

字典存储的数据未经排序,因此要获取一个清单,按出现次数从高到低的顺序列出所有单词,需要将字典转换为元组列表,如下所示。

#findword.py 找出出现次数较多的单词 d = { 'a': 2, 'long': 1, 'time': 1, 'ago': 1, 'in': 1, 'galaxy': 1, 'far': 2, 'away': 1 } ​ def findword(): lst = [] for k in d: pair = (d[k], k) lst.append(pair) print(lst) lst.sort() #排序 print(lst) lst.reverse() #逆序 print(lst)

其中的for循环将字典d转换为由元组(count, word)组成的列表。经过这样的转换后,就可使用列表函数sort按出现次数对数据排序。默认情况下,函数sort按从小到大的顺序排列数据,因为我们反转列表的排列顺序,将出现次数最多的单词(通常也是我们最感兴趣的单词)放在列表开头。

将lst中的单词按出现次数从高到低排列后,就可使用切片来获取出现次数最多的3个单词:

print(lst[:3])

如果要让输出更整洁,可以这样做:

#findword.py 找出出现次数较多的单词 d = { 'a': 2, 'long': 1, 'time': 1, 'ago': 1, 'in': 1, 'galaxy': 1, 'far': 2, 'away': 1 } def findword(): lst = [] for k in d: pair = (d[k], k) lst.append(pair) print(lst) lst.sort() #排序 print(lst) lst.reverse() #逆序 #print(lst) #print(lst[:3]) for count, word in lst: print('%4s %s' % (count, word))

注意到在每个单词的出现次数前面,都有3个空格。这是因为print语句包含格式命令%4s,它让数字在宽度为4的字段中右对齐。只要没有单词出现的次数达到或超过1000,这就可确保出现次数完全对齐。

11.5 将字符串转换为次数字典

下面来编写一个函数,它接受字符串s并生成一个字典,该字典的键为s中的单词,值为单词出现的次数:

#stringToDict.py #包含所有要保留的字符的集合 keep = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ' ', '-', "'"} def normalize(s): """Convert s to a normalized string. """ result = '' for c in s.lower(): if c in keep: result +=c return result def make_freq_dict(s): """Returns a dictionary whose keys are the words of s, dnd whose values are the counts of those words. """ s = normalize(s) words = s.split() d = {} for w in words: if w in d: #看到w出现过? d[w] += 1 else: d[w] = 1 return d

运行:

这个函数遍历字符串s的每个单词,并将其加入到字典中。如果w是包含在d中的键,if语句if w in d 的条件将为True,否则为False。如果w是包含在d中的键,则说明w出现过,因此将其出现次数加1;如果w未包含在d中,就使用语句d[w] = 1将其作为新键加入到字典中。

组织在一起

现在万事俱备,可以编写一个函数,计算并显示给定文本文件的统计数据:

#countfileword.py #包含所有要保留的字符的集合 keep = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ' ', '-', "'"} def normalize(s): """Convert s to a normalized string. """ result = '' for c in s.lower(): if c in keep: result +=c return result def make_freq_dict(s): """Returns a dictionary whose keys are the words of s, dnd whose values are the counts of those words. """ s = normalize(s) words = s.split() d = {} for w in words: if w in d: #看到w出现过? d[w] += 1 else: d[w] = 1 return d def print_file_stats(fname): """Print statistics for the given file. """ s = open(fname, 'r').read() num_chars = len(s) #在规范化s之前计算 num_lines = s.count('\n') #在规范化s之前计算 d = make_freq_dict(s) num_words = sum(d[w] for w in d) #计算s包含多少个单词 #创建一个列表,其中的元素由出现次数和单词组成的元组 #并按出现次数从高到低排列 lst = [(d[w], w) for w in d] lst.sort() lst.reverse() print("The file '%s' has: " % fname) print(" %s characters" % num_chars) print(" %s lines" % num_lines) print(" %s words" % num_words) print("\nThe top 10 most frequent words are:") i = 1 # i为列表元素编号 for count, word in lst[:10]: print('%2s. %4s %s' % (i, count, word)) i += 1

运行:

练习

1.修改函数print_file_stats,使其也打印文件中不同的单词总数。

2.修改函数print_file_stats,使其打印文件中单词的平均长度。

3.罕用语(hapax hegomenon)是在文件中只出现过一次的单词。请修改函数print_file_stats,使其打印罕用语总数。

4。前面说过,文件bill.txt中出现频率最高的10个单词都是功能词,如the和and。我们通常对这些单词不感兴趣,因此我们可创建一个排除词(stop word)集合,其中包含要忽略的所有单词。

在函数print_file_stats中新增一个名为stop_words的变量,如下所示:

stop_words = {'the', 'and', 'i', 'to', 'of', 'a', 'you', 'my', 'that', 'in'}

当然,你可根据自已的喜好修改排除词集合。现在,修改程序的代码,在计算所有统计数据时,都将stop_list中的单词排除在外。

5.(较难)函数print_file_stats将一个文件名作为输入,并将整个文件都读取到一个字符串变量中。这种做法的问题在于,如果文件很大,将整个文件都放在一个字符串变量中将占用大量内存。

另一种做法是以每次一行的方式读取文件,这样占用的内存通常少得多。

请编写一个名为print_file_stats_lines的新函数,其功能与print_file_stats完全相同,但逐行读取输入文件。使用相同的文件调用这两个函数时,它们的输出应该相同。

#countwordpractice.py

from time import strftime, localtime #时间相关的模块

import datetime #获取系统时间

#包含所有要保留的字符的集合

keep = {'a', 'b', 'c', 'd', 'e',

'f', 'g', 'h', 'i', 'j',

'k', 'l', 'm', 'n', 'o',

'p', 'q', 'r', 's', 't',

'u', 'v', 'w', 'x', 'y',

'z', ' ', '-', "'"}

def normalize(s):

"""Convert s to a normalized string.

"""

result = ''

for c in s.lower():

if c in keep:

result +=c

return result

def make_freq_dict(s):

"""Returns a dictionary whose keys are the words of s,

dnd whose values are the counts of those words.

"""

s = normalize(s)

words = s.split()

d = {}

for w in words:

if w in d: #看到w出现过?

d[w] += 1

else:

d[w] = 1

return d

def print_file_stats(fname):

"""Print statistics for the given file.

"""

start = datetime.datetime.now()

s = open(fname, 'r').read()

num_chars = len(s) #在规范化s之前计算

num_lines = s.count('\n') #在规范化s之前计算

d = make_freq_dict(s)

#print("print_file_stats======d===========", d)

num_words = sum(d[w] for w in d) #计算s包含多少个单词

#创建一个列表,其中的元素由出现次数和单词组成的元组

#并按出现次数从高到低排列

lst = [(d[w], w) for w in d]

lst.sort()

lst.reverse()

print("The file '%s' has: " % fname)

print(" %s characters" % num_chars)

print(" %s lines" % num_lines)

print(" %s words" % num_words)

end = datetime.datetime.now()

print(end - start) #打印运行时间

#return

print("\nThe top 10 most frequent words are:")

i = 1 # i为列表元素编号

for count, word in lst[:10]:

print('%2s. %4s %s' % (i, count, word))

i += 1

#1.打印文件中不同的单词总数。

nWordCount = len(lst)

print("1.打印文件中不同的单词总数 word count = %d" % nWordCount)

#print(lst)

#2.打印文件中单词的平均长度。

#[(12, 'the'), (10, 'modules'), (8, 'of'), (6, 'are')...]

#(count, word)是一组键值对

# | |

# 12 the

nCharCount = 0

for count, word in lst:

#print('%2s. %4s %s' % (i, count, len(word)))

#i += 1

nCharCount += len(word)

print("2.打印文件中单词的平均长度 average word length = %d" % (nCharCount / nWordCount))

#罕用语(hapax hegomenon)是在文件中只出现过一次的单词。

#请修改函数print_file_stats,使其打印罕用语总数

hapax = 0

for count, word in lst:

if count > 1: #单词数大于1过滤掉

continue

hapax += 1

print("3.打印文件中打印罕用语总数hapax hegomenon = %d" % hapax)

#4。前面说过,文件bill.txt中出现频率最高的10个单词都是功能词,如the和and。

#我们通常对这些单词不感兴趣,因此我们可创建一个排除词(stop word)集合,

#其中包含要忽略的所有单词。在函数print_file_stats中新增一个名为stop_words的变量,

#如下所示:

stop_words = {'the', 'and', 'i', 'to', 'of', 'a', 'you', 'my', 'that', 'in'}

#当然,你可根据自已的喜好修改排除词集合。现在,修改程序的代码,在计算所有统计数据时,

#都将stop_list中的单词排除在外。

validWord = [] #创建空的列表

j = 0

for count, word in lst:

if not (word in stop_words):

#print('validWord[%3s]= %4s %s' % (j, count, word))

j += 1

validWord.append(word) #如果单词不在stop_words里,添加到有效单词中

print("4.有效单词个数 validWard = %d" % len(validWord))

"""

5.(较难)函数print_file_stats将一个文件名作为输入,并将整个文件都读取到一个字符串变量中。

这种做法的问题在于,如果文件很大,将整个文件都放在一个字符串变量中将占用大量内存。

另一种做法是以每次一行的方式读取文件,这样占用的内存通常少得多。

请编写一个名为print_file_stats_lines的新函数,其功能与print_file_stats完全相同,

但逐行读取输入文件。使用相同的文件调用这两个函数时,它们的输出应该相同。

"""

def print_file_stats_lines(fname):

"""Print statistics for the given file.

"""

#with open(fname, 'r') as file_object:

# contents = file_object.read()

# print(contents)

start = datetime.datetime.now()

#print(strftime("%Y-%m-%d %H:%M:%S", localtime())) # 打印当前时间

#逐行读取

num_lines = 0

num_chars = 0

d = {}

s = ''

with open(fname, 'r') as file_object:

lines = file_object.readlines()

for line in lines:

#print("line[%3s]======%s" % (num_lines, line))

num_lines += 1

num_chars += len(line)

#d += make_freq_dict(line)

#s = normalize(line)

for c in line.lower():

if c in keep:

s += c

words = s.split()

for w in words:

if w in d: #看到w出现过?

d[w] += 1

else:

d[w] = 1

#print("print_file_stats_lines=====d===========", d)

num_words = sum(d[w] for w in d) #计算s包含多少个单词

#print("num_words===========%d" % num_words)

#创建一个列表,其中的元素由出现次数和单词组成的元组

#并按出现次数从高到低排列

lst = [(d[w], w) for w in d]

lst.sort()

lst.reverse()

print("The file '%s' has: " % fname)

print(" %s characters" % num_chars)

print(" %s lines" % num_lines)

print(" %s words" % num_words)

end = datetime.datetime.now()

print(end - start) #打印运行时间

#return

print("\nThe top 10 most frequent words are:")

i = 1 # i为列表元素编号

for count, word in lst[:10]:

print('%2s. %4s %s' % (i, count, word))

i += 1

#1.打印文件中不同的单词总数。

nWordCount = len(lst)

print("1.打印文件中不同的单词总数 word count = %d" % nWordCount)

#print(lst)

#2.打印文件中单词的平均长度。

#[(12, 'the'), (10, 'modules'), (8, 'of'), (6, 'are')...]

#(count, word)是一组键值对

# | |

# 12 the

nCharCount = 0

for count, word in lst:

#print('%2s. %4s %s' % (i, count, len(word)))

#i += 1

nCharCount += len(word)

print("2.打印文件中单词的平均长度 average word length = %d" % (nCharCount / nWordCount))

#罕用语(hapax hegomenon)是在文件中只出现过一次的单词。

#请修改函数print_file_stats,使其打印罕用语总数

hapax = 0

for count, word in lst:

if count > 1: #单词数大于1过滤掉

continue

hapax += 1

print("3.打印文件中打印罕用语总数hapax hegomenon = %d" % hapax)

#4。前面说过,文件bill.txt中出现频率最高的10个单词都是功能词,如the和and。

#我们通常对这些单词不感兴趣,因此我们可创建一个排除词(stop word)集合,

#其中包含要忽略的所有单词。在函数print_file_stats中新增一个名为stop_words的变量,

#如下所示:

stop_words = {'the', 'and', 'i', 'to', 'of', 'a', 'you', 'my', 'that', 'in'}

#当然,你可根据自已的喜好修改排除词集合。现在,修改程序的代码,在计算所有统计数据时,

#都将stop_list中的单词排除在外。

validWord = [] #创建空的列表

j = 0

for count, word in lst:

if not (word in stop_words):

#print('validWord[%3s]= %4s %s' % (j, count, word))

j += 1

validWord.append(word) #如果单词不在stop_words里,添加到有效单词中

print("4.有效单词长度 length = %d" % len(validWord))

11.8 最终的程序

最终的程序代码如下:

#wordstats.py #包含所有要保留的字符的集合 keep = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ' ', '-', "'"} def normalize(s): """Convert s to a normalized string. """ result = '' for c in s.lower(): if c in keep: result +=c return result def make_freq_dict(s): """Returns a dictionary whose keys are the words of s, and whose values are the counts of those words. """ s = normalize(s) words = s.split() d = {} for w in words: if w in d: #如果w出现过,就将其出现次数加1 d[w] += 1 else: d[w] = 1 #如果w是第一次出现,就将其出现次数设置为1 return d def print_file_stats(fname): """Print statistics for the given file. """ s = open(fname, 'r').read() num_chars = len(s) #在规范化s之前计算字符数 num_lines = s.count('\n') #在规范化s之前计算行数 d = make_freq_dict(s) num_words = sum(d[w] for w in d) #计算s包含多少个单词 #创建一个列表,其中的元素由出现次数和单词组成的元组 #并按出现次数从高到低排列 lst = [(d[w], w) for w in d] lst.sort() lst.reverse() #在屏幕上打印结果 print("The file '%s' has: " % fname) print(" %s characters" % num_chars) print(" %s lines" % num_lines) print(" %s words" % num_words) print("\n The top 10 most frequent words are:") i = 1 #i为列表元素编号 for count, word in lst[:10]: print('%2s. %4s %s' % (i, count, word)) i += 1 def main(): print_file_stats('bill.txt') ​ if __name__ == '__main__': main()

运行:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 14:54:31

MSP430超低功耗开发实战指南:应用笔记与参考设计精要

简介:本资源是面向嵌入式初学者、单片机工程师及高校电子类专业学生的MSP430全栈学习资料合集,聚焦低功耗系统开发中的软硬件协同设计痛点,覆盖选型、外设驱动、电源管理、无线通信、传感器采集与LCD显示等典型应用场景。压缩包共含百余份高质…

作者头像 李华
网站建设 2026/9/5 14:50:54

MODBUS协议调试实战:从帧格式到CRC校验的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 14:48:38

STM32项目实战:三大典型坑及底层排查思路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 14:48:22

Spring Boot 3.4.1+Vue校园招聘系统全栈开发实战与架构解析

简介:本资源是一套面向计算机专业本科生与毕业设计学习者的校园求职招聘系统完整实现方案,基于Spring Boot 3.4.1与Vue技术栈,采用前后端分离架构,精准覆盖校园场景下的用户管理、职位发布、简历投递、面试安排及招聘后台等核心业…

作者头像 李华
网站建设 2026/9/5 14:47:38

STM32C5轮询读取LSM6D3TR-C陀螺仪数据:从寄存器配置到dps换算

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华