用 PHP Levenshtein 比较 5000 个字符串

Posted

技术标签:

【中文标题】用 PHP Levenshtein 比较 5000 个字符串【英文标题】:Compare 5000 strings with PHP Levenshtein 【发布时间】:2009-12-24 11:30:57 【问题描述】:

我在一个数组中有 5000 个,有时甚至更多的街道地址字符串。我想将它们与 levenshtein 进行比较以找到相似的匹配项。如果不遍历所有 5000 并直接将它们与其他所有 4999 进行比较,我该如何做到这一点?

编辑:如果有人有建议,我也对替代方法感兴趣。总体目标是根据用户提交的街道地址找到相似的条目(并消除重复项)。

【问题讨论】:

关于您的更新,您可能需要应用一些输入清理以使您的生活更轻松。 (例如:如果您在存储之前将“Ave”转换为“Avenue”,“Rd”转换为“Road”等,则使用 soundex 将成为更现实的选择。) 如何定义相似地址?您是否有一些 Lehvenstein 距离的最大值是相似性的限制等? 类似的将是“12 Bird Road, Apt 6”和“12 Bird Rd. #6” “923 Bird Road, Apt 6”和“23 Bird Road, Apt 6”怎么样?它们相似吗? Lehvenstein 距离会说它们是,毕竟只有一个字符差异,但在地理上人们可以认为它们不是很相似。 @Juha:这是一个很好的观点。另外,我的地址集中在方圆一百英里内。所以使用原始的 Levenshtein 可能毫无价值。 【参考方案1】:

我认为对相似地址进行分组的更好方法是:

    创建一个包含两张表的数据库 - 一张用于地址(和一个 id),一张用于地址中单词或文字数字的音译(使用地址表的外键)

    将地址大写,将 [A-Z] 或 [0-9] 以外的任何内容替换为空格

    按空格分割地址,计算每个“单词”的 soundex,保留任何只有数字的内容,并将其与您开始使用的地址的外键一起存储在 soundexes 表中

    为每个地址(id $target)找到最相似的地址:

    SELECT similar.id, similar.address, count(*) 
    FROM adress similar, word cmp, word src
    WHERE src.address_id=$target
    AND src.soundex=cmp.soundex
    AND cmp.address_id=similar.id
    ORDER BY count(*)
    LIMIT $some_value;
    

    计算您的源地址与查询返回的前几个值之间的 levenstein 差异。

(对大型数组进行任何类型的操作通常在数据库中更快)

【讨论】:

我会在上述算法中使用标准化地址。也就是说,已删除缩写的地址等(“Ave.”更改为“Avenue”等)【参考方案2】:

我认为您无法避免遍历数组,因为 levenstein() 函数只接受字符串而不是数组作为输入。

你可以这样做:

for($i=0;$i<count($array)-1;$i++)

    for($j=$i+1;$j<count($array);$j++)
    
        $lev = levenshtein($array[$i],$array[$j]);
        if($lev == 0)
        
            // exact match
        
        else if($lev <= THRESHOLD)
        
            // similar
        
    

【讨论】:

我害怕这个,因为它会进行 2500 万次比较。 @phirschybar:实际上是1250万,我们只比较不同的对,如果比较pair(X,Y),我们跳过比较pair(Y,X)。【参考方案3】:

您可以使用bk-tree 来加快搜索/比较速度。

http://blog.notdot.net/2007/4/Damn-Cool-Algorithms-Part-1-BK-Trees 说:

现在我们可以对 Levenshtein 距离进行一个特别有用的观察:它形成了一个度量空间。[...] 假设我们有两个参数,查询,我们在搜索中使用的字符串,以及字符串可以与查询的最大距离并且仍然被返回。假设我们采用任意字符串,对其进行测试并将其与查询进行比较。调用合成距离 d。因为我们知道三角不等式成立,所以我们所有的结果与测试的距离必须最多为 d+n,并且距离测试的距离至少为 d-n。[...] 测试表明,搜索距离为 1 的查询不超过树的 5-8%,搜索两个错误的查询不超过树的 17-25% - 与检查每个节点相比有了很大的改进!

