为什么我需要自己的diff工具?
我经常使用git跟踪我的编码项目、文章、业务工作等等。git的一个美妙之处在于,你可以通过简单地使用其内置的diff功能来轻松地比较你的工作的不同状态。要使用这个功能,你只需要满足两个约束:首先,你需要一个git存储库,其次,该文件需要由git存储库进行跟踪。
但是,如果您只想修改单个文件,并将其与旧版本进行比较,所有这些操作都不需要用到git存储库,那该怎么办呢?这就是本文的意义所在。本文的目标是创建一个diff工具,它允许你比较一个文件的两个版本:
- 不需要git存储库和
- 构建在Python标准库之上!
此外,我们的diff工具应该能够将计算的差异导出到一个HTML文件中。要阅读这篇文章,你只需要会Python。本文是专门为Python 3.8.2 (CPython)编写的。你可以在GitHub上找到源代码。就介绍到这里,让我们来看看它吧!
unified_diff() 函数
Python标准库包含一个名为difflib的模块。根据文档,这个模块提供了用于比较序列的类和函数。此外,还有各种可用的输出格式[1]。
在检查该模块时,unified_diff()函数从所有其他函数中脱颖而出。通过查看文档提供的示例和生成的输出,我发现它与我们正在寻找的差异计算函数非常相似。它需要多达8个参数,但只有两个是必需的:
- a:字符串列表(必需)
- b:字符串列表(必需)
- fromfile:用于显示第一个文件的名称(默认值:'')
- tofile:用于显示第二个文件的名称(默认值:'' )
- fromfiledate:第一个文件的修改时间(默认值: '')
- tofiledate:第二个文件的修改时间(默认值: '')
- n:上下文行数*(默认值:3)
- lineterm:添加在控制行(带有—、+++或@@的行)末尾的字符,以便io.IOBase.readlines()和io.IOBase.writelines()能被正确处理(默认值:'\n')。
*上下文行用于向用户在发生更改的地方提供上下文
实际上,unified_diff()函数会接受两个字符串列表并对它们进行比较。如果它们是相等的,delta就为空。如果存在任何差异,则返回各自的delta。举个简单的例子:你打算和你最好的朋友一起举办一个披萨派对,然后写下了你需要先买的配料。为简单起见,此购物清单是一个简单的文本文件(my_shopping_list.txt),内容如下所示:
你把它发送给你的朋友,他加了一种配料,因为你们家里都没有了:salami(意大利腊肠)。此外,他还识别出了你的打字错误并进行了纠正。为了能够正确地将这些更改传递给我们的diff工具,他复制了该清单并将其重命名为friends_shopping_list.txt。以下是最终的购物清单:
当然,这是一个相当简单的例子,文本不是很长,所以能很容易地被人为处理。但是,让我们增加点乐趣,我们继续看这个例子。为了计算这两个文件之间的差异,我们将它们读入内存,并将它们传递给unified_diff()函数:
首先,我们导入了difflib和sys。其次,我们读取这两个文件的内容,并将它们保存到单独的变量(file1和file2)中。因为我们需要字符串列表,所以我们使用了readlines()。随后,我们计算了两个列表的delta,并通过sys.stdout.writelines()将其写到stdout。
执行脚本后生成的结果如下:
从输出来看,单词cheese没有被修改,而是被用作上下文行,因为下面的行被修改了。tomates被移除了,tomatoes和salami被加了进去。如果您查看打印出的delta的头部,你可以看到,我们并没有得到有关--- 和 +++代表什么,或者它们代表什么文件的任何信息。让我们通过将文件名添加到unified_diff()函数来调整一下脚本:
现在,运行该脚本生成结果如下:
太棒了!我们实现了一个简单的脚本,它可以计算并打印两个文件内容之间的差异。让我们继续并将其转换为一个命令行工具。
构建一个命令行工具
为了将我们的脚本转换成一个有用的命令行工具,我们使用了Python的argparse模块。首先,我们将之前编写的代码放入一个名为create_diff()的函数中,该函数接受两个参数,old_file和new_file。它们都是Path对象[2]。我们使用传递的Path对象来读取它们的内容,并使用该Path对象的name属性来获取所提供的文件的名称。由于我们的小脚本现在是要更加通用,不再局限于购物清单,所以我们将我们的代码放入一个名为diff_tool.py的新文件中(这个名称更适合我们的脚本)。到目前为止,该脚本是这样的:
接着,我们定义一个新函数main(),它负责总的工作流:
首先,我们定义了一个新的参数解析器。我们告诉该解析器接受两个参数,old_file_version和new_file_version。两者都是必需的。调用parse_args()将解析命令行输入并将输入转换为正确的格式。随后,两个命令行参数都会被访问并且被转换为Path对象。然后,我们使用old_file和new_file作为参数调用create_diff()。
备注:如果您想要了解更多关于argparse模块的内容,我强烈推荐Python的argparse教程[3],它提供了更详细的Python命令行解析介绍。
现在,如果我们不带任何参数执行该脚本,它会告诉我们,哪些参数是必需的:
提供两个购物清单后仍然会生成预期的输出:
到目前为止,我们通过将你的简短脚本从头开始转换成一个简单的命令行工具来构建了一个简单的diff工具—很酷!现在,我们将添加更多的行来支持HTML输出。
以HMTL格式提供差异
difflib模块提供了一个HtmlDiff类,它可以被用来创建一个HTML表(或一个包含表的完整HTML文件),该表会通过将行间和行内变为高亮显示来显示文本的并排、逐行比较。在我们的示例中,我们使用了HtmlDiff.make_file()函数,它返回了一个表示完整HTML文件的字符串。后者逐行高亮显示了任何差异。
因此,我们将我们的脚本扩展如下:
create_diff()函数现在接受一个额外的第三个参数output_file,它也是一个Path对象。我们会将我们的HTML差异写入这个文件。我们检查是否传递了output_file。如果是,我们以HTML格式计算差异并将其保存到这个传递的文件中。
备注:我们使用w模式进行写操作。如果该文件已经存在,则会被提前清空。
如果output_file没有被传递,我们会计算标准差异并将其写到stdout。
我们通过注册一个附加的可选命令行参数--html使其以一个文件名作为输入来扩展了main()函数。如果提供了文件名,则将其转换为Path对象并传递给create_diff()。
执行以下命令之后,您的当前工作目录中就有了一个diff.html文件,你可以使用你最喜欢的浏览器来打开该文件去查看实际的差异。
总结
恭喜,你已经通过本文创建了它!在阅读本文时,你了解了如何使用Python的difflib模块计算一个简单的diff。此外,你还能够使用Python的argparse模块将您的短小diff脚本转换成一个命令行工具。随后,你添加了几行代码来支持以HTML作为输出格式。
接下来是什么?你可以查看difflib文档[1],了解计算diff的各种方法,搜索其他类型的diff,并进一步扩展你的diff-tool。另外,你可以查看本文的GitHub存储库并计算file.md和file_update.md之间的差异。你找到所有的变化了吗?
希望你喜欢阅读这篇文章。一定要与你的朋友和同事进行分享哦!
参考资料
- difflib文档
- Path文档
- argparse文档
- 内置open()函数