Java案例如何实现合同比对?

wen python案例 1

本文目录导读:

Java案例如何实现合同比对?

  1. 方案一:基于LCS算法的文本行比对(最核心、最常用)
  2. 方案二:处理大文件的优化(行级流式处理)
  3. 方案三:检测“修改”而非仅“新增/删除”(更智能的合同比对)
  4. 方案四:对比Word文档(.docx)格式的合同(生产环境重点)
  5. 方案五:直接使用成熟的Java Diff库(强烈推荐)
  6. 完整项目建议架构
  7. 总结建议

这是一个专业性很强的Java应用需求,合同比对的核心在于文本差异分析,而非简单的字符串相等判断。

在实际的合同比对场景中(如法务、金融),通常关注的不仅是“改了哪里”,还包括新增删除修改以及格式变化

下面我会从最经典的 Diff算法(最长公共子序列,LCS) 实现、到处理大文件的优化方案、以及如何结合文档格式(如Word)三个层面,给出一个完整的Java实现方案。

基于LCS算法的文本行比对(最核心、最常用)

这是最标准的做法,将合同按分割,然后找出两段文本的“最长公共子序列”,未被包含的部分即为差异。

核心算法:计算Diff矩阵(LCS)

import java.util.*;
public class DiffUtils {
    /**
     * 计算两个文本数组的最长公共子序列(LCS)路径
     * @param original 原始文本行列表
     * @param revised  修改后文本行列表
     * @return 差异操作列表
     */
    public static List<DiffEntry> computeDiff(List<String> original, List<String> revised) {
        int m = original.size();
        int n = revised.size();
        // 1. 构建LCS长度矩阵
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (original.get(i - 1).equals(revised.get(j - 1))) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        // 2. 回溯,生成Diff操作
        List<DiffEntry> result = new ArrayList<>();
        int i = m, j = n;
        // 使用栈来反转顺序(因为回溯是从后往前)
        Deque<DiffEntry> stack = new ArrayDeque<>();
        while (i > 0 || j > 0) {
            if (i > 0 && j > 0 && original.get(i - 1).equals(revised.get(j - 1))) {
                // 相等:保留
                stack.push(new DiffEntry(DiffType.EQUAL, original.get(i - 1)));
                i--;
                j--;
            } else if (j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j])) {
                // 插入:新增的行
                stack.push(new DiffEntry(DiffType.INSERT, revised.get(j - 1)));
                j--;
            } else if (i > 0) {
                // 删除:移除的行
                stack.push(new DiffEntry(DiffType.DELETE, original.get(i - 1)));
                i--;
            }
        }
        // 从栈中弹出,得到正确顺序
        while (!stack.isEmpty()) {
            result.add(stack.pop());
        }
        return result;
    }
    // 差异类型枚举
    public enum DiffType {
        EQUAL,   // 相同
        INSERT,  // 新增
        DELETE   // 删除
    }
    // 差异条目
    public static class DiffEntry {
        private DiffType type;
        private String line;
        public DiffEntry(DiffType type, String line) {
            this.type = type;
            this.line = line;
        }
        public DiffType getType() { return type; }
        public String getLine() { return line; }
        @Override
        public String toString() {
            return (type == DiffType.INSERT ? "+ " : 
                    type == DiffType.DELETE ? "- " : "  ") + line;
        }
    }
}

使用示例 & 可视化输出

public class ContractComparator {
    public static void main(String[] args) {
        String text1 = """
                合同第一条:关于货款
                甲方应于2024年1月1日前支付。
                逾期未付,按日收取万分之五的违约金。
                """;
        String text2 = """
                合同第一条:关于货款
                甲方应于2024年3月1日前支付。
                逾期未付,按日收取万分之三的违约金。
                若逾期超过30天,甲方可解除合同。
                """;
        // 1. 按行分割
        List<String> originalLines = Arrays.asList(text1.split("\n"));
        List<String> revisedLines = Arrays.asList(text2.split("\n"));
        // 2. 执行比对
        List<DiffUtils.DiffEntry> diffs = DiffUtils.computeDiff(originalLines, revisedLines);
        // 3. 输出比对结果(类似Git diff风格)
        System.out.println("=== 合同比对结果 ===");
        diffs.forEach(entry -> {
            switch (entry.getType()) {
                case INSERT:
                    System.out.println("\033[32m+ " + entry.getLine() + "\033[0m"); // 绿色
                    break;
                case DELETE:
                    System.out.println("\033[31m- " + entry.getLine() + "\033[0m"); // 红色
                    break;
                case EQUAL:
                    System.out.println("  " + entry.getLine());
                    break;
            }
        });
    }
}

处理大文件的优化(行级流式处理)

如果合同非常大(如几十万行),上述二维数组dp[m+1][n+1]会耗尽内存,可以采用Myers差分算法滑动窗口优化

这里提供一个改进版:只保留两行DP数组(空间复杂度 O(n)),适合行数极多但差异不大的情况。

