第2章 业务服务监控详解

业务服务监控是运维体系中最重要的环节,是保证业务服务质量的关键手段。如何更有效地实现业务服务,是每个运维人员应该思考的问题,不同业务场景需定制不同的监控策略。Python在监控方面提供了大量的第三方工具,可以帮助我们快速、有效地开发企业级服务监控平台,为我们的业务保驾护航。本章涉及文件与目录差异对比方法、HTTP质量监控、邮件告警等内容。

2.1 文件内容差异对比方法

本节介绍如何通过difflib模块实现文件内容差异对比。difflib作为Python的标准库模块,无需安装,作用是对比文本之间的差异,且支持输出可读性比较强的HTML文档,与Linux下的diff命令相似。我们可以使用difflib对比代码、配置文件的差别,在版本控制方面是非常有用。Python 2.3或更高版本默认自带difflib模块,无需额外安装,我们先通过一个简单的示例进行了解。

2.1.1 示例1:两个字符串的差异对比

本示例通过使用difflib模块实现两个字符串的差异对比,然后以版本控制风格进行输出。

【/home/test/difflib/simple1.py】

        #!/usr/bin/python
        import difflib
        text1 = """text1:    #定义字符串1
        This module provides classes and functions for comparing sequences.
        including HTML and context and unified diffs.
        difflib document v7.4
        add string
        """
        text1_lines = text1.splitlines()    #以行进行分隔,以便进行对比
        text2 = """text2:    #定义字符串2
        This module provides classes and functions for Comparing sequences.
        including HTML and context and unified diffs.
        difflib document v7.5"""
        text2_lines = text2.splitlines()
        d = difflib.Differ()    #创建Differ()对象
        diff = d.compare(text1_lines, text2_lines)    # 采用compare方法对字符串进行比较
        print '\n'.join(list(diff))

本示例采用Differ()类对两个字符串进行比较,另外difflib的SequenceMatcher()类支持任意类型序列的比较,HtmlDiff()类支持将比较结果输出为HTML格式,示例运行结果如图2-1所示。

图2-1 示例运行结果

为方便大家理解差异关系符号,表2-1对各符号含义进行说明。

表2-1 符号含义说明

2.1.2 生成美观的对比HTML格式文档

采用HtmlDiff()类的make_file()方法就可以生成美观的HTML文档,对示例1中代码按以下进行修改:

        d = difflib.Differ()
        diff = d.compare(text1_lines, text2_lines)
        print '\n'.join(list(diff))

替换成:

        d = difflib.HtmlDiff()
        print d.make_file(text1_lines, text2_lines)

将新文件命名为simple2.py,运行# python simple2.py > diff.html,再使用浏览器打开diff.html文件,结果如图示2-2所示,HTML文档包括了行号、差异标志、图例等信息,可读性增强了许多。

图2-2 在浏览器中打开diff.html文件

2.1.3 示例2:对比Nginx配置文件差异

当我们维护多个Nginx配置时,时常会对比不同版本配置文件的差异,使运维人员更加清晰地了解不同版本迭代后的更新项,实现的思路是读取两个需对比的配置文件,再以换行符作为分隔符,调用difflib.HtmlDiff()生成HTML格式的差异文档。实现代码如下:

        【/home/test/difflib/simple3.py】
        #!/usr/bin/python
        import difflib
        import sys
        try:
            textfile1=sys.argv[1]    #第一个配置文件路径参数
            textfile2=sys.argv[2]    #第二个配置文件路径参数
        except Exception,e:
            print "Error:"+str(e)
            print "Usage: simple3.py filename1 filename2"
            sys.exit()
        def readfile(filename):    #文件读取分隔函数
            try:
            fileHandle = open (filename, 'rb' )
            text=fileHandle.read().splitlines()    #读取后以行进行分隔
            fileHandle.close()
            return text
            except IOError as error:
            print('Read file Error:'+str(error))
            sys.exit()
        if textfile1=="" or textfile2=="":
            print "Usage: simple3.py filename1 filename2"
            sys.exit()
        text1_lines = readfile(textfile1)    #调用readfile函数,获取分隔后的字符串
        text2_lines = readfile(textfile2)
        d = difflib.HtmlDiff()    #创建HtmlDiff()类对象
        print d.make_file(text1_lines, text2_lines)    #通过make_file方法输出HTML格式的比对结果
        运行如下代码:
        # python simple3.py nginx.conf.v1 nginx.conf.v2 > diff.html

从图2-3中可以看出nginx.conf.v1与nginx.conf.v2配置文件存在的差异。

图2-3 nginx.conf.v1与nginx.conf.v2配置文件对比结果

参考提示

2.1节示例参考官网文档http://docs.python.org/2/library/difflib.html。

2.2 文件与目录差异对比方法

当我们进行代码审计或校验备份结果时,往往需要检查原始与目标目录的文件一致性,Python的标准库已经自带了满足此需求的模块filecmp。filecmp可以实现文件、目录、遍历子目录的差异对比功能。比如报告中输出目标目录比原始多出的文件或子目录,即使文件同名也会判断是否为同一个文件(内容级对比)等,Python 2.3或更高版本默认自带filecmp模块,无需额外安装,下面进行详细介绍。

2.2.1 模块常用方法说明

