在当今的计算机高速发展的时代,对于文章的查重等涉及到数据比对的需求越来越高了。
为了识别字面上相似的文档,日常生活中我们所做的就是比对两个文档中相似的语句的比重,如果大部分内容都是相同的话,那么我们就会判定这两篇文档很大程度上是有抄袭嫌疑的。其实这个过程完全是可以类比到计算中来的,自己看了资料(大数据 互联网大规模数据挖掘与分布式处理,此博客中大部分的理论都是引用于此书)刚好写了一个简单的文档相似度分析的程序,刚好分享下。
在编程中,我们可以利用集合的思想对文档的相似度进行分析,进而将文档表示成一个集合。将文档表示成集合的最有效的方法是构建文档中的短字符串集合,如果文档采用这样的集合表示,那么有相同句子甚至短语的文档之间将会拥有很多公共的集合元素,即使这两篇文档中的语序并不相同也是如此的,这是为什么呢?这也是正是为什么会采用短字符串作为集合元素的原因,因为在一篇中,都是由基本的词组成的,那么试想一下两个极端的情况:(1)一种是将短字符串放大到整篇文章,也就是说一篇问的文档中的所有词放在一起作为一个元素,可以想象,这是毫无用处的;(2)另一种是将文档中的每一个单独的词作为一个元素,这样乍一看感觉确实不错,但我们仔细想一下啊,无论是那种语言,它的词的个数是有限的,在文档中经常是重复使用了相同的词,也就是说作为经常使用的词,以英文为例:a、the、is、an。。。这些词语几乎在文档中都会出现,那么我们如何根据这些词去判断文档的相似度呢?
集合的Jaccard相似度
这里我们可以关注一个特定的“相似度”概念,即通过计算交集的相对大小来获得集合之间的相似度。这种相似度称为Jaccard相似度。
它的定义为:集合S和T的Jaccard相似度为|S∩T|/|S∪T|,也就是集合S和集合T的交集和并集大小之间的比值。下面将S和T的Jaccard相似度记为SIM(S,T)。
文档的Shingling
下面说一下很常用的一种方法:k-shingle
对于一篇文章而言,就是一个大的字符串(此处主要讨论英文的查重)。文档的k-shingle定义为其中任意长度为k的字符串,因此,每篇文档可以表示成文档中出现一次或者多次的k-shingle的集合。
比如说一个文档中有字符串"abcdabd",当选择k=2时,则此文档中的所有2-shingle组成的集合为 {ab,bc,cd,da,bd}。我们也许会注意到,子串ab在文档中出现了两次,但是在集合中我们只算了1次。其实shingle的有一种变形是将文档表示成包,在包内每个shingle的出现次数也被考虑在内,当然,这里我们主要讨论基于集合的方法。
对于空白串(空格、tab即回车等)的处理存在多种方法,常常将任意长度的空白字符串替换为单个空格。
当然,说到文档,我们首先想到的应该是如何读取一个文档,代码如下:
"""
此函数用于获得fileName文件中的内容,文件内容存放在字符串中返回
"""
def getFileContent(fileName):
file=open(fileName,"r")
fileContent=file.read()
fileContent=fileContent.replace("\t"," ")
fileContent=fileContent.replace("\n"," ")
fileContent=fileContent.replace("\r"," ")
file.close()
return fileContent
既然是比较文档之间的相似度,那么自然不可能只是读取一篇文档了,下面的这个函数可以通过传入一个文件夹名称的字符串参数,返回该文档下所有文件的名称的字符串 列表:
"""
此函数用于获取dir文件夹中的文件的内容
"""
def getFilesName(dir):
fileList=[]
t=os.walk(dir)
file=dir+'\\'
for item in t:
for name in item[2]:
fileList.append(file+name)
return fileList
下面本来应该是对文档的内容进行shingle的,但是先不急,因为这里将会把shingle和哈希进行一块操作。
shingle的大小的选择
理论上,我们可以选择任意的常数作为k。但是,正如前面所说的,如果选择的k太小,比如手k=1,那么可以推测大部分长度为k的字符串会出现在大部分的文档中。如果这样做,那么我们就会有很多 Jaccard相似度很高的文档,即使他们之间没有任何相同的句子甚至短语。极端的就是在k为1的情况下,大部分文档中都有很常见的字符,而其它字符相对较少,因此 ,此时几乎所有的web网页之间都有较高的Jaccard相似度。
k值的选择依赖于文档的典型长度以及典型的字符表大小。我们需要记住的是:k应该选择得足够大,以保证任意给定的shingle出现在任意文档中的概率最低。
因此,如果文档集由邮件组成,那么选择k=5应该比较合适。为理解这其中的原因,假定邮件中只有字符和普通的空白符(尽管实际当中,大部分可打印ASCII字符都有可能偶尔出现在邮件中)。于是,所有可能的5-shingle个数为27ʌ5=14348907,由于典型的邮件长度会远远低于1400万字符,所以我们希望k=5将会处理得很好,实际上的确如此。
对shingle进行哈希
可以不讲字符串直接用成shingle,而是同过某个哈希函数将长度为k的字符串映射为桶编号,然后将得出的的桶编号看成最终的shingle。于是,可以将文档表示成这些桶编号整数构成的集合,这些桶编号代表一个或 多个文档中出现的k-shingle。举例来说,对于文档可以构件9-shingle集合,然后将每个9-shingle映射到0到2ʌ32-1之间的一个桶编号。因此,每个shingle由4个字节而不是9个字节来表示,这是因为计算机中整数一般占4个字节,而1个字节是八位,那么整数在计算机中就是占了32位,它的范围可以表示到2ʌ32-1。这样做不仅数据上得到了压缩,而且可以对哈希后得到的整数shingle进行单字机器运算。
在这里我就偷个懒了,其实也是由于比较纠结哈希函数的选择,所以干脆跳过了这一步,改而使用python提供的集合set代替哈希(因为集合其实就是通过哈希创造出来的,当然这样就会由于没有针对性而造成效率上的损失,以后会做出改进)。
下面列出一个简单的函数处理:
"""
此函数用于对各个文件中的内容进行k-shingle,然后对词条进行哈希(此处就用字典存储了)
其中dir是文件夹的名称字符串类型,k是int型
"""
def getShingleList(dir,k):
fileList=getFilesName(dir)
shingleList=list()
for fileName in fileList:
fileContent=getFileContent(fileName)
shingle = set()
for index in range(0,len(fileContent)-k+1):
shingle.add(fileContent[index:index+k])
shingleList.append(shingle)
return shingleList
此篇博客暂时就说这么多,之后会继续跟进。