系列文章参考自《Python自动化运维 技术与最佳实践》,原书有些代码在Python 3不支持,已改为支持格式。
python通过difflib标准库模块实现文件内容差异对比,支持文本和html格式输出。
difflib与Linux下的diff命令相似,可以使用difflib对比代码、配置文件的差异,在版本控制方面非常有用。Python 2.3开始默认自带difflib模块,无需额外安装。
本示例采用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格式输出如下:
n 1 SELECT BusinessEntityID, FirstName, LastName, EmailPromotion n 1 SELECT BusinessEntityID, FirstName, LastName,
2 FROM [AdventureWorks2014].[Person].[Person] 2 FROM [AdventureWorks2014].[Person].[Person]
t 3 WHERE EmailPromotion > 0 t 3 WHERE Emailpromotion > 0
4 ORDER BY LastName 4 ORDER BY FirstName,LastName
Legends
Colors
Added
Changed
Deleted
Links
(f)irst change
(n)ext change
(t)op
保存到html文件用使用浏览器打开可以看到
读取两个需对比文件(以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
输出:
当进行代码审计或校验备份结果时,往往需要检查原始与目标目录的文件一致性,Python标准库自带了满足此需求的模块filecmp。filecmp可以实现文件、目录、遍历子目录的差异对比功能。比如报告中输出目标目录比原始多出的文件或子目录,即使文件同名也会判断是否为同一个文件(内容级对比)等,Python 2.3或更高版本默认自带filecmp模块,无需额外安装。
单文件对比采用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
多文件对比采用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是参加比较的目录名。
dircmp提供了三个输出报告的方法:
为输出更加详细的比较结果,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