public static List<DiffEntry> computeDiffOptimized(List<String> original, List<String> revised) {
    int m = original.size();
    int n = revised.size();
    // 只能得到长度,无法直接回溯路径
    // 实际生产环境中通常使用 "分治策略" + "线性空间LCS"
    // 这里简化:对于大文件,建议直接调用成熟的库(见下文)
    throw new UnsupportedOperationException("大规模文件建议使用google-diff-match-patch或git diff");
}

检测“修改”而非仅“新增/删除”(更智能的合同比对)

合同比对中,“万分之五” 被改为 “万分之三” ,更合理的展示应该是修改操作,而不是先删一行再加一行。

实现方式:在回溯完成后,对相邻的 DELETE + INSERT 进行合并。

public static List<DiffEntry> mergeModifications(List<DiffEntry> diffs) {
    List<DiffEntry> merged = new ArrayList<>();
    for (int i = 0; i < diffs.size(); i++) {
        DiffEntry current = diffs.get(i);
        // 如果当前是DELETE,且下一个是INSERT,合并为MODIFY
        if (current.getType() == DiffType.DELETE 
            && i + 1 < diffs.size() 
            && diffs.get(i + 1).getType() == DiffType.INSERT) {
            DiffEntry next = diffs.get(i + 1);
            merged.add(new DiffEntry(DiffType.MODIFY, 
                    "旧: " + current.getLine() + " | 新: " + next.getLine()));
            i++; // 跳过下一个INSERT
        } else {
            merged.add(current);
        }
    }
    return merged;
}
// 别忘了在DiffType枚举中添加 MODIFY 类型

对比Word文档(.docx)格式的合同(生产环境重点)

合同通常以Word格式存在,直接对比文本会丢失格式信息。

使用 Apache POI 提取Word文本 + Diff

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>
import org.apache.poi.xwpf.usermodel.*;
public static String extractTextFromDocx(String filePath) throws Exception {
    StringBuilder sb = new StringBuilder();
    try (XWPFDocument doc = new XWPFDocument(new FileInputStream(filePath))) {
        for (XWPFParagraph para : doc.getParagraphs()) {
            sb.append(para.getText()).append("\n");
        }
        // 还可以提取表格中的文本
        for (XWPFTable table : doc.getTables()) {
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    sb.append(cell.getText()).append("\t");
                }
                sb.append("\n");
            }
        }
    }
    return sb.toString();
}

直接使用成熟的Java Diff库(强烈推荐)

手写LCS算法适合学习,但生产环境建议使用以下高性能、高稳定性的库:

google-diff-match-patch(Google出品,最强)

<dependency>
    <groupId>org.bitbucket.cowwoc</groupId>
    <artifactId>diff-match-patch</artifactId>
    <version>1.2</version>
</dependency>

核心代码(支持字符级、词级、行级 + 语义清理):

import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch;
DiffMatchPatch dmp = new DiffMatchPatch();
LinkedList<DiffMatchPatch.Diff> diffs = dmp.diffMain(text1, text2);
dmp.diffCleanupSemantic(diffs); // 语义优化
String html = dmp.diffPrettyHtml(diffs);
System.out.println(html); // 可以直接展示在网页上

java-diff-utils(轻量级,易用)

<dependency>
    <groupId>io.github.java-diff-utils</groupId>
    <artifactId>java-diff-utils</artifactId>
    <version>4.12</version>
</dependency>
import com.github.difflib.text.DiffRowGenerator;
DiffRowGenerator generator = DiffRowGenerator.create()
    .showInlineDiffs(true)  // 显示行内差异(如万->三)
    .mergeOriginalRevised(true)
    .build();
List<DiffRow> rows = generator.generateDiffRows(
    Arrays.asList(text1.split("\n")),
    Arrays.asList(text2.split("\n"))
);
rows.forEach(row -> {
    System.out.println(row.getOldLine() + " | " + row.getNewLine());
});

完整项目建议架构

src/
├── service/
│   ├── ContractComparator.java        // 调用Diff入口
│   ├── DiffAlgorithm.java             // LCS或Myers实现
│   └── DiffDisplayFormatter.java      // 格式化输出(HTML/控制台/JSON)
├── model/
│   └── DiffEntry.java                 // 差异实体
├── parser/
│   ├── TextParser.java                // 文本、PDF、Word解析
│   └── DocxParser.java                // Apache POI解析docx
└── Application.java                   // 主程序

总结建议

  1. 入门/学习:使用代码中的 LCS算法 + 合并修改
  2. 生产环境:直接使用 java-diff-utilsgoogle-diff-match-patch,它们的性能、语义处理、边缘情况处理都比手写的好很多。
  3. 特殊格式:如果需要保留Word格式对比(高亮、批注),可能需要更复杂的解决方案(如基于XML差分),推荐使用商业库或 Apache POI + difflib 组合。

希望这个案例能帮助你快速实现合同比对功能!

抱歉,评论功能暂时关闭!