Изучаем основы работы с phpMorphy, путем написания простого скрипта

в 18:39, , рубрики: php, phpmorphy, Программирование, скрипты, метки: , , ,
С чего все начиналось

Здравствуйте, уважаемыее! Вначале расскажу краткую предысторию этого скрипта. В сети есть браузерная игрушка с чатом, и двумя враждующими мирами. При написании сообщения из одного мира в другой, если вы не находились в специальном месте, текст шифровался, то есть буквы алфавита «перемешивались» — к примеру, буква «о» заменялась на букву «е», буква «з» на букву «в» и так далее. При этом несколько букв могли «превращаться» в одну и ту же, например, буквы «е» и «э» превращались в «а». Из-за этого слово «только» могло превратиться в «сефыче», что абсолютно нечитаемо. А представленные другими людьми переводчики возвращали слова вида: «(б, т, ф)ол(у, ь)(к, р)о», прочесть которые тоже не раз плюнуть. Поэтому я, и еще один товарищ, решили создать скрипт, который неким образом возвращал словам истинное обличье. У нас было несколько идей, в том числе, придумать алгоритм эвристического анализа слова на «русскость» (очевидно, что слишком много согласных никак не могут идти подряд и т.п.). В конце концов была использована весьма интересная библиотека phpMorphy, найденная на просторах всемирной паутины. О работе с ней и пойдет речь в этой статье на примере переводчика из «языка» одного мира в другой.

Открываем блокнот любимый текстовый редактор, и пишем класс!

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

class Translater
{
	private $morph;  // Указатель на класс словаря
	private $word;
	private $text;   // Обрабатываемое слово
	public  $phrase; // Обрабатываемая фраза
	public  $stack = array();   // Слоты возвращаемых слов
	public  $trash = array();   // Слоты дополнительных слов
	private $stackIndex = 0;	// Текущий номер слота
	const maxSolveLength = 10;  // Максимальная длина слова для расшифровки
	const maxSolveTrash  = 10;  // Максимальное количество подстановок
	const defaultPhrase  = "сасу сефыче та тшисифе сфот";
 
	// Массив начальный, на текущий момент содержит только массив кириллицы
	private $map_init = array(
	// Кирилица
	array(
    	"а", "б", "в", "г", "д", "е", "ё", "ж", "з", "и", "й", "к", "л", "м", "н", "о",
        "п", "р", "с", "т", "у", "ф", "х", "ц", "ч", "ш", "щ", "ъ", "ы", "ь", "э", "ю", "я")
	);
	// Массив перевода, на текущий момент содержит только массив кириллицы
	private $map_sudo = array(
	// Кириллица
    array(
    	array("е", "э"), "-", "з", "-", "-", "о", "-", "щ", "ш", array("а", "ю"), "ъ", "ц",
        "-", "-", "-", "и", array("д", "ж"), "-", array("б", "т", "ф"), array("г", "н", "х"),
        "я", "л", "м", "п", array("к", "р"), array("в", "с"), array("й", "ч"), "-", array("у", "ь"), "-",
        "-", "ы", "ё")
	);
 
