文章内容相似度计算几种方式及优缺点

PHP 内置方法 similar_text

similar_text 是PHP内置的字符串相似度对比函数,是使用方式最便捷的一种,但是因为它的时间复杂度是 O(N**3),处理时间会随着内容长度增加,若比较5000字以上的文章,或者比较文章的量级比较大不建议使用,只是单篇文章对单篇文章可以使用。

通过分词进行余弦相似度对比

解决方案是首先进行文章分词可以用结巴或者迅搜分词服务进行文章分词,然后将需要对比的文章分词结果存入redis,在有新文章进行对比的时候从redis将所有文章的分词结果从内存中取出来然后进行相似度对比,逐词进行相似度计算。相似度计算的准确性很高,但是对比的文章量非常大的时候,处理时间还是会很长,5000文章的相似度计算需要近30S

主要计算代码:

Class TextSimilarity
{
    /**
     * [排除的词语]
     *
     * @var array
     */
    private $_excludeArr = array('的', '了', '和', '呢', '啊', '哦', '恩', '嗯', '吧');

    /**
     * [词语分布数组]
     *
     * @var array
     */
    private $_words = array();

    /**
     * [分词后的数组一]
     *
     * @var array
     */
    private $_segList1 = array();

    /**
     * [分词后的数组二]
     *
     * @var array
     */
    private $_segList2 = array();

    private static $test1 = array();
    private static $test2 = array();

    /**
     * [分词两段文字]
     *
     * @param [type] $text1 [description]
     * @param [type] $text2 [description]
     */
    public function __construct($text1, $text2)
    {
        $this->_segList1 = is_array( $text1 ) ? $text1 : $this->segment( $text1 );
        $this->_segList2 = is_array( $text2 ) ? $text2 : $this->segment( $text2 );
    }

    /**
     * [外部调用]
     *
     * @return [type] [description]
     */
    public function run()
    {
        $this->analyse();
        $rate = $this->handle();
        return $rate ? $rate : 'errors';
    }

    /**
     * [分析两段文字]
     */
    private function analyse()
    {
        //t1
        foreach ($this->_segList1 as $v) {
            if (!in_array($v, $this->_excludeArr)) {
                if (!array_key_exists($v, $this->_words)) {
                    $this->_words[$v] = array(1, 0);
                } else {
                    $this->_words[$v][0] += 1;
                }
            }
        }

        //t2
        foreach ($this->_segList2 as $v) {
            if (!in_array($v, $this->_excludeArr)) {
                if (!array_key_exists($v, $this->_words)) {
                    $this->_words[$v] = array(0, 1);
                } else {
                    $this->_words[$v][1] += 1;
                }
            }
        }
    }

    /**
     * [处理相似度]
     *
     * @return [type] [description]
     */
    private function handle()
    {
        $sum = $sumT1 = $sumT2 = 0;
        foreach ($this->_words as $word) {
            $sum += $word[0] * $word[1];
            $sumT1 += pow($word[0], 2);
            $sumT2 += pow($word[1], 2);
        }

        $rate = $sum / (sqrt($sumT1 * $sumT2));
        return $rate;
    }

    /**
     * [分词  【http://www.xunsearch.com/scws/docs.php#pscws23】]
     *
     * @param [type] $text [description]
     *
     * @return [type] [description]
     *
     * @description 分词只是一个简单的例子,你可以使用任意的分词服务
     */
    private function segment( $text )
    {
        $outText = array();
        $xs = new XS('demo');  // 必须先创建一个 xs 实例,否则会抛出异常
        $tokenizer = new XSTokenizerScws;   // 直接创建实例
        $tokenizer->setIgnore();
        //处理
        $outText = $tokenizer->setMulti(1)->getResult($text);
        $outText = array_column( $outText, 'word');
        $res = $xs->getScwsServer();
        $res->close();
        return $outText;
    }
}
    

SimHash

SimHash的原理是将很长的一段文字降维成一个0和1组成的字符串,然后计算两个01字符串的相似度,从而算出两篇文章的相似程度。也是将文章先分词,计算存量文章的相似度存入redis或者mysql,需要的时候取出来对比,对比速度20000篇文章的计算时间基本上在2s以内,但是当文章字数非常小并且重复词非常多的时候会出现文章不相同但是相似度非常高的问题。
主要计算代码:

    class SimHash
{
    protected static $length = 256;
    protected static $search = array('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f');
    protected static $replace = array('0000','0001','0010','0011','0100','0101','0110','0111','1000','1001','1010','1011','1100','1101','1110','1111');
    /**
     * [排除的词语]
     *
     * @var array
     */
    private static $_excludeArr = array('的', '了', '和', '呢', '啊', '哦', '恩', '嗯', '吧','你','我',' ');

    public static function get(array &$set)
    {
        $boxes = array_fill(0, self::$length, 0);
        if (is_int(key($set)))
            $dict = array_count_values($set);
        else
            $dict = &$set;

        foreach ($dict as $element => $weight) {
            if ( in_array($element, self::$_excludeArr )){
                continue;
            }

            $hash = hash('sha256', $element);
            $hash = str_replace(self::$search, self::$replace, $hash);
            $hash = substr($hash, 0, self::$length);
            $hash = str_pad($hash, self::$length, '0', STR_PAD_LEFT);

            for ( $i=0; $i < self::$length; $i++ ) {
                $boxes[$i] += ($hash[$i] == '1') ? $weight : -$weight;
            }
        }
        $s = '';
        foreach ($boxes as $box) {
            if ($box > 0)
                $s .= '1';
            else
                $s .= '0';
        }
        return $s;
    }

    public static function hd($h1, $h2)
    {
        $dist = 0;
        for ($i=0;$i<self::$length;$i++) {
            if ( $h1[$i] != $h2[$i] )
                $dist++;
        }
        return (self::$length - $dist) / self::$length;
    }

SeaseeYuol
38 声望6 粉丝

大块镇第五小学三年二班吴彦祖是也。