lexicon/Lexicon/Semantic.php
2025-12-06 01:00:01 +04:00

596 lines
22 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Lexicon: Библиотека для автоматической генерации иерархии понятийных уровней.
*
* @copyright Copyright (c) 2021, Zoviet <alexandr@asustem.ru>
* @version 0.1
* @link http://github.com/Zoviet/Lexicon
* @author Zoviet (Alexandr Pavlov / @Zoviet)
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @site https://Zoviet.github.io/
*/
/**
* Lexicon: класс грамматических преобразований слов и предложений. *
*
* Класс использует грамматические правила, а не словари. Исключение: для работы методов выделения уровней предметов и методов из текста используется Yandex.Speller. Класс хорошо подходит для работы со сложными, но хорошо формально структурированными текстами и высказываниями, а также со словами, для которых не существует словарных синонимов - например, сложными научными терминами.
*
*
* Функционал:
*
* 1. Выделение в тексте частей речи
* 2. Удаление окончаний у слов по двум алгоритмам (Мартина Поттера и по словарю окончаний)
* 3. Преобразование текста в массив слов
* 4. Очистка текста от незначимых частей речи
* 5. Поиск в тексте объекта, подлежащего и определения
* 6. Образование словоформ от стэмов слов
* 7. Преобразование прилагательных, являющимися определениями в тексте, в существительные
* 8. Преобразование существительных, являющихся субъектами в тексте, в прилагательные
* 9. Преобразование слов из одной части речи в другую
* 10. Добавление оконечных суффиксов к стэмам
* 12. Образование возможных суффиксальных словоформ от слова *
*
*/
namespace Lexicon;
require 'Speller.php';
use \Stem\LinguaStemRu;
class Semantic {
/*
Ошибки класса
*/
protected const ERRORS = array(
'S01' => 'Для семантической обработки необходимо передать строку',
);
/*
Знаки алфавита
*
*/
private const ALPHABET = 'АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯя';
/*
Группы окончаний:
*
*/
private const LEMMS = [
/*
Прилагательные
*/
'ADJS' => array ('ее','ие','ые','ое','ими','ыми','ей','ий','ый','ой','ем','им','ым','ом', 'его','ого','ему','ому','их','ых','ую','юю','ая','яя','ою','ею','кий','кый','тий','тый','вий','вый','кие','кые','тые','вие','вые','вший'),
/*
Причастия
*/
'PARTS' => array ('ивш','ывш','ующ','ем','нн','вш','ющ','ущи','ющи','ящий','щих','щие','ляя'),
/*
Глаголы
*/
'VERBS' => array ('ила','ыла','ена','ейте','уйте','ите','или','ыли','ей','уй','ил','ыл','им','ым','ен', 'ило','ыло','ено','ят','ует','уют','ит','ыт','ены','ить','ыть','ишь','ую','ю','ла','на','ете','йте', 'ли','й','л','ем','н','ло','ет','ют','ны','ть','ешь','нно'),
/*
Существительные
*/
'NOUNS' => array ('а','ев','ов','ье','иями','ями','ами','еи','ии','и','ией','ей','ой','ий','й','иям','ям','ием','ем','ам','ом','о','у','ах','иях','ях','ы','ь','ию','ью','ю','ия','ья','я','ок', 'мва', 'яна', 'ровать','ег','ги','га','сть','сти','ики','ик'),
/*
Наречия
*/
'ADVS' => array ('чно', 'еко', 'соко', 'боко', 'роко', 'имо', 'мно', 'жно', 'жко','ело','тно','льно','здо','зко','шо','хо','но','сегодня','завтра','вчера'),
/*
Числительные
*/
'NUMS' => array ('чуть','много','мало','еро','вое','рое','еро','сти','одной','двух','рех','еми','яти','ьми','ати','дного','сто','ста','тысяча','тысячи','две','три','одна','умя','тью','мя','тью','мью','тью','одним'),
/*
Союзы
*/
'UNIS' => array ('более','менее','очень','крайне','скоре','некотор','кажд','други','котор','когд','однак', 'если','чтоб','хот','смотря','как','также','так','зато','что','или','потом','эт','тог','тоже','словно', 'ежели','кабы','коли','ничем','чем'),
/*
Предлоги
*/
'PRES' => array ('в','на','по','из','и','до')
];
/*
* Суффиксы
*
*/
private const SUFFIX = [
/*
* Образующие прилагательные
*/
'ADJS' => array('оватеньк','еватеньк','ехоньк','охоньк','ешеньк','ошеньк','ическ','тельн','остьн','альн','отан','оньк','еньк','евит','оват','овит','овск','енск','инск','ческ', 'еват','еск','ляв','льн','чат','янн','ист', 'озн', 'ивн', 'чив','лив','ов','ев','ив','ав','яв','ев','ск','ин','ич','яч','ущ','ан','ой','ый','ий','н'),
/*
* Образующие существительные
*/
'NOUNS' => array('','мость','льник','льщик','чанин','ность','итель','ство','ость','ация','яция','льня','овец','овеч','евич','анец','янец','анин','янин','ина','тор','ник','ант','ние','нье','ент','льн','чик','щик','изм','ура','лка','ист','ец','ач','аж','ло','ие','ье','ка','ин'),
];
/*
Интерпретация результатов
*
* Части речи
*/
public const TYPES = [
'UNKN' => 'Не определено',
'PRES' => 'Предлог',
'NUMS' => 'Числительное',
'ADVS' => 'Наречие',
'NOUNS' => 'Существительное',
'VERBS' => 'Глагол',
'PARTS' => 'Причастие',
'ADJS' => 'Прилагательное',
'UNIS' => 'Союзы'
];
/*
* Члены предложения
*/
public const PARTS = [
'UNKN' => 'Не определено',
'SUBJ' => 'Подлежащее',
'ADDN' => 'Дополнение',
'PRED' => 'Сказуемое',
'DFN' => 'Определение',
'CIRC' => 'Обстоятельство'
];
/**
* Свойства
* @var protected string string Принимаемая строка для обработки.
* @var public string words Слова строки.
* @var public array result Массив слов по частям речи (в зависимости от глубины обработки: лишенных оснований или нет).
* @var private int deep Текущая глубина анализа
*
*/
protected $string;
public $words = array();
public $result = array();
private $deep = 0;
/**
* Конструктор
* @param string данные для обработки
* @return void
*/
public function __construct($string=NULL) {
mb_internal_encoding('UTF-8');
if (!empty($string)) {
$this->text($string);
}
}
/**
* Запуск методов конвейера через запрос свойств
* @return void
*/
public function __get($name) {
call_user_func(array($this, $name));
}
/**
* Установка текста для анализа
* @return void
*/
public function text($string) {
if (!is_string($string)) {
throw new \Exception(self::ERRORS['S01']);
} else {
$this->words = array();
$this->result = array();
$this->string = trim($string);
}
return $this;
}
/**
* Отбрасывание из строки скобок вместе с содержимым
*/
public function remove_braces() {
$this->string = preg_replace('#\(.*?\)#is','',$this->string);
return $this;
}
/**
* Преобразование в массив слов
*/
public function words($array=NULL) {
$this->words = (empty($array)) ? str_word_count($this->string,1,self::ALPHABET) : $array;
return $this;
}
/**
* Очистка массива слов от предлогов
*/
public function remove_pres() {
$this->remover(self::LEMMS['PRES']);
return $this;
}
/**
* Очистка массива слов от союзов
*/
public function remove_unis() {
$this->remover(self::LEMMS['UNIS']);
return $this;
}
/**
* Очистка массива слов от числительных
*/
public function remove_nums() {
$this->remover(self::LEMMS['NUMS']);
return $this;
}
/**
* Очистка массива слов от союзов, числительных и предлогов
*/
public function remove_all() {
$this->remove_nums()->remove_pres()->remove_unis();
return $this;
}
/**
* Очистка массива слов от элементов, входящих в переданный массив
*/
public function remover($array) {
if (isset($this->words)) {
foreach ($this->words as $key=>$value) {
if (in_array($value,$array)) unset($this->words[$key]);
}
$this->words = array_values($this->words);
}
return $this;
}
/**
* Разбор массива слов на части речи
*/
public function explore() {
if (empty($this->words)) $this->words();
foreach ($this->words as $word) {
$this->result[self::test_word($word)][] = $word;
}
return $this;
}
/**
* Базовая обработка слова
*/
public static function prepare_word($word) {
$word = trim(mb_strtolower($word));
return str_replace('ё', 'е', $word);
}
/**
* Определение части речи слова, вторым параметром передается необходимость интерпретации в текстовом виде
*/
public static function test_word($word,$interpretate=FALSE) {
$result[0] = 'UNKN'; //результат по умолчанию: 'не определено'
$word = self::prepare_word($word);
$lenght = mb_strlen($word);
foreach (self::LEMMS as $name=>$set) {
foreach ($set as $lemma) {
$lemma_len = mb_strlen($lemma);
$ver = round(($lemma_len/$lenght)*100); //доверительная вероятность каждого результата
switch ($name) {
case 'PARTS': //причастие
if (mb_strpos($word,$lemma)>=(round(2*$lenght)/5)) {
$result[$ver] = $name; //результаты храним в массиве, где длина совпадения леммы = доверительной вероятности
break 2;
}
break;
case 'UNIS': //союзы
if (mb_substr($word,0,$lemma_len)==$lemma) {
$result[$ver] = $name;
break 2;
}
break;
case 'PRES': //предлоги
if ($word == $lemma) {
$result[$ver] = $name;
break 2;
}
break;
default: //во всех остальных случаях
if ($word == $lemma or mb_substr($word,-mb_strlen($lemma)) == $lemma) {
$result[$ver] = $name;
break;
}
}
}
}
ksort($result); //выбираем результат с наибольшей доверительной вероятностью
$result = array_pop($result);
$result = ($interpretate) ? self::TYPES[$result] : $result;
return $result;
}
/**
* Избавление слов из массива слов по типам от окончаний по словарю по массиву типов
*/
protected function remove_endings() {
if(empty($this->result)) $this->explore();
foreach ($this->result as $type=>$set) {
foreach ($set as $key=>$word) {
$this->result[$type][$key] = self::remove_ending($word,$type);
}
}
return $this;
}
/**
* Избавление слов из массива слов по типам от окончаний по словарю по массиву слов
*/
protected function remove_words_endings() {
if(empty($this->words)) $this->words();
foreach ($this->words as $key=>$word) {
$this->words[$key] = self::remove_ending($word);
}
return $this;
}
/**
* Обертка для методов избавления от окончаний, не чувствительная к типу обработки
*/
public function stemming() {
if(!empty($this->result)) {
$this->remove_endings();
} else {
$this->remove_words_endings();
}
return $this;
}
/**
* Избавление слова от окончаний по словарю, второй параметр - указатель на тип слова
* Возвращает обрезанное слово
*/
public static function remove_ending($word,$type=FALSE) {
$word = self::prepare_word($word);
$w_end = '';
if (empty($type)) $type = self::test_word($word);
if ($type!=='UNKN' and $type!=='PARTS') {
foreach (self::LEMMS[$type] as $lemma) {
if (mb_substr($word,-mb_strlen($lemma)) == $lemma) {
$w_end = mb_substr($word,0, mb_strlen($word)-mb_strlen($lemma));
break;
}
}
if (mb_strlen($w_end) == 0) $w_end = $word;
} else {
$w_end = self::stem($word);
}
return $w_end;
}
/**
* Стемминг слова по алгоритму Мартина Портера
*/
public static function stem($word) {
$stem = new \Stem\LinguaStemRu;
return $stem->stem_word($word);
}
/**
* Выделение значимого существительного из массива слов.
* Если не найдено, возвращает NULL.
*/
public function subject() {
$result = NULL;
if(empty($this->result)) $this->explore(); //если нет никакого предварительного результата, не с окончаниями, ни без
if (isset($this->result['NOUNS'][$this->deep])) {
$result = $this->result['NOUNS'][$this->deep];
}
return $result;
}
/**
* Выделение значимого прилагательного из массива слов.
* Если не найдено, возвращает NULL.
*/
public function definition() {
$result = NULL;
if(empty($this->result)) $this->explore(); //если нет никакого предварительного результата, не с окончаниями, ни без
if (isset($this->result['ADJS'][$this->deep])) {
$count_nouns = count($this->result['NOUNS']); //количество существительных
$count_adjs = count($this->result['ADJS']); //количество прилагательных
$count_verbs = count($this->result['VERBS']); //количество глаголов
$words = count($this->words); //длина фразы
foreach ($this->result['ADJS'] as $adj) {
if (mb_substr($adj,-2) == 'ий' or mb_substr($adj,-2) == 'ый') {
$result = $adj;
break;
}
}
$result = (empty($result)) ? $this->result['ADJS'][$this->deep] : $result;
}
return $result;
}
/**
* Выделение значимого глагола из массива слов.
* Если не найдено, возвращает NULL.
*/
public function predict() {
$result = NULL;
if(empty($this->result)) $this->explore(); //если нет никакого предварительного результата, не с окончаниями, ни без
if (isset($this->result['VERBS'][$this->deep])) {
$result = $this->result['VERBS'][$this->deep];
}
return $result;
}
/**
* Получение удаленного окончания слова сравнением строк (т.к. могут использоваться разные методы избавления от окончаний)
*/
protected function get_ending($word,$stem) {
return trim(str_replace($stem,'',$word));
}
/**
* Трансформация слова в другой тип
* Возвращает FALSE если преобразование невозможно,
* NULL - если преобразовать не удалось
* Наиболее вероятный вариант трансформации если list = false
* Массив всех преобразований (словоформ) если list = true
* word_type - прямое указание на тип обрабатываемого слова
*/
public static function transform($word,$type='ADJS',$list=FALSE) {
$type_word = (!empty($word)) ? self::test_word($word) : NULL;
if (($type_word == 'ADJS' or $type_word == 'NOUNS') and ($type == 'ADJS' or $type == 'NOUNS')) {
$stem = self::remove_suffix($word,$type_word);
$stem = self::add_suffix($stem,$type,$list);
} else {
$stem = FALSE;
}
return $stem;
}
/**
* Трансформация слова из прилагательного в существительное
*/
public function to_noun($list=FALSE) {
$adj = $this->definition();
return (!empty($adj)) ? self::transform($adj,'NOUNS',$list) : NULL;
}
/**
* Трансформация слова из существительного в прилагательное
*/
public function to_adj($list=FALSE) {
$noun = $this->subject();
return (!empty($noun)) ? self::transform($noun,'ADJS',$list) : NULL;
}
/**
* Комплексный глубокий анализ фразы, возвращает массив преобразованных существительных и прилагательных
* Принимает глубину анализа или строку или bool - в этом случае глубина принимается относительно размера словаря.
* Отдает массив уровней - образованных прилагательных и предметов - образованных существительных
*/
public function analise($deep=0) {
if (!is_numeric($deep)) { //автоматический
if(empty($this->words)) $this->words();
$this->deep = round(count($this->words)/5);
}
$return = array();
for ($this->deep;$this->deep<=$deep;$this->deep++) {
$return['level'][$this->deep] = $this->to_adj();
$return['subject'][$this->deep] = $this->to_noun();
}
$this->deep = 0;
$return['level'] = array_diff($return['level'], array(null));
$return['subject'] = array_diff($return['subject'], array(null));
return $return;
}
/**
* Рекурсивное убирание суффиксов по спискам вложенности.
* Возвращает либо слово, либо массив.
* Если не найдено, возвращает NULL.
*/
public static function remove_suffix($word=NULL,$type='ADJS') {
$stem = self::remove_ending($word,$type); //убираем окончание
foreach (self::SUFFIX[$type] as $suffix) {
if (mb_substr($stem,-mb_strlen($suffix)) == $suffix) {
$stem = str_replace($suffix,'',$stem);
$stem = self::remove_suffix(self::stem($stem));
break;
}
}
return $stem;
}
/**
* Добавление окончаний к стэмму с проверкой на существование
* Второй параметр - получение всего списка образованных словоформ или одного наиболее вероятного
* Возвращает либо слово, либо массив.
* Если не найдено, возвращает NULL.
*/
public static function add_suffix($stem,$type='ADJS',$list=FALSE) {
$return = NULL;
$forms = self::forms($stem,$type);
if (!empty($forms)) {
foreach ($forms as $form) {
$data = Speller::get_data($form);
if (is_array($data) and empty($data) and $list!==TRUE) {
return $form;
} else {
if (!empty($data[0]->s[0])) {
foreach ($data[0]->s as $word) {
$word = explode(' ',$word)[0];
if (self::test_word($word)==$type) {
$return[] = ($type=='ADJS') ? $word : self::stem($word);
}
}
}
}
}
}
if (is_array($return)) {
$return = array_count_values($return);
if (max($return)>1) arsort($return);
if ($list!==TRUE) {
$return = array_keys($return);
$return = array_shift($return);
}
}
return $return;
}
/**
* Образование "грязных" словоформ по типу от стемма
*/
public static function forms($stem,$type) {
$forms = NULL;
if (is_string($stem)) {
foreach (self::SUFFIX[$type] as $suffix) {
if ($type=='ADJS') {
$forms[] = $stem.$suffix.'ий';
$forms[] = $stem.$suffix.'ый';
} else {
$forms[] = $stem.$suffix;
}
}
}
return $forms;
}
}