python自动化运维——文件、目录差异对比

2022-09-17 10:05:48

系列文章参考自《Python自动化运维 技术与最佳实践》,原书有些代码在Python 3不支持,已改为支持格式。

一、 文件内容差异对比 difflib

python通过difflib标准库模块实现文件内容差异对比,支持文本和html格式输出。

difflib与Linux下的diff命令相似,可以使用difflib对比代码、配置文件的差异,在版本控制方面非常有用。Python 2.3开始默认自带difflib模块,无需额外安装。

1. 对比字符串差异

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

import  difflib
text1="""SELECT BusinessEntityID, FirstName, LastName, EmailPromotion
FROM [AdventureWorks2014].[Person].[Person]
WHERE EmailPromotion > 0
ORDER BY LastName"""
#以“换行符”分隔,以便对比
text1_lines=text1.splitlines()

text2="""SELECT BusinessEntityID, FirstName, LastName,
FROM [AdventureWorks2014].[Person].[Person]
WHERE Emailpromotion > 0
ORDER BY FirstName,LastName"""
text2_lines=text2.splitlines()

#文本格式,创建Differ()对象
d=difflib.Differ()
diff=d.compare(text1_lines,text2_lines)
print('\n'.join(list(diff)))

#html格式,创建HtmlDiff()对象
d=difflib.HtmlDiff()
print(d.make_file(text1_lines,text2_lines))

文本格式输出如下:

- SELECT BusinessEntityID, FirstName, LastName, EmailPromotion
?                                              ---------------

+ SELECT BusinessEntityID, FirstName, LastName,
  FROM [AdventureWorks2014].[Person].[Person]
- WHERE EmailPromotion > 0
?            ^

+ WHERE Emailpromotion > 0
?            ^

- ORDER BY LastName
+ ORDER BY FirstName,LastName
?          ++++++++++

