精通Python自动化脚本-运维人员宝典完整目录:
第一章 Python脚本概述
第二章 Python脚本调试和性能测试
第三章 单元测试-单元测试框架的介绍
第四章 自动化常规运维活动
第五章 文件、目录和数据处理
第六章 文件存档、加密和解密
第七章 文本处理和正则表达式
第八章 文档和报告
第九章 操作各类文件
第十章 网络基础 – Socket编程
第十一章 使用Python脚本处理邮件
第十二章 使用Telnet和SSH远程监控主机
第十三章 创建图形化用户界面
第十四章 处理Apache和其它的日志文件
第十五章 SOAP和REST API通讯
第十六章 网络抓取 – 从网站上提取有用的信息
第十七章 数据收集及报表
第十八章 MySQL和SQLite数据库管理
本章中我们将学习日志文件相关知识。我们会学习如何解析日志文件。我们还将学习为什么需要在程序中编写异常。解析不同文件的不同方式也很重要。我们会学习错误日志和访问日志的知识。最后我们将学习如何解析其它日志文件。
本章中我们将学习如下内容:
- 解析复杂的日志文件
- 对异常的需要
- 解析不同文件的技巧
- 错误日志
- 访问日志
- 解析其它日志文件
解析复杂的日志文件
首先我们将查看复杂日志文件的概念。解析日志文件是个具有挑战性的工作,因为大部分日志文件都是普通文本格式,并且该格式不遵循任何规则。这些文件可被修改而不产生任何警告。用户和开发应用的人员均可决定在日志文件中存储什么样的数据以及存储为什么格式。
在进入解析或修改日志文件配置的示例之前,首先我们需要理解一个典型的日志文件中有什么内容。据此我们了解如何对其进行操作或从中获取信息。我们还可以看看日志文件中的常用术语,这样我们可以使用这些常用术语来获取数据。
通常我们可以看到日志文件中生成的大部内容都是通过应用容器,以及系统访问状态记录(换句话说是日志开启和日志关闭)或通过网络访问系统的记录。因此,通过远程网络访问系统时,这种远程连接的记录会被保存到日志文件中。我们来获取这种情况的示例。我们已有一个带有日志信息的名为access.log的日志文件。
那么我们来创建一个脚本read_apache_log.py并在其中编写如下内容:
1 2 3 4 5 6 7 8 9 |
def read_apache_log(logfile): with open(logfile) as f: # log_obj = f.read() # print(log_obj) for i in range(5): print(next(f)) if __name__ == '__main__': read_apache_log('access.log') |
译者注:read()会读取整个文件,为避免刷屏以上做了修改仅读取前5行
运行脚本将得到如下输出:
1 2 3 4 5 6 7 8 9 10 |
$ python3 read_apache_log.py 117.188.30.192 - - [03/Mar/2019:03:14:30 +0000] "GET /category/odoo/page/4/ HTTP/1.1" 200 8336 "http://alanhou.org/category/odoo/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" 117.188.30.192 - - [03/Mar/2019:03:14:31 +0000] "GET / HTTP/1.1" 200 9197 "http://alanhou.org/category/odoo/page/4/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" 117.188.30.192 - - [03/Mar/2019:03:14:42 +0000] "GET /category/odoo/page/3/ HTTP/1.1" 200 9256 "http://alanhou.org/category/odoo/page/4/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" 117.188.30.192 - - [03/Mar/2019:03:14:44 +0000] "GET /category/odoo/page/3/ HTTP/1.1" 200 9251 "http://alanhou.org/category/odoo/page/4/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" 117.188.30.192 - - [03/Mar/2019:03:14:45 +0000] "GET / HTTP/1.1" 200 9197 "http://alanhou.org/category/odoo/page/3/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" |
上例中,我们创建了一个read_apache_log函数来读取Apache日志文件。其中,我们打开了日志文件并打印出了日志中的信息。在定义了read_apache_log()函数之后,我们传入了Apache日志文件来调用该函数。本例中的日志文件为access.log。
在读取了access.log文件中的各行之后,我们将从日志文件中解析IP地址。创建一个脚本parse_ip_address.py并在其中编写如下内容:
1 2 3 4 5 6 7 8 9 10 11 |
import re from collections import Counter r_e = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' with open('access.log') as f: print('Reading Apache log file') Apache_log = f.read(50000) get_ip = re.findall(r_e, Apache_log) no_of_ip = Counter(get_ip) for k, v in no_of_ip.items(): print('Available IP Address in log file ' + '=> ' + str(k) + ' ' + 'Count ' + '=> ' + str(v)) |
译者注:如日志文件较大可像 Alan 这样指定仅读取前 xx 字节
运行脚本,我们将得到如下输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
$ python3 parse_ip_address.py Reading Apache log file Available IP Address in log file => 117.188.30.192 Count => 65 Available IP Address in log file => 66.249.79.227 Count => 1 Available IP Address in log file => 111.202.101.7 Count => 1 Available IP Address in log file => 46.229.168.146 Count => 1 Available IP Address in log file => 46.229.168.133 Count => 1 Available IP Address in log file => 54.36.148.177 Count => 1 Available IP Address in log file => 23.239.1.95 Count => 1 Available IP Address in log file => 91.121.155.172 Count => 4 Available IP Address in log file => 64.233.172.168 Count => 1 Available IP Address in log file => 54.36.148.58 Count => 1 Available IP Address in log file => 116.227.9.111 Count => 1 Available IP Address in log file => 207.46.13.34 Count => 1 Available IP Address in log file => 66.249.79.203 Count => 1 Available IP Address in log file => 40.77.167.115 Count => 4 Available IP Address in log file => 136.243.70.151 Count => 2 Available IP Address in log file => 37.115.190.120 Count => 3 Available IP Address in log file => 42.156.137.108 Count => 8 Available IP Address in log file => 42.156.136.64 Count => 1 Available IP Address in log file => 42.120.161.64 Count => 2 Available IP Address in log file => 42.156.138.64 Count => 1 Available IP Address in log file => 42.156.139.108 Count => 1 Available IP Address in log file => 207.46.13.33 Count => 1 Available IP Address in log file => 1.10.187.34 Count => 9 Available IP Address in log file => 66.249.79.229 Count => 3 Available IP Address in log file => 109.228.56.115 Count => 1 Available IP Address in log file => 66.249.79.231 Count => 2 Available IP Address in log file => 183.143.43.108 Count => 9 Available IP Address in log file => 42.156.137.83 Count => 2 Available IP Address in log file => 42.156.139.83 Count => 2 Available IP Address in log file => 42.120.161.83 Count => 1 Available IP Address in log file => 42.120.160.95 Count => 1 Available IP Address in log file => 42.120.161.95 Count => 1 Available IP Address in log file => 182.134.133.186 Count => 1 Available IP Address in log file => 157.55.39.109 Count => 1 Available IP Address in log file => 42.156.136.22 Count => 2 Available IP Address in log file => 42.156.137.22 Count => 3 Available IP Address in log file => 42.156.138.22 Count => 2 Available IP Address in log file => 42.120.160.22 Count => 4 Available IP Address in log file => 42.156.139.22 Count => 4 Available IP Address in log file => 37.9.87.213 Count => 4 Available IP Address in log file => 54.36.149.17 Count => 1 Available IP Address in log file => 54.36.148.229 Count => 1 Available IP Address in log file => 54.36.149.22 Count => 1 Available IP Address in log file => 168.181.61.154 Count => 1 Available IP Address in log file => 125.82.16.199 Count => 2 Available IP Address in log file => 123.125.71.95 Count => 1 Available IP Address in log file => 111.206.198.79 Count => 1 Available IP Address in log file => 111.206.198.100 Count => 1 Available IP Address in log file => 54.36.149.64 Count => 1 Available IP Address in log file => 42.84.39.146 Count => 1 Available IP Address in log file => 180.173.173.168 Count => 1 Available IP Address in log file => 125.70.190.216 Count => 7 Available IP Address in log file => 54.36.148.104 Count => 1 Available IP Address in log file => 42.120.160.114 Count => 2 |
上例中,我们创建了Apache日志解析器来获取对应的 IP 地址及其对服务器的请求次数。因此,很明确我们无需整个Apache日志文件的所有行,仅需获取日志文件中的 IP 地址。实现这一获取,我们需要定义一个模式来搜索 IP 地址,我们可通过正则表达式来实现。因此我们导入了 re 模块。然后我们导入了Collection模块来代替 Python 的内置数据类型:字典、列表、集合和元组。该模块有特定的容器数据类型。在导入所需模块后,我们使用正则表达式编写了一个模式来匹配指定条件来从日志文件中映射 IP 地址。
在这个匹配模式中,\d为0到9之间的任意数字,\r表示原生字符串。然后,我们打开了名为access.log的Apache日志文件并进行了读取。之后我们对Apache日志文件应用了正则表达式条件,接着使用Collection模块中的Counter 函数来对以re条件获取到的 IP 地址进行计数。最后,正如在输出中所见我们打印出了执行的结果。
对异常处理的需要
这一部分中,我们将来看Python编程中对异常处理的需要。正常的程序流包含事件和信号。异常则是在程序中出现了问题。导演可以是任何类型的,如除零错误、导入错误、属性错误或断言错误。这些异常在函数未能正常执行对应任务时发生。发生异常时程序停止执行,并且编译器会进入异常处理进程。异常处理进程包含在try…except代码块中编写的代码。进行异常处理的原因是程序中出现了预期外的情况。
分析异常
这一部分中,我们将来了解异常的分析。每个发生的异常都必须要进行处理。我们日志文件也会包含一些异常。如果类似类型的异常获取到了数次,那么程序存在问题,我们应尽快对其进行必要的修改。
运行如下示例:
1 2 3 |
f = open('logfile', 'r') print(f.read()) f.close() |
运行程序后将会得到如下输出:
1 2 3 4 |
Traceback (most recent call last): File "sample.py", line 1, in <module> f = open('logfile', 'r') FileNotFoundError: [Errno 2] No such file or directory: 'logfile' |
这个例子中,我们尝试读取一个目录中不存在的文件,结果得到了如上所示的错误。从错误中我们分析出可以使用什么样的解决方案。处理这类错误,我们可以使用异常处理技术。那么让我们来看一个使用异常处理技术来处理异常的示例。
1 2 3 4 5 6 |
try: f = open('logfile', 'r') print(f.read()) f.close() except: print("File not found. Please check whether the file is present in you directory or not.") |
此时再运行脚本,将得到如下输出:
1 |
File not found. Please check whether the file is present in you directory or not. |
这个示例中,我们尝试读取目录中不存在的文件。但我们在示例中使用了文件异常技术,将代码放在的try: 和except: 代码块中。因此try: 代码块中发生了任何错误或异常,都会跳过该错误并执行except: 代码块中的代码。这里我们只是在except: 代码块是添加了打印语句。因此在运行脚本后,在try: 代码块中发生异常时,会跳过异常并执行except:代码块中的代码。那么except代码块中的打印语句会被执行,我们在以上的输出中可以看到。
解析不同文件的技巧
这一部分中,我们将学习用于解析不同文件的技巧。在开始进行实际解析之前,我们必须先读取数据。我们需要了解从哪里获取数据。但是,必须记住所有的日志文件大小都不同。为简化任务,可遵循以下清单:
- 记住日志文件既可以是普通文本也可以是压缩文件
- 所有的日志文件普通文本后缀名为.log,bzip2文件后缀名为log.bz2
- 我们应根据名称来处理一组文件
- 日志文件的所有解析必须合并为单个报告
- 我们使用的工作应从指定目录和不同目录中操作所有文件。子目录中的日志文件也应包含在内
错误日志
这一部分中,我们将学习错误日志。错误日志的相关指令如下:
- ErrorLog
- LogLevel
服务器日志的位置和名称由ErrorLog指令设置。这是最重要的日志文件。Apache httpd服务发送其中的信息以及处理过程中生成的记录。在服务器上发生错误时,首先应看看这里。它包含发生问题的细节以及修复的过程。
错误日志写入到一个文件中。在Unix系统中,错误可以服务器发送到syslog或我们通过管道发送到程序。日志记录行首先是消息的日期和时间,第二条是错误严重级别的记录。
LogLevel指定处理通过限制严重级别发送到错误日志的错误。第三条包含生成错误的客户端的信息。这一信息为 IP 地址。下一条为消息本身。它包含服务器配置拒绝客户端访问的信息。服务端随后报告请求文档的文件系统路径。
消息的不同类型可以在错误日志文件中出现。错误日志文件也包含从CGI脚本调试输出。写入到stderr的所有信息都会直接拷贝到错误日志中。
错误日志不可自定义。处理请求的错误日志中的词条会在访问日志中有对应的词条。我们应一直监控测试时问题的错误日志。在Unix系统中,可以运行如下命令来进行查看:
1 |
$ tail -f error_log |
访问日志
这部分中,我们将学习访问日志的知识。服务器访问日志将记录所有由服务器处理的请求。CustomLog指令控制访问日志的位置和内容。LogFormat指令用于选取日志的内容。
在访问日志中存储信息表示开启日志管理。下一步是分析有助于获取有用的数据统计的信息。 Apache httpd服务有多个版本。这些版本使用一些其它模块和指定来控制访问日志。我们可以配置访问日志的格式。该格式通过format字符串来指定。
常用日志格式
这一部分中,我们将学习常用的日志格式。如下语法显示了访问日志的配置:
1 2 |
LogFormat "%h %l %u %t \"%r\" %>s %b" nick_name CustomLog logs/access_log nick_name |
这个字符串会定义一个别名,然后将别名 与日志格式字符串进行关联。日志格式字符串由百分号指令组成。每个百分号指令告诉服务器记录一个指定信息。这个字符串中可能包含带含义的字符。这些字符在会直接拷贝到日志输出中。
CustomLog指令将借助定义了的别名设置一个新的日志文件。访问日志的文件名相对路径为ServerRoot,除非在前面加了斜杠。
我们前述的配置会在常用日志格式(Common Log Format (CLF))中写入日志词条。这是一个标准格式,可由很多不同的web服务器生成。很多日志分析程序可读取这一日志格式。
下面我们来看每个百分号指令的含义:
- %h:这向我们显示向web服务器服务器发送请求的客户端 IP 地址。如果开启了HostnameLookups,那么服务器会确定主机名并在 IP 地址处记录它。
- %l:这一条用于表示请求未能获得的信息(译者注:该信息为客户端identd确定,通常无法获取,显示为-)。
- %u:这是请求文档的用户 ID。同一值由CGI脚本在REMOTE_USER环境变量中传递。
- %t:这一条用于检测服务器处理请求结束的时间。格式如下:
[day/month/year:hour:minute:second zone]
对于day参数接收两个数字。对于month,我们需要定义三个字母。对于year,因为年有4个字符,我们需要接收4个数字。在day, month和year之后,我们需要为小时、分钟和秒分别接收两个数字。
- \”%r\”:用于请求行,由客户端放在双引号中给定。这一请求行有一些有用的信息。请求客户端使用GET,协议使用HTTP。
- %>s:这一条定义客户端的状态码。状态码非常重要且有用,因为它表示客户端向服务端发送的请求是否成功。
- %b:这一条定义返回客户端时对象的总大小。这一总大小不包含响应头的大小。
解析其它日志文件
我们系统中还有包含Apache日志文件在内的不同日志文件。在Linux发行版中,日志文件在根文件系统的/var/log/文件夹下,如下所示:
译者注:从这个截图开始的数据均直接来自原书
在以上截图中,我们可以很容易的看到针对不同操作的不同日志文件格式(例如,验证日志文件auth.log,系统日志文件syslog和内核日志kern.log)。如前所示,我们对Apache日志文件执行了操作,我们也可以对其它本地日志文件执行同样的操作。下面来看一个解析另一种日志文件的示例。创建一个脚本simple_log.py并编写如下内容:
1 2 3 4 5 6 |
f=open('/var/log/kern.log','r') lines = f.readlines() for line in lines: kern_log = line.split() print(kern_log) f.close() |
运行脚本,我们将得到如下输出:
1 2 3 4 5 6 7 8 9 10 |
student@ubuntu:~$ python3 simple_log.py # 输出内容: ['Dec', '26', '14:39:38', 'ubuntu', 'NetworkManager[795]:', '<info>', '[1545815378.2891]', 'device', '(ens33):', 'state', 'change:', 'prepare', '->', 'config', '(reason', "'none')", '[40', '50', '0]'] ['Dec', '26', '14:39:38', 'ubuntu', 'NetworkManager[795]:', '<info>', '[1545815378.2953]', 'device', '(ens33):', 'state', 'change:', 'config', '->', 'ip-config', '(reason', "'none')", '[50', '70', '0]'] ['Dec', '26', '14:39:38', 'ubuntu', 'NetworkManager[795]:', '<info>', '[1545815378.2997]', 'dhcp4', '(ens33):', 'activation:', 'beginning', 'transaction', '(timeout', 'in', '45', 'seconds)'] ['Dec', '26', '14:39:38', 'ubuntu', 'NetworkManager[795]:', '<info>','[1545815378.3369]', 'dhcp4', '(ens33):', 'dhclient', 'started', 'with', 'pid', '5221'] ['Dec', '26', '14:39:39', 'ubuntu', 'NetworkManager[795]:', '<info>', '[1545815379.0008]', 'address', '192.168.0.108'] ['Dec', '26', '14:39:39', 'ubuntu', 'NetworkManager[795]:', '<info>', '[1545815379.0020]', 'plen', '24', '(255.255.255.0)'] ['Dec', '26', '14:39:39', 'ubuntu', 'NetworkManager[795]:', '<info>', '[1545815379.0028]', 'gateway', '192.168.0.1'] |
上例中,首先我们创建了一个简单文件对象f,在其中以只读模式打开了kern.log文件。然后,我们对文件对象应用了readlines()函数来在for循环中逐行读取文件的数据。然后我们对内核日志文件的每一行应用了split()函数并对整个文件使用了print函数,通过输出可以查看。
和读取内核日志文件一样,我们还可以对它执行其它操作,下面我们就将执行一些操作。下面我们将通过索引来访问内核日志文件中的内容。通过split日志可进行实现,因为它将文件中的信息分割为另一个迭代。那么我们来看这一情况的一个示例。创建脚本simple_log1.py并在其中编写如下脚本内容:
1 2 3 4 5 |
f=open('/var/log/kern.log','r') lines = f.readlines() for line in lines: kern_log = line.split()[1:3] print(kern_log) |
运行脚本,我们将得到如下输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
student@ubuntu:~$ python3 simple_log1.py # 输出结果: ['26', '14:37:20'] ['26', '14:37:20'] ['26', '14:37:32'] ['26', '14:39:38'] ['26', '14:39:38'] ['26', '14:39:38'] ['26', '14:39:38'] ['26', '14:39:38'] ['26', '14:39:38'] ['26', '14:39:38'] ['26', '14:39:38'] ['26', '14:39:38'] |
上例中,我们仅仅是在split函数后添加了[1:3],换句话说,添加了切片。序列的子序列称为切片,这一运算提取的子序列称为切片内容。本例中,我们使用方括号[ ]来作为切片运算符并在其中放置了两个整型值,以冒号:进行分隔。运算符[1:3]返回序列第一到第三个元素这部分内容,包含第一个但排除最后一个。在对序列进行切片时,我们获取的子序列总是与操作的原序列的类型相同,但是列表或元组的元素可以是任意类型,与我们应用的切片无关,获取的切片仍为列表或元组。因此在对日志文件应用切片后,我们获取的结果如上面的输出所示。
总结
本章中,我们学习了如何处理不同类型的日志文件。我们还学习了解析复杂的日志文件以及在处理文件时对异常处理的需要。解析日志文件的技巧有助于平滑地执行解析。我们还学习了错误日志和访问日志。
下一章中,我们将学习SOAP和REST通讯。
课后问题
- Python中运行时和编译时的异常有什么区别?
- 什么是正则表达式?
- 研究Linux中的head, tail, cat和awk命令
- 编写一个Python程序将一个文件的内容追加到另一个文件中
- 编写一个Python程序来倒序读取文件内容
- 以下表达式的输出是什么?
- re.search(r’C\Wke’, ‘C@ke’).group()
- re.search(r’Co+kie’, ‘Cooookie’).group()
- re.match(r'<.*?>’, ‘<h1>TITLE</h1>’).group()
扩展阅读
- Python日志: https://docs.python.org/3/library/logging.html
- 正则表达式: https://docs.python.org/3/howto/regex.html
- 异常处理: https://www.pythonforbeginners.com/error-handling/python-try-and-except