filecmp提供了三个操作方法,分别为cmp(单文件对比)、cmpfiles(多文件对比)、dircmp(目录对比),下面逐一进行介绍:

单文件对比,采用filecmp.cmp(f1, f2[, shallow])方法,比较文件名为f1和f2的文件,相同返回True,不相同返回False,shallow默认为True,意思是只根据os.stat()方法返回的文件基本信息进行对比,比如最后访问时间、修改时间、状态改变时间等,会忽略文件内容的对比。当shallow为False时,则os.stat()与文件内容同时进行校验。

示例:比较单文件的差异。

        >>> filecmp.cmp("/home/test/filecmp/f1","/home/test/filecmp/f3")
        True
        >>> filecmp.cmp("/home/test/filecmp/f1","/home/test/filecmp/f2")
        False

多文件对比,采用filecmp.cmpfiles(dir1, dir2, common[, shallow])方法,对比dir1与dir2目录给定的文件清单。该方法返回文件名的三个列表,分别为匹配、不匹配、错误。匹配为包含匹配的文件的列表,不匹配反之,错误列表包括了目录不存在文件、不具备读权限或其他原因导致的不能比较的文件清单。

示例:dir1与dir2目录中指定文件清单对比。

两目录下文件的md5信息如下,其中f1、f2文件匹配;f3不匹配;f4、f5对应目录中不存在,无法比较。

        [root@SN2013-08-020 dir2]# md5sum *
        d9dfc198c249bb4ac341198a752b9458  f1
        aa9aa0cac0ffc655ce9232e720bf1b9f  f2
        33d2119b71f717ef4b981e9364530a39  f3
        d9dfc198c249bb4ac341198a752b9458  f5
        [root@SN2013-08-020 dir1]# md5sum *
        d9dfc198c249bb4ac341198a752b9458  f1
        aa9aa0cac0ffc655ce9232e720bf1b9f  f2
        d9dfc198c249bb4ac341198a752b9458  f3
        410d6a485bcf5d2d2d223f2ada9b9c52  f4

使用cmpfiles对比的结果如下,符合我们的预期。

        >>>filecmp.cmpfiles("/home/test/filecmp/dir1","/home/test/filecmp/dir2",['f1','f2',
        'f3','f4','f5'])
        (['f1', 'f2'], ['f3'], ['f4', 'f5'])

目录对比,通过dircmp(a, b[, ignore[, hide]])类创建一个目录比较对象,其中a和b是参加比较的目录名。ignore代表文件名忽略的列表,并默认为['RCS', 'CVS', 'tags'];hide代表隐藏的列表,默认为[os.curdir,os.pardir]。dircmp类可以获得目录比较的详细信息,如只有在a目录中包括的文件、a与b都存在的子目录、匹配的文件等,同时支持递归。

dircmp提供了三个输出报告的方法:

❑report(),比较当前指定目录中的内容;

❑report_partial_closure(),比较当前指定目录及第一级子目录中的内容;

❑report_full_closure(),递归比较所有指定目录的内容。

为输出更加详细的比较结果,dircmp类还提供了以下属性:

❑left,左目录,如类定义中的a;

❑right,右目录,如类定义中的b;

❑left_list,左目录中的文件及目录列表;

❑right_list,右目录中的文件及目录列表;

❑common,两边目录共同存在的文件或目录;

❑left_only,只在左目录中的文件或目录;

❑right_only,只在右目录中的文件或目录;

❑common_dirs,两边目录都存在的子目录;

❑common_files,两边目录都存在的子文件;

❑common_funny,两边目录都存在的子目录(不同目录类型或os.stat()记录的错误);

❑same_files,匹配相同的文件;

❑diff_files,不匹配的文件;

❑funny_files,两边目录中都存在,但无法比较的文件;

❑subdirs,将common_dirs目录名映射到新的dircmp对象,格式为字典类型。

示例:对比dir1与dir2的目录差异。

通过调用dircmp()方法实现目录差异对比功能,同时输出目录对比对象所有属性信息。

【/home/test/filecmp/ simple1.py】

        import filecmp
        a="/home/test/filecmp/dir1"    #定义左目录
        b="/home/test/filecmp/dir2"    #定义右目录
        dirobj=filecmp.dircmp(a,b,['test.py'])    #目录比较,忽略test.py文件
        #输出对比结果数据报表,详细说明请参考filecmp类方法及属性信息
        dirobj.report()
        dirobj.report_partial_closure()
        dirobj.report_full_closure()
        print "left_list:"+ str(dirobj.left_list)
        print "right_list:"+ str(dirobj.right_list)
        print "common:"+ str(dirobj.common)
        print "left_only:"+ str(dirobj.left_only)
        print "right_only:"+ str(dirobj.right_only)
        print "common_dirs:"+ str(dirobj.common_dirs)
        print "common_files:"+ str(dirobj.common_files)
        print "common_funny:"+ str(dirobj.common_funny)
        print "same_file:"+ str(dirobj.same_files)
        print "diff_files:"+ str(dirobj.diff_files)
        print "funny_files:"+ str(dirobj.funny_files)

为方便理解,通过tree命令输出两个目录的树结构,如图2-4所示。

图2-4 通过tree命令输出的两个目录

