本文要点
用 splitlines(keepends=True) 把整段文字切成带换行符的行列表,这是文档示例里用的方式
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秒无旁白要点导览,文字来自已审核正文;完整步骤、条件及参考资料见下方。
参考资料
以下原始资料提供本文依据;请结合日期、适用条件和产品说明查阅。
请结合原始资料的适用条件阅读。
返回效率工具 →