文本相似度计算以及可视化设计

第一部分  绪论

灵感

模拟论文查重的各种软件,使用java编写一个类似的小程序,实现较为简单的文本相似度计算和可视化。

主要功能设计

GUI设计,分词操作,匹配操作,高亮操作,相似度计算。

主要功能设计

GUI设计

        使用边界型布局,主要分为三个部分。

        北部为标题,南部为操作执行以及结果展示,中部为文本以及文本相关的功能按键布局。

分词操作

        分词操作是其中需要考虑的最多的地方。为了使正则表达式既能对中文生效,又能对英文生效,我这里采用的是多个组去匹配得到结果。

        匹配中文句子:\s*([^\sa-zA-Z?.!。?!;:——]+)\s*

        匹配英文句子:\s*([a-zA-z ]+)\s*

        本质上,这里还是以自然的标点符号,如逗号,句号,感叹号等等来截取下整个话,然后对整句话进行分析,这样可以应对大部分的文本匹配问题。需要强调的是,完全准确地处理文本分句(尤其是在包含复杂语法和标点符号的混合语言文本中)通常需要一个完整的自然语言处理(NLP)系统,而不是仅仅依靠正则表达式。

匹配操作

        我们分词可得到的两个字符串数组arr1, arr2。将arr1和arr2中的元素彼此两两求出最长公共子串,并将所有计算结果记录下来,并且算出字符串之间的关系得分score。score的计算公式如下。

        score = len(subString) / max(len(a), len(b)),其中a, b是两字符串subString是最长公共子串。

        score是判断两个字符串之间相似性的评判标准,这里当 score >= STATUS 时,认为这两个字符串相似,并将相似部分高亮处理,否则认为这两个字符串无关。

        最长公共子串的求法这里采用动态规划法。

        时间复杂度: O(len(a) * len(b))。

        空间复杂度: O(len(a) * len(b))。

高亮操作

        高亮操作函数将根据匹配的结果,将符合规定的字符串在文本中高亮显示。为了区别不同语句,高亮颜色每次选择一个随机值用于区分。

相似度计算

        我们基于文本2,分析文本1中的内容与文本2中的内容的相似程度,所以我们自定义文本的相似度计算公式如下。

        相似度的计算公式: similarity = commonLength / totalLength。其中,

        totalLength = text1中得到的所有子串的长度,即posStrings1中所有字符串的总长度。

        commonLength = posStrings1和posStrings2匹配得到的所有最长子串的长度的总和。

样例测试

说明

        测试的结果受到STATUS的取值的影响,STATUS的值越高,两句话十分相似才认为这两句话有关系(也就是高亮显示),反之,STATUS的值越低,两句话部分相似便认为两句话有关系。

结果展示

此处STATUS取值为0.36。

中英文混例

20287aed6acc490b8d880e7c38a269c3.png

纯中文样例:(文本2节选自《荷塘月色》)

8a2fd210999f41bab9b8dc013d7b7c74.png

附录(源代码)

package TextComparison;

import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Highlighter;
import javax.swing.text.DefaultHighlighter.DefaultHighlightPainter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.ArrayList;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TextComparison extends JFrame {
    public final static double STATUS = 0.36;    // [待修正的参数]  评判标准(0 -> 1)(宽泛 -> 精确)
    double similarity = 0;

    /* 标题 */
    JPanel titleJpl = new JPanel();
    JTextField titleJtf = new JTextField("文本比较器", 25);  // 文本显示

    /* 文本选择 */
    // 文本1
    JPanel jpl1 = new JPanel();
    JPanel tempJpl1 = new JPanel();
    JLabel jlb1 = new JLabel("文本1");
    JButton jbt1 = new JButton("导入文本");
    JTextArea jta1 = new JTextArea(15, 25);
    JScrollPane jsp1 = new JScrollPane(jta1);
    Highlighter hl1 = jta1.getHighlighter();
    // 文本2
    JPanel jpl2 = new JPanel();
    JPanel tempJpl2 = new JPanel();
    JLabel jlb2 = new JLabel("文本2");
    JButton jbt2 = new JButton("导入文本");
    JTextArea jta2 = new JTextArea(15,25);
    JScrollPane jsp2 = new JScrollPane(jta2);
    Highlighter hl2 = jta2.getHighlighter();
    // 合并
    JSplitPane textSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, jpl1, jpl2);
    DefaultHighlightPainter painter;

    /* 匹配及匹配详情 */
    JPanel compareJpl = new JPanel();
    JButton compareJbt = new JButton("匹配及高亮显示");
    JTextField compareResultJtf = new JTextField();

    public TextComparison() {

        /* 初始设置 */
        this.setTitle("TextCompare");
        this.setLocation(400, 200);
        this.setSize(800, 500);
        this.setVisible(true);
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        this.setLayout(new BorderLayout());

        /* 北部 */
        // 标题
        titleJpl.setLayout(new BorderLayout());
        titleJtf.setEditable(false);    // 不可编辑
        titleJtf.setFont(new Font("宋体", Font.BOLD,15)); // 设置字体
        titleJtf.setHorizontalAlignment(SwingConstants.CENTER);        // 居中显示
        titleJpl.add(titleJtf, BorderLayout.CENTER);

        /* 南部 */
        // 匹配及匹配详情
        compareJpl.setLayout(new BorderLayout());
        compareJbt.setPreferredSize(new Dimension(125,50));    // 设置优选大小
        compareJpl.add(compareJbt, BorderLayout.WEST);
        compareResultJtf.setEditable(false);
        compareResultJtf.setFont(new Font("宋体", Font.BOLD,24));
        compareResultJtf.setHorizontalAlignment(SwingConstants.CENTER);        // 居中显示
        compareJpl.add(compareResultJtf, BorderLayout.CENTER);

        /* 中部 */
        JPanel central = new JPanel();
        central.setLayout(new BorderLayout());
        // 文本一
        tempJpl1.setLayout(new BorderLayout());
        jlb1.setPreferredSize(new Dimension(75, 15));
        jlb1.setHorizontalAlignment(SwingConstants.CENTER);
        tempJpl1.add(jlb1, BorderLayout.WEST);
        tempJpl1.add(jbt1, BorderLayout.CENTER);
        jpl1.setLayout(new BorderLayout());
        jpl1.add(tempJpl1, BorderLayout.NORTH);
        jta1.setEditable(false);
        jta1.setLineWrap(true);
        jta1.setFont(new Font("宋体", Font.PLAIN,15));
        jpl1.add(jsp1, BorderLayout.CENTER);
        // 文本二
        tempJpl2.setLayout(new BorderLayout());
        jlb2.setPreferredSize(new Dimension(75, 15));
        jlb2.setHorizontalAlignment(SwingConstants.CENTER);
        tempJpl2.add(jlb2, BorderLayout.WEST);
        tempJpl2.add(jbt2, BorderLayout.CENTER);
        jpl2.setLayout(new BorderLayout());
        jpl2.add(tempJpl2, BorderLayout.NORTH);
        jta2.setEditable(false);
        jta2.setLineWrap(true);
        jta2.setFont(new Font("宋体", Font.PLAIN,15));
        jpl2.add(jsp2, BorderLayout.CENTER);
        // 合并
        textSplit.setDividerLocation(this.getSize().width / 2 - 10); // 设置分隔条位置
        central.add(textSplit, BorderLayout.CENTER);
        /* 总体布局 */
        this.add(titleJpl, BorderLayout.NORTH);
        this.add(compareJpl, BorderLayout.SOUTH);
        this.add(central, BorderLayout.CENTER);

        /* 补充 */
        // 按钮添加事件处理
        jbt1.addActionListener(actionListener);
        jbt2.addActionListener(actionListener);
        compareJbt.addActionListener(actionListener);
    }

    ActionListener actionListener = new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            JButton tempJbt = (JButton) e.getSource();  // 判断操作来源
            if (tempJbt == jbt1) {
                getText(jta1);
            }
            else if(tempJbt == jbt2) {
                getText(jta2);
            }
            else if (tempJbt == compareJbt) {
                ComparisonAndHighlight();
            }
            else {
                JOptionPane.showMessageDialog(TextComparison.this, "不可识别事件!");
            }
        }
    };

    public void getText(JTextArea jta) {
        JFileChooser fChooser = new JFileChooser();
        int ok = fChooser.showOpenDialog(this);
        if (ok != JFileChooser.APPROVE_OPTION) return;
        jta.setText("");    // 清空之前的记录

        File choosenLib = fChooser.getSelectedFile();    // 获取选择的文件
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(choosenLib));
            while (true) {
                String str = br.readLine();
                if (str == null) break;
                jta.append(str + "\n");
            }
            br.close();
        } catch (FileNotFoundException e1) {
            JOptionPane.showMessageDialog(null, "文件不存在");
            e1.printStackTrace();
        } catch (IOException e1) {
            JOptionPane.showMessageDialog(null, "文件读取失败");
            e1.printStackTrace();
        }
    }

    /* 辅助算法设计的两个数据结构 */
    // PositionedString记录,分割得到的字符串以及在原串中被分割的起始位置
    static class PositionedString {
        public String string;
        public int startPosition;

        public PositionedString(String string, int startPosition) {
            this.string = string;
            this.startPosition = startPosition;
        }

        @Override
        public String toString() {
            return "String: '" + string + "', Start Position: " + startPosition;
        }
    }
    // Result记录,两个字符串a,b计算的最长公共子串c,以及c在a,b中的起始位置,和c的得分
    static class Result {
        public String substring;
        public int indexA;
        public int indexB;
        public double score = 0;    // score = len(subString) / max(len(a), len(b))

        Result(String substring, int indexA, int indexB, double score) {
            this.substring = substring;
            this.indexA = indexA;
            this.indexB = indexB;
            this.score = score;
        }

        @Override
        public String toString() {
            return "subString: " + substring + ", " + "indexA: " + indexA + ", " + " indexB: " + indexB + ", " + "score: " + score;
        }
    }

    /* 拆分 -> 匹配 -> 高亮;
     * 拆分:将text通过正则表达式拆分为子串,并记录拆分位置。
     * 匹配:使text1和text2中的所有子串两两匹配,记录匹配结果。
     * 高亮:根据每一个匹配结果的得分score,给定一个标准STATUS,不小于这个标准意味着两串相似,高亮显示。
     * */
    public void ComparisonAndHighlight() {
        hl1.removeAllHighlights();
        hl2.removeAllHighlights();  // 取消之前的高亮
        String text1 = jta1.getText();
        String text2 = jta2.getText();
        if (text1.equals("") || text2.equals(""))   return;

        // 获取分割的字符串及其位置
        ArrayList<PositionedString> positionedStrings1 = GetPositionedString(text1);
        ArrayList<PositionedString> positionedStrings2 = GetPositionedString(text2);

        /* 获取子串匹配结果(暴力);
         * 时间复杂度: O(posStr1 * posStr2)。
         * 空间复杂度: O(posStr1 * posStr2)。
         * */
        ArrayList<ArrayList<Result>> results = new ArrayList<>();
        for(PositionedString ps1: positionedStrings1) {
            ArrayList<Result> result = new ArrayList<>();
            for(PositionedString ps2: positionedStrings2) {
                Result r = LongestCommonSubstring(ps1.string, ps2.string);
                result.add(r);
            }
            results.add(result);
        }

        /* 根据匹配结果高亮视图;
         * (自定义)根据text1的匹配情况定义相似度;
         * totalLength = text1中得到的所有子串的长度,即positionedStrings1中所有字符串的总长度。
         * commonLength = positionedStrings1和positionedStrings2匹配得到的所有最长子串的长度的总和。
         * 相似度的计算公式: similarity = commonLength / totalLength。
         * */
        int totalLength = 0, commonLength = 0;
        for (int i = 0; i < results.size(); i++) {
            totalLength += positionedStrings1.get(i).string.length();
            int longestSubString = 0;
            for (int j = 0; j < results.get(i).size(); j++) {
                Result result = results.get(i).get(j);
                if(result.score > STATUS || result.score == STATUS) {
                    HighlightString(result, positionedStrings1.get(i).startPosition, positionedStrings2.get(j).startPosition);
                    longestSubString = Math.max(result.substring.length(), longestSubString);
                }
            }
            commonLength += longestSubString;
        }

        similarity = (double)commonLength / totalLength;
        compareResultJtf.setText("文本相似度:" + similarity * 100 + "%");
    }

    // 获取分割结果
    public ArrayList<PositionedString> GetPositionedString (String text) {
        /* 匹配中文句子:\s*([^\sa-zA-Z?.!。?!;:——]+)\s*
         * 匹配英文句子:\s*([a-zA-z ]+)\s*
         * */
        Pattern pattern = Pattern.compile("\\s*([^\\sa-zA-Z?.!。?!;:——]+)\\s*|\\s*([a-zA-Z ,'\"]+)\\s*"); // 同时匹配中英文
        Matcher matcher = pattern.matcher(text);
        ArrayList<PositionedString> positionedStrings = new ArrayList<>();

        while (matcher.find()) {
            String match;
            int startPosition;
            // 中文句子
            if (matcher.group(1) != null) {
                match = matcher.group(1);
                startPosition = matcher.start(1);
            }
            // 英文句子
            else {
                match = matcher.group(2);
                startPosition = matcher.start(2);
            }
            positionedStrings.add(new PositionedString(match, startPosition));
        }

        return positionedStrings;
    }

    // 高亮显示(不同子串的匹配需要更改高亮颜色,此处随机生成颜色)
    public void HighlightString(Result result, int offset1, int offset2) {
        Random random = new Random();
        int r = 56 + random.nextInt(200);
        int g = 56 + random.nextInt(200);
        int b = 56 + random.nextInt(200);
        Color color = new Color(r, g, b);   // 随机生成一个较浅的颜色

        painter = new DefaultHighlightPainter(color);

        try {
            // offset:子串在text中的偏移量,index:最长公共子串中子串中的偏移量
            hl1.addHighlight(offset1 + result.indexA, offset1 + result.indexA + result.substring.length(), painter);    // 高亮显示匹配到的词语
            hl2.addHighlight(offset2 + result.indexB, offset2 + result.indexB + result.substring.length(), painter);
        } catch (BadLocationException e) {
            e.printStackTrace();
        }
    }

    /* 求最小公共子串(动态规划);
     * 时间复杂度: O(len(a) * len(b))。
     * 空间复杂度: O(len(a) * len(b))。
     * */
    public Result LongestCommonSubstring(String a, String b) {
        int m = a.length();
        int n = b.length();
        int[][] dp = new int[m + 1][n + 1];
        int maxLength = 0;
        int endIndexA = 0;
        int endIndexB = 0;

        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (a.charAt(i - 1) == b.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    if (dp[i][j] > maxLength) {
                        maxLength = dp[i][j];
                        endIndexA = i - 1;
                        endIndexB = j - 1;
                    }
                }
            }
        }

        String longestSubstring = a.substring(endIndexA - maxLength + 1, endIndexA + 1);
        double score = (double)longestSubstring.length() / Math.max(a.length(), b.length());
        return new Result(longestSubstring, endIndexA - maxLength + 1, endIndexB - maxLength + 1, score);
    }

    public static void main(String[] args) {
        TextComparison mainFrame = new TextComparison();
    }
}

 

 

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值