5.3 为文本文件中数据的任意数目分类计算统计量
前面两个应用程序演示了如何通过 CSV 文件和 Excel 文件中的数据完成具体的任务。确实,本书的大部分内容都重点介绍如何分析和处理这些文件中的数据。对于 CSV 文件,我们使用 Python 内置的 csv 模块。对于 Excel 文件,我们下载并使用扩展模块 xlrd。CSV 文件和 Excel 文件是商业中常用的文件类型,所以知道如何处理这些文件是非常重要的。
同样,文本文件(也称平面文件)也是商业中常用的文件类型。前面已经说过,CSV 文件实际上就是以逗号分隔的文本文件形式保存的。活动日志、错误日志和交易记录是商业数据保存在文本文件中的几个更常见的例子。因为文本文件和 CSV 文件、Excel 文件一样,也经常应用于商业过程,但直到现在本书还没有详细介绍如何分析文本文件,所以我们使用下面这个应用程序演示如何从文本文件中提取数据并根据这些数据计算统计量。
正如前面一段提到的,错误日志通常是以文本文件形式存储的。MySQL 数据库系统中的错误日志就是以这种形式保存的。在上一章中,我们下载并安装了 MySQL 数据库系统,所以如果你在上一章中实现了示例程序,那么就可以访问 MySQL 错误日志了。但请记住,你可以根据自己的需要定位并访问 MySQL 系统的错误日志,完成本节中的示例并不要求你必须访问 MySQL 错误日志。
要在 Windows 系统中访问 MySQL 系统的错误日志文件,先打开资源管理器,选择 C 磁盘驱动器,然后打开 ProgramData,接着打开 MySQL 文件夹,再打开 MySQL Server <Version> 文件夹(例如,MySQL Server 5.6),最后打开 data 文件夹。在 data 文件夹中,应该有一个以扩展名 .err 结尾的错误日志文件。使用鼠标右击这个文件,用像 Notepad 或 Notepad++ 这样的文本编辑器打开这个文件,就可以看到 MySQL 系统的错误记录已经被写到这个日志文件中了。如果在这个路径下找不到文件夹,可以打开资源管理器,选择 C 磁盘驱动器,在右上角的搜索框中输入“.err”,然后等待系统找到错误日志文件。如果系统找到了多个错误日志文件,那么就选择与上面描述的路径最接近的那一个。
macOS 用户应该可以在这里找到错误日志文件 /usr/local/mysql/data/<hostname>.err。
因为你的 MySQL 错误日志文件中的数据肯定与我的 MySQL 错误日志文件中的数据不一样,所以在这个应用程序中,我们使用一个独立的、有代表性的 MySQL 错误日志文件。这样就可以把重点放在 Python 代码上,而不是处理各种错误日志文件的不同之处。要为这个应用程序创建一个典型的 MySQL 错误日志文件,先打开一个文本编辑器,输入图 5-11 所示的各行文本,然后将文件保存为 mysql_server_error_log.txt。
图 5-11:mysql_server_error_log.txt 中的 MySQL 数据库错误日志数据示例,显示在 Notepad++ 中
你可以看到,在 MySQL 错误日志文件中,记录着 mysqld 启动与结束的时间,以及在服务器运行时发生的各种关键错误的信息。例如,文件的第 1 行就显示了启动的时间,第 7 行则显示了这一天中 mysqld 结束的时间。第 2~6 行显示了这一天中当服务器运行时发生的关键错误。这些行的开头都是一个日期和时间戳,后面是由关键词 [Note] 引导的关键的错误消息。文件中其余各行包含着同样的信息,只是日期不同。
为了减少创建文件时的输入量,我重复了时间戳和很多关键错误消息。因此,创建文件时你只需输入 1~7 行,然后将这些行复制粘贴两次,修改一下日期和错误消息即可。
既然我们有了 MySQL 错误日志文件,那么下面就讨论一下商业应用。这种情况下的文本文件经常保存着一些分散的数据,可以进行分析、聚集和解释,以产生一些新的知识。举例来说,在我们这个应用中,错误日志文件记录着错误的类型和发生时间。在它的原始状态下,我们很难看出是否有某种错误发生的比其他错误更频繁,或者是否某种错误的频率会随着时间发生变化。通过解析这个文本文件,将相关信息聚集起来,然后以合适的形式写入一个输出文件,就可以从数据中获得知识,促使我们采取正确的行动。你使用的文本文件可能不是 MySQL 错误日志,但是,能够分析文本文件,找出关键数据,并将数据聚集起来以产生新知识,是处理文本文件的一项常用的重要技能。
既然我们已经理解了商业应用的需求,下面需要做的就是编写 Python 代码来进行错误消息的分析和计算。要完成这个操作,在文本编辑器中输入下列代码,然后将文件保存为 3parse_text_file.py:
1 #!/usr/bin/env python3 2 import sys 3 4 input_file = sys.argv[1] 5 output_file = sys.argv[2] 6 messages = { } 7 notes = [ ] 8 with open(input_file, 'r', newline='') as text_file: 9 for row in text_file: 10 if '[Note]' in row: 11 row_list = row.split(' ', 4) 12 day = row_list[0].strip() 13 note = row_list[4].strip('\n').strip() 14 if note not in notes: 15 notes.append(note) 16 if day not in messages: 17 messages[day] = { } 18 if note not in messages[day]: 19 messages[day][note] = 1 20 else: 21 messages[day][note] += 1 22 filewriter = open(output_file, 'w', newline='') 23 header = ['Date'] 24 header.extend(notes) 25 header = ','.join(map(str,header)) + '\n' 26 print(header) 27 filewriter.write(header) 28 for day, day_value in messages.items(): 29 row_of_output = [ ] 30 row_of_output.append(day) 31 for index in range(len(notes)): 32 if notes[index] in day_value.keys(): 33 row_of_output.append(day_value[notes[index]]) 34 else: 35 row_of_output.append(0) 36 output = ','.join(map(str,row_of_output)) + '\n' 37 print(output) 38 filewriter.write(output) 39 filewriter.close()
在这个应用中,与 CSV 文件和 Excel 文件不同,因为我们分析的文本文件只包含纯文本,所以不需要导入 csv 或 xlrd 模块,只需要在第 2 和 3 行代码中导入 Python 内置的 sys 和 string 模块即可。前面已经介绍过,这两个模块分别可以使我们处理字符串和从命令行中读取输入参数。
第 4 和 5 行代码读取我们在命令行中提供的两个输入参数:包含 MySQL 错误日志的文本文件的路径名和 CSV 输出文件的路径名,CSV 输出文件中将保存每天发生的各种错误类型的信息。这两个输入参数被分别赋给两个变量:input_file 和 output_file。
第 6 行代码创建了一个空字典 messages。和前一个应用程序中的字典一样,messages 也是一个嵌套字典。外部字典的键是错误发生的具体日期,与之对应的值是另一个字典。内部字典的键是错误消息,与之对应的值是某一天错误发生的次数。
第 7 行代码创建了一个空列表 notes。列表 notes 将保存输入的错误日志文件中所有日期发生的全部错误消息。将所有错误消息放在一个独立的数据结构中(也就是说放在一个列表中,而不是放在前面的字典中),可以更容易地在错误日志文件中搜索错误消息,将错误消息作为标题行写入输出文件,并在字典和列表中分别进行迭代,以将日期和数据计数写在输出文件中。
第 8 行代码使用 Python 的 with 语法打开输入文件以供读取。第 9 行代码创建了一个 for 循环,在输入文件的各行之间循环。
第 10 行代码是一个 if 语句,检验字符串 [Note] 是否在这一行中。包含字符串 [Note] 的行就是包含错误消息的行。你会发现没有与 if 语句相对应的 else 语句,所以代码对不包含字符串 [Note] 的行不做任何处理。对于包含字符串 [Note] 的行,第 11~21 行代码对这行数据进行解析,将某些特定的数据加载到前面的列表和字典中。
第 11 行代码使用 string 模块的 split() 方法按照空格将行进行拆分(最多使用 4 个空格拆分 4 次),然后将从每行中拆分出的 5 个部分赋给一个列表变量 row_list。我们限制了 split() 方法使用空格拆分的次数,因为前 4 个空格用来分隔出数据的 4 个不同片段,其余的空格出现在错误消息中,应该保留为错误消息的一部分。
第 12 行代码取出 row_list 中的第一个元素(一个日期),并除去日期两端任何多余的空格、制表符和换行符,然后将其赋给变量 day。
第 13 行代码取出 row_list 中的第五个元素(错误消息),并除去错误消息两端任何多余的空格、制表符和换行符,然后将其赋给变量 note。
第 14 行代码是一个 if 语句,检验变量 note 中的错误消息是否还没有包含在列表 notes 中。如果没有,那么第 15 行代码就使用 append() 方法将错误消息追加到列表中。通过检验错误消息是否存在,并仅将不在列表中的错误消息添加到列表中,就可以保证得到一个包含输入文件中所有不重复的错误消息的列表。
第 16 行代码是一个 if 语句,检验变量 day 中的日期是否还不是字典 messages 中的一个键。如果不是,那么第 17 行代码就将这个日期作为字典键添加到 messages 字典中,并创建一个空字典,作为与这个新的日期键对应的值。
第 18 行代码是一个 if 语句,检验变量 note 中的错误消息是否还不是内部字典中的一个键,这个内部字典是外部字典中与某个日期键对应的值。如果不是,那么第 19 行代码就将错误消息作为内部字典的键添加到内部字典中,并将与这个键对应的值设为 1,这个值是用来计数的。
第 20 行代码是一个 else 语句,是第 18 行中 if 语句的补充。这行代码处理一条具体错误消息在某一天出现多次的情况。当这种情况发生时,第 21 行代码将与这条错误消息对应的计数值增加 1。第 20 和 21 行代码保证与每条错误消息对应的计数值都能够反映出这条错误消息在某一天出现的次数。
当脚本处理了错误日志文件中的所有行之后,字典 messages 中将包含很多键-值对。这些键是有错误消息发生的所有不重复的日期,与其对应的值是另外一些字典,其中有各自的键-值对。这些内部字典中的键是发生于某个日期的不重复的错误消息,与这些键对应的值是一个整数计数值,表示这个错误消息在某个日期发生的次数。
第 22 行代码打开输出文件以供输出,并创建一个写文件对象 filewriter,用来将数据写入输出文件。
第 23 行代码创建了一个列表变量 header,并将字符串 Date 赋给这个列表。第 24 行代码使用 extend() 方法将列表变量 notes 中的内容扩展到列表变量 header 中。这样,header 中的第一个元素就是字符串 Date,其他元素则是来自于列表 notes 的不重复的错误消息。
第 25 行代码使用 str() 和 map() 函数以及 join() 方法,将列表变量 header 中的内容在写入输出文件之前转换成一个长字符串。map() 函数在 header 中的每个元素上应用 str() 函数,确保 header 中的每个元素都是字符串。然后使用 string 模块的 join() 方法在变量 header 中的每个字符串之间插入一个逗号,创建一个由逗号分隔各个值的长字符串。最后,向长字符串的最后添加一个换行符。这个长字符串包含由逗号分隔的列标题,末尾有一个换行符,将作为写入 CSV 输出文件中的第一行输出数据。
第 26 行代码在命令行窗口(或终端窗口)中打印出 header 中的值,也就是一个包含由逗号分隔的列标题的长字符串,这样我们就可以检查一下要写入输出文件的内容了。然后第 27 行代码使用 filewriter 对象的 write 方法将这个标题行写入输出文件。
第 28 行代码创建了一个 for 循环,并使用 items 函数在 messages 字典的键(变量 day)和值(变量 day_value)之间迭代。和前面的示例一样,第 29 行代码创建了一个空列表变量 row_of_output,用来保存写入输出文件的每行数据。因为我们已经将标题行写入了输出文件,所以知道第一列数据是日期。因此,应该将日期作为第一个追加到 row_of_output 中的数据。确实,第 30 行代码使用 append 方法将字典 messages 中的第一个日期追加到了 row_of_output 中。
此后,第 31~35 行代码在列表变量 notes 中的错误消息之间迭代,并判断在当前正在处理的日期中,每条错误消息是否发生。如果已经发生,代码就将与这条错误消息对应的计数值添加到行中的正确位置。如果在当前正在处理的日期中,这条错误消息没有发生,那么代码就将 0 添加到行中的正确位置。
特别地,第 31 行是一个 for 循环,使用 range 和 len 函数按照索引位置在 notes 中的各个值之间迭代。
第 32 行代码是一个 if 语句,检验 notes 中的每条错误消息是否出现在当前处理日期对应的错误消息列表中。换句话说,day_value 是与当前处理日期对应的内部字典,keys 函数创建了一个内部字典键的列表,内部字典的键就是在当前处理日期发生的错误消息。
对于 notes 中的每条错误消息,如果错误消息在当前处理日期发生,也就出现在了当前处理日期对应的错误消息列表中,那么第 33 行代码就使用 append 方法将与错误消息对应的计数值追加到 row_of_output 中。通过使用 notes 中错误消息的索引值,可以保证为每条错误消息提取出正确的计数值。
例如,列表 notes 中的第一条错误消息是 InnoDB: Compressed tables use zlib 1.2.3,你可以使用 notes[0] 引用这个字符串。当你运行这个脚本时,在命令行窗口或终端窗口中可以看到,这个字符串是输出文件中第二列的标题。从屏幕上的输出还可以看出,这条错误消息在 2014-03-07 出现了 3 次。
再回顾一下脚本中的第 28~35 行代码,并记住这个日期和错误消息,看看代码是如何将正确的数据写到输出文件中的。第 28 行代码创建了一个 for 循环,在字典 messages 中的所有日期之间循环,所以在某个时候,for 循环会处理到 2014-03-07 这个日期。当第 28 行代码开始处理 2014-03-07 时,代码已经准备好了内部字典中的错误消息和与之对应的计数值(因为它们就是 day_value 字典中的键和值)。在处理 2014-03-07 时,第 31 行代码创建了另一个 for 循环,在列表 notes 的索引值之间循环。第一次循环时,索引值为 0,所以在第 32 行代码中,notes[0] 等于 InnoDB: Compressed tables use zlib 1.2.3。因为正在处理 2014-03-07 这个日期,notes[0] 中的值位于与这个日期对应的内部字典的键的集合中,所以第 32 行代码为 True,第 33 行代码被执行。在第 33 行代码中,notes[index] 就是 notes[0],day_value[notes[0]] 就是 day_value["InnoDB: Compressed tables use zlib 1.2.3"],这个表达式指向的就是内部字典中与这个键对应的值,对于 2014-03-07,这个值就是整数 3。以上操作的结果就是,在输出文件的 2014-03-07 这一行中,错误消息 InnoDB: Compressed tables use zlib 1.2.3 这一列的值为 3。
第 34 和 35 行代码处理列表 notes 中的错误消息没有出现在一个特定日期的错误消息列表中的情况。在这种情况下,第 35 行代码向输出行中正确的列的位置添加一个 0。例如,列表 notes 中的最后一条错误消息是 InnoDB:IPv6 is available.。这条错误消息没有发生在 2014-03-07,所以在 2014-03-07 这条输出行中,需要在对应这条错误消息的列中记录一个 0。当第 32 行代码检验 notes 中的最后一个值(notes[5],InnoDB:IPv6 is available.)是否在内部字典的键集合中时,结果就是 Fasle,所以执行第 34 行代码中的 else 语句,使用第 35 行代码将 0 添加到 row_of_output 的最后一列中。其余日期和错误消息的计数值也是以同样的方式添加的。
第 36 行代码使用与第 25 行代码同样的处理过程,将列表变量 row_of_output 中的内容在写入输出文件之前转换成一个长字符串。map 函数对列表变量 row_of_output 中的每个元素使用 str 函数,保证变量中的每个元素都是一个字符串。然后使用 string 模块的 join 方法在变量 row_of_output 中的每个字符串之间插入一个逗号,创建一个由逗号分隔各个值的长字符串。最后,向长字符串的最后添加一个换行符。这个长字符串包含由逗号分隔的列标题,末尾有一个换行符,被赋给变量 output,将作为一行输出写入 CSV 输出文件。
第 37 行代码在命令行窗口或终端窗口打印出 output 中的值,也就是一个由逗号分隔各个值的长字符串,这样你可以检查一下要写入输出文件的内容。然后第 38 行代码使用 filewriter 对象的 write 方法将这行输出写入输出文件。
最后,第 39 行代码使用 filewriter 的 close 方法关闭 filewriter 对象。
我们已经完成了 Python 脚本,现在可以使用脚本计算不同的错误随着时间变化发生的次数,并将结果写入 CSV 输出文件了。要完成这个操作,在命令行中输入以下命令,然后按回车键:
python 3parse_text_file.py mysql_server_error_log.txt\ output_files\3app_output.csv
你可以看到在命令行窗口或终端窗口打印出的输出,如图 5-12 所示。
图 5-12:在 MySQL 错误日志文件 mysql_server_error_log.txt 上运行 3parse_text_file.py 的结果,显示在命令行窗口中
打印在命令行窗口中的输出展示了写在输出文件 3app_output.csv 中的数据。输出的第一行是标题行,展示了输出文件中所有列的标题。第一个列标题 Date 说明第一列的内容是输入文件中记录错误消息的日期。这一列说明输入文件中包含 3 个不重复的日期。其余 6 个列标题是输入文件中出现的错误消息,因此输入文件中包含 6 个不重复的错误消息。这 6 列中包含了输入文件中一个具体的错误消息在每个日期出现的次数。例如,输出中的最后一列表示错误消息 InnoDB:IPv6 is available. 在 2014-10-27 出现了 2 次,在 2014-02-03 出现了 0 次,在 2014-03-07 出现了 0 次。
输出文件 3app_output.csv 中的内容和打印在命令行窗口中的输出一样,如图 5-13 所示。
图 5-13:CSV 文件 3app_output.csv 中的 3parse_tex_file.py 的输出(某个错误消息在某一天发生的次数),显示在 Excel 工作表中
这个屏幕截图是在 Excel 中打开的 CSV 输出文件,显示了写在输出文件中的数据。在标题行中,日期标题后面是 6 个错误消息,由于横向空间的限制,在屏幕截图中你看不到完整的错误消息(如果你创建了这个文件,可以通过增加列的宽度,看看完整的消息)。
你可以看到,在输出文件中,行表示某个日期,列表示某个错误消息,这使得这个文件看上去比较怪。在这个简单的例子中,错误消息(列)的数量多于日期(行)的数量,似乎更应该使用行来表示错误消息。通常,在更大的日志文件中,日期的数量很可能比错误消息多。在这种情况下,就更应该用行表示日期(因为行更多一些),用列表示错误消息。最终,是用行表示日期,列表示错误消息,还是相反,取决于你的实际分析需要和个人偏好。你可以修改现有代码将输出进行转置,用列表示日期,用行表示错误消息,这会是一个很好的练习。
这个应用程序综合运用我们在第 1 章中学习的几种技术(例如填充一个嵌套字典)来解决一个常见的实际问题。商业分析师经常需要分析文本文件,找出关键数据,并将数据进行聚集或摘要,以获取新的知识。在很多情况下,我们需要以不同方式处理成千上万条数据,所以手动分析数据是不可能的。
这一节演示了一种可扩展的方法,这种方法可以从文本文件中解析出数据,并使用解析出的数据计算基本的统计量。为了使这个示例尽量简单,我们仅仅使用了一个很短的错误日志文件。但是,这种方法具有很好的扩展性,你可以使用它来解析更大的日志文件,也可以修改一下代码,来处理多个文本文件中的数据。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论