	// Стандартный конструктор __construct вызывается при создании класса
	public function __construct($phrase)
	{
    	// Двухбайтный текст в нижний регистр
    	$this->text = mb_strtolower($phrase);
 
    	// Создание класса для словаря
        include("morphy/common.php");
        $this->morph = new phpMorphy(
        	new phpMorphy_FilesBundle("morphy/dicts", "ru_ru"), array('storage' => PHPMORPHY_STORAGE_FILE,
            	'with_gramtab' => false,
            	'predict_by_suffix' => false,
            	'predict_by_db' => false
        	)
    	);
    	// Автозапуск переводчика
    	if (isset($phrase)) {
        	$this->phrase = $this->str_safe($phrase);
            $this->translatePhrase($this->phrase);
    	} else {
        	$this->phrase = self::defaultPhrase;
    	}
}

Далее следует функция для получения из исходного символа зашифрованной фразы его «расшифрованную версию»:

// Получение кода соотнесенного символа (с=б, а=у и т.п.)
protected function indexbychar($char, $map_index)
{
    	for ($index = 0; $index < count($this->map_init[$map_index]); $index++) {
        	if ($this->map_init[$map_index][$index] == $char) return $index;
    	}
    	return -1;
}

И еще одна служебная функция. Дело в том, что иногда вместо русской «c» или «у» хитрый чат подсовывает английский «аналог»: латинскую букву «c» или «y». И чтобы у переводчика не случился разрыв шаблона необходимо перевести транслит в кириллический вид:

// Смена латыни на кириллицу
public function translateTranslit($word)
{
    	// Массив латиницы
    	$eng = array(
        	"a", "c", "e", "y", "o", "p", "x"
    	);
    	// Массив кириллицы
    	$rus = array(
            "а", "с", "е", "у", "о", "р", "х"
        );
    	$word = str_replace($eng, $rus, $word);
 
    	return $word;
}

После этого идет ядро переводчика, если его можно так назвать. Именно дальнейшие функции и отвечают за «перевод» фразы.

// Посимвольное сравнение полученного и начального слова
protected function str_compare($sample, $value) 
{
    	for ($i = 0; $i < strlen($sample); $i++) {
      	  if ($sample[$i] == $value[$i]) return false;
    	}
    	return true;
}
 
// Определение слова в словаре, если слово есть, то словарь вернет массив
protected function str_dict($solve)
{
    	$lemma = $this->morph->lemmatize(mb_strtoupper($solve, "windows-1251"));
    	return is_array($lemma);
}
 
protected function str_safe($value)
{
    	return trim(stripslashes(htmlspecialchars($value, ENT_NOQUOTES)));
}
 
// Запись найденного слова в основной слот
protected function stack_push($value)
{
    	if (!isset($this->stack[$this->stackIndex]))
            $this->stack[$this->stackIndex] = array();
        array_push($this->stack[$this->stackIndex], $value);
}
 
// Запись найденного слова в дополнительный слот
protected function trash_push($value)
{
    	if (!isset($this->trash[$this->stackIndex]))
            $this->trash[$this->stackIndex] = array();
        array_push($this->trash[$this->stackIndex], $value);
}
 
// Перебор букв слова
protected function bruteforce($solve, $charindex, $sudoindex)
{
    	// Если все буквы слова обработаны - то выход
    	if (!isset($solve[$charindex])) return false;
 
    	// Цикл по буквам слова, в рекурсии - цикл от последней обработанной буквы
    	for ($solveindex = $charindex; $solveindex < strlen($solve); $solveindex++)
       {
        	// Цикл по доступным массивам перевода
        	for ($map = 0; $map < count($this->map_sudo); $map++)
        	{
            		// Не перебирать букву, если она уже заменена
            		if ($solve[$charindex] != $this->word[$charindex]) continue;
            		// Поиск позиции сотнесенной буквы
            		$subindex = $this->indexbychar($solve[$charindex], $map);
            		if ($subindex == -1) continue;
            		// Цикл по соотнесенным буквам, в рекурсии - цикл от последней соотнесенной буквы
            		for ($index = $sudoindex; $index < count($this->map_sudo[$map][$subindex]); $index++)
               		{
                    		// Получение нового слова для отображения
                    		$solve[$charindex] = $this->map_sudo[$map][$subindex][$index];
                    		// Если слово не содержит начальных букв и имеется в словаре, то запись в стек слов
                    		if ($this->str_compare($this->word, $solve)) {
                    			if ($this->str_dict($solve)) {
                           			$this->stack_push($solve);
                    			} else {
                        			if (count(@$this->trash[$this->stackIndex]) < self::maxSolveTrash) {
                            				$this->trash_push($solve);
                        			}
                        		}
                    		}
                    		// Переход к следующей букве слова в рекурсии
                    		$this->bruteforce($solve, $solveindex + 1, 0);
               		}
       		}
	}
} 

Теперь осталось только реализовать функции для перевода одного слова и целой строки. На этом реализацию класса можно считать законченной.

// Перевод одного слова
public function translateSolve($solve)
{
    	$solve = $this->translateTranslit($solve);
 
    	$this->stackIndex++;
    	$this->stack_push("");
    	$this->word = $solve;
 
    	if (strlen($solve) > self::maxSolveLength) {
        	$this->stack_push("<span class='errorsolve'>".$solve."</span>");
        	return false;
    	} else {
        	$this->bruteforce($solve, 0, 0);
    	}
}
 
// Перевод строки
public function translatePhrase()
{
    	if (!preg_match_all("/[a-zа-я]+/ms", $this->text, $phrase))
        	return false;
 
    	foreach($phrase[0] as $solve) {
        	$this->translateSolve($solve);
        }
}
} //Закрывающая скобка класса

Финишная прямая

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

$Translater = new Translater(@$_POST["textdata"]);
$stream = "";
 
foreach($Translater->stack as $stackindex => $stack) {
        $stream .= "<div class='output'>";
        foreach ($stack as $solveindex => $solve) {
                if ($solve != "" ) $stream .= $solve."<br>";
        }
        if (isset($Translater->trash[$stackindex])) {
                $stream .= "<hr><p>";
       	        foreach ($Translater->trash[$stackindex] as $trash) {
                        $stream .= $trash."<br>";
                }
                $stream .= "</p>";
         }
    	$stream .= "</div>";
}
 
$template = file_get_contents("default.html");
$template = str_replace("#TEXTDATA", $Translater->phrase, $template);
$template = str_replace("#STREAM", $stream, $template);
echo str_replace(array("rn", "  "), "", $template);

Вот, в принципе и все. Надеюсь, данный пример будет полезен и поможет хотя бы нескольким людям!

P.S. рабочий пример переводчика можно увидеть тут.

Автор: InvalidPointer

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js