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.


.