跳到正文
效率工具实用笔记关于本站
效率工具

比较两段文本改了什么:unified_diff基础用法

约 8 分钟阅读AI 辅助整理

用自建字符串列表演示 difflib.unified_diff:保留行尾换行、设置 fromfile 与 tofile 标签、读懂 @@ 与加减号,并说明它只报告差异、不代表原创度。

本文要点

  1. 用 splitlines(keepends=True) 把整段文字切成带换行符的行列表,这是文档示例里用的方式

  2. unified_diff 的默认输出开头是两行控制行,形如 --- 和 +++,后面跟文件名和修改时间

  3. 输出里最需要看懂的是 @@ 那一行和它下面的加减号

比较两段文本改了什么:unified_diff基础用法的知识要点示意图
依据已审核正文摘取要点制作的示意图,非实物照片;请结合正文中的条件与范围阅读。要点摘自已审核正文;完整条件与说明见正文。查看高清图解 ↗
本文目录

先明确:unified_diff 比较的是两个字符串列表

如果你手上有两段文本,想知道第二段相对第一段改了哪些行,difflib.unified_diff 就是为这个场景准备的。

按 Python 官方 difflib 文档的说明,unified_diff(a, b) 比较 a 和 b 两个字符串列表,返回一个生成器,逐行产出统一差异格式(unified diff)的结果。这种格式和 diff、git diff 输出的格式属于同一类:只显示发生变化的行,外加几行上下文。

关键前提有两个。第一,输入是列表,不是整段字符串。第二,列表里的每个元素通常是一行,并且要带上行尾换行符,这样输出才能直接写回文件。

本文只处理一个自建场景:两个自己写的字符串列表,比较它们改了什么。不读写真实文件,不修改任何内容,也不判断两段文本的相似度是否等于原创度。下面所有文本都是练习用的自建示例。

动手前建议在练习目录里新建脚本,例如 exercise/diff_demo.py,不要拿正在用的文件直接试。

自建两个字符串列表:行尾换行要保留

先造两段练习文本。用 splitlines(keepends=True) 把整段文字切成带换行符的行列表,这是文档示例里用的方式。

from difflib import unified_diff

text1 = """第一行:准备材料
第二行:清洗器具
第三行:开始操作
""".splitlines(keepends=True)

text2 = """第一行:准备材料
第二行:清洗器具
第三行:开始操作
第四行:记录结果
""".splitlines(keepends=True)

这里 keepends=True 的作用是保留每行末尾的换行符。文档说明,unified_diff 默认给控制行(以 ---、+++、@@ 开头的行)加上行尾换行符,这样从 readlines() 得到的输入产生的差异结果,可以直接用 writelines() 写回文件。

如果你的输入没有行尾换行符,文档给出的做法是把 lineterm 参数设为空字符串,让输出统一不带换行。两种输入方式要配套使用,不要一半带换行、一半不带。

加上 fromfile 和 tofile:让输出带上文件标签

unified_diff 的默认输出开头是两行控制行,形如 --- 和 +++,后面跟文件名和修改时间。文件名由 fromfile 和 tofile 两个参数指定,默认都是空字符串。

from difflib import unified_diff

s1 = ["第一行:准备材料\n", "第二行:清洗器具\n", "第三行:开始操作\n"]
s2 = ["第一行:准备材料\n", "第二行:清洗器具\n", "第三行:开始操作\n", "第四行:记录结果\n"]

diff = unified_diff(s1, s2, fromfile="before.txt", tofile="after.txt")
print("".join(diff))

输出大致如下(这是自建示例的运行结果,不是对真实文件的记录):

--- before.txt
+++ after.txt
@@ -1,3 +1,4 @@
 第一行:准备材料
 第二行:清洗器具
 第三行:开始操作
+第四行:记录结果

fromfile 和 tofile 只是标签,用来标明哪一段是旧、哪一段是新。它们不会去读磁盘上同名的文件,也不会创建或修改任何文件。资料还提到 fromfiledate 和 tofiledate 两个参数,用来写修改时间,通常用 ISO 8601 格式;不指定时默认为空。

读懂 @@ 行和加减号:定位改在哪一段

