前言

本文是具体实现,如果你想查看文本对比的算法和diff等细节,可查看我另一篇文章
Java 文本内容差异对比实现介绍

注意:

本文的对比方式只支持对比文本文件,也就是支持能用文本编辑器打开的文本或代码源文件。如:java、js、css、c、cpp、py、txt 等格式。
不支持 word 文档对比,即不支持 doc、docx 格式的文件对比。

方法一:java + html实现

1.、准备

1.Java使用的库 java-diff-utils

下载:

jar下载网址:java-diff-utils.jar下载

如果你使用Maven:

<dependency>
  <groupId>io.github.java-diff-utils</groupId>
  <artifactId>java-diff-utils</artifactId>
  <version>4.11</version>
</dependency>

2.前端使用的库 diff2html

2.、后端实现

用java-diff-utils 封装了一个工具类 DiffHandleUtils .java

package com.util;


import com.github.difflib.UnifiedDiffUtils;
import com.github.difflib.patch.Patch;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class DiffHandleUtils {

    /**
     * 对比两文件的差异,返回原始文件+diff格式
     *
     * @param original 原文件内容
     * @param revised  对比文件内容
     */
    public static List<String> diffString(List<String> original, List<String> revised) {
        return diffString(original, revised, null, null);
    }

    /**
     * 对比两文件的差异,返回原始文件+diff格式
     *
     * @param original         原文件内容
     * @param revised          对比文件内容
     * @param originalFileName 原始文件名
     * @param revisedFileName  对比文件名
     */
    public static List<String> diffString(List<String> original, List<String> revised, String originalFileName, String revisedFileName) {
        originalFileName = originalFileName == null ? "原始文件" : originalFileName;
        revisedFileName = revisedFileName == null ? "对比文件" : revisedFileName;
        //两文件的不同点
        Patch<String> patch = com.github.difflib.DiffUtils.diff(original, revised);
        //生成统一的差异格式
        List<String> unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(originalFileName, revisedFileName, original, patch, 0);
        int diffCount = unifiedDiff.size();
        if (unifiedDiff.size() == 0) {
            //如果两文件没差异则插入如下
            unifiedDiff.add("--- " + originalFileName);
            unifiedDiff.add("+++ " + revisedFileName);
            unifiedDiff.add("@@ -0,0 +0,0 @@");
        } else if (unifiedDiff.size() >= 3 && !unifiedDiff.get(2).contains("@@ -1,")) {
            unifiedDiff.set(1, unifiedDiff.get(1));
            //如果第一行没变化则插入@@ -0,0 +0,0 @@
            unifiedDiff.add(2, "@@ -0,0 +0,0 @@");
        }
        //原始文件中每行前加空格
        List<String> original1 = original.stream().map(v -> " " + v).collect(Collectors.toList());
        //差异格式插入到原始文件中
        return insertOrig(original1, unifiedDiff);
    }


    /**
     * 对比两文件的差异,返回原始文件+diff格式
     *
     * @param filePathOriginal 原文件路径
     * @param filePathRevised  对比文件路径
     */
    public static List<String> diffString(String filePathOriginal, String filePathRevised) {
        //原始文件
        List<String> original = null;
        //对比文件
        List<String> revised = null;
        File originalFile = new File(filePathOriginal);
        File revisedFile = new File(filePathRevised);
        try {
            original = Files.readAllLines(originalFile.toPath());
            revised = Files.readAllLines(revisedFile.toPath());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return diffString(original, revised, originalFile.getName(), revisedFile.getName());
    }

    /**
     * 通过两文件的差异diff生成 html文件,打开此 html文件便可看到文件对比的明细内容
     *
     * @param diffString 调用上面 diffString方法获取到的对比结果
     * @param htmlPath   生成的html路径,如:/user/var/mbos/ent/21231/diff.html
     */
    public static void generateDiffHtml(List<String> diffString, String htmlPath) {
        StringBuilder builder = new StringBuilder();
        for (String line : diffString) {
            builder.append(escapeStr(line));
            builder.append("\n");
        }
        //如果打开html为空白界面,可能cdn加载githubCss失败 ,githubCss 链接可替换为 https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/github.min.css
        String githubCss = "https://cdn.jsdelivr.net/gh/1506085843/fillDiff@master/src/main/resources/css/github.min.css";
        //如果打开html为空白界面,可能cdn加载diff2htmlCss失败 ,diff2htmlCss 链接可替换为 https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css
        String diff2htmlCss = "https://cdn.jsdelivr.net/gh/1506085843/fillDiff@master/src/main/resources/css/diff2html.min.css";
        //如果打开html为空白界面,可能cdn加载diff2htmlJs失败, diff2htmlJs 链接可替换为 https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js
        String diff2htmlJs = "https://cdn.jsdelivr.net/gh/1506085843/fillDiff@master/src/main/resources/js/diff2html-ui.min.js";
        //如果githubCss、diff2htmlCss、diff2htmlJs都加载失败可从 https://github.com/1506085843/java-file-diff 项目的resources目录下下载css和js手动引入到html

        String template = "<!DOCTYPE html>\n" +
                "<html lang=\"en-us\">\n" +
                "  <head>\n" +
                "    <meta charset=\"utf-8\" />\n" +
                "    <link rel=\"stylesheet\" href=\"" + githubCss + "\" />\n" +
                "     <link rel=\"stylesheet\" type=\"text/css\" href=\"" + diff2htmlCss + "\" />\n" +
                "    <script type=\"text/javascript\" src=\"" + diff2htmlJs + "\"></script>\n" +
                "  </head>\n" +
                "  <script>\n" +
                "    const diffString = `\n" +
                "temp\n" +
                "`;\n" +
                "\n" +
                "\n" +
                "     document.addEventListener('DOMContentLoaded', function () {\n" +
                "      var targetElement = document.getElementById('myDiffElement');\n" +
                "      var configuration = {\n" +
                "        drawFileList: true,\n" +
                "        fileListToggle: true,\n" +
                "        fileListStartVisible: true,\n" +
                "        fileContentToggle: true,\n" +
                "        matching: 'lines',\n" +
                "        outputFormat: 'side-by-side',\n" +
                "        synchronisedScroll: true,\n" +
                "        highlight: true,\n" +
                "        renderNothingWhenEmpty: true,\n" +
                "      };\n" +
                "      var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);\n" +
                "      diff2htmlUi.draw();\n" +
                "      diff2htmlUi.highlightCode();\n" +
                "    });\n" +
                "  </script>\n" +
                "  <body>\n" +
                "    <div id=\"myDiffElement\"></div>\n" +
                "  </body>\n" +
                "</html>";
        template = template.replace("temp", builder.toString());
        FileWriter f = null; //文件读取为字符流
        try {
            f = new FileWriter(htmlPath);
            BufferedWriter buf = new BufferedWriter(f); //文件加入缓冲区
            buf.write(template); //向缓冲区写入
            buf.close(); //关闭缓冲区并将信息写入文件
            f.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    //对字符串中的特殊字符进行转义
    private static String escapeStr(String linStr) {
        //如果含有反斜杠对其转义为\\
        if (linStr.contains("\\")) {
            linStr = linStr.replaceAll("\\\\", "\\\\\\\\");
        }
        //如果含有</script>将其转义为<\/script> ,否则浏览器解析到</script>将报错
        if (linStr.contains("</script>")) {
            linStr = linStr.replaceAll("</script>", "<\\\\/script>");
        }
        //如果含有字符串模板的反引号对其转义为\`
        if (linStr.contains("`")) {
            linStr = linStr.replaceAll("`", "\\\\`");
        }
        //如果含有$符号对其转义为\$
        if (linStr.contains("$")) {
            linStr = linStr.replaceAll("\\$", "\\\\\\$");
        }
        return linStr;
    }
    
    //统一差异格式插入到原始文件
    public static List<String> insertOrig(List<String> original, List<String> unifiedDiff) {
        List<String> result = new ArrayList<>();
        //unifiedDiff中根据@@分割成不同行,然后加入到diffList中
        List<List<String>> diffList = new ArrayList<>();
        List<String> d = new ArrayList<>();
        for (int i = 0; i < unifiedDiff.size(); i++) {
            String u = unifiedDiff.get(i);
            if (u.startsWith("@@") && !"@@ -0,0 +0,0 @@".equals(u) && !u.contains("@@ -1,")) {
                List<String> twoList = new ArrayList<>();
                twoList.addAll(d);
                diffList.add(twoList);
                d.clear();
                d.add(u);
                continue;
            }
            if (i == unifiedDiff.size() - 1) {
                d.add(u);
                List<String> twoList = new ArrayList<>();
                twoList.addAll(d);
                diffList.add(twoList);
                d.clear();
                break;
            }
            d.add(u);
        }

        //将diffList和原始文件original插入到result,返回result
        for (int i = 0; i < diffList.size(); i++) {
            List<String> diff = diffList.get(i);
            List<String> nexDiff = i == diffList.size() - 1 ? null : diffList.get(i + 1);
            //含有@@的一行
            String simb = i == 0 ? diff.get(2) : diff.get(0);
            String nexSimb = nexDiff == null ? null : nexDiff.get(0);
            //插入到result
            insert(result, diff);
            //解析含有@@的行,得到原文件从第几行开始改变,改变了多少(即增加和减少的行)
            Map<String, Integer> map = getRowMap(simb);
            if (null != nexSimb) {
                Map<String, Integer> nexMap = getRowMap(nexSimb);
                int start = 0;
                if (map.get("orgRow") != 0) {
                    start = map.get("orgRow") + map.get("orgDel") - 1;
                }
                int end = nexMap.get("orgRow") - 2;
                //插入不变的
                insert(result, getOrigList(original, start, end));
            }

            int start = (map.get("orgRow") + map.get("orgDel") - 1);
            start = start == -1 ? 0 : start;
            if (simb.contains("@@ -1,") && null == nexSimb && map.get("orgDel") != original.size()) {
                insert(result, getOrigList(original, start, original.size() - 1));
            } else if (null == nexSimb && (map.get("orgRow") + map.get("orgDel") - 1) < original.size()) {
                insert(result, getOrigList(original, start, original.size() - 1));
            }
        }
        //如果你想知道两文件有几处不同可以放开下面5行代码注释,会在文件名后显示总的不同点有几处(即预览图中的xxx different),放开注释后有一个小缺点就是如果对比的是java、js等代码文件那代码里的关键字就不会高亮颜色显示,有一点不美观。
        //int diffCount = diffList.size() - 1;
        //if (!"@@ -0,0 +0,0 @@".equals(unifiedDiff.get(2))) {
        //    diffCount = diffList.size() > 1 ? diffList.size() : 1;
        //}
        //result.set(1, result.get(1) + " ( " + diffCount + " different )");
        return result;
    }

    //将源文件中没变的内容插入result
    public static void insert(List<String> result, List<String> noChangeContent) {
        for (String ins : noChangeContent) {
            result.add(ins);
        }
    }

    //解析含有@@的行得到修改的行号删除或新增了几行
    public static Map<String, Integer> getRowMap(String str) {
        Map<String, Integer> map = new HashMap<>();
        //正则校验字符串是不是 "@@ -3,1 +3,1 @@" 这样的diff格式
        String pattern = "^@@\\s-*\\d+,\\d+\\s+\\+\\d+,\\d+\\s+@@$";
        if (Pattern.matches(pattern, str)) {
            String[] sp = str.split(" ");
            String org = sp[1];
            String[] orgSp = org.split(",");
            //源文件要删除行的行号
            map.put("orgRow", Integer.valueOf(orgSp[0].substring(1)));
            //源文件删除的行数
            map.put("orgDel", Integer.valueOf(orgSp[1]));

            String[] revSp = sp[2].split(",");
            //对比文件要增加行的行号
            map.put("revRow", Integer.valueOf(revSp[0].substring(1)));
            map.put("revAdd", Integer.valueOf(revSp[1]));
        }
        return map;
    }

    //从原文件中获取指定的部分行
    public static List<String> getOrigList(List<String> original1, int start, int end) {
        List<String> list = new ArrayList<>();
        if (original1.size() >= 1 && start <= end && end < original1.size()) {
            for (; start <= end; start++) {
                list.add(original1.get(start));
            }
        }
        return list;
    }
}

3.、测试和效果

    public static void main(String[] args)  {
        //对比 F:\n1.txt和 F:\n2.txt 两个文件,获得不同点
        List<String> diffString = DiffHandleUtils.diffString("F:\\n1.txt","F:\\n2.txt");
        //在F盘生成一个diff.html文件,打开便可看到两个文件的对比
        DiffHandleUtils.generateDiffHtml(diffString,"F:\\diff.html");
    }

浏览器打开 diff.html 效果如下:

在这里插入图片描述
在这里插入图片描述

因为 diff.html 里所需的 css、js 都是从外网加载的,所有如果网络有问题会出现 页面空白 的情况,浏览器 F12 控制台的报错信息是 js 和 css 加载或下载失败,你可以去下面我的 github 下载 css、js 手动加载。

所有代码和 css、js 都上传到了 我的 github https://github.com/1506085843/java-file-diff , 你可以 下载源码 进行测试和手动引用 js、css,也可修改上面工具类的第 98、100、102 行的引用路径

你也可以看下我 gitgub上使用 javaFX 开发的一个应用: XTool 里面集成了文本对比功能。

方法二:Mergely纯javascript实现

Mergely 是一个 JavaScript 组件,用于在浏览器中交互式地区分和合并文件(diff/merge),提供丰富的 API,使您能够轻松地将 Mergely 集成到现有的 Web 应用程序中。适用于在线比较文本文件,例如.txt、.html、.xml、.c、.cpp、.java等。

Mergely 具有最长公共子序列 (LCS) 差异算法的 JavaScript 实现,以及可定制的标记引擎。

Mergely官网
Mergely的github地址

1.Mergely使用示例

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8" /><title>Mergely - Simple Example</title>
	<meta http-equiv="X-UA-Compatible" content="chrome=1, IE=edge">
	<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
	<meta name="description" content="Merge and Diff your documents with diff online and share" />
	<meta name="keywords" content="diff,merge,compare,jsdiff,comparison,difference,file,text,unix,patch,algorithm,saas,longest common subsequence" />
	<meta name="author" content="Jamie Peabody" />

	<!-- Requires jQuery -->
	<script type="text/javascript" src="jquery.min.js"></script>

	<!-- Requires CodeMirror -->
	<script type="text/javascript" src="codemirror.min.js"></script>
	<script type="text/javascript" src="searchcursor.min.js"></script>
	<link type="text/css" rel="stylesheet" href="codemirror.min.css" />

	<!-- Requires Mergely -->
	<script type="text/javascript" src="mergely.js"></script>
	<link type="text/css" rel="stylesheet" href="mergely.css" />

	<script type="text/javascript">
	var left =`若你忽然问我
为什么要写诗

为什么 不去做些你渴望的和
别的有用的事

那么 我也不知道
该怎样回答

我如金匠 日夜捶击敲打
只为把痛苦延展成
薄如蝉翼的金饰

不知道这样努力的
把忧伤的来源转化成为
光泽细柔的词句
是不是 也有一种
美丽的价值

《爱的筵席》

是令人日渐消瘦的心事
是举箸前莫名的伤悲
是记忆里一场不散的筵席
是不能饮不可饮 也要拼却的
一醉

 如歌的行板

一定有些什么
是我所不能了解的

不然 草木怎么都会
循序生长
而侯鸟都能飞回故乡

一定有些什么
是我所无能无力的

不然 日与夜怎么交替得
那样快 所有的时刻
都已错过 忧伤蚀我心怀

一定有些什么 在叶落之后
是我所必须放弃的

是十六岁时的那本日记
还是 我藏了一生的

那些美丽的如山百合般的
秘密`;

var right =`若你忽然问我
为什么要写诗

为什么 不去做些
别的有用的事

那么 我也不知道
该怎样回答

我如金匠 每天捶击敲打
只为把痛苦延展成
薄如蝉翼的金饰

不知道这样努力地
把忧伤的来源转化成
光泽细柔的词句
是不是 也有一种
美丽的价值

 如歌的行板

一定有些什么
是我所不能了解的

不然 草木怎么都会
循序生长
而侯鸟都能飞回故乡

一定有些什么
是我所无能无力的

不然 日与夜怎么交替得
那样快 所有的时刻
都已错过 忧伤蚀我心怀

一定有些什么 在叶落之后
是我所必须放弃的

是十六岁时的那本日记
还是 我藏了一生的

  那些美丽的如山百合般的
秘密

不再回头的
不再是古老的辰光
也不只是那些个夜晚的
星群和月亮`;

		$(document).ready(function () {
			$('#mergely').mergely({
				sidebar:true,   //是否显示侧边栏,设置成false可以提高大型文档的性能
				ignorews:false,  //是否忽略空格对比
				license: 'lgpl',
				cmsettings: {
					readOnly: false //false则展示合并箭头,运行两边能够合并
				},
				lhs: function(setValue) {
					setValue(left);
				},
				rhs: function(setValue) {
					setValue(right);
				}
			});
		});
	</script>
</head>

<body>
	<div>
		<div class="mergely-full-screen-8">
			<div class="mergely-resizer">
				<div id="mergely"></div>
			</div>
		</div>
	</div>
</body>

</html>

2.效果图

在这里插入图片描述

3.使用示例中用到的css和js下载

百度云下载:https://pan.baidu.com/s/1363h_lK3tIDhSW7ADU5Xkw
提取码:ezwo

或者:
CSDN下载

方法三:Git + Delta 实现

如果你经常用命令行来对比文本以及提交记录,那这种方式就很适合你。

我们平常会使用 git diff 命令来对比提交记录和文本,而 Delta 是一个开源美化工具用于美化和增强命令行的对比。
可以让你在命令行、idea 里的命令行更方便美观的并排对比,并且 Delta 有多种主题可配置,并且可自定义主题颜色。

Delta 的使用安装可参考我另一篇文章:Delta 一个新的 git diff 对比显示工具

它的效果如下:

在这里插入图片描述

在这里插入图片描述


参考:
vue中使用mergely.js

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