Недавно мне нужно было написать парсер для строки поиска, который приводит строки вида
(aa&bb)^(!cc^!(dd^ee)) в строку вида куска SQL: (?f LIKE "%aa%" AND ?f LIKE "%bb%") OR (?f NOT LIKE "%cc%" OR !((?f LIKE "%dd%" OR ?f LIKE "%ee%")) ). Я написал like и SQL для упращения, на самом деле там был SPHINX, да и не оптребовалось оно в конце концов, но разговор о том как я этого добился написав формальные грамматики и реализовав их на PHP.
Коль уж зашли в пост, то откроюсь пред вами: несмотря на высшее математическое образование я делал это в первый раз, а до этого слыхивал, но не видывал как это делается. Поэтому прошу всех высоколобых математиков и гуру закидывать меня правильными решениями и всячески поправлять, а пока что я расскажу о своих результатах и о том как я к ним пришел.
Было несколько вариантов достичь цели:
первое — это написать рекурсивную (вспомним "?R") регуляру, но я отбросил такой вариант как «не спортивный», о да покарают меня за это лемминги =)
второе — это написать построение дерева вручную, но как-то уж больно много писать пришлось бы да и как-то больно топорно.
третье — это вспомнить про слова «формальная грамматика». Да уж страшными они кажутся на первый взгляд, однако на практике все намного лучше и полезнее — его я и избрал. Повторюсь, что это первая моя реализация подобной концепции так что указывайте мне смело на мои ошибки!
итак приступим:
Составим алфавит
Алфавит = {(,),!,&,^,(,),a-Z, пробел тут,-,_, а-Я}
составим метаалфавит под нашу задачу с учетом приоритетов операций (! приоритетнее & и ^, а последние одинакового приоритета)
F -> T|T&F|T^F
T* -> I|!I|!S
I -> (F)|S
S -> C|SC
C -> [a-Z_ а-Я-]
Это правила по которым формируются элементы метаалфавита снизу вверх
F — формула, T — терм, I — итем, S- строка, C — символ. Назвал как понравилось.
* в T внесен !S чтобы "! бала бала бла" трактовать как ?f NOT LIKE "%бала бала бла%", а не как !(?f LIKE "%бала бала бла%"), короче можно и без нее.
Как видно грамматика, содержащая разбор отрицания (!) стоит ниже грамматики содержащей разбор других операций (& и ^), а грамматика содержащая разбор скобок еще ниже — таким образом формируется приоритет операций.
у меня для такой задачи получилась контекстно не зависимая грамматика G2 по иерархии Хомского — простая короче, но не такая простая как конечный автомат (поэтому чистыми «регулярами», без рекурсий и не решаются такие задачи).
Теперь осталось все это реализовать. Не думаю что интересно как я кодил, поэтому не томя читателя выложу конечный код, замечу лишь что в нем не реализован поиск синтаксических ошибок (Хотя душа заболела и разбор пары самых вопиющих ошибок я все же добавил в код).
посмотреть пример:
UPD сделал раскраску кода — спасибо s-c.me
(aa&bb)^(!cc^!(dd^ee)) в строку вида куска SQL: (?f LIKE "%aa%" AND ?f LIKE "%bb%") OR (?f NOT LIKE "%cc%" OR !((?f LIKE "%dd%" OR ?f LIKE "%ee%")) ). Я написал like и SQL для упращения, на самом деле там был SPHINX, да и не оптребовалось оно в конце концов, но разговор о том как я этого добился написав формальные грамматики и реализовав их на PHP.
Коль уж зашли в пост, то откроюсь пред вами: несмотря на высшее математическое образование я делал это в первый раз, а до этого слыхивал, но не видывал как это делается. Поэтому прошу всех высоколобых математиков и гуру закидывать меня правильными решениями и всячески поправлять, а пока что я расскажу о своих результатах и о том как я к ним пришел.
Было несколько вариантов достичь цели:
первое — это написать рекурсивную (вспомним "?R") регуляру, но я отбросил такой вариант как «не спортивный», о да покарают меня за это лемминги =)
второе — это написать построение дерева вручную, но как-то уж больно много писать пришлось бы да и как-то больно топорно.
третье — это вспомнить про слова «формальная грамматика». Да уж страшными они кажутся на первый взгляд, однако на практике все намного лучше и полезнее — его я и избрал. Повторюсь, что это первая моя реализация подобной концепции так что указывайте мне смело на мои ошибки!
итак приступим:
Составим алфавит
Алфавит = {(,),!,&,^,(,),a-Z, пробел тут,-,_, а-Я}
составим метаалфавит под нашу задачу с учетом приоритетов операций (! приоритетнее & и ^, а последние одинакового приоритета)
F -> T|T&F|T^F
T* -> I|!I|!S
I -> (F)|S
S -> C|SC
C -> [a-Z_ а-Я-]
Это правила по которым формируются элементы метаалфавита снизу вверх
F — формула, T — терм, I — итем, S- строка, C — символ. Назвал как понравилось.
* в T внесен !S чтобы "! бала бала бла" трактовать как ?f NOT LIKE "%бала бала бла%", а не как !(?f LIKE "%бала бала бла%"), короче можно и без нее.
Как видно грамматика, содержащая разбор отрицания (!) стоит ниже грамматики содержащей разбор других операций (& и ^), а грамматика содержащая разбор скобок еще ниже — таким образом формируется приоритет операций.
у меня для такой задачи получилась контекстно не зависимая грамматика G2 по иерархии Хомского — простая короче, но не такая простая как конечный автомат (поэтому чистыми «регулярами», без рекурсий и не решаются такие задачи).
Теперь осталось все это реализовать. Не думаю что интересно как я кодил, поэтому не томя читателя выложу конечный код, замечу лишь что в нем не реализован поиск синтаксических ошибок (Хотя душа заболела и разбор пары самых вопиющих ошибок я все же добавил в код).
Copy Source | Copy HTML
-
- class GrammarParcer2SQLToken{
- private $string = '';
- private $index = 0;
- private $sql_token = '';
- private $error = null;
- public function __construct($s=null){
- if (!empty($s))
- $this->string = $s;
- $this->index = 0;
- $this->error = null;
- }
-
- public function parce($s=null, $e=null){
- if (!empty($s))
- $this->string = $s;
- $this->index = 0;
- $this->error = null;
- $this->F();
- $e = $this->error;
- return $this->sql_token;
- }
-
- public function getError(){
- return $this->error;
- }
-
- protected function setError($mes){
- $this->error = $mes;
- }
-
- protected function isEnd(){
- if (!empty($this->error) or $this->index >= strlen($this->string))
- return true;
- return false;
- }
-
- protected function F(){
- $this->T();
- if ($this->isEnd())
- return;
- if ($this->string{$this->index} == '&'){
- $this->sql_token.= ' AND ';
- }elseif ($this->string{$this->index} == '^' or $this->string{$this->index} == '|'){
- $this->sql_token.= ' OR ';
- }else{
- return;
- }
- $this->index++;
- $this->F();
- }
-
- protected function T(){
- $this->I();
- if ($this->isEnd())
- return;
- if ($this->string{$this->index} == '!'){
- $this->index++;
- if (!$this->S('invert')){
- $this->sql_token.= '!(';
- $this->I();
- $this->sql_token.= ')';
- }
- }
- }
-
- protected function I(){
- if ($this->string{$this->index} == '('){
- $this->sql_token.= '(';
- $this->index++;
- $this->F();
- if ($this->string{$this->index} !== ')'){
- $this->setError('parse error, expected ")" on offset: '.$this->index);
- //нет закрывающей скобки.
- }
- $this->sql_token.= ')';
- $this->index++;
- }else{
- $this->S();
- }
- }
-
- protected function S($invert=false){
- if ($this->isEnd())
- return false;
- $string='';
- while(!$this->isEnd()){
- if (preg_match('~[0-9a-zа-я_ -]~i', $this->string{$this->index})){
- $string.=$this->string{$this->index};
- $this->index++;
- continue;
- }elseif (!preg_match('~[&)(!|^]~i', $this->string{$this->index})){
- $this->setError('parse error, unexpected "'.$this->string{$this->index}.'" on offset: '.$this->index);
- //ненормальный символ =)
- }
- break;
- }
- if (empty($string))
- return false;
- $this->sql_token.= '?f '.($invert?'NOT ':'').'LIKE "%'.mysql_escape_string($string).'%"';
- return true;
- }
- }
посмотреть пример:
Copy Source | Copy HTML
- $input = '(aa&bb)^(!cc^!(dd^ee))';
- $parcer = new GrammarParcer2SQLToken();
- echo $parcer->parce($input);
- echo "\n".$parcer->getError();
UPD сделал раскраску кода — спасибо s-c.me