确定两个名称是否彼此接近

时间:2014-01-27 11:21:43

标签: php mysql regex compare levenshtein-distance

我正在为我的学校制作一个系统,我们可以在派对和其他活动中查看学生是否被列入黑名单。我很容易检查学生是否被黑名单,因为我可以在我的数据库中查看学生,看看他/她是否被列入黑名单。

这是困难的地方。

在我们的聚会上,每个学生都可以邀请一个人。理论上,黑名单的学生可以被另一名学生邀请并绕过该系统。 我无法查看黑名单上的学生的客人表,因为当您邀请客人时只提供姓名。

所以我需要检查一个列入黑名单的名字是否接近客人名称,如果它们已经关闭则会显示警告,不幸的是有些东西需要考虑。

名称可能完全不同。在丹麦,标准名称包含三个“名称”,如“Niels Faurskov Andersen” 但是学生可能会输入“Niels Faurskov”或“Niels Andersen”,甚至可以删除一些角色。

所以像Niels Faurskov Andersen这样的全名可能是

  • Niels Andersen
  • Niels Faurskov
  • Niels Faurskov Andersen
  • Nils Faurskov Andersen
  • Nils Andersen
  • niels faurskov
  • niels Faurskov

等等......

另一件事是丹麦字母除了通常的a-z之外还包含“æøå”。据说,整个站点和数据库都是UTF-8编码的。

我已经研究过各种方法来检查两个字符串之间的区别,而Levenshtein距离并没有这么做。

我在StackOverflow上找到了这个帖子:Getting the closest string match

这似乎提供了正确的数据, 但是我不太确定选择什么方法

我在php编写这部分代码,是否有人知道如何做到这一点?也许与MySQL?或Levenshtein距离的修改版本?正则表达式可以吗?

2 个答案:

答案 0 :(得分:13)

简介

现在您的匹配条件可能过于宽泛。但是,您可以使用levenshtein距离来检查您的单词。用它来实现所有期望的目标可能并不容易,例如声音相似性。因此,我建议将您的问题分成其他一些问题。

