Программное разбиение слова на слоги

    Недавно я столкнулся с проблемой реализации переноса слов средствами PHP. Продолжительное домогательство до поисковиков не дало результата — готовый скрипт не был обнаружен. Да что там скрипт, даже с поиском алгоритма возникли трудности. Посему я, вооружившись блокнотом и карандашом отправился на ФилФак Уфимского нашего БГУ, что бы выспросить у знакомых студентов-филологов, как оно всё на самом деле работает. А потом вооружился NotePad'ом ++ и написал простенький такой скрипток, способный худо-бедно с поставленной задачей справиться. Что из этого получилось — читаем по катом.



    Итак, для начала, разберёмся, зачем нам это всё вообще надо. Мне необходимо было реализовать алгоритм разбиения слов для переноса, причем, как мы помним из школьного курса, подойти к вопросу можно тремя разными способами:
    1) «Графическим», исходя из которого мы строим переносы так, что бы не затруднять зрительное восприятие слова или фразы как графического целого, что, впрочем, фактически не реализуемо программно — ну как задать машине понятие «графического целого»??
    2) «Морфемным», согласно которому при переносе не разбиваются значащие части слова. В плюсах у него понятная и машине и пользователю логика переноса, в минусах — необходимость составлять словарь морфем, котрых ой как не мало.
    3) «Фонетическим», то есть «слоговым», где переносы реализуются так, что бы не затруднять чтение слова(как мы помним всё из той же пресловутой школьной программы, именно слог является в русском языке единицей чтения и письма). Так вот, именно этот способ и был избран для реализации. В плюсах у него — легкость составления первоначального кода, скажем так, «ядра», в минусах — необходимость в ведении системы правил, по которой обрабатывается первичный результат, и неочевидная для пользователя логика разбиения, не то, что на слова, а даже и вовсе на слоги. Связано это вот с чем:

    Слогораздел в русском литературном языке обусловлен принципом восходящей звучности. По степени звучности обычно обозначают: гласные4, звонкие сонорные согласные3. звонкие шумные согласные2, глухие согласные1.
    Отсюда:
    друзья — 23434 — дру-зья;
    капуста — 1414114 — ка-пу-ста;
    черная — 143344 — че-рна-я;
    ястреб — 3411343 — я-стреб;
    хоккей — 141143 — хо-ккей;
    каменная --143433434 — ка-ме-нна-я;
    Дарья — 24334 — Да-рья;
    коньки — 14314 — конь-ки;
    семья — 14334 — се-мья;
    Да, это не похоже на то, как обычно делят слова на слоги в школе (лично мы делили совсем не так...), но поймайте за рукав случайно пробегающего мимо филолога и заставьте говорить — он подтвердит выше написанное.

    Итак, теперь ясно, каким образом мы собираемся делить слова. Давайте посмотрим теперь на код, а что бы попутно разобраться в алгоритме, я снабдил его комментариями:

    <?php
    //для начала я решил, что Юникод — это хорошо (не буду вдаваться в подробности, топик не об том), посему наше слово и все операции над его составными частями будут происходить в символах Юникода
    //в этом нам поможет следюущая функция:
    function win2uni($s)
     {
     // преобразование win1251 -> iso8859-5:
     $s = convert_cyr_string($s,'w','i');
     // преобразование iso8859-5 -> unicode:
     for ($result='', $i=0; $i<strlen($s); $i++) {
          $charcode = ord($s[$i]);
          $result .= ($charcode>175)?"&#".(1040+($charcode-176)).";":$s[$i];
     }
     return $result;
     }
    //тепрь, разобравшись с кодировками, разделим буквы на группы, так, как описано выше.
    //конечно, для абсолютной парвдоподобности нам надо бы обрабатывать не буквы, а звуки, но я решил упростить себе задачу.
    //мы не будем обрабатывать варианты смягчения звука (Ь) так, как это положено по правилам слого раздела, а просто условимся так:
    //символ переноса никогда (!) не может стоять перед «ь» и «ъ»
    //в процессе обработки мы просто будем их игнорировать и в слчуае необходимости передвигать знак переноса
    //вот. поехали:
    $group_4 = array (win2uni(«а»), win2uni(«е»), win2uni(«ё»), win2uni(«и»), win2uni(«о»), win2uni(«у»), win2uni(«э»), win2uni(«ю»), win2uni(«я»));
    $group_3 = array (win2uni(«л»), win2uni(«м»), win2uni(«н»), win2uni(«р»), win2uni(«й»));
    $group_2 = array (win2uni(«б»),win2uni(«в»), win2uni(«г»), win2uni(«д»), win2uni(«з»), win2uni(«ж»));
    $group_1 = array (win2uni(«к»), win2uni(«п»), win2uni(«с»), win2uni(«ф»), win2uni(«т»), win2uni(«ш»), win2uni(«щ»), win2uni(«х»), win2uni(«ц»), win2uni(«ч»));
    //теперь опишем используемые скрипотом переменные:
    $word = «кошка»; //слово, которое мы дробим на слоги
    $split = array();//массив, в котором мы храним принадлежность каждого символа слова к одной из описанных групп
    $word_split = array();//разбитое на символы слово
    $start=0;//начало цикла
    $end=strlen($word);//конец цикла
    //итак, начнём обработку:

    //перелопатим исходное слово:

    while ($start < $end)
    {
    $word_split[$start] = win2uni(substr($word,$start,1)); //отковыриваем символ
    $is_group1 = in_array(win2uni($word_split[$start]),$group_1); //если символ принадлежит первой группе, выставляем соотв. флагу true
    $is_group2 = in_array(win2uni($word_split[$start]),$group_2); //аналогично
    $is_group3 = in_array(win2uni($word_split[$start]),$group_3); //аналогично
    $is_group4 = in_array(win2uni($word_split[$start]),$group_4); //аналогично
    //(вообще то можно обойтись и без флагов, они мне помогали в процессе отладки, а убирать их потом было лень...)

    //теперь проверяем сатусы флагов:
    if (!empty($is_group1)) //символ активировал первый флаг!
    {
    $split[$start] = 1; //записываем принадлежность символа к первой группе в соотв. массив
    }
    elseif (!empty($is_group2)) //аналогично
    {
    $split[$start] = 2;
    }
    elseif (!empty($is_group3)) //аналогично
    {
    $split[$start] = 3;
    }
    elseif (!empty($is_group4)) //аналогично
    {
    $split[$start] = 4;
    }
    elseif (empty($is_group1) and empty($is_group2) and empty($is_group3) and empty($is_group4)) //а если этого символа нет в ни в одной из групп (это мягкий знак, например), то
    {
    $split[$start] = $word_split[$start]; //запишем его как есть, а дальше разберёмся
    }

    $start++;
    }
    //вот так, слово расковыряли. дальше - тестовый вывод $split, посмотреть, что получилось

    foreach ($split as $s)
    {
    echo $s;
    }

    echo "<br>";

    //и тестовый вывод $word_split, кроме всего, надо же вывод $split с чем-то сравнивать =)
    foreach ($word_split as $w)
    {
    echo $w;
    }
    echo "<br>";

    //ну а тепрь, собственно, бьём слово на слоги:
    //(я поленился сохранять результат вывода в переменную, а потом её выводить, поэтому вывожу сразу в цикле):
    $count=0; //у нас новый счётчик =) старый я уволил =) =)

    while ($count <= count($split))
    {
    $a=$split[$count]; //принадлежность к группе текущего символа
    $b=$split[$count+1]; //принадлежность к группе следующего символа
    //вычисляем разницу между группой текущего и следющего символов.
    if ($a-$b == 0 and $b==4 ) //если она равна 0 и это гласные
       {
       echo $word_split[$count];
       echo "-"; //впихиваем между ними перенос
       }
    else
    {
       if (!is_numeric($b) or $a-$b<=0) //если дальше «мягкий знак» или нет спада звучности
          {
          echo $word_split[$count]; //то никакого символа переноса не ставим
          }
       else //если есть спад звучности
          {
          echo $word_split[$count];
          echo "-";//вставляем символ переноса
          }
    }
    $count++;
    }

    echo "<br>";
    //вот и всё =)
    ?>
    * This source code was highlighted with Source Code Highlighter.


    Вроде бы программа готова. Но — давайте проведем тест на описанных выше, в теории переноса, словах:
    например, возьмём слово «ястеб»: переносится оно как «я-стреб», но программа бъёт его на «я-стре-б», потому что между «е» и «б» наблюдается спад звучности. Баг? Можно обработать вывод регулярными выражениями, и таким образом закрыть дырку. А слово «ландскнехт» программа перенесет вообще непонятно как. Почему? да оно просто не русское и русским законам слогораздела не подчиняется. В общем, вы видите, что код придётся допиливать целой системой правил, разработку которой оставляю на совести читателей. Это неплохая тренировка для мозгов, к тому же, поднатореете в филологии))

    Вот, собственно, и всё, о чём я хотел сегодня написать. Спасибо за внимание.

    P.S. Мой первый пост. Я таки это сделал! =)
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 38

      +10
      А толку, если это с правилами русского языка не согласуется?
      Вот правила: gramota.ru/spravka/rules/? rub=perenos
      Если написано нельзя — значит и не надо оправдывать неправильные переносы. Лучше вообще не переносить, чем переносить чёрт знает как.
        +6
        По моему увидев перенос «хоккей — 141143 — хо-ккей;» мало кто поверит, что надо «хо-ккей» а не привычное «хок-кей». И я побоюсь использовать такие переносы.
          0
          ну, привычнее, не привычнее, но это правильно. погуглите на тему «принцип восходящей звучности»
            0
            Так вон же выше ссылку привели на правила:

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

            То есть «хок-кей» правильно, а «хо-ккей» — неправильно.
              0
              так в примере речь не про перенос а про слогораздел. наверное, я нелостаточно чётко выразился. слогораздел и перенос слов разные вещи. а что программа перенсёт слово не так, это да. но в коце сатьи я написал, что в принципе, эту бадягу можно обойти правилами на основе регулярных выражений. а реализацию оставил вам на сладкое. сам тоже над ней работаю =)
          0
          это с правилами русского языка согласовано: слогораздел именно так и происходит. А перенос слов по слогам вполне возмоден. Читайте: www.gramma.ru/RUS/? id=4.21
            0
            \\возможен
              –2
              >> Недавно я столкнулся с проблемой реализации переноса слов средствами PHP.
              Вот это и наводит на мысль что не слогораздел а именно перенос по слогам =)
                0
                ну да, такой способ переноса слов в принципе возможен [ www.gramma.ru/RUS/? id=4.21 ]. и для него принципиален слогораздел))
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                спасибо за линк. я просто пытался реализовать всё в максиамльном соответсвии с правилами слогораздела. мой первоначальный алгоритм работал более чётко, но он расставлял переносы после всех гласных а потом прогонял вывод по регулярным выражениям. получается, принёс удобство в жертву «научности» =(
                0
                Вообще-то есть специализированный софт для этого дела.
                И вот он как раз всё делает по правилам (ну или почти всё).
                  0
                  ну есть то-есть, а где его найти в паблике? я не нашёл, и озадачился. сейчас работаю над реализацией морфемного способа переноса. как сделаю словарь — выложу на хабре.
                    0
                    Наш программист как-то нашёл этот софт.
                    Одной из его находок был т.н. «лемматизер» aot.ru.
                    Возможно там же он нашёл и работу со слогами.
                      0
                      спасибо, будем тестить и изучать)
                      0
                      ну есть то-есть, а где его найти в паблике?
                      Ээээ… Прочитать на Википедии? Там есть и ссылки на статьи и на реализации. Правда для русского нет ничего — это правда.
                      0
                      кстати, скорее всего такой софт работает на основе морфем…
                      0
                      если есть сомения на счёт слогораздела, почитайте forum.gramota.ru/forum/read.php? f=15&i=5072&t=5072, все мои примеры оттуда родом.
                        –4
                        Очень не плохо!
                        Для лучшего восприятия кода, советую воспользоваться source.virtser.net/
                          0
                          он вроде как php не подсвечивает(
                            0
                            ну можно выбрать C#
                            php как известно, является не далеким родственником по структуре…

                            Все равно приятней будет читать код.
                              0
                              сделано
                                +1
                                Подсветка — это, конечно, здорово, но сейчас код нельзя скопипастить и запустить.
                                Например, из-за кавычек.
                          0
                          Ну про сам алгоритм написали выше.
                          Напишу про код — он очень, очень странный.

                          Зачем переводить строку из cp1251 в юникод и потом жутко извращенным способом отщипывать символы из старой кодировки и переводить в новую, может стоит сразу использовать mb_string?
                          function win2uni($s) — это вообще очень странная функция, этакий аналог iconv('windows-1251', 'utf-8', $s).

                          У вас чуть менее чем полностью код состоит из copy-paste. Так писать не следует.
                          $group_[1-4] сгруппируйте в один массив, если вы все еще так настойчиво сам код сохраняете в кодировке cp1251, а потом переводите символы своим велосипедом в utf8, то либо сохраните наконец-то его в utf8, либо используйте array_map.

                          Вместо этого ужаса if… elseif… elseif и тд по одной строчке в каждом переделайте код на switch case.
                            0
                            согласен, код не красивый. но вылизывать его не хватило терпения, хотелось побыстрее написать на хабр, как тока он заработал…
                              +2
                              Поспешишь — людей насмешишь :-)
                              Привести код в порядок занимает не так уж и много времени.
                                +1
                                И да, сравните количество кода. Чувствуете разницу?
                                А если уж очень хочется true-utf-compliance, то одноименные функции для работы со строками взять из библиотеки mb_string, кода больше не станет.
                            0
                            Вы «ландскнехт» неправильно написали :)
                              +1
                              упс, опечатка, щас исправлю…
                              +3
                              Да что там скрипт, даже с поиском алгоритма возникли трудности
                              Аааа… Держите меня семеро. Ну сколько нужно времени чтобы найти суперсекретную статью в ультранедоступном источнике с названием Wikipedia? Версии для PHP там нет, правда (есть где руки кодеру размять, да), но есть для Perl и Ruby. Таблицы для русского языка берутся из естественного места.

                              Оставьте студентов-филологов в покое: всё уже украдено до вас!
                                +1
                                Не знаю можно так переносить или нельзя, но выглядит как то неорганично. Особенно «я-стреб». «Яст-реб» как то привычнее (Викисловарь тоже так считает).
                                  0
                                  Нельзя оставлять одну букву на строке. Алгоритм Кнута это учитывает отдельно. \lefthyphenmin=2 \righthyphenmin=2 для русского, \lefthyphenmin=2 \righthyphenmin=3 для английского…
                                    +2
                                    Вы не путайте орфографию и типографику пожалуйста.
                                      0
                                      Oops. Я и не заметил что там вообще слово было неправильно разбито на слоги. Посыпаю голову пеплом. Кстати правила для русского языка из babel'я работают хуже, чем для английского — что можно заметить и по количеству исключений (184 для русского и 14 для английского). Но по сравнению с тем, что эти эвристики творят это всё мелочи, конечно…
                                        +1
                                        Ну, в русском таких слов достаточно. «Изображение» или «идиот» например. Насколько я знаю, оставлять одну букву на строке можно, но не принято.
                                          0
                                          Нельзя. Уже же ссылку давали. Но в случае с яст-ре-бом до этого правила там дело не должно доходить даже. Такой перенос должен отсекаться на уровне разбиения на слоги. У Кнута не доходят.
                                            0
                                            Да уж, правил у нас немало.
                                              0
                                              В общем, при разбиении на слоги одна буква оставаться может, при переносе — нет. А то тут немного свалили в кучу одно и другое.

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

                                  Самое читаемое