題目重復度檢測---四種相似度檢測的方案+PHP改進計算字符串相似度的函數similar_text()、levenshtein()


需求

題庫系統中對題目進行重復度檢測,把所有重復的題目展示出來。

如何定義重復?

我剛開始是按100%重復,才算重復。
現在公司要求,70%的重復,也算重復。

分析

背景知識:題目=題干+選項
1.100%重復的情況下,只需要,獲取題干數組,php獲取重復的項,再獲取對應的文章id就好了。
2.php如何獲取數組中,70%重復的題目id呢?

好像可以借用php中重復檢測的函數,網上一種有四種

I similar_text()

php內置函數,具體使用方法,請百度,對中文字符串支持不好

II levenshtein()

php內置函數,具體方法,百度
這個好像更快,但是對字符串長度有限制,超過255,就無法檢測。報錯信息如下:

Warning: levenshtein(): Argument string(s) too long in D:\phpstudy_pro\WWW\index.php on line 17
1.0018518518519

III 自定義的一個php檢測類,不考慮性能,無字符限制,但是中文檢測不准

<?php 

class LCS {
    var $str1;
    var $str2;
    var $c = array();
    /*返回串一和串二的最長公共子序列
*/
    function getLCS($str1, $str2, $len1 = 0, $len2 = 0) {
        $this->str1 = $str1;
        $this->str2 = $str2;
        if ($len1 == 0) $len1 = strlen($str1);
        if ($len2 == 0) $len2 = strlen($str2);
        $this->initC($len1, $len2);
        return $this->printLCS($this->c, $len1 - 1, $len2 - 1);
    }
    /*返回兩個串的相似度
*/
    function getSimilar($str1, $str2) {
        $len1 = strlen($str1);
        $len2 = strlen($str2);
        $len = strlen($this->getLCS($str1, $str2, $len1, $len2));
        return $len * 2 / ($len1 + $len2);
    }
    function initC($len1, $len2) {
        for ($i = 0; $i < $len1; $i++) $this->c[$i][0] = 0;
        for ($j = 0; $j < $len2; $j++) $this->c[0][$j] = 0;
        for ($i = 1; $i < $len1; $i++) {
            for ($j = 1; $j < $len2; $j++) {
                if ($this->str1[$i] == $this->str2[$j]) {
                    $this->c[$i][$j] = $this->c[$i - 1][$j - 1] + 1;
                } else if ($this->c[$i - 1][$j] >= $this->c[$i][$j - 1]) {
                    $this->c[$i][$j] = $this->c[$i - 1][$j];
                } else {
                    $this->c[$i][$j] = $this->c[$i][$j - 1];
                }
            }
        }
    }
    function printLCS($c, $i, $j) {
        if ($i == 0 || $j == 0) {
            if ($this->str1[$i] == $this->str2[$j]) return $this->str2[$j];
            else return "";
        }
        if ($this->str1[$i] == $this->str2[$j]) {
            return $this->printLCS($this->c, $i - 1, $j - 1).$this->str2[$j];
        } else if ($this->c[$i - 1][$j] >= $this->c[$i][$j - 1]) {
            return $this->printLCS($this->c, $i - 1, $j);
        } else {
            return $this->printLCS($this->c, $i, $j - 1);
        }
    }
} 
 
$lcs = new LCS();



 $str1='我是雷鋒123';

 $str2='我是雷鋒abc';


//返回最長公共子序列
$lcs->getLCS($str1,$str2);
//返回相似度
echo $lcs->getSimilar($str1,$str2);


IV 改進

PHP改進計算字符串相似度的函數similar_text()、levenshtein()

最終選用四的方法中,對leveshtein的改進

優勢:1.計算准確 2.打破255的長度限制

