比较两个DOC文档的相似性
文章目录
问题重述
编写一个小程序,比较两个DOC文档的相似性。
具体要求如下:
- 两个文档均为.DOC文件格式;
- 统计这两个文档中有多少字符相同,有多少个字符不同,统计出前10个高频字或词;
理论分析与讨论
1.相似度度量标准
两个文档的相似度比较,可以考虑使用几种相似性度量的方法,比如余弦距离、欧氏距离和杰卡德(Jaccard)方法等,从宏观的角度对于两个文档的相似性进行评估。
几个较为适用方法列举如下:
余弦距离
也称为余弦相似度,是用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小的度量。当两个文档的向量夹角余弦等于1时,这两个文档完全重复;当夹角的余弦值接近于1时,两个文档相似(也可以用作文本分类);夹角的余弦越小,两个文档越不相关。
欧氏距离
衡量的是空间各点的绝对距离,跟各个点所在的位置坐标直接相关。相比较而言,余弦距离衡量的是空间向量的夹角,更加体现在方向上的差异。如下图,如果保持A点位置不变,B点朝原方向远离坐标轴原点,那么这个时候余弦距离是保持不变的(因为夹角没有发生变化),而A、B两点的距离显然在发生改变,这就是欧氏距离和余弦距离之间的不同之处。
杰卡德相似性度量
包括两个衡量量,杰卡德相似系数和杰卡德距离。
两个集合A和B交集元素的个数在A、B并集中所占的比例,称为这两个集合的杰卡德相似系数,用符号 J(A,B) 表示。
杰卡德距离用两个两个集合中不同元素占所有元素的比例来衡量两个集合的区分度。
2.文档内容的导入和引用
要求中说明是.doc文档,不同于.txt文本文件,txt格式是流式的,没有格式可言(最多就是分段,换行等),而Word、Excel、PPT等是记录式的,文件格式的解析相对来说会复杂一些。因此需要考虑.doc的格式问题,即Microsoft Office 97-2003版本文档的格式化读取。
3.文档内容的处理
对于一个.doc文档,其内容可以是文字、图像和图表等形式,因为考虑到要求中是要统计相同字符和不同字符的个数,于是把文档的内容固定为仅包含文字文本,包括中文和英文。如果是中文汉字,为了方便进行第二步统计字符数和高频词的操作,需要对中文的句子进行分词处理,分词后以空格分割,作为一个字符串类型来存储。
考虑到一个文档中可能一个词出现过多次,可以先把一个文档内出现的所有字词计数收集,建立一个字典,包括键和键值。再和另外一个文档比较,就可以通过直接比较两个文档的字典,以及字典中键值对应的频率来确定有多少字符相同、多少字符不同,以及高频词的统计都可以在一个相对低的时间复杂度内很方便的实现。
关于高频词的统计,这里避免了统计汉字中类似于的、地、得、和、啊、与、或、还这种本身没有充分实际意义的连词、语气词和转折词等,从而更加精确地比较两个文档中内容的相似度。
编程思路和代码
1.doc文档的导入
由于doc文件和txt文件不同,具有复杂的格式需求,于是借助于POI——一个开源的API来读写。
Apache POI是Apache软件基金会的开放源码函式库,POI提供API给Java程序对Microsoft Office格式档案读和写的功能。其中,HWPF能提供读写Microsoft Word格式档案的功能(即操纵读写.doc文件)。
.doc文件的读取,需要导入poi-scratchpad的jar包和相关依赖包。
利用导入外部jar文件的语句:
import org.apache.poi.hwpf.HWPFDocument;
再配合java.io.FileInputStream内置的文件输入流即可获得对于.doc文件的操纵,然后使用字符串变量StringBuilder(速度上会快于StringBuffer和String类型)类型存储从.doc文件中导出的文本文件。
代码部分展示:
File file1 = new File("C:\\Users\\123\\Desktop\\testfile1.doc");
HWPFDocument doc1 = null;
String text1;
try {
doc1 = new HWPFDocument(new FileInputStream(file1));
} catch (IOException e) {
e.printStackTrace();
}
//通过 Doc对象直接获取Text
text1 = doc1.getDocumentText();
StringBuilder sb1 = doc1.getText();
System.out.println("文档1:");
System.out.println(sb1.toString());
File file2 = new File("C:\\Users\\123\\Desktop\\testfile2.doc");
HWPFDocument doc2 = null;
String text2;
try {
doc2 = new HWPFDocument(new FileInputStream(file2));
} catch (IOException e) {
e.printStackTrace();
}
text2 = doc2.getDocumentText();
StringBuilder sb2 = doc2.getText();
System.out.println("文档2:");
System.out.println(sb2.toString());
2.对于中文文字组成的文本进行分词
这一步是后面统计字符相同、字符不同以及出现频率的基础。
一般分词可以考虑多模式匹配算法AC自动机(Aho-Corasick automaton)和字典树(trie树),能够加快字符串比较的效率;至于准确度,由于在获得匹配后可能存在多种不同的组合,例如“大学生活”,分词结果是“大学生”“活”或是“大学”“生活”,需要考虑到所有分词组合中最合适的一个,有一些分词器选择最少词策略,即正向匹配,但是准确性可能不高。于是,有的算法考虑存储词与词之间可能的关系但是这样词典的空间消耗太大,会是。所以尽量要对其进行简化,计算概率的时候只考虑其前后两到三个字词,这是基于马尔可夫链做的简化,将每个词的出现视为独立事件,计算每个词出现的概率即可。一个词的出现概率 = 词出现的次数 / 文本库中词总量。
但是考虑到分词的准确度和文本样本容量的大小,这里仍然使用开源的IKSegmenter分词来对我们的文本进行分词。
分词方面,Lucene可以说是最经典和专业的软件,并且完全开源。Lucene现在已经更新到了8.0版本,并且仍然保持着极高的更新速度。Lucene使用java语言编写,可以在官网直接下载jar包和相关文档。Lucene除了分词功能外,还具有强大的归类索引,自定义字典等功能,是进行自然文本处理的强大工具。美中不足的是,其对中文的支持性很差,而且其极高的更新速度使得很多中文分词软件在与其配合上出现了问题,比如IKAnalyzer。
由于Lucene的分词效果经过检验之后确实比较差,而IKAnalyzer在3.0版本就提供了不再依赖于Lucene的分词方式。于是考虑使用IK的两个类来完成独立于Lucene的分词操作,它们分别是IKSegmenter和Lexeme。
IKSegmenter是IK分词器的核心类,起着类似于之前提到的IKAnalyzer类的作用,常用的构造函数为
public IKSegmenter(Reader input , boolean useSmart)
。其中第一个参数是字符输入读取,第二个参数代表是否采用智能切分策略。true使用智能切分,false使用最细粒度切分(这里我采用了智能切分)。
使用IKSegmenter进行分词十分简单,只需要调用它的next()成员函数即可返回Lexeme类型的下一个词元,如果返回null,则表示已完成了对输入对象的分词操作。
Lexeme是IK分词器的语义单元对象,也就是我们分词得到的词元结果。它的常用成员函数如下:
public int getBeginPosition()
说明:获取语义单元的起始字符在文本中的位置
public int getEndPosition()
说明:获取语义单元的结束字符的下一个位置
public int getLength()
说明:获取语义单元包含字符串的长度
public String getLexemeText()
说明:获取语义单元包含字符串内容
获得了切分后的两个文本,这里我先对整个文档的所有词按词分类并按词频排序,返回一个用**List<Map.Entry<String, Integer>>**存储的整个文档的键值对,由于已经排序,第三步的统计工作就很容易了。
部分代码展示:
StringReader sr = new StringReader(text1);
IKSegmenter ik = new IKSegmenter(sr, true);
Lexeme lex = null;
while((lex = ik.next()) != null){
System.out.print(lex.getLexemeText() + " ");
}
System.out.println();
3.统计相同和不同的字符数和前十个高频词
在前一个分词和统计词频的基础上,这里以第一个文档为基准,按照Key值依次比较两个文档的所有词,如果相同,那么计数器自加上第一个文档中此键对应的键值,在遍历的过程中同时统计总词数,最后计数器的值就是第一个文档中出现的和第二个文档中重复的词数;用总词数减去相同的,就是第一个文档中出现但是第二个文档中没有出现的词数,即不相同的字符数。
至于前十个高频词的统计,由于我是对键按照词频排序了,于是最先匹配的一定是出现频率最高的词,依次输出前十个即可。
- 注:这里所有的词数、相同词和高频词等均不考虑在没有语境时本身没有实际意义或内涵的连词、语气词和转折词等,例如:的、地、得、和、啊、与、或、还等等。
int topWordsCount = 3;
Map<String,Integer> wordsFrenMaps1 = getTextDef(text1);
List<Map.Entry<String, Integer>> wordFrenList1 = sortSegmentResult(wordsFrenMaps1, topWordsCount);
tagForFileCharater = false;
Map<String,Integer> wordsFrenMaps2 = getTextDef(text2);
List<Map.Entry<String, Integer>> wordFrenList2 = sortSegmentResult(wordsFrenMaps2, topWordsCount);
statisticsoffile(wordFrenList1, wordFrenList2); //统计相同和不同字符
statisticsfrenchara(wordFrenList1, wordFrenList2); //统计前十个高频词
getTextDef函数:
public static Map<String, Integer> getTextDef(String text) throws IOException {
Map<String, Integer> wordsFren = new HashMap<String, Integer>();
IKSegmenter ikSegmenter = new IKSegmenter(new StringReader(text), true);
Lexeme lexeme;
while ((lexeme = ikSegmenter.next()) != null) {
if(lexeme.getLexemeText().length() > 1){
if(wordsFren.containsKey(lexeme.getLexemeText())){
wordsFren.put(lexeme.getLexemeText(), wordsFren.get(lexeme.getLexemeText()) + 1);
}else {
wordsFren.put(lexeme.getLexemeText(), 1);
}
}
}
return wordsFren;
}
sortSegmentResult函数:
public static List<Entry<String, Integer>> sortSegmentResult(Map<String,Integer> wordsFrenMaps, int topWordsCount){
List<Map.Entry<String, Integer>> wordFrenList = new ArrayList<>(wordsFrenMaps.entrySet());
Collections.sort(wordFrenList, new Comparator<Map.Entry<String, Integer>>() {
public int compare(Map.Entry<String, Integer> obj1, Map.Entry<String, Integer> obj2) {
return obj2.getValue() - obj1.getValue();
}
});
System.out.println("按词频排序:>>>>>>>>>>>>>>>>>>>>>>>");
for(int i = 0; i < wordFrenList.size(); i++){
Map.Entry<String,Integer> wordFrenEntry = wordFrenList.get(i);
System.out.println(wordFrenEntry.getKey() + " 次数:" + wordFrenEntry.getValue());
if(tagForFileCharater) {
characterNumTotal += wordFrenEntry.getValue();
}
}
if(tagForFileCharater) {
System.out.println("第一个文档总字符数:" + characterNumTotal);
}
return wordFrenList;
}
4.程序运行结果展示
使用了两个内容上具有一定相似度的.doc文档来测试。
分词效果展示:
按词频排序结果:
第一个文档和第二个文档。
相同和不同的字符数和前十个高频词:
很容易统计,在这个分词组合下,这个结果是正确无误的。
其他扩展
- 分词
可以考虑jieba分词,它在Github上开源,有不少爱好者对其进行维护。
这个分词策略虽然起源于Python,但是已经有很多前辈开发出了面向其他编程语言的版本,分词效果应该会更好,后面可以加以尝试。
jieba是一个以中文分词为主要应用目的的Python代码包,支持三种分词模式:
- 精确模式
试图将句子最精确地切开,适合文本分析; - 全模式
把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义; - 搜索引擎模式
在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。
- 对于多个文档,可以使用词袋模型配合TF-IDF算法来进行文档的相似度分析。
TF是词频(Term Frequency),IDF是逆文本频率指数(Inverse Document Frequency)。其主要思想是:字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。
这种算法具有“滤噪音”的能力,对区别文档最有意义的词语应该是那些在文档中出现频率高,而在整个文档集合的其他文档中出现频率少的词语,所以如果特征空间坐标系取TF词频作为测度,就可以体现同类文本的特点。
另外,程序可能有所不足,如果大佬们有什么指教的,可以联系我,这里放个小号:3626698122.
and这是第一次写博,嘿嘿嘿。