运行前面的代码并输出,结果如下:

        # python simple1.py
        -------------------report---------------------
        diff /home/test/filecmp/dir1 /home/test/filecmp/dir2
        Only in /home/test/filecmp/dir1 : ['f4']
        Only in /home/test/filecmp/dir2 : ['aa', 'f5']
        Identical files : ['f1', 'f2']
        Differing files : ['f3']
        Common subdirectories : ['a']
        -------------report_partial_closure-----------
        diff /home/test/filecmp/dir1 /home/test/filecmp/dir2
        Only in /home/test/filecmp/dir1 : ['f4']
        Only in /home/test/filecmp/dir2 : ['aa', 'f5']
        Identical files : ['f1', 'f2']
        Differing files : ['f3']
        Common subdirectories : ['a']
        diff /home/test/filecmp/dir1/a /home/test/filecmp/dir2/a
        Identical files : ['a1']
        Common subdirectories : ['b']
        -------------report_full_closure--------------
        diff /home/test/filecmp/dir1 /home/test/filecmp/dir2
        Only in /home/test/filecmp/dir1 : ['f4']
        Only in /home/test/filecmp/dir2 : ['aa', 'f5']
        Identical files : ['f1', 'f2']
        Differing files : ['f3']
        Common subdirectories : ['a']
        diff /home/test/filecmp/dir1/a /home/test/filecmp/dir2/a
        Identical files : ['a1']
        Common subdirectories : ['b']
        diff /home/test/filecmp/dir1/a/b /home/test/filecmp/dir2/a/b
        Identical files : ['b1', 'b2', 'b3']
        left_list:['a', 'f1', 'f2', 'f3', 'f4']
        right_list:['a', 'aa', 'f1', 'f2', 'f3', 'f5']
        common:['a', 'f1', 'f2', 'f3']
        left_only:['f4']
        right_only:['aa', 'f5']
        common_dirs:['a']
        common_files:['f1', 'f2', 'f3']
        common_funny:[]
        same_file:['f1', 'f2']
        diff_files:['f3']
        funny_files:[]

2.2.2 实践:校验源与备份目录差异

有时候我们无法确认备份目录与源目录文件是否保持一致,包括源目录中的新文件或目录、更新文件或目录有无成功同步,定期进行校验,没有成功则希望有针对性地进行补备份。本示例使用了filecmp模块的left_only、diff_files方法递归获取源目录的更新项,再通过shutil.copyfile、os.makedirs方法对更新项进行复制,最终保持一致状态。详细源码如下:

【/home/test/filecmp/simple2.py】

        #!/usr/bin/env python
        import os, sys
        import filecmp
        import re
        import shutil
        holderlist=[]
        def compareme(dir1, dir2):    #递归获取更新项函数
            dircomp=filecmp.dircmp(dir1,dir2)
            only_in_one=dircomp.left_only    #源目录新文件或目录
            diff_in_one=dircomp.diff_files    #不匹配文件,源目录文件已发生变化
            dirpath=os.path.abspath(dir1)    #定义源目录绝对路径
            #将更新文件名或目录追加到holderlist
            [holderlist.append(os.path.abspath(os.path.join(dir1,x))) for x in only_in_one]
            [holderlist.append(os.path.abspath(os.path.join(dir1,x))) for x in diff_in_one]
            if len(dircomp.common_dirs) > 0:    #判断是否存在相同子目录,以便递归
                for item in dircomp.common_dirs:    #递归子目录
                    compareme(os.path.abspath(os.path.join(dir1,item)), \
                    os.path.abspath(os.path.join(dir2,item)))
                return holderlist
        def main():
            if len(sys.argv) > 2:    #要求输入源目录与备份目录
                dir1=sys.argv[1]
                dir2=sys.argv[2]
            else:
                print "Usage: ", sys.argv[0], "datadir backupdir"
                sys.exit()
            source_files=compareme(dir1,dir2)    #对比源目录与备份目录
            dir1=os.path.abspath(dir1)
            if not dir2.endswith('/'): dir2=dir2+'/'    #备份目录路径加“/”符
            dir2=os.path.abspath(dir2)
            destination_files=[]
            createdir_bool=False
            for item in source_files:    #遍历返回的差异文件或目录清单
            destination_dir=re.sub(dir1, dir2, item)      #将源目录差异路径清单对应替换成
                                                          #备份目录
            destination_files.append(destination_dir)
            if os.path.isdir(item):    #如果差异路径为目录且不存在,则在备份目录中创建
                  if not os.path.exists(destination_dir):
                      os.makedirs(destination_dir)
                      createdir_bool=True    #再次调用compareme函数标记
            if createdir_bool:    #重新调用compareme函数,重新遍历新创建目录的内容
            destination_files=[]
            source_files=[]
            source_files=compareme(dir1,dir2)    #调用compareme函数
            for item in source_files:    #获取源目录差异路径清单,对应替换成备份目录
                  destination_dir=re.sub(dir1, dir2, item)
                  destination_files.append(destination_dir)
            print "update item:"
            print source_files    #输出更新项列表清单
            copy_pair=zip(source_files,destination_files)    #将源目录与备份目录文件清单拆分成元组
            for item in copy_pair:
            if os.path.isfile(item[0]):       #判断是否为文件,是则进行复制操作
                  shutil.copyfile(item[0], item[1])
        if __name__ == '__main__':
            main()