输出里最需要看懂的是 @@ 那一行和它下面的加减号。

@@ -1,3 +1,4 @@ 这种写法表示:旧文本从第 1 行起共 3 行,新文本从第 1 行起共 4 行。它给出的是差异块的位置和范围,不是行号列表。

差异块内部,每行开头的符号含义是:

- 空格开头:这一行两段都有,没有变化,属于上下文行。

- 减号开头:这一行只出现在旧文本里,新文本删掉了它。

- 加号开头:这一行只出现在新文本里,是新增的。

上下文行数由 n 参数控制,默认是 3。也就是说,每个差异块前后各显示 3 行没变的行,方便你判断改动发生在什么位置。n 设得越大,上下文越多;设得越小,输出越短。

如果你只想看改动本身、不想看上下文,可以把 n 设为 0。但要注意,上下文太少时,你可能不容易判断这段改动落在原文的哪个位置。

检查结果:先看控制行,再看差异块

拿到输出后,建议按固定顺序核对,避免漏看。

1. 看开头两行。--- 后面跟的是 fromfile 标签,+++ 后面跟的是 tofile 标签。如果这里显示的不是你预期的名字,说明参数传错了。

2. 看 @@ 行。它告诉你差异块在旧文本和新文本里的起止范围。如果 @@ 行显示的起止和你的预期对不上,先检查输入列表的行数和顺序。

3. 逐块看加减号。减号行是旧文本独有的,加号行是新文本独有的。如果同一处既有减号又有加号,说明这一行被替换了。

4. 确认没有变化的行。空格开头的行是两段共有的,它们不该出现在加减号里。如果一段你确认没改的内容出现在减号里,多半是行尾换行符或空格不一致造成的。

一个常见现象:两段文本看起来一样,却报告有差异。这通常是因为一处的行尾带了换行符、另一处没带,或者行尾多了空格。unified_diff 是按元素逐个比较的,换行符和空格都是元素的一部分。

输出为空说明什么:两段列表完全一致

如果 unified_diff 没有产出任何行,说明两个列表在元素层面完全一致。

这不是错误,而是结果。文档说明,unified_diff 返回的是生成器,只有存在差异时才会产出内容。所以判断两段文本是否相同,可以直接看输出是否为空。

要检查这一点,可以把生成器转成列表再判断长度:

from difflib import unified_diff

s1 = ["第一行:准备材料\n", "第二行:清洗器具\n"]
s2 = ["第一行:准备材料\n", "第二行:清洗器具\n"]

result = list(unified_diff(s1, s2, fromfile="a.txt", tofile="b.txt"))
print(len(result))

这段自建示例的输出是 0,因为两个列表逐元素相同。

需要注意:生成器只能遍历一次。如果你先把它转成列表保存,后续可以反复查看;如果直接遍历打印,再想用同一份结果就要重新调用一次 unified_diff。

使用限制:它报告差异,不判断原创度

unified_diff 能回答的问题是:第二段文本相对第一段,哪些行被删、哪些行被加、改动落在什么位置。它不能回答的问题包括:这两段文本是不是同一作者写的、第二段是不是抄的、相似度是多少。

文档里确实有 ratio() 这类相似度度量方法,但那是 SequenceMatcher 的功能,不是 unified_diff 的输出。即便拿到一个相似度数值,它也只是基于序列匹配算出的比例,不能等同于原创度判断。本文不把差异结果和原创度挂钩。

另外几点限制:

- 它只比较你传进去的两个列表,不读文件、不写文件、不修改任何内容。

- 它按元素逐个比较,行尾换行符和空格都会影响结果。

- 它默认给控制行加换行符,输入不带换行时要相应设置 lineterm。

- 它不判断哪一版更好,只报告差异。

本文所有示例都是自建的练习数据,放在练习目录里操作。不要拿正在使用的文件直接做实验;如果要在真实文件上练习,先复制一份副本。

观看知识要点视频 18 秒

18秒无旁白要点导览,文字来自已审核正文;完整步骤、条件及参考资料见下方。

参考资料

以下原始资料提供本文依据;请结合日期、适用条件和产品说明查阅。

请结合原始资料的适用条件阅读。

返回效率工具 →