<?php
//拆分字符串
function mbStringToArray($string, $encoding = 'UTF-8') {
$arrayResult = array();
while ($iLen = mb_strlen($string, $encoding)) {
array_push($arrayResult, mb_substr($string, 0, 1, $encoding));
$string = mb_substr($string, 1, $iLen, $encoding);
}
return $arrayResult;
}
//編輯距離
function levenshtein_cn($str1, $str2, $costReplace = 1, $encoding = 'UTF-8') {
$count_same_letter = 0;
$d = array();
$mb_len1 = mb_strlen($str1, $encoding);
$mb_len2 = mb_strlen($str2, $encoding);
$mb_str1 = mbStringToArray($str1, $encoding);
$mb_str2 = mbStringToArray($str2, $encoding);
for ($i1 = 0; $i1 <= $mb_len1; $i1++) {
$d[$i1] = array();
$d[$i1][0] = $i1;
}
for ($i2 = 0; $i2 <= $mb_len2; $i2++) {
$d[0][$i2] = $i2;
}
for ($i1 = 1; $i1 <= $mb_len1; $i1++) {
for ($i2 = 1; $i2 <= $mb_len2; $i2++) {
// $cost = ($str1[$i1 - 1] == $str2[$i2 - 1]) ? 0 : 1;
if ($mb_str1[$i1 - 1] === $mb_str2[$i2 - 1]) {
$cost = 0;
$count_same_letter++;
} else {
$cost = $costReplace; //替換
}
$d[$i1][$i2] = min($d[$i1 - 1][$i2] + 1, //插入
$d[$i1][$i2 - 1] + 1, //刪除
$d[$i1 - 1][$i2 - 1] + $cost);
}
}
return $d[$mb_len1][$mb_len2];
//return array('distance' => $d[$mb_len1][$mb_len2], 'count_same_letter' => $count_same_letter);
}


$str1='我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是雷鋒123111{dsadas}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}';

$str2='我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}我是喬峰abc{[dsds]}';

//計算編輯長度並輸出
echo levenshtein_cn($str1, $str2);
echo '<br/>';

//獲取兩者中最長的字符串長度
$len=mb_strlen($str1)>=mb_strlen($str2)?mb_strlen($str1):mb_strlen($str2);

//計算相似度,並輸出
echo 1-levenshtein_cn($str1, $str2)/$len;

如何獲取題干中,70%重復的元素呢

$arr=['是的1111','我好的很哦','你好的很呀','是的1111','我好的很哦','你好的很呀','是的1111','我好的很哦','你好的很呀','是的1111','我好的很哦','你好的很呀','是的1111','我好的很哦'];
echo count($arr);
echo '<br/>';
foreach($arr as $k=>$v ){

	
	for($i=0;$i<count($arr);$i++){
		$str1=$v;
		$str2=$arr[$i];
		
		//獲取兩者中最長的字符串長度
		$len=mb_strlen($str1)>=mb_strlen($str2)?mb_strlen($str1):mb_strlen($str2);

		//計算相似度,並輸出
		echo $k.'和'.$i.'的重復度是'.(1-levenshtein_cn($str1, $str2)/$len);
		echo '<br/>';
	}
}

s數據量巨大的時候,有什么高效的算法嗎

有人跟我遇到了同樣的問題-----數據兩兩比較的高效算法?
好像這里也沒人解決掉了,但是需求是一樣的。

百度到了js的算法,改了改,就得到了php數組的兩兩比較算法

https://www.cnblogs.com/cn-oldboy/p/13580690.html

繼續有新的問題,題目眾多,比如10W條數據

即便是倒三角算法,還是有着很龐大的計算量。

最終得到檢測重復度的方案

1.使用改進版的levenshtein_cn()函數
2.分塊檢測,不同欄目重復度為0
3.分長度檢測,元素的長度如果相差20個字符。直接判斷重復度為0
4.分題型檢測,不用題型,重復度為0
5.使用倒三角,減少對比次數
6.為了獲取豐富的文章信息,以便限制對比次數,推薦使用
獲取單列或多列字段值:column( )
參考地址:https://www.php.cn/php/php-column-method.html

關於分塊檢測又思考如下:thinkphp5能不能在循環中,使用db類?

兩種方法
1.我們是一次獲取所有題干,並帶上欄目信息
2.循環所有欄目,在欄目內部,使用db類,獲取題干數組,進行對比
聽人說,不建議在循環中使用db,多次操作數據庫,會增加服務器開銷。
那就使用第一種方法吧

繼續操作,如何從數據庫一次性獲取所有文章,然后根據不同的欄目,生成新的數組

答案:過濾+閉包=====https://www.cnblogs.com/cn-oldboy/p/13583868.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM