第一部分 绪论
灵感
模拟论文查重的各种软件,使用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。
中英文混例
纯中文样例:(文本2节选自《荷塘月色》)
附录(源代码)
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();
}
}