例如,你可以创建一些自定义检查器,它将使用传递的可调用输入,它接受两个字符串然后回答问题是否相同(对于levenshtein,其距离小于某个值,{{1 - 相似性的某些百分比 - 由你来定义规则。


相似,基于单词

好吧,如果我们在寻找部分匹配时讨论大小写,所有内置函数都将失败 - 特别是如果它是关于非有序匹配的话。因此,您需要创建更复杂的比较工具。你有:

  • 数据字符串(例如,将在DB中)。它看起来像D = D 0 D 1 D 2 ... D n
  • 搜索字符串(将是用户输入)。它看起来像S = S 0 S 1 ... S m

这里空格符号表示任何空间(我假设空格符号不会影响相似性)。还similar_text。根据此定义,您的问题是 - 在n > m中查找与m类似的D个字词集。 S我指的是任何无序序列。因此,如果我们在set中找到任何此类序列,则D类似于S

显然,如果D,则输入包含的字数多于数据字符串。在这种情况下,您可能会认为它们不相似或行为与上面相似,但是切换数据和输入(但是,这看起来有点奇怪,但在某种意义上适用)


实施

要做这些事情,您需要能够创建一组字符串,这些字符串来自n < m来自m个字词的部分。根据我的this question,您可以执行以下操作:

D

- 对于任何数组,protected function nextAssoc($assoc) { if(false !== ($pos = strrpos($assoc, '01'))) { $assoc[$pos] = '1'; $assoc[$pos+1] = '0'; return substr($assoc, 0, $pos+2). str_repeat('0', substr_count(substr($assoc, $pos+2), '0')). str_repeat('1', substr_count(substr($assoc, $pos+2), '1')); } return false; } protected function getAssoc(array $data, $count=2) { if(count($data)<$count) { return null; } $assoc = str_repeat('0', count($data)-$count).str_repeat('1', $count); $result = []; do { $result[]=array_intersect_key($data, array_filter(str_split($assoc))); } while($assoc=$this->nextAssoc($assoc)); return $result; } 将返回由每个getAssoc()项组成的无序选择数组。

下一步是关于生产选择的顺序。我们应该在m字符串中搜索Niels AndersenAndersen Niels。因此,您需要能够为数组创建排列。这是非常常见的问题,但我也会把我的版本放在这里:

D

在此之后,您将能够创建protected function getPermutations(array $input) { if(count($input)==1) { return [$input]; } $result = []; foreach($input as $key=>$element) { foreach($this->getPermutations(array_diff_key($input, [$key=>0])) as $subarray) { $result[] = array_merge([$element], $subarray); } } return $result; } 个单词的选择,然后排列每个单词,获取所有变体以与搜索字符串m进行比较。每次比较都将通过一些回调来完成,例如S。这是样本:

levenshtein

这将检查基于用户回调的相似性,该回调必须接受至少两个参数(即比较的字符串)。此外,您可能希望返回触发回调正回报的字符串。请注意,此代码的大小写不会有所不同 - 但您可能不希望出现此类行为(然后只需替换public function checkMatch($search, callable $checker=null, array $args=[], $return=false) { $data = preg_split('/\s+/', strtolower($this->data), -1, PREG_SPLIT_NO_EMPTY); $search = trim(preg_replace('/\s+/', ' ', strtolower($search))); foreach($this->getAssoc($data, substr_count($search, ' ')+1) as $assoc) { foreach($this->getPermutations($assoc) as $ordered) { $ordered = join(' ', $ordered); $result = call_user_func_array($checker, array_merge([$ordered, $search], $args)); if($result<=$this->distance) { return $return?$ordered:true; } } } return $return?null:false; } )。

this listing中提供了完整代码的示例(我没有使用沙箱,因为我不确定代码清单会在多长时间内可用)。使用此示例:

strtolower()

你会得到如下结果:

Testing "Niels Faurskov Andersen"

Name "Niels Andersen" has matched with "niels andersen"
Name "Niels Faurskov" has matched with "niels faurskov"
Name "Niels Faurskov Andersen" has matched with "niels faurskov andersen"
Name "Nils Faurskov Andersen" has matched with "niels faurskov andersen"
Name "Nils Andersen" has matched with "niels andersen"
Name "niels faurskov" has matched with "niels faurskov"
Name "niels Faurskov" has matched with "niels faurskov"
Name "niffddels Faurskovffre" has mismatched

- here是此代码的演示,以防万一。


复杂性

因为你不仅关心任何方法,而且关心 - 它有多好,你可能会注意到,这样的代码会产生相当多的操作。我的意思是,至少,生成字符串部分。这里的复杂性由两部分组成:

  • 字符串部件生成部分。如果你想生成所有的字符串部分 - 你必须这样做,就像我上面所描述的那样。可能的改进点 - 生成无序字符串集(在置换之前)。但我仍然怀疑它是否可以完成,因为提供的代码中的方法不会使用“暴力”生成它们,而是以数学方式计算它们(基数为enter image description here
  • 相似性检查部分。这里您的复杂性取决于给定的相似性检查器。例如,similar_text()具有O(N 3 )复杂度,因此对于较大的比较集,它将非常慢。

但您仍然可以通过即时检查来改进当前的解决方案。现在,此代码将首先生成所有字符串子序列,然后逐个开始检查它们。在通常情况下,您不需要这样做,因此您可能希望将其替换为行为,在生成下一个序列后,将立即检查它。然后,您将提高具有肯定答案的字符串的性能(但不适用于那些没有匹配的字符串)。

答案 1 :(得分:1)

(对午餐有点想法)

我认为,基本上你要做的事情甚至不一定要知道两个名字听起来是否相似,但是如果它们的字母顺序相似,那么我认为最好的选择可能就是“扔掉”常见的人物,看看其余的。这应该可以使用正则表达式 - 如果名称存储在MySQL数据库中,您可能希望使用REGEXP ...

假设您有一个带有单个“名称”字段的HTML表单,这样的内容可能会达到您的目的:

1:捕获名称并删除常用字符(基本上是元音,但也可能是丹麦语重音元音,为了简单起见,我将使用'aeiou')但是暂时保留空白:

// using 'Niels Faurskov Andersen' as the example...
$sName = str_to_lower( preg_replace( '/[aeiou]/', '', $_POST['name'] ) );

// you should now have 'nls frskv ndrsn'

2:假设forename始终是第一个,您可以构建一个SQL REGEXP查询,该查询匹配forename的(余数)以及以下任一名称:

// taking $sName from (1) 'nls frskv ndrsn'

// explode $sName on whitespace
$aName = explode(' ', $sName);

// if the exploded $sName has more than 1 element assume forename + surname(s)
if(count($aName) > 1) {

  // extract the forename
  $sForename = $aName[0];

  // extract the surname(s)
  $aSurnames = array_shift($aName);

  // build up the name-matching part of the SQL query
  $sNameSQLPattern = $sForename . '\s+(' . implode('\s*|', $aSurnames) . '\s*)';

  // you should now have a REGEXP insert for MySQL like 'nls\s+(frskv\s*|ndrsn\s*)'
  // this will match 'nls' followed by either 'frsky' or 'ndrsn' (or both)
}

// if there are no whitespace characters in the exploded string...
else {
  // ... just use the name as is (with common characters replaced)
  // appearing anywhere in the 'full name'
  $sNameSQLPattern = ".*{$sName}.*";
}

3:查询数据库

// build the SQL SELECT statement 
// remembering to do the same 'common character' replacement
// unfortunately there's no way to do a RegExp replacement in MySQL...
$sFindNameQuery = "SELECT `blacklist`.`fullname` "
    . "FROM `blacklist` "
    . "WHERE "
    . "REPLACE( "
    . "REPLACE( "
    . "REPLACE( "
    . "REPLACE( "
    . "REPLACE( LOWER(`blacklist`.`fullname`), 'a', '' ), "
    . "'e', ''), "
    . "'i', ''), "
    . "'o', ''), "
    . "'u', '')  "
    . "REGEXP {$sNameSQLPattern} ";

那是丑陋的罪,但本质上应该给你一个正常的表达模式匹配用户名的一种基本的“指纹” - 它应该是相当宽容的,所以如果没有匹配你可以(合理地)安全地假设此人尚未被列入黑名单,但如果有一个或多个匹配,则可以将其拉出来进行人工审核。

在删除重音字符时,您可以使用PHP中的iconv将这些字符音译为ASCII - 这对于构建指纹很好:http://www.php.net/iconv

不幸的是,你需要在SQL中匹配它 - 为此你最好将整个字符替换('REPLACE'块)放入函数中,因为你需要映射了大量替换:How to remove accents in MySQL?

请记住,无论你在PHP方面做了什么替换,你还必须在数据库查询中进行 - 所以创建一个PHP函数和一个基本上反映彼此功能的MySQL函数可能会更好。

希望这有一些帮助...它有点散漫:\