soundex для русского языка
Стандартная функция PHP soundex() не работает с кирилицей.
На хабрхабре предложили алгоритм soundex'a для русского языка. В тот момент когда он мне понадобился выяснилось, что алгоритм не совершенен (см. комментарии на хабре) и часто путает совершенно разные слова. Тем не менее, как правильно заметил автор алгоритма - для фолбэка и спецсистем он вполне сгодится. Для своей же задачи я использовал как это ни странно metaphone для английского языка, предварительно транслитерировав русские слова.
Исходный код алгоритма soundex для русского языка:
function dmword($string, $is_cyrillic = true)
{
static $codes = array(
'A' => array(array(0, -1, -1),
'I' => array(array(0, 1, -1)),
'J' => array(array(0, 1, -1)),
'Y' => array(array(0, 1, -1)),
'U' => array(array(0, 7, -1))),
'B' => array(array(7, 7, 7)),
'C' => array(array(5, 5, 5), array(4, 4, 4),
'Z' => array(array(4, 4, 4),
'S' => array(array(4, 4, 4))),
'S' => array(array(4, 4, 4),
'Z' => array(array(4, 4, 4))),
'K' => array(array(5, 5, 5), array(45, 45, 45)),
'H' => array(array(5, 5, 5), array(4, 4, 4),
'S' => array(array(5, 54, 54)))),
'D' => array(array(3, 3, 3),
'T' => array(array(3, 3, 3)),
'Z' => array(array(4, 4, 4),
'H' => array(array(4, 4, 4)),
'S' => array(array(4, 4, 4))),
'S' => array(array(4, 4, 4),
'H' => array(array(4, 4, 4)),
'Z' => array(array(4, 4, 4))),
'R' => array(
'S' => array(array(4, 4, 4)),
'Z' => array(array(4, 4, 4)))),
'E' => array(array(0, -1, -1),
'I' => array(array(0, 1, -1)),
'J' => array(array(0, 1, -1)),
'Y' => array(array(0, 1, -1)),
'U' => array(array(1, 1, -1))),
'F' => array(array(7, 7, 7),
'B' => array(array(7, 7, 7))),
'G' => array(array(5, 5, 5)),
'H' => array(array(5, 5, -1)),
'I' => array(array(0, -1, -1),
'A' => array(array(1, -1, -1)),
'E' => array(array(1, -1, -1)),
'O' => array(array(1, -1, -1)),
'U' => array(array(1, -1, -1))),
'J' => array(array(4, 4, 4)),
'K' => array(array(5, 5, 5),
'H' => array(array(5, 5, 5)),
'S' => array(array(5, 54, 54))),
'L' => array(array(8, 8, 8)),
'M' => array(array(6, 6, 6),
'N' => array(array(66, 66, 66))),
'N' => array(array(6, 6, 6),
'M' => array(array(66, 66, 66))),
'O' => array(array(0, -1, -1),
'I' => array(array(0, 1, -1)),
'J' => array(array(0, 1, -1)),
'Y' => array(array(0, 1, -1))),
'P' => array(array(7, 7, 7),
'F' => array(array(7, 7, 7)),
'H' => array(array(7, 7, 7))),
'Q' => array(array(5, 5, 5)),
'R' => array(array(9, 9, 9),
'Z' => array(array(94, 94, 94), array(94, 94, 94)), // special case
'S' => array(array(94, 94, 94), array(94, 94, 94))), // special case
'S' => array(array(4, 4, 4),
'Z' => array(array(4, 4, 4),
'T' => array(array(2, 43, 43)),
'C' => array(
'Z' => array(array(2, 4, 4)),
'S' => array(array(2, 4, 4))),
'D' => array(array(2, 43, 43))),
'D' => array(array(2, 43, 43)),
'T' => array(array(2, 43, 43),
'R' => array(
'Z' => array(array(2, 4, 4)),
'S' => array(array(2, 4, 4))),
'C' => array(
'H' => array(array(2, 4, 4))),
'S' => array(
'H' => array(array(2, 4, 4)),
'C' => array(
'H' => array(array(2, 4, 4))))),
'C' => array(array(2, 4, 4),
'H' => array(array(4, 4, 4),
'T' => array(array(2, 43, 43),
'S' => array(
'C' => array(
'H' => array(array(2, 4, 4))),
'H' => array(array(2, 4, 4))),
'C' => array(
'H' => array(array(2, 4, 4)))),
'D' => array(array(2, 43, 43)))),
'H' => array(array(4, 4, 4),
'T' => array(array(2, 43, 43),
'C' => array(
'H' => array(array(2, 4, 4))),
'S' => array(
'H' => array(array(2, 4, 4)))),
'C' => array(
'H' => array(array(2, 4, 4))),
'D' => array(array(2, 43, 43)))),
'T' => array(array(3, 3, 3),
'C' => array(array(4, 4, 4),
'H' => array(array(4, 4, 4))),
'Z' => array(array(4, 4, 4),
'S' => array(array(4, 4, 4))),
'S' => array(array(4, 4, 4),
'Z' => array(array(4, 4, 4)),
'H' => array(array(4, 4, 4)),
'C' => array(
'H' => array(array(4, 4, 4)))),
'T' => array(
'S' => array(array(4, 4, 4),
'Z' => array(array(4, 4, 4)),
'C' => array(
'H' => array(array(4, 4, 4)))),
'C' => array(
'H' => array(array(4, 4, 4))),
'Z' => array(array(4, 4, 4))),
'H' => array(array(3, 3, 3)),
'R' => array(
'Z' => array(array(4, 4, 4)),
'S' => array(array(4, 4, 4)))),
'U' => array(array(0, -1, -1),
'E' => array(array(0, -1, -1)),
'I' => array(array(0, 1, -1)),
'J' => array(array(0, 1, -1)),
'Y' => array(array(0, 1, -1))),
'V' => array(array(7, 7, 7)),
'W' => array(array(7, 7, 7)),
'X' => array(array(5, 54, 54)),
'Y' => array(array(1, -1, -1)),
'Z' => array(array(4, 4, 4),
'D' => array(array(2, 43, 43),
'Z' => array(array(2, 4, 4),
'H' => array(array(2, 4, 4)))),
'H' => array(array(4, 4, 4),
'D' => array(array(2, 43, 43),
'Z' => array(
'H' => array(array(2, 4, 4))))),
'S' => array(array(4, 4, 4),
'H' => array(array(4, 4, 4)),
'C' => array(
'H' => array(array(4, 4, 4))))));
$length = strlen($string);
$output = '';
$i = 0;
$previous = -1;
while ($i < $length)
{
$current = $last = &$codes[$string[$i]];
for ($j = $k = 1; $k < 7; $k++)
{
if (!isset($string[$i + $k]) ||
!isset($current[$string[$i + $k]]))
break;
$current = &$current[$string[$i + $k]];
if (isset($current[0]))
{
$last = &$current;
$j = $k + 1;
}
}
if ($i == 0)
$code = $last[0][0];
elseif (!isset($string[$i + $j]) || ($codes[$string[$i + $j]][0][0] != 0))
$code = $is_cyrillic ? (isset($last[1]) ? $last[1][2] : $last[0][2]) : $last[0][2];
else
$code = $is_cyrillic ? (isset($last[1]) ? $last[1][1] : $last[0][1]) : $last[0][1];
if (($code != -1) && ($code != $previous))
$output .= $code;
$previous = $code;
$i += $j;
}
return str_pad(substr($output, 0, 6), 6, '0');
}
function dmstring($string)
{
$is_cyrillic = false;
if (preg_match('#[А-Яа-я]#i', $string) === 1)
{
$string = translit($string);
$is_cyrillic = true;
}
$string = preg_replace(array('#[^\w\s]|\d#i', '#\b[^\s]{1,3}\b#i', '#\s{2,}#i', '#^\s+|\s+$#i'),
array('', '', ' '), strtoupper($string));
if (!isset($string[0]))
return null;
$matches = explode(' ', $string);
foreach($matches as $key => $match)
$matches[$key] = dmword($match, $is_cyrillic);
return $matches;
}
function translit($string)
{
static $ru = array(
'А', 'а', 'Б', 'б', 'В', 'в', 'Г', 'г', 'Д', 'д', 'Е', 'е', 'Ё', 'ё', 'Ж', 'ж', 'З', 'з',
'И', 'и', 'Й', 'й', 'К', 'к', 'Л', 'л', 'М', 'м', 'Н', 'н', 'О', 'о', 'П', 'п', 'Р', 'р',
'С', 'с', 'Т', 'т', 'У', 'у', 'Ф', 'ф', 'Х', 'х', 'Ц', 'ц', 'Ч', 'ч', 'Ш', 'ш', 'Щ', 'щ',
'Ъ', 'ъ', 'Ы', 'ы', 'Ь', 'ь', 'Э', 'э', 'Ю', 'ю', 'Я', 'я'
);
static $en = array(
'A', 'a', 'B', 'b', 'V', 'v', 'G', 'g', 'D', 'd', 'E', 'e', 'E', 'e', 'Zh', 'zh', 'Z', 'z',
'I', 'i', 'J', 'j', 'K', 'k', 'L', 'l', 'M', 'm', 'N', 'n', 'O', 'o', 'P', 'p', 'R', 'r',
'S', 's', 'T', 't', 'U', 'u', 'F', 'f', 'H', 'h', 'C', 'c', 'Ch', 'ch', 'Sh', 'sh', 'Sch', 'sch',
'\'', '\'', 'Y', 'y', '\'', '\'', 'E', 'e', 'Ju', 'ju', 'Ja', 'ja'
);
$string = str_replace($ru, $en, $string);
return $string;
}
echo 'Арнольд Шварцнеггер '.implode(' ', dmstring('Арнольд Шварцнеггер'))."\n";
echo 'Arnold Schwarzenegger '.implode(' ', dmstring('Arnold Schwarzenegger'))."\n";
echo 'Орнольд Шворцнегир '.implode(' ', dmstring('Орнольд Шворцнегир'))."\n";
Еще один вариант ru_soundex:
# to Arthur
// (cc) me, 23/08/2007-27/08/2007
$str = "к скалам бурым";
print "ru_soundex($str) = ".ru_soundex($str)."<br />\r\n";
$str = "с каламбуром";
print "ru_soundex($str) = ".ru_soundex($str)."<br />\r\n";;
function ru_soundex($source)
{
$res = '';
$literal = [];
// ассоциативный массив букв
// параметры звуков гласный / согласный
// для гласных переход буквы в звук(и), редуцированный/нет, предполагаемые правила ударения исходя из кол-ва слогов (stressed syllable)
// реализована проверка предполагаемого ударения
// для согласных переход букв[ы] в звук(и), редуцируемый/нет, правила редуцирования
// vowel
$literal['А'] = array('status'=>'гласный','sound'=>'а','stressed'=>'а'); // никогда не меняется
$literal['Е'] = array('status'=>'гласный','sound'=>'и','stressed'=>'э', 'АаЕеЁёИиОоУуЭэЮюЯяЬьЫыЪъ' => 'йэ'); // - особые правила, для этой буквы, стоящей после указанных, а также в начале слов
$literal['Ё'] = array('status'=>'гласный','sound'=>'о','stressed'=>'о', 'АаЕеЁёИиОоУуЭэЮюЯяЬьЫыЪъ' => 'йо');
$literal['И'] = array('status'=>'гласный','sound'=>'и','stressed'=>'и');
$literal['О'] = array('status'=>'гласный','sound'=>'а','stressed'=>'о');
$literal['У'] = array('status'=>'гласный','sound'=>'у','stressed'=>'у');
$literal['Ы'] = array('status'=>'гласный','sound'=>'ы','stressed'=>'ы');
$literal['Э'] = array('status'=>'гласный','sound'=>'э','stressed'=>'э');
$literal['Ю'] = array('status'=>'гласный','sound'=>'у','stressed'=>'у', 'АаЕеЁёИиОоУуЭэЮюЯяЬьЫыЪъ' => 'йу');
$literal['Я'] = array('status'=>'гласный','sound'=>'а','stressed'=>'а', 'АаЕеЁёИиОоУуЭэЮюЯяЬьЫыЪъ' => 'йа'); // заяц произносится как [зайец]
$v_pattern = 'АаЕеЁёИиОоУуЭэЮюЯяЬьЫыЪъ';
// кстати, надо добавить выкусывание гласных из концов слов, заканчивающихся на согласный-гласный-звонкий согласный (-ром, -лем, итд) гласная очень часто сглатывается
// зы: это здесь не реализовано %)
// проверено: soundex и сам с этим неплохо справляется
// звонкие согласные редуцируются при удвоении.
// звонкие согласные переходят в парный глухой перед глухим
// глухие редуцируются полностью перед глухими.
// consonant
// в отличие от гласных, для согласных условие "стоит перед указанной или в конце слова"
$literal['Б'] = array('status'=>'согласный','sound'=>'б', 'КкПпСсТтФфХхЦцЧчШшЩщ' => 'п');
$literal['В'] = array('status'=>'согласный','sound'=>'в', 'КкПпСсТтФфХхЦцЧчШшЩщ' => 'ф');
$literal['Г'] = array('status'=>'согласный','sound'=>'Г', 'КкПпСсТтФфХхЦцЧчШшЩщ' => 'к');
$literal['Д'] = array('status'=>'согласный','sound'=>'д', 'КкПпСсТтФфХхЦцЧчШшЩщ' => 'т');
$literal['Ж'] = array('status'=>'согласный','sound'=>'ж', 'КкПпСсТтФфХхЦцЧчШшЩщ' => 'ш');
$literal['З'] = array('status'=>'согласный','sound'=>'з', 'КкПпСсТтФфХхЦцЧчШшЩщ' => 'с');
$literal['Й'] = array('status'=>'согласный','sound'=>'й');
$literal['К'] = array('status'=>'согласный','sound'=>'к', 'КкПпСсТтФфХхЦцЧчШшЩщ' => '');
$literal['Л'] = array('status'=>'согласный','sound'=>'л');
$literal['М'] = array('status'=>'согласный','sound'=>'м');
$literal['Н'] = array('status'=>'согласный','sound'=>'н');
$literal['П'] = array('status'=>'согласный','sound'=>'п', 'КкПпСсТтФфХхЦцЧчШшЩщ' => '');
$literal['Р'] = array('status'=>'согласный','sound'=>'р');
$literal['С'] = array('status'=>'согласный','sound'=>'с'); // а вот С не хочет редуцироваться, на первый взгляд...
$literal['Т'] = array('status'=>'согласный','sound'=>'т', 'КкПпСсТтФфХхЦцЧчШшЩщ' => '');
$literal['Ф'] = array('status'=>'согласный','sound'=>'ф', 'КкПпСсТтФфХхЦцЧчШшЩщ' => ''); // спорно
$literal['Х'] = array('status'=>'согласный','sound'=>'х');
$literal['Ц'] = array('status'=>'согласный','sound'=>'ц');
$literal['Ч'] = array('status'=>'согласный','sound'=>'чь'); // всегда мягкий
$literal['Ш'] = array('status'=>'согласный','sound'=>'ш');
$literal['Щ'] = array('status'=>'согласный','sound'=>'щь');
// спецсимволы
$literal['Ъ'] = array('status'=>'знак','sound'=>' '); // только разделительный. делит жёстко
$literal['Ь'] = array('status'=>'знак','sound'=>'ь'); // даже если делит, то мягко
$literal['ТС'] = array('status'=>'сочетание','sound'=>'ц');
$literal['ТЬС'] = $literal['ТС'];
$literal['ШЬ'] = array('status'=>'сочетание','sound'=>'ш'); // всегда твёрдый. и это не единстенный рудимент языка
$literal['СОЛНЦ'] = array('status'=>'сочетание','sound'=>'сонц');
$literal['ЯИЧНИЦ'] = array('status'=>'сочетание','sound'=>'еишниц');
$literal['КОНЕЧНО'] = array('status'=>'сочетание','sound'=>'канешно');
$literal['ЧТО'] = array('status'=>'сочетание','sound'=>'што');
$literal['ЗАЯ'] = array('status'=>'сочетание','sound'=>'зайэ'); // да-да. не только [зайэц], но и [зайэвльэнийэ]
$sound = str_to_upper($source);
// сначала сочетания
foreach( array_filter($literal,
create_function('$item','if( $item["status"] === "сочетание") return true; return false;'))
as $sign => $translate )
$sound = str_replace($sign,$translate["sound"],$sound);
// потом знаки
foreach( array_filter($literal,
create_function('$item','if( $item["status"] === "знак") return true; return false;'))
as $sign => $translate )
$sound = str_replace($sign,$translate["sound"],$sound);
// разделяем на слова, определяем кол-во слогов, заменяем ударный/безударный гласный (единственный или предполагая второй в двух-трёхсложном слове, предпредпоследний - в остальных)
$words = preg_split('~[,.\~`1234567890-=\~!@#$%^&*()_+|{}\]\];:\'"<>/? ]~', $sound, -1, PREG_SPLIT_NO_EMPTY);
// гласные
foreach( array_filter($literal,
create_function('$item','if( $item["status"] === "гласный") return true; return false;'))
as $sign => $translate )
{
// для каждого слова
foreach( $words as &$word )
{
// кол-во гласных
$vowel = preg_match_all("~[$v_pattern]~", $word, $del_me );
// готовим
$cur_pos = 0;
$cur_vowel = 0;
while( false !== $cur_pos = strpos($word,$sign,$cur_pos) )
{
$cur_vowel++;
// print $cur_pos.' = '.$sound[$cur_pos]."<br />\r\n";
if( sizeof($translate)==4 && ($cur_pos === 0 || strpos( $v_pattern , $word[$cur_pos-1] )))
{
$word = substr_replace($word,$translate[$v_pattern],$cur_pos,1);
}
elseif( 1 == $vowel )
$word = substr_replace($word,$translate["stressed"],$cur_pos,1); //
elseif( 2 == $vowel && 1 == $cur_vowel )
$word = substr_replace($word,$translate["stressed"],$cur_pos,1); // предполагаем, что в двухсложных словах первый слог ударный
elseif( 3 <= $vowel && $cur_vowel == $vowel - 2 )
$word = substr_replace($word,$translate["stressed"],$cur_pos,1); // предполагаем, что слог ударный предпредпоследний
else
$word = substr_replace($word,$translate["sound"],$cur_pos,1);
$cur_pos++;
}
}
}
$sound = implode( $words, ' ' ); // клеим обратно
// согласные
foreach( array_filter($literal,
create_function('$item','if( $item["status"] === "согласный") return true; return false;'))
as $sign => $translate )
{
// готовим
$cur_pos = 0;
while( false !== $cur_pos = strpos($sound,$sign,$cur_pos) )
{
// print $cur_pos.' = '.$sound[$cur_pos]."<br />\r\n";
if( sizeof($translate)==3 )
{
$x = array_pop(array_keys($translate)); // снимаем третий элемент
if( strpos( $x, $sound[$cur_pos+1] ) || $cur_pos === strlen($sound) )
{
$sound = substr_replace($sound,$translate[$x],$cur_pos,1);
} elseif ( $sound[$cur_pos] === $sound[$cur_pos+1] )
$sound = substr_replace($sound,$translate["sound"],$cur_pos,2); // все двойные редуцируются
else
$sound = substr_replace($sound,$translate["sound"],$cur_pos,1);
} else
{
$sound = substr_replace($sound,$translate["sound"],$cur_pos,1);
}
$cur_pos++;
}
}
// алес. фонемы привели к одному виду
// дальше используем любой алгоритм для вычисления числового эквивалента
// но остаётся сомнение. очень хочется расстаться с глухими предлогами перед глухими согласными ("к скалам")
$sound = preg_replace('~[,.\~`1234567890-=\~!@#$%^&*()_+|{}\]\];:\'"<>/? ]~','',$sound) ;
// print $sound;
// print str_to_translit($sound);
// print soundex(str_to_translit($sound));
$res = str_to_upper($source[0]).substr(soundex(str_to_translit($sound)),1);
return $res;
}
// (c) http://ru2.php.net/manual/en/function.strtoupper.php#74574
//Russian
function str_to_upper($str){
return strtr($str,
"abcdefghijklmnopqrstuvwxyz".
"\xE0\xE1\xE2\xE3\xE4\xE5".
"\xb8\xe6\xe7\xe8\xe9\xea".
"\xeb\xeC\xeD\xeE\xeF\xf0".
"\xf1\xf2\xf3\xf4\xf5\xf6".
"\xf7\xf8\xf9\xfA\xfB\xfC".
"\xfD\xfE\xfF",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".
"\xC0\xC1\xC2\xC3\xC4\xC5".
"\xA8\xC6\xC7\xC8\xC9\xCA".
"\xCB\xCC\xCD\xCE\xCF\xD0".
"\xD1\xD2\xD3\xD4\xD5\xD6".
"\xD7\xD8\xD9\xDA\xDB\xDC".
"\xDD\xDE\xDF");
}
function str_to_translit($str){
return strtr($str,
"abcdefghijklmnopqrstuvwxyz".
"\xE0\xE1\xE2\xE3\xE4\xE5".
"\xb8\xe6\xe7\xe8\xe9\xea".
"\xeb\xeC\xeD\xeE\xeF\xf0".
"\xf1\xf2\xf3\xf4\xf5\xf6".
"\xf7\xf8\xf9\xfA\xfB\xfC".
"\xfD\xfE\xfF",
"abcdefghijklmnopqrstuvwxyz".
"abvgde".
"?*ziik".
"lmnopr".
"stufhc".
"4ww\"y`".
"eua");
}
Взято отсюда: http://ru-php.livejournal.com/1062493.html.
.
Прокомментировать/Отблагодарить