更新源目录dir1中的f4、code/f3文件后,运行程序结果如下:

        # python simple2.py /home/test/filecmp/dir1 /home/test/filecmp/dir2
        update item:
        ['/home/test/filecmp/dir1/f4', '/home/test/filecmp/dir1/code/f3']
        # python simple2.py /home/test/filecmp/dir1 /home/test/filecmp/dir2
        update item:
        []    #再次运行时已经没有更新项了

参考提示

❑ 2.2.1节模块方法说明参考http://docs.python.org/2/library/filecmp.html。

❑ 2.2.2节示例参考http://linuxfreelancer.com/how-do-you-compare-two-folders-and-copy-the-difference-to-a-third-folder。

2.3 发送电子邮件模块smtplib

电子邮件是最流行的互联网应用之一。在系统管理领域,我们常常使用邮件来发送告警信息、业务质量报表等,方便运维人员第一时间了解业务的服务状态。本节通过Python的smtplib模块来实现邮件的发送功能,模拟一个smtp客户端,通过与smtp服务器交互来实现邮件发送的功能,这可以理解成Foxmail的发邮件功能,在第一次使用之前我们需要配置smtp主机地址、邮箱账号及密码等信息,Python 2.3或更高版本默认自带smtplib模块,无需额外安装。下面详细进行介绍。

2.3.1 smtplib模块的常用类与方法

SMTP类定义:smtplib.SMTP([host[, port[, local_hostname[, timeout]]]]),作为SMTP的构造函数,功能是与smtp服务器建立连接,在连接成功后,就可以向服务器发送相关请求,比如登录、校验、发送、退出等。host参数为远程smtp主机地址,比如smtp.163.com;port为连接端口,默认为25;local_hostname的作用是在本地主机的FQDN(完整的域名)发送HELO/EHLO(标识用户身份)指令,timeout为连接或尝试在多少秒超时。SMTP类具有如下方法:

❑SMTP.connect([host[, port]])方法,连接远程smtp主机方法,host为远程主机地址,port为远程主机smtp端口,默认25,也可以直接使用host:port形式来表示,例如:SMTP.connect(“smtp.163.com”,“25”)。

❑SMTP.login(user, password)方法,远程smtp主机的校验方法,参数为用户名与密码,如SMTP.login(“python_2014@163.com”,“sdjkg358”)。

❑SMTP.sendmail(from_addr, to_addrs, msg[, mail_options, rcpt_options])方法,实现邮件的发送功能,参数依次为是发件人、收件人、邮件内容,例如:SMTP.sendmail(“python_2014@163.com”,“demo@domail.com”,body),其中body内容定义如下:

        """From: python_2014@163.com
        To: demo@domail.com
        Subject: test mail
        test mail body"""

❑SMTP.starttls([keyfile[, certfile]])方法,启用TLS(安全传输)模式,所有SMTP指令都将加密传输,例如使用gmail的smtp服务时需要启动此项才能正常发送邮件,如SMTP.starttls()。

❑SMTP.quit()方法,断开smtp服务器的连接。

下面通过一个简单示例帮助大家理解,目的是使用gmail向QQ邮箱发送测试邮件,代码如下:

        #!/usr/bin/python
        import smtplib
        import string
        HOST = "smtp.gmail.com"    #定义smtp主机
        SUBJECT = "Test email from Python"    #定义邮件主题
        TO = "testmail@qq.com"    #定义邮件收件人
        FROM = "mymail@gmail.com"    #定义邮件发件人
        text = "Python rules them all!"    #邮件内容
        BODY = string.join((    #组装sendmail方法的邮件主体内容,各段以"\r\n"进行分隔
            "From: %s" % FROM,
            "To: %s" % TO,
            "Subject: %s" % SUBJECT ,
            "",
            text
            ), "\r\n")
        server = smtplib.SMTP()    #创建一个SMTP()对象
        server.connect(HOST,"25")    #通过connect方法连接smtp主机
        server.starttls()    #启动安全传输模式
        server.login("mymail@gmail.com","mypassword")    #邮箱账号登录校验
        server.sendmail(FROM, [TO], BODY)    #邮件发送
        server.quit()    #断开smtp连接

我们将收到一封这样的邮件,如图2-5所示。

图2-5 收到的邮件

2.3.2 定制个性化的邮件格式方法

通过邮件传输简单的文本已经无法满足我们的需求,比如我们时常会定制业务质量报表,在邮件主体中会包含HTML、图像、声音以及附件格式等,MIME(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展)作为一种新的扩展邮件格式很好地补充了这一点,更多MIME知识见http://zh.wikipedia.org/wiki/MIME。下面介绍几个Python中常用的MIME实现类:

❑email.mime.multipart.MIMEMultipart([_subtype[, boundary[, _subparts[, _params]]]]),作用是生成包含多个部分的邮件体的MIME对象,参数_subtype指定要添加到"Content-type:multipart/subtype"报头的可选的三种子类型,分别为mixed、related、alternative,默认值为mixed。定义mixed实现构建一个带附件的邮件体;定义related实现构建内嵌资源的邮件体;定义alternative则实现构建纯文本与超文本共存的邮件体。

❑email.mime.audio.MIMEAudio (_audiodata[, _subtype[, _encoder[, **_params]]]),创建包含音频数据的邮件体,_audiodata包含原始二进制音频数据的字节字符串。

