文章目录
前言
GUN diff 与 Google diff-match-patch的区别
最近研究了google工程师开发的计算文本之间差异值的diff算法。该算法可以计算出不同文本之间的差异值并且生成patch文件来体现文本之间的差异。
GNU diff对于diff的计算是基于行的,也就是说如果使用GUN diff来计算两段文本之间的差异,两段文本的某一行绝大多数内容都相同,但是行内的某些字符有轻微的不同之处,那么GUN diff就会直接将其判定为冲突。比如:
第一行:
Google diff-match-patch源代码解析
第二行:
Google diff-patch源代码解析
那么使用GUN diff计算的结果就是:
< Google diff-match-patch源代码解析
---
> Google diff-patch源代码解析
而且使用GUN diff,如果两个文本同时修改了同一行,那么很容易引起冲突;冲突之后会将冲突结果存放入一个文件之中。如果使用基于GUN diff的merge方法,那么会将冲突结果直接写入合并后的文本之中。形如:
<<<<<<< 1.txt
Google diff-match-patch源代码解析=======
Google diff-patch源代码解析>>>>>>> 2.txt
而Google diff-match-patch的优势之处在于,它没有GUN diff这么死板。 Google diff-match-patch的算法是基于字符的,也就是说尽管一行之内有不同,但是Google的算法还是能找到行内的不同之处和相同之处。
还是上面那个例子,如果使用Google diff算法就会出现下面的结果:
最奇妙的是,Google diff算法还可以根据用户不同的需要进行定制化的diff操作,包括但不限于:
- 语义化优先的diff计算
- 效率优先的diff计算(可以设置diff单元的最小粒度)
- 设置ddl时间的diff计算(在n秒内完成diff计算操作)
如果想体验google diff-match-patch算法,这里提供了一个基于JS实现的在线demo。
另外该算法是开源的,具有多种语言的实现版本(包括但不限于:Java、JS、Python),git仓库在这里。
注释
这篇文章准备分几个部分来写,根据目前的构思,想分为三个部分完成这一篇博文:
- diff_match_patch的diff算法是如何实现的?
- diff_match_patch如何对生成的diff进行语义上的优化?
- 如何进一步优化diff_match_patch,以支持对富文本(标签文本)的diff计算?
这是第一篇博文,分析diff_match_patch的diff算法。
diff算法实现——如何定义一系列操作,来将字符串A转换为字符串B?
操作的定义
或许有人记得动态规划算法的教学题——编辑距离。如果想把字符串A转换为字符串B,那么最少需要多少步的操作?
很遗憾这里使用的似乎并不是动态规划算法;但是这两个问题之间有一个很重要的共同点:对于操作的定义。字符串的修改,无非“插入字符”、“删除字符”、“修改字符”。而为了简化操作的种类,“修改字符”可以用“删除字符”和“修改字符”的组合来完成。比如A字符串为aaa
,B字符串为aab
,从A到B的转换可以是:
- 将A字符串的最后一位替换为
b
- 将A字符串的最后一位删除;在A字符串的最后一位添加字符`b``
那么Google diff-match-patch算法中是怎么定义操作的呢?
/**
* 将操作分为三种枚举类型:Delete,Insert,Equal
*/
public enum Operation {
DELETE, INSERT, EQUAL
}
public static class Diff {
/**
* INSERT, DELETE , EQUAL.这三种枚举操作类型中的一种
*/
public Operation operation;
/**
* 操作的文本内容
*/
public String text;
... ...
}
这里定义的三种操作:INSERT, DELETE, EQUAL对应的三种Diff情形,举个例子:
比如,文本AHelloworld
到文本BGoodbyeworld
的转换就可以定义为三个Diff:
- Diff(Operation.DELETE, “Hello”): delete “Hello” //删除Hello文本
- Diff(Operation.INSERT, “Goodbye”): add “Goodbye” //增添Goodbye文本
- Diff(Operation.EQUAL, " world."): keep " world." //保持原有的world不变
定义完了操作,基于这个操作集,任意(String A,String B)的转换经过计算,都可以转换为一系列Diff的组合——LinkedList<Diff>:哪里删除了,哪里增加了,哪里没有变。
Diff的计算
其实整个Google diff-match-patch的源代码带上注释有大约2500行(Java实现版本)。但是真正“单纯”的计算Diff的代码只有不到300行。剩下的很多代码其实都是用于后续Diff类转化为文本服务的——为了应对客户对于导出Diff定制化的需求:有的想快速把Diff导出,有的想基于语义导出Diff等等。
这里的导出Diff指的是,将计算出LinkedList<Diff>的重新生成为用户易于理解的Diff文本。
首先,整个进行Diff计算的入口(一上来就利用多态进行两次跳转,如果不进行特殊的参数配置则默认使用下面代码框中的第三段代码):
public LinkedList<Diff> diff_main(String text1, String text2) {
return diff_main(text1, text2, true); // 这里的true暂时可以忽略(跟本篇的关系不大),绝大多数情况下可以直接置为True。
}
public LinkedList<Diff> diff_main(String text1, String text2, boolean checklines) {
......
return diff_main(text1, text2, checklines, deadline);
}
/**
* 计算两个文本之间的Diff。 通过给相似的文本“掐