Пишем синонимайзер на PHP

Категория: / DEV Блог / PHP (LAMP)
Определимся с форматом словаря.
Я для себя остановился на таком,

слово => синоним | синоним | синоним | ...
слово1 => синоним | синоним
слово2 => @слово
...


так как для работы с ним уже был написан код для другого проекта.

@собачка указывает что синоним является алиасом для другого синонима.


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

белок# || вытаращить арабские белки
Белый#, белоснежный, светлый, седой. Белее снега. См. чистый || профессор белой и черной магии, сделать белее снега, сказка о белом бычке, сказка про белого бычка
бельмес# || ни бельмеса, ни бельмеса не смыслить
бельмо# || вылупить бельма, выпучить бельма, выпялить бельма, вытаращить бельма, налить бельма
беляк (-чка)# [опрятный человек, щеголек (Даль, белый)] см. франт
бенефис# || устроить бенефис
Бередить#, вередить, растравлять, растрагивать, задевать. Ср. <Возбуждать>.
Бережливость#, бережь, домовитость, расчетливость, экономия. "Бережь лучше прибытка" (посл.).
Бережливый#, бережный, домовитый, расчетливый, экономный, хозяйственный, умеренный. Жить с расчетом (бережливо). Ср. <Скупой>. Прот. <Расточительный>. См. скупой
бережь# [береженье и бережливость; охрана, сохранение; осторожность (Даль)] см. бережливость, заботливость


Строчим код конвертора

Файл dicconv.php - Конвертор словаря

<?php
setlocale (LC_ALL, 'ru_RU.CP-1251');
#исходный словарь
$buffer = file_get_contents("original.txt");
#step1
$buffer = preg_replace("/\[.*\] см\./mU", "", $buffer);
$buffer = preg_replace("/\(.*\)/U", "", $buffer);
$buffer = preg_replace("/\<.*\>/U", "", $buffer);
$buffer = preg_replace("/^(.*)\..*$/mU", "$1", $buffer);
$buffer = str_replace('||', '', $buffer);
$buffer = str_replace(array('#,', '# ,'), '#', $buffer);
$buffer = str_replace('#', ' =>', $buffer);
$buffer = str_replace(',', ' |', $buffer);
$buffer = strtolower($buffer);
 
$abuffer = explode("\n", $buffer);
$kbuffer = array();
 
foreach ($abuffer as $v) {
$_v = explode('=>', $v);
$_k = trim($_v[0]);
$_ak = explode('|', $_k);
$_k = preg_replace("#^([\w[:space:]]+).*$#", "$1", $_k);
 
$_v = trim($_v[1]);
if (!empty($_k) && !empty($_v)) {
  $kbuffer[$_k] = $_v;
 
  if (count($_ak) > 1) {
    array_shift($_ak);
    foreach ($_ak as $_akv) { if (($__akv = trim($_akv)) && !empty($__akv)) $kbuffer[$__akv] = "@{$_k}"; }
  }
 
}
}
 
uksort($kbuffer, "kcmp");
#implode to text
$buffer = '';
unset($abuffer);
 
foreach ($kbuffer as $k => $v) {
$buffer .= sprintf("%s => %s\n", $k, $v);
}
 
$buffer = preg_replace("#[\040]{2,}#", ' ', $buffer);
 
#done - кладем словарь в файл dic.txt
file_put_contents('dic.txt', $buffer);
 
/// callback
function kcmp($a, $b) { return strlen($a) > strlen($b); }


Имеем - словарь original.txt. Запускаем скрипт php ./dicconvert.php, на выходе получаем файл dic.txt,
ключи (синонимы) упорядочены по длине фразы - так чтобы короткие слова заменялись сразу, длинные - в конце.

белок => вытаращить арабские белки
волос => шерсть | щетина | пух | пушок | ворса; вихор | грива | хохол | челка | чуб | шевелюра | коса | борода | усы | бакенбарды | бакены | баки | висок | кудер | косма | прядь
антик => редкость | чудак
впрок => заготовлять впрок | не впрок
багаж => вещи | кладь | поклажа | клажа | ноша | груз | фрахт
вкось => и вкривь | и вкось
беляк => франт
...


Этот файл съест в последствии наш обработчик словаря.

Класс synonimizer.class.php - Парсер словаря

Собственно библиотека для работы со словарем.

Класс может использоваться автономно, пример:

$syn = new synonimizer();
$result_text = $syn->syn("любой текст");


В конструкторе читаем файл dic.txt и разбирает его в миссив внутреннего фомата.
Для наблюдения за процессом предусмотрена константа DEBUG, установка которой выдаст при работе
в консоль отладочную информацию о выполненых заменах.

<?php
 
class synonimizer {
 
 const DEBUG = 0;
 
 const ROW_DELIM   = '=>';  
 const VALUE_DELIM = '|';  
 
 private $_dic_file = 'dic.txt';
 
 /**
 * @private array 'syn' = array(value, value, value)
 */

