Pull to refresh

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

Reading time 5 min
Views 7.3K
Недавно я столкнулся с проблемой реализации переноса слов средствами 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. Мой первый пост. Я таки это сделал! =)
Tags:
Hubs:
+19
Comments 38
Comments Comments 38

Articles