编辑:但这对您的(“12 Bird Road, Apt 6”和“12 Bird Rd.#6”)问题没有帮助。仅通过您的蛮力 m*n 比较。

【讨论】:

【参考方案4】:

由于 Levenshtein 算法的性质(特别是它是两个字符串之间的比较这一事实),我看不出这是怎么可能的。

您当然可以通过首先执行一些基本匹配要求来减少比较次数,但这超出了您所要求的范围。

作为一个(很可能无关紧要的)选项,您始终可以使用soundex 之类的东西,它可以让您预先计算字符串值。 (我相信你也可以直接在 mysql 中使用。)

【讨论】:

【参考方案5】:

您可以根据 soundexes 对它们进行分组,然后将比较限制为最接近的 N 个案例...

 $mashed=array();
 foreach ($address as $key=>$val) 
      $mashed[$key]=soundex($val);
 
 sort($mashed);

然后遍历 $mashed 的键。

C.

【讨论】:

我从未使用过 soundexes。知道它们与街道地址类型缩写(如“St”)的配合情况如何。 ? Soundex (en.wikipedia.org/wiki/Soundex) 设计用于处理人名。如果将它们应用于地址,则将 soundex 算法应用于地址的每个部分是有意义的,例如 "23 Bird Road" -> SOUNDEX("Bird") 和 SOUNDEX("Road") 此解决方案类似于O(2n)O(2nm)(均未简化),其中m 是地址中的每个单词。这是对O(n^2) 的巨大改进。 它不能插入缩写 - 但 St 可以是 Street 或 Saint 或...?取决于上下文。 C.【参考方案6】:

如果您想找到 所有 个相似的值,则必须将所有项目与所有其他项目进行比较。但是选择正确的数组函数会显着加快速度。这是一个简单的示例(结果数组可能会更好):

$results = array();
$count = count($entries);
while ($count != 0) 
    # The entry to process
    $entry = array_shift($entries);
    # Get levenshtein distances to all others
    $result = array_map(
        'levenshtein',
        # array_map() needs two arrays, this one is an array consisting of
        # multiple entries of the value that we are processing
        array_fill($entry, 0, $count),
        $toCompare
    );
    $results[] = array($entry => $result);
    $count--;

【讨论】:

【参考方案7】:

鉴于您的问题,如果您想使用Lehvenstein distance,我认为除了将每个地址与其他地址进行比较之外别无他法。

首先,你应该规范化地址,去掉缩写等等。

大道 -> 大道 路。 -> 道路

对于类似的地址,您可以有一些固定的最大 Lehvenstein 距离 (N)。

如果是这样,当您确定当前地址对的编辑距离大于 N 时,您可以中止 Lehvenstein 算法。为此,您需要编写 Lehvenstein 算法的自定义版本。这将使算法更快一点。

还有一些相关的琐碎优化。例如:如果地址 A 的长度为 10 个字符,地址 B 的长度为 20 个字符,并且您认为 Lehvenstein 距离小于 8 的地址是相似的。您可以查看地址的长度并立即确定它们不相似。

【讨论】:

【参考方案8】:
$stringA = "this is php programming language";
$stringB = "this is complete programming script in which java php and  all other minor languages include";

echo "string 1---->".$stringA."<br />";
echo "string 2---->".$stringB."<br />";
// changing string to arrays
$array1 = explode(' ', $stringA);
$array2 = explode(' ', $stringB);

// getting same element from two strings
$c = array_intersect($array1, $array2);
// changing array to the string
$d=implode(' ',$c);

echo "string same elements---> ".$d."<br />";


// getting difrent  element from two arrays
$result = array_diff($array2, $array1);
// changing array to the string
$zem = implode(' ',$result);

if (!empty($zem)) 
  echo "string diffrence---> ".$zem."<br />";
 else 
  echo "string diffrence--->both strings are same <br />";


similar_text($stringA, $d, $p);
echo " similarity between the string is ".$p."% <br />";

【讨论】:

以上是关于用 PHP Levenshtein 比较 5000 个字符串的主要内容,如果未能解决你的问题,请参考以下文章

如何调整 Levenshtein 距离算法以将匹配限制为单个单词?

python-levenshtein

odds ratio怎么用python计算

Spark Java API 计算 Levenshtein 距离

通俗解析莱文斯坦距离(Levenshtein Distance)计算原理(最小编辑距离)

除了 Levenshtein 之外,用于有序词集和后续聚类的更好距离度量