 private $parsed = array();
 
 
 function __construct() {
     
     $_parsed = file(dirname(__FILE__) . '/' . $this->_dic_file);
     
    foreach ($_parsed as $k => $v) {
      $v = trim($v);  
      if (empty($v) || 0 === strpos($v, '#')) {
        ; // nop
      }
      else {
        $v = explode(self::ROW_DELIM, $v);
        $_key = trim($v[0]);
        $_data = array();
        if (strpos($v[1], self::VALUE_DELIM) !== false) {
            $data = explode(self::VALUE_DELIM, $v[1]);
            foreach ($data as $dk => $dv) {
                $_data[$dk] = trim($dv);
            }
        }
        else {
            $_data = array(trim($v[1]));
            if (strpos($_data[0], '@') === 0) {
                // this is alias
                $_data = $this->parsed[substr($_data[0], 1)];
            }
        }
     
        // save
        $this->parsed[$_key] = $_data;
      }
    }
 }
 
 /**
 * Callback
 */

 static $_c_matches = false;
 static function syn_callback($matches) {
     $out = $matches[2];
     if (self::DEBUG) echo " -- " . $out . "\n";
     if (!empty(self::$_c_matches)) {
         $i = count(self::$_c_matches) - 1;
         $i = ($i > 0) ? mt_rand(0, $i) : 0;
         $out = self::$_c_matches[$i];
     }
     if (self::DEBUG)  echo " ++ " . $out . "\n\n";
     return $matches[1] . $out . $matches[3];
 }
 
 private static $index = 0;
 
 public function syn($text) {
 
   $text = ' ' . str_replace(array('\r\n', '\n'), "\r\n", $text) . ' ';
   self::$index++;
   echo ">>> " . self::$index . "\n";
   
   if (self::DEBUG) {
       echo str_repeat("-", 80) . "\n" . wordwrap($text, 80) . "\n" .  str_repeat("-", 80) . "\n\n";
    }
 
    foreach ($this->parsed as $key => $matches) {
        self::$_c_matches = &$matches;
        $text = preg_replace_callback(
        '#([^\w\d\-])(' . preg_quote($key) . ')([^\w\d\-])#i'// ([^\w\d\-]) //([\s\.\,])
        , 'synonimizer::syn_callback'
        , $text
        );
     
    }
 
    $text = str_replace("\r\n", '\r\n', substr($text, 1, -1));
    if (self::DEBUG) echo "--> " . wordwrap($text, 80) . " \n\n";
    return $text;
 }
}


Предполагается, что у нас имеется достаточно большой дамп таблицы в файле source.1251.sql.
В нем нам нужно отсинонимизировать поля title, text, description.

UPDATE `texts` SET `id` = 1,`title` = 'Под Тверью открылся рок-фестиваль "Эммаус-2006"',
`description` = 'За два дня здесь ожидается \nвыступление более 20 начинающих групп. Как говорят зрители \nфестиваля: "В его афише хорошие  музыканты, это привлекает, как \nи само место проведения, все здорово!".'
 WHERE  `ss_news_export_news`.`id` = 1;
 
UPDATE `texts` SET `id` = 2,`title` = 'Samsung представил новый игровой телефон',
`description` = 'Компания Samsung Electronics представила новый игровой сотовый телефон. Мобильник SCH-B450 будет отличаться стильным дизайном и широким набором мультимедийных функций. Видеоадаптер мобильника позволит запускать на SCH-B450 игры с трехмерной графикой, а сп',`category_id` = 2,
WHERE  `ss_news_export_news`.`id` = 2;
 
...


Файл synmake.php - Синонимайзер

На входе - дамп, на выходе - файл result.sql.
Разницу между файлами можно оценить diffом или под виндой например с помощью winmerge.

<?php
$source = "source.1251.sql";
require "synonimizer.class.php";
ob_implicit_flush(true);
 
$syn = new synonimizer();
$buffer = file_get_contents($source);
 
echo "working...\n";
 
  function kupper($matches) {
    return '. ' . ucfirst($matches[1]);
  }
 
 // функция обратного вызова
  function kreplace($matches)
  {
    GLOBAL $syn;
    $text = $syn->syn($matches[2]);
    $text = preg_replace_callback('#\. ([а-я]+)#', "kupper", $text);
    $text = ucfirst($text);
    $text = $matches[1] . $text . $matches[3];
    return $text;
  }
 
$buffer = preg_replace_callback(
              '#(`(?:title|text|description)` = \')(.*)(\')#U',
              "kreplace",
              $buffer);
 
 
file_put_contents('result.sql', $buffer);


Синонимайзер дополнительно сделает прописными все буквы слов, идущих после точки или стоящих в начале предложения.

Стартовый файл start.bat Для windows, юниксойды добавляют в начало #!/usr/local/bin/php и запускают

@echo off chcp 1251
php synmake.php


Запускаем, получаем результаты.