❑email.mime.image.MIMEImage(_imagedata[, _subtype[, _encoder[, **_params]]]),创建包含图片数据的邮件体,_imagedata是包含原始图片数据的字节字符串。

❑email.mime.text.MIMEText (_text[, _subtype[, _charset]]),创建包含文本数据的邮件体,_text是包含消息负载的字符串,_subtype指定文本类型,支持plain(默认值)或html类型的字符串。

2.3.3 定制常用邮件格式示例详解

前面两小节介绍了Python的smtplib及email模块的常用方法,那么两者在邮件定制到发送过程中是如何分工的?我们可以将email.mime理解成smtplib模块邮件内容主体的扩展,从原先默认只支持纯文本格式扩展到HTML,同时支持附件、音频、图像等格式,smtplib只负责邮件的投递即可。下面介绍在日常运营工作中邮件应用的几个示例。

示例1:实现HTML格式的数据报表邮件。

纯文本的邮件内容已经不能满足我们多样化的需求,本示例通过引入email.mime的MIMEText类来实现支持HTML格式的邮件,支持所有HTML元素,包含表格、图片、动画、CSS样式、表单等。本示例使用HTML的表格定制美观的业务流量报表,实现代码如下:

【/home/test/smtplib/simple2.py】

        #coding: utf-8
        import smtplib
        from email.mime.text import MIMEText    #导入MIMEText类
        HOST = "smtp.gmail.com"     #定义smtp主机
        SUBJECT = u"官网流量数据报表"    #定义邮件主题
        TO = "testmail@qq.com"    #定义邮件收件人
        FROM = "mymail@gmail.com"    #定义邮件发件人
        msg = MIMEText("""    #创建一个MIMEText对象,分别指定HTML内容、类型(文本或html)、字
                              #符编码
            <table width="800" border="0" cellspacing="0" cellpadding="4">
            <tr>
                  <td bgcolor="#CECFAD" height="20" style="font-size:14px">*官网数据   <a
        href="monitor.domain.com">更多>></a></td>
            </tr>
            <tr>
                <td bgcolor="#EFEBDE" height="100" style="font-size:13px">
        1)日访问量:<font color=red>152433</font>  访问次数:23651 页面浏览量:45123
        点击数:545122  数据流量:504Mb<br>
            2)状态码信息<br>
            &nbsp;&nbsp;500:105  404:3264  503:214<br>
            3)访客浏览器信息<br>
            &nbsp;&nbsp;IE:50%      firefox:10% chrome:30% other:10%<br>
            4)页面信息<br>
            &nbsp;&nbsp;/index.php 42153<br>
            &nbsp;&nbsp;/view.php 21451<br>
            &nbsp;&nbsp;/login.php 5112<br>
            </td>
            </tr>
            </table>""","html","utf-8")
        msg['Subject'] = SUBJECT    #邮件主题
        msg['From']=FROM    #邮件发件人,邮件头部可见
        msg['To']=TO    #邮件收件人,邮件头部可见
        try:
            server = smtplib.SMTP()    #创建一个SMTP()对象
            server.connect(HOST,"25")    #通过connect方法连接smtp主机
            server.starttls()    #启动安全传输模式
            server.login("mymail@gmail.com","mypassword")    #邮箱账号登录校验
            server.sendmail(FROM, TO, msg.as_string())    #邮件发送
            server.quit()    #断开smtp连接
            print "邮件发送成功!"
        except Exception, e:
            print "失败:"+str(e)

代码运行结果如图2-6所示,我们将业务日志分析结果定期推送给管理员,以方便管理员了解业务的服务情况。

图2-6 示例1运行结果

示例2:实现图文格式的服务器性能报表邮件。

示例1通过MIMEText类来实现HTML格式的邮件,当要求包含图片数据的邮件内容时,需要引用MIMEImage类,若邮件主体由多个MIME对象组成,则同时需引用MIMEMultipart类来进行组装。本示例通过MIMEText与MIMEImage类的组合来实现图文格式的服务器性能报表邮件的定制,实现代码如下:

【/home/test/smtplib/simple3.py】

        #coding: utf-8
        import smtplib
        from email.mime.multipart import MIMEMultipart    #导入MIMEMultipart类
        from email.mime.text import MIMEText    #导入MIMEText类
        from email.mime.image import MIMEImage    #导入MIMEImage类
        HOST = "smtp.gmail.com"    #定义smtp主机
        SUBJECT = u"业务性能数据报表"    #定义邮件主题
        TO = "testmail@qq.com"    #定义邮件收件人
        FROM = "mymail@gmail.com"    #定义邮件发件人
        def addimg(src,imgid):    #添加图片函数,参数1:图片路径,参数2:图片id
            fp = open(src, 'rb')    #打开文件
            msgImage = MIMEImage(fp.read())    #创建MIMEImage对象,读取图片内容并作为参数
            fp.close()    #关闭文件
            msgImage.add_header('Content-ID', imgid)    #指定图片文件的Content-ID,<img>
                                                        #标签src用到
            return msgImage    #返回msgImage对象
        msg = MIMEMultipart('related')    #创建MIMEMultipart对象,采用related定义内嵌资源
                                          #的邮件体
        msgtext = MIMEText("""    #创建一个MIMEText对象,HTML元素包括表格<table>及图片<img>
        <table width="600" border="0" cellspacing="0" cellpadding="4">
            <tr bgcolor="#CECFAD" height="20" style="font-size:14px">
                <td colspan=2>*官网性能数据  <a href="monitor.domain.com">更多>></a></td>
            </tr>
            <tr bgcolor="#EFEBDE" height="100" style="font-size:13px">
                <td>
              <img src="cid:io"></td><td>
              <img src="cid:key_hit"></td>
            </tr>
            <tr bgcolor="#EFEBDE" height="100" style="font-size:13px">
              <td>
              <img src="cid:men"></td><td>
              <img src="cid:swap"></td>
            </tr>
            </table>""","html","utf-8")       #<img>标签的src属性是通过Content-ID来引用的
        msg.attach(msgtext)    #MIMEMultipart对象附加MIMEText的内容
        msg.attach(addimg("img/bytes_io.png","io"))    #使用MIMEMultipart对象附加MIMEImage
                                                       #的内容
        msg.attach(addimg("img/myisam_key_hit.png","key_hit"))
        msg.attach(addimg("img/os_mem.png","men"))
        msg.attach(addimg("img/os_swap.png","swap"))
        msg['Subject'] = SUBJECT    #邮件主题
        msg['From']=FROM     #邮件发件人,邮件头部可见
        msg['To']=TO    #邮件收件人,邮件头部可见
        try:
            server = smtplib.SMTP()    #创建一个SMTP()对象
            server.connect(HOST,"25")    #通过connect方法连接smtp主机
            server.starttls()    #启动安全传输模式
            server.login("mymail@gmail.com","mypassword")    #邮箱账号登录校验
            server.sendmail(FROM, TO, msg.as_string())    #邮件发送
            server.quit()    #断开smtp连接
            print "邮件发送成功!"
        except Exception, e:
            print "失败:"+str(e)

代码运行结果如图2-7所示,我们将业务服务器性能数据定期推送给管理员,以方便管理员了解业务的服务情况。

图2-7 示例2运行结果

示例3:实现带附件格式的业务服务质量周报邮件。

本示例通过MIMEText与MIMEImage类的组合,实现图文邮件格式。另通过MIMEText类再定义Content-Disposition属性来实现带附件的邮件。我们可以利用这些丰富的特性来定制周报邮件,如业务服务质量周报。实现代码如下:

【/home/test/smtplib/simple4.py】

        #coding: utf-8
        import smtplib
        from email.mime.multipart import MIMEMultipart    #导入MIMEMultipart类
        from email.mime.text import MIMEText    #导入MIMEText类
        from email.mime.image import MIMEImage    #导入MIMEImage类
        HOST = "smtp.gmail.com"    #定义smtp主机
        SUBJECT = u"官网业务服务质量周报"    #定义邮件主题
        TO = "testmail@qq.com"     #定义邮件接收人
        FROM = "mymail@gmail.com"    #定义邮件发件人
        def addimg(src,imgid):     #添加图片函数,参数1:图片路径,参数2:图片id
            fp = open(src, 'rb')    #打开文件
            msgImage = MIMEImage(fp.read())     #创建MIMEImage对象,读取图片内容作为参数
            fp.close()    #关闭文件
            msgImage.add_header('Content-ID', imgid)    #指定图片文件的Content-ID,<img>
                                                        #标签src用到
            return msgImage    #返回msgImage对象
        msg = MIMEMultipart('related')       #创建MIMEMultipart对象,采用related定义内嵌资源
                                             #的邮件体
        #创建一个MIMEText对象,HTML元素包括文字与图片<img>
        msgtext = MIMEText("<font color=red>官网业务周平均延时图表:<br><img src=\"cid:weekly\"
        border=\"1\"><br>详细内容见附件。</font>","html","utf-8")
        msg.attach(msgtext)    #MIMEMultipart对象附加MIMEText的内容
        msg.attach(addimg("img/weekly.png","weekly"))     #使用MIMEMultipart对象附加
                                                          # MIMEImage的内容
        #创建一个MIMEText对象,附加week_report.xlsx文档
        attach = MIMEText(open("doc/week_report.xlsx", "rb").read(), "base64", "utf-8")
        attach["Content-Type"] = "application/octet-stream"    #指定文件格式类型
        #指定Content-Disposition值为attachment则出现下载保存对话框,保存的默认文件名使用
        #filename指定
        #由于qqmail使用gb18030页面编码,为保证中文文件名不出现乱码,对文件名进行编码转换
        attach["Content-Disposition"] = "attachment; filename=\"业务服务质量周报(12周).xlsx\"".
        decode("utf-8").encode("gb18030")
        msg.attach(attach)    #MIMEMultipart对象附加MIMEText附件内容
        msg['Subject'] = SUBJECT    #邮件主题
        msg['From']=FROM    #邮件发件人,邮件头部可见
        msg['To']=TO    #邮件收件人,邮件头部可见
        try:
            server = smtplib.SMTP()    #创建一个SMTP()对象
            server.connect(HOST,"25")    #通过connect方法连接smtp主机
            server.starttls()    #启动安全传输模式
            server.login("mymail@gmail.com","mypassword")    #邮箱账号登录校验
            server.sendmail(FROM, TO, msg.as_string())    #邮件发送
            server.quit()    #断开smtp连接
            print "邮件发送成功!"
        except Exception, e:
            print "失败:"+str(e)

