Определите, близки ли два имени друг к другу


Я создаю систему для своей школы, где мы можем проверить, внесен ли ученик в черный список, на вечеринках и других мероприятиях. Мне легко проверить, внесен ли студент в черный список, так как я могу просто найти студента в своей базе данных и посмотреть, внесен ли он/она в черный список.

Однако здесь все становится сложнее.

На наши вечеринки каждый студент может пригласить одного человека. Теоретически студент, занесенный в черный список, может быть приглашен другим студентом и обойти систему. Я не могу проверить гостевой стол для студентов занесен в черный список, потому что при приглашении вашего гостя указывается только имя.

Поэтому мне нужно проверить, близко ли имя, внесенное в черный список, к имени гостя, и отобразить предупреждение, если они близки, к сожалению, есть некоторые вещи, которые следует учитывать.

Имена могут быть совершенно разными. В Дании стандартное имя содержит три "имени", например "Нильс Фаурсков Андерсен". Но студент может просто набрать "Нильс Фаурсков" или "Нильс Андерсен", или даже некоторые персонажи удаленный.

Таким образом, полное имя, такое как Нильс Фаурсков Андерсен, может быть

  • Нильс Андерсен
  • Нильс Фаурсков
  • Нильс Фаурсков Андерсен
  • Нильс Фаурсков Андерсен
  • Нильс Андерсен
  • нильс фаурсков
  • Нильс Фаурсков

И так далее...

Другое дело, что датский алфавит содержит "æøå", кроме обычного а-я. С учетом сказанного весь сайт и база данных закодированы в кодировке UTF-8.

Я посмотрел в различные методы, чтобы проверить разницу между двумя строками, и расстояние Левенштейна не совсем подходит для этого.

Я нашел этот поток в StackOverflow: Получение ближайшего совпадения строк

Который, казалось, предоставил правильные данные, однако я не был вполне уверен, какой метод тоже выбрать

Я кодирую эту часть на php, у кого-нибудь есть идея, как это сделать? может быть, с MySQL? или модифицированная версия расстояния Левенштейна? Возможно ли регулярное выражение?

Author: Community, 2014-01-27

2 answers

Введение

Теперь ваши условия соответствия могут быть слишком широкими. Однако вы можете использовать расстояние Левенштейна, чтобы проверить свои слова. С его помощью может быть не слишком легко достичь всех желаемых целей, таких как звуковое сходство. Таким образом, я предлагаю разделить вашу проблему на несколько других вопросов.

Например, вы можете создать некоторую пользовательскую проверку, которая будет использовать передаваемый вызываемый ввод, который принимает две строки, а затем отвечать на вопрос о том, являются ли они одинаковыми (для levenshtein, который будет быть расстоянием, меньшим некоторого значения, для similar_text - некоторого процента сходства и т. Д. - вам решать определять правила).


Сходство, основанное на словах

Ну, все встроенные функции не будут работать, если мы говорим о случае, когда вы ищете частичное совпадение, особенно если речь идет о неупорядоченном совпадении. Таким образом, вам потребуется создать более сложный инструмент сравнения. У вас есть:

  • Строка данных (которая будет находиться, например, в БД). Это выглядит как D= D0 D1 D2 ... Dn
  • Строка поиска (это будет ввод пользователем). Это выглядит как S = S0 С1 ... Sм

Здесь символы пробела означают просто любое пространство (я предполагаю, что символы пробела не повлияют на сходство). Также n > m. С этим определением ваша проблема заключается в том, чтобы найти набор слов m в D, которые будут похожи на S. По set Я имею в виду любую неупорядоченную последовательность. Следовательно, если мы найдем любая такая последовательность в D, то S аналогична D.

Очевидно, что если n < m, то ввод содержит больше слов, чем строка данных. В этом случае вы можете либо подумать, что они не похожи, либо действовать так, как описано выше, но переключать данные и ввод (что, однако, выглядит немного странно, но в некотором смысле применимо)


Реализация

Чтобы сделать это, вам нужно будет иметь возможность создавать набор строк, которые являются частями m слов из 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 Andersen, так и Andersen 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;
}

Это проверит сходство на основе обратного вызова пользователя, который должен принимать по крайней мере два параметра (т.Е. сравниваемые строки). Также вы можете захотеть вернуть строку, которая вызвала положительный возврат обратного вызова. Пожалуйста, обратите внимание, что этот код не будет отличаться верхним и нижним регистром, но может быть, вы не хотите такого поведения (тогда просто замените strtolower()).

Пример полного кода доступен в этом списке (я не использовал песочницу, так как не уверен, как долго там будет доступен список кода). С этим примером использования:

$data   = 'Niels Faurskov Andersen';
$search = [
    'Niels Andersen',
    'Niels Faurskov',
    'Niels Faurskov Andersen',
    'Nils Faurskov Andersen',
    'Nils Andersen',
    'niels faurskov',
    'niels Faurskov',
    'niffddels Faurskovffre'//I've added this crap
];

$checker = new Similarity($data, 2);

echo(sprintf('Testing "%s"'.PHP_EOL.PHP_EOL, $data));
foreach($search as $name)
{
   echo(sprintf(
      'Name "%s" has %s'.PHP_EOL, 
      $name, 
      ($result=$checker->checkMatch($name, 'levenshtein', [], 1))
         ?sprintf('matched with "%s"', $result)
         :'mismatched'
      )
   );

}

Вы получите результат, подобный:

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

- вот демонстрация этого кода, на всякий случай.


Сложность

Поскольку вы заботитесь не только о каких-либо методах, но и о том, насколько это хорошо, вы можете заметить, что такой код будет производить довольно избыточные операции. Я имею в виду, по крайней мере, генерацию струнных частей. Сложность здесь состоит из двух частей:

  • Часть генерации частей строк. Если вы хотите сгенерировать все строковые части - вам придется сделать это так, как я описал выше. Возможный пункт для улучшения - генерация неупорядоченных наборов строк (это происходит до перестановки). Но все же я сомневаюсь, что это можно сделать, потому что метод в предоставленном коде будет генерируйте их не "грубой силой", а так, как они математически рассчитаны (с мощностью enter image description here)
  • Часть проверки сходства. Здесь ваша сложность зависит от заданной проверки сходства. Например, similar_text() имеет O(N3) сложность, поэтому при больших наборах сравнения это будет чрезвычайно медленно.

Но вы все еще можете улучшить текущее решение с помощью проверки на лету. Теперь этот код сначала сгенерирует все строковые подпоследовательности, а затем начнет проверяя их одного за другим. В общем случае вам не нужно этого делать, поэтому вы можете заменить это поведением, когда после генерации следующей последовательности оно будет немедленно проверено. Затем вы увеличите производительность для строк с положительным ответом (но не для тех, у которых нет совпадения).

 13
Author: Alma Do, 2017-05-23 12:25:47

(немного подумал за обедом)

Я думаю, по сути, то, что вы пытаетесь сделать, даже не обязательно выяснять, похожи ли два имени, но если у них одинаковые буквы в одинаковом порядке, поэтому я думаю, что лучшим вариантом может быть "выбросить" общие символы и просто посмотреть на остальное. Это должно быть возможно с помощью регулярного выражения - и если имена хранятся в базе данных MySQL, вы, вероятно, захотите использовать REGEXP...

Что-то подобное может послужить ваши цели предполагая, что у вас есть HTML-форма с одним полем "имя":

1: запишите имя и удалите общие символы (гласные в основном, но потенциально также гласные с датским акцентом для простоты в SQL, я просто собираюсь использовать "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: предполагая, что имя всегда является первым, вы можете создать запрос SQL REGEXP, соответствующий (оставшейся части) имени, плюс одно из следующих имен:

// 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} ";

Это уродливо, как грех, но, по сути, должно дать вам соответствие шаблону регулярного выражения своего рода базовому "отпечатку пальца" имени пользователя - это должно быть достаточно прощающим, поэтому, если совпадений нет, вы можете (разумно) безопасно предположить, что человек не был внесен в черный список, но если есть одно или несколько совпадений, их можно просмотреть вручную.

Когда дело доходит до удаления символов с ударением, вы можете использовать iconv в PHP для транслитерации этих символов в ASCII - который подходит для создания отпечатка пальца: http://www.php.net/iconv

К сожалению, вам тогда потребуется сопоставить это в SQL - и для этого вам было бы лучше поместить всю замену символов (этот блок "ЗАМЕНИТЬ") в функцию, поскольку вам понадобится сопоставить множество замен: Как удалить акценты в MySQL?

Помните, однако, какие бы замены вы ни делали на стороне PHP, вы также должны делать в запросе базы данных - так что вероятно, было бы лучше создать как функцию PHP, так и функцию MySQL, которые по существу отражают функциональность друг друга.

Надеюсь, это поможет... это немного бессвязно:\

 1
Author: CD001, 2017-05-23 12:17:25