html格式输出如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>
    <meta http-equiv="Content-Type"
          content="text/html; charset=utf-8" />
    <title></title>
    <style type="text/css">
        table.diff {font-family:Courier; border:medium;}
        .diff_header {background-color:#e0e0e0}
        td.diff_header {text-align:right}
        .diff_next {background-color:#c0c0c0}
        .diff_add {background-color:#aaffaa}
        .diff_chg {background-color:#ffff77}
        .diff_sub {background-color:#ffaaaa}
    </style>
</head>

<body>
    
    <table class="diff" id="difflib_chg_to0__top"
           cellspacing="0" cellpadding="0" rules="groups" >
        <colgroup></colgroup> <colgroup></colgroup> <colgroup></colgroup>
        <colgroup></colgroup> <colgroup></colgroup> <colgroup></colgroup>
        
        <tbody>
            <tr><td class="diff_next" id="difflib_chg_to0__1"><a href="#difflib_chg_to0__1">n</a></td><td class="diff_header" id="from0_1">1</td><td nowrap="nowrap">SELECT&nbsp;BusinessEntityID,&nbsp;FirstName,&nbsp;LastName,<span class="diff_sub">&nbsp;EmailPromotion</span></td><td class="diff_next"><a href="#difflib_chg_to0__1">n</a></td><td class="diff_header" id="to0_1">1</td><td nowrap="nowrap">SELECT&nbsp;BusinessEntityID,&nbsp;FirstName,&nbsp;LastName,</td></tr>
            <tr><td class="diff_next"></td><td class="diff_header" id="from0_2">2</td><td nowrap="nowrap">FROM&nbsp;[AdventureWorks2014].[Person].[Person]</td><td class="diff_next"></td><td class="diff_header" id="to0_2">2</td><td nowrap="nowrap">FROM&nbsp;[AdventureWorks2014].[Person].[Person]</td></tr>
            <tr><td class="diff_next"><a href="#difflib_chg_to0__top">t</a></td><td class="diff_header" id="from0_3">3</td><td nowrap="nowrap">WHERE&nbsp;Email<span class="diff_chg">P</span>romotion&nbsp;&gt;&nbsp;0</td><td class="diff_next"><a href="#difflib_chg_to0__top">t</a></td><td class="diff_header" id="to0_3">3</td><td nowrap="nowrap">WHERE&nbsp;Email<span class="diff_chg">p</span>romotion&nbsp;&gt;&nbsp;0</td></tr>
            <tr><td class="diff_next"></td><td class="diff_header" id="from0_4">4</td><td nowrap="nowrap">ORDER&nbsp;BY&nbsp;LastName</td><td class="diff_next"></td><td class="diff_header" id="to0_4">4</td><td nowrap="nowrap">ORDER&nbsp;BY&nbsp;<span class="diff_add">FirstName,</span>LastName</td></tr>
        </tbody>
    </table>
    <table class="diff" summary="Legends">
        <tr> <th colspan="2"> Legends </th> </tr>
        <tr> <td> <table border="" summary="Colors">
                      <tr><th> Colors </th> </tr>
                      <tr><td class="diff_add">&nbsp;Added&nbsp;</td></tr>
                      <tr><td class="diff_chg">Changed</td> </tr>
                      <tr><td class="diff_sub">Deleted</td> </tr>
                  </table></td>
             <td> <table border="" summary="Links">
                      <tr><th colspan="2"> Links </th> </tr>
                      <tr><td>(f)irst change</td> </tr>
                      <tr><td>(n)ext change</td> </tr>
                      <tr><td>(t)op</td> </tr>
                  </table></td> </tr>
    </table>
</body>

</html>

保存到html文件用使用浏览器打开可以看到

2. 对比文件差异

读取两个需对比文件(以db_install.rsp内容为例),以换行符作为分隔,生成HTML格式差异文档。

代码如下:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import difflib
import sys

try:
    textfile1 = sys.argv[1]  # 第一个配置文件路径参数
    textfile2 = sys.argv[2]  # 第二个配置文件路径参数
except Exception as e:
    print (e.args)
    print ("Usage: diff.py filename1 filename2")
    sys.exit()

def readfile(filename):  # 文件读取分隔函数
    try:
        fileHandle = open(filename, 'rb')
        text = fileHandle.read().decode().splitlines()  # 读取后以行进行分隔
        fileHandle.close()
        return text
    except IOError as error:
        print('Read file Error:' + str(error))
        sys.exit()

if textfile1 == "" or textfile2 == "":
    print
    "Usage: diff.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))

用法:

D:\pythonProgram>python diff.py par1.txt par2.txt > diff.html

输出:

二、 文件与目录差异对比 filecmp

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

1.单文件对比cmp

单文件对比采用filecmp.cmp(f1, f2[, shallow])方法,比较文件名为f1和f2的文件,相同返回True,不同返回False。

shallow默认为True,即只根据os.stat()方法返回的文件基本信息(最后访问时间、修改时间、状态改变时间等)进行对比,而忽略文件内容的对比。当shallow为False时,则同时校验os.stat()与文件内容。

简单用法:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import filecmp
print(filecmp.cmp('par1.txt','par1_copy.txt'))
print(filecmp.cmp('par1.txt','par1_copy.txt',shallow=False))

#输出
True
True

2. 多文件对比cmpfiles

多文件对比采用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'])

3. 目录对比dircmp

目录对比,通过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()方法实现目录差异对比功能,同时输出目录对比对象所有属性信息。

import filecmp

a="/home/test/filecmp/dir1"    #定义左目录
b="/home/test/filecmp/dir2"    #定义右目录

#目录比较,忽略test.py文件
dirobj=filecmp.dircmp(a,b,['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命令输出两个目录的树结构,如下图。

代码输出结果如下:

-------------------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']

-------------dircmp类属性输出--------------

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:[]

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

有时候我们无法确认备份目录与源目录文件是否保持一致,包括源目录中的新文件或目录、更新文件或目录有无成功同步,定期进行校验,没有成功则希望有针对性地进行补备份。

本示例使用了filecmp模块的left_only、diff_files方法递归获取源目录的更新项,再通过shutil.copyfile、os.makedirs方法对更新项进行复制,最终保持一致状态。详细源码如下:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import os, sys
import filecmp
import re
import shutil

holderlist=[]
#递归获取更新项函数
def compareme(dir1, dir2):
    
    dircomp=filecmp.dircmp(dir1,dir2)
    only_in_source=dircomp.left_only  #源目录新文件或目录
    diff_in_source=dircomp.diff_files  #不匹配文件,源目录文件已发生变化
    dirpath = os.path.abspath(dir1)  # 定义源目录绝对路径
    
    #将更新文件名或目录追加到holderlist
    [holderlist.append(os.path.abspath(os.path.join(dir1,x))) for x in only_in_source]
    [holderlist.append(os.path.abspath(os.path.join(dir1,x))) for x in diff_in_source]

    #判断是否存在相同子目录,以便递归
    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():
    # 输入参数超过2个则报错
    if len(sys.argv) > 2:
        dir1=sys.argv[1]
        dir2=sys.argv[2]
    else:
        print ("Usage: python "+sys.argv[0]+ " datadir backupdir")
        sys.exit()

    source_files=compareme(dir1,dir2)  #对比源目录与备份目录
    #源目录绝对路径
    dir1=os.path.abspath(dir1)

    # 若备份目录不以指定/字符结尾,则备份目录路径加/(Windows为\\)
    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) #re.sub(源字符串,目标字符串,替换目标)
        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 diff.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:
[]    #再次运行时已经没有更新项了

参考

https://book.2cto.com/201411/48244.html

http://docs.python.org/2/library/filecmp.html

http://linuxfreelancer.com/how-do-you-compare-two-folders-and-copy-the-difference-to-a-third-folder

  • 作者:Hehuyi_In
  • 原文链接:https://blog.csdn.net/Hehuyi_In/article/details/102517637
    更新时间:2022-09-17 10:05:48