代码运行结果如图2-8所示,实现了发送业务服务质量周报的邮件功能。

图2-8 示例3的运行结果

参考提示

❑ 2.3.1节smtplib模块的常用类与方法内容参考https://docs.python.org/2.7/library/smtplib.html。

❑ 2.3.2节email.mime常用类定义内容参考https://docs.python.org/2.7/library/email. mime.html。

2.4 探测Web服务质量方法

pycurl(http://pycurl.sourceforge.net)是一个用C语言写的libcurl Python实现,功能非常强大,支持的操作协议有FTP、HTTP、HTTPS、TELNET等,可以理解成Linux下curl命令功能的Python封装,简单易用。本节通过调用pycurl提供的方法,实现探测Web服务质量的情况,比如响应的HTTP状态码、请求延时、HTTP头信息、下载速度等,利用这些信息可以定位服务响应慢的具体环节,下面详细进行说明。

pycurl模块的安装方法如下:

        easy_install pycurl    #pip安装方法
        pip install pycurl    #easy_install安装方法
        #源码安装方法
        # 要求curl-config包支持,需要源码方式重新安装curl
        # wget http://curl.haxx.se/download/curl-7.36.0.tar.gz
        # tar -zxvf curl-7.36.0.tar.gz
        # cd curl-7.36.0
        # ./configure
        # make && make install
        # export LD_LIBRARY_PATH=/usr/local/lib
        #
        # wget https://pypi.python.org/packages/source/p/pycurl/pycurl-7.19.3.1.tar.gz
        --no-check-certificate
        # tar -zxvf pycurl-7.19.3.1.tar.gz
        # cd pycurl-7.19.3.1
        # python setup.py install --curl-config=/usr/local/bin/curl-config

校验安装结果如下:

        >>> import pycurl
        >>> pycurl.version
        'PycURL/7.19.3.1 libcurl/7.36.0 OpenSSL/1.0.1e zlib/1.2.3'

2.4.1 模块常用方法说明

pycurl.Curl()类实现创建一个libcurl包的Curl句柄对象,无参数。更多关于libcurl包的介绍见http://curl.haxx.se/libcurl/c/libcurl-tutorial.html。下面介绍Curl对象几个常用的方法。

❑close()方法,对应libcurl包中的curl_easy_cleanup方法,无参数,实现关闭、回收Curl对象。

❑perform()方法,对应libcurl包中的curl_easy_perform方法,无参数,实现Curl对象请求的提交。

❑setopt(option, value)方法,对应libcurl包中的curl_easy_setopt方法,参数option是通过libcurl的常量来指定的,参数value的值会依赖option,可以是一个字符串、整型、长整型、文件对象、列表或函数等。下面列举常用的常量列表:

        c = pycurl.Curl()    #创建一个curl对象
        c.setopt(pycurl.CONNECTTIMEOUT, 5)    #连接的等待时间,设置为0则不等待
        c.setopt(pycurl.TIMEOUT, 5)    #请求超时时间
        c.setopt(pycurl.NOPROGRESS, 0)    #是否屏蔽下载进度条,非0则屏蔽
        c.setopt(pycurl.MAXREDIRS, 5)    #指定HTTP重定向的最大数
        c.setopt(pycurl.FORBID_REUSE, 1)    #完成交互后强制断开连接,不重用
        c.setopt(pycurl.FRESH_CONNECT,1)    #强制获取新的连接,即替代缓存中的连接
        c.setopt(pycurl.DNS_CACHE_TIMEOUT,60)    #设置保存DNS信息的时间,默认为120秒
        c.setopt(pycurl.URL,"http://www.baidu.com")    #指定请求的URL
        c.setopt(pycurl.USERAGENT,"Mozilla/5.2 (compatible; MSIE 6.0; Windows NT 5.1;
        SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50324)")    #配置请求HTTP头的User-Agent
        c.setopt(pycurl.HEADERFUNCTION, getheader)  #将返回的HTTP HEADER定向到回调函数getheader
        c.setopt(pycurl.WRITEFUNCTION, getbody)     #将返回的内容定向到回调函数getbody
        c.setopt(pycurl.WRITEHEADER, fileobj)     #将返回的HTTP HEADER定向到fileobj文件对象
        c.setopt(pycurl.WRITEDATA, fileobj)    #将返回的HTML内容定向到fileobj文件对象

❑getinfo(option)方法,对应libcurl包中的curl_easy_getinfo方法,参数option是通过libcurl的常量来指定的。下面列举常用的常量列表:

        c = pycurl.Curl()    #创建一个curl对象
        c.getinfo(pycurl.HTTP_CODE)    #返回的HTTP状态码
        c.getinfo(pycurl.TOTAL_TIME)    #传输结束所消耗的总时间
        c.getinfo(pycurl.NAMELOOKUP_TIME)    #DNS解析所消耗的时间
        c.getinfo(pycurl.CONNECT_TIME)    #建立连接所消耗的时间
        c.getinfo(pycurl.PRETRANSFER_TIME)    #从建立连接到准备传输所消耗的时间
        c.getinfo(pycurl.STARTTRANSFER_TIME)    #从建立连接到传输开始消耗的时间
        c.getinfo(pycurl.REDIRECT_TIME)    #重定向所消耗的时间
        c.getinfo(pycurl.SIZE_UPLOAD)    #上传数据包大小
        c.getinfo(pycurl.SIZE_DOWNLOAD)    #下载数据包大小
        c.getinfo(pycurl.SPEED_DOWNLOAD)    #平均下载速度
        c.getinfo(pycurl.SPEED_UPLOAD)    #平均上传速度
        c.getinfo(pycurl.HEADER_SIZE)    #HTTP头部大小

我们利用libcurl包提供的这些常量值来达到探测Web服务质量的目的。

2.4.2 实践:实现探测Web服务质量

HTTP服务是最流行的互联网应用之一,服务质量的好坏关系到用户体验以及网站的运营服务水平,最常用的有两个标准,一为服务的可用性,比如是否处于正常提供服务状态,而不是出现404页面未找到或500页面错误等;二为服务的响应速度,比如静态类文件下载时间都控制在毫秒级,动态CGI为秒级。本示例使用pycurl的setopt与getinfo方法实现HTTP服务质量的探测,获取监控URL返回的HTTP状态码,HTTP状态码采用pycurl. HTTP_CODE常量得到,以及从HTTP请求到完成下载期间各环节的响应时间,通过pycurl. NAMELOOKUP_TIME、pycurl. CONNECT_TIME、pycurl. PRETRANSFER_TIME、pycurl. R等常量来实现。另外通过pycurl.WRITEHEADER、pycurl.WRITEDATA常量得到目标URL的HTTP响应头部及页面内容。实现源码如下:

【/home/test/pycurl/simple1.py】

        # -*- coding: utf-8-*-
        import os,sys
        import time
        import sys
        import pycurl
        URL="http://www.google.com.hk"    #探测的目标URL
        c = pycurl.Curl()    #创建一个Curl对象
        c.setopt(pycurl.URL, URL)    #定义请求的URL常量
        c.setopt(pycurl.CONNECTTIMEOUT, 5)    #定义请求连接的等待时间
        c.setopt(pycurl.TIMEOUT, 5)    #定义请求超时时间
        c.setopt(pycurl.NOPROGRESS, 1)    #屏蔽下载进度条
        c.setopt(pycurl.FORBID_REUSE, 1)    #完成交互后强制断开连接,不重用
        c.setopt(pycurl.MAXREDIRS, 1)     #指定HTTP重定向的最大数为1
        c.setopt(pycurl.DNS_CACHE_TIMEOUT,30)    #设置保存DNS信息的时间为30秒
        #创建一个文件对象,以”wb”方式打开,用来存储返回的http头部及页面内容
        indexfile = open(os.path.dirname(os.path.realpath(__file__))+"/content.txt",
    "wb")
        c.setopt(pycurl.WRITEHEADER, indexfile)     #将返回的HTTP HEADER定向到indexfile文件
    对象
        c.setopt(pycurl.WRITEDATA, indexfile)    #将返回的HTML内容定向到indexfile文件对象
        try:
            c.perform()    #提交请求
        except Exception,e:
            print "connecion error:"+str(e)
            indexfile.close()
            c.close()
        sys.exit()
        NAMELOOKUP_TIME =  c.getinfo(c.NAMELOOKUP_TIME)    #获取DNS解析时间
        CONNECT_TIME =  c.getinfo(c.CONNECT_TIME)    #获取建立连接时间
        PRETRANSFER_TIME =   c.getinfo(c.PRETRANSFER_TIME)    #获取从建立连接到准备传输所消
                                                              #耗的时间
        STARTTRANSFER_TIME = c.getinfo(c.STARTTRANSFER_TIME)    #获取从建立连接到传输开始消
                                                                #耗的时间
        TOTAL_TIME = c.getinfo(c.TOTAL_TIME)    #获取传输的总时间
        HTTP_CODE =  c.getinfo(c.HTTP_CODE)    #获取HTTP状态码
        SIZE_DOWNLOAD =  c.getinfo(c.SIZE_DOWNLOAD)    #获取下载数据包大小
        HEADER_SIZE = c.getinfo(c.HEADER_SIZE)     #获取HTTP头部大小
        SPEED_DOWNLOAD=c.getinfo(c.SPEED_DOWNLOAD)    #获取平均下载速度
        #打印输出相关数据
        print "HTTP状态码:%s" %(HTTP_CODE)
        print "DNS解析时间:%.2f ms"%(NAMELOOKUP_TIME*1000)
        print "建立连接时间:%.2f ms" %(CONNECT_TIME*1000)
        print "准备传输时间:%.2f ms" %(PRETRANSFER_TIME*1000)
        print "传输开始时间:%.2f ms" %(STARTTRANSFER_TIME*1000)
        print "传输结束总时间:%.2f ms" %(TOTAL_TIME*1000)
        print "下载数据包大小:%d bytes/s" %(SIZE_DOWNLOAD)
        print "HTTP头部大小:%d byte" %(HEADER_SIZE)
        print "平均下载速度:%d bytes/s" %(SPEED_DOWNLOAD)
        #关闭文件及Curl对象
        indexfile.close()
        c.close()

代码的执行结果如图2-9所示。

图2-9 探测到的Web服务质量

查看获取的HTTP文件头部及页面内容文件content.txt,如图2-10所示。

图2-10 content.txt截图

参考提示

❑ 2.4.1节pycurl模块的常用类与方法说明参考官网http://pycurl.sourceforge.net/doc/index.html。