Я продолжаю очень медленно и по чуть-чуть пилить "химический поиск" ChWiSe.Net. И хотя он пока что в стостоянии протипа, некоторый ненулевой поток запросов есть. Я заметил, что пользователи не всегда хотят использовать SMILES-нотацию. Думаю, она не особо известна за пределами сообщества професионалов-химиков и хемоинформатиков. Вместо этого пользователи пытаются найти вещества по обычной полуструктурной формуле, например, всем известный этиловый спирт пытаются найти запросом C2H5OH вместо SMILES-формулы CCO. Что ж, надо поддержать и такие запросы.
Нам понадобится создать парсер таких формул. Для формул вида (NH4)2CO3, Al2(SO4)3, (CH3)2O или CH3COOH необходимо правильно разобрать все скобочки, числовые коэфициенты, выделить отдельные химические элементы и группы атомов, такие, как сульфат-анион, и использовать эти выделенные составляющие формулы для поиска похожих веществ.
Я решил реализовать парсер формул в соответствии с очень простой грамматикой: терминалами являются круглые скобки, числа, символы химических элементов и "стандартные" группы атомов ( COOH, OH, SO4, и.т.д. ). Правил вывода всего пять:
ELEMENT -> elementToken
ELEMENTARYGROUP -> elementaryGroupToken
GROUP -> '(' FORMULA ')'
TERM -> (GROUP|ELEMENTARYGROUP|ELEMENT) NUMBER?
FORMULA -> TERM*
Человеческим языком это можно описать так (снизу вверх):
Для своих целей я выбрал JavaCC (Java Compiler Compiler). Этот простой генератор парсеров является рабочей лошадкой во многих проектах. Из open source могу упомянуть парсеры поисковых запросов Lucene и Solr.
Описание парсера состоит из двух основных частей: описания сканера и собственно парсера. Сканер осуществляет выделение из поступающей на вход строки отдельных терминалов. У меня он определяется так:
Секция TOKEN определяет известные парсеру типы токенов. У меня их 5 типов: открывающаяся скобка, закрывающаяся скобка, число (NUM), символ химического элемента (ELEMENT), и то, что я называю "элементарная группа" (ELEMENTARYGROUP).
Во второй важнейшей часть необходимо описать правила вывода парсера. В JavaCC эти правила описываются "декларативно" с примесью Java-кода. Пять правил, описанных выше на языке JavaCC записывются так:
Файл описания грамматики целиком можно посмотреть здесь.
Каждое такое правило сгенерирует одноимённый метод, который будет сопостовлять фрагменту строки соответствующий элемент грамматики. В случае успешного сопоставления будет вызываться пользовательский java-код. В моём случае пользовательский код строит некое синтаксическое дерево формулы.
Нам понадобится создать парсер таких формул. Для формул вида (NH4)2CO3, Al2(SO4)3, (CH3)2O или CH3COOH необходимо правильно разобрать все скобочки, числовые коэфициенты, выделить отдельные химические элементы и группы атомов, такие, как сульфат-анион, и использовать эти выделенные составляющие формулы для поиска похожих веществ.
Я решил реализовать парсер формул в соответствии с очень простой грамматикой: терминалами являются круглые скобки, числа, символы химических элементов и "стандартные" группы атомов ( COOH, OH, SO4, и.т.д. ). Правил вывода всего пять:
ELEMENT -> elementToken
ELEMENTARYGROUP -> elementaryGroupToken
GROUP -> '(' FORMULA ')'
TERM -> (GROUP|ELEMENTARYGROUP|ELEMENT) NUMBER?
FORMULA -> TERM*
Человеческим языком это можно описать так (снизу вверх):
- Формула (FORMULA) это некоторое количество идущих друг за другом "термов" (TERM). Звёздочка после элемента обозначает некоторое (возможно, нулевое) количество повторений этого элемента.
- Терм (TERM) это или группа (GROUP) или "элементарная группа" (ELEMENTARYGROUP), или элемент (ELEMENT) после чего может следовать число (NUMBER). Числа может и не быть: знак вопроса после NUMBER обозначает что этот элемент необязателен.
- Группа (GROUP) - это формула (FORMULA) в скобках.
- Элементарная группа (ELEMENTARYGROUP) - терминал elementaryGroup
- Элемент (ELEMENT) - терминал символа химического элемента
Для своих целей я выбрал JavaCC (Java Compiler Compiler). Этот простой генератор парсеров является рабочей лошадкой во многих проектах. Из open source могу упомянуть парсеры поисковых запросов Lucene и Solr.
Описание парсера состоит из двух основных частей: описания сканера и собственно парсера. Сканер осуществляет выделение из поступающей на вход строки отдельных терминалов. У меня он определяется так:
Секция TOKEN определяет известные парсеру типы токенов. У меня их 5 типов: открывающаяся скобка, закрывающаяся скобка, число (NUM), символ химического элемента (ELEMENT), и то, что я называю "элементарная группа" (ELEMENTARYGROUP).
Во второй важнейшей часть необходимо описать правила вывода парсера. В JavaCC эти правила описываются "декларативно" с примесью Java-кода. Пять правил, описанных выше на языке JavaCC записывются так:
Файл описания грамматики целиком можно посмотреть здесь.
Каждое такое правило сгенерирует одноимённый метод, который будет сопостовлять фрагменту строки соответствующий элемент грамматики. В случае успешного сопоставления будет вызываться пользовательский java-код. В моём случае пользовательский код строит некое синтаксическое дерево формулы.
Генерация парсера
Вызов этой команды сгенерирует несколько *.java файлов:
SemistructuredFormulaParserConstants.java
SemistructuredFormulaParser.java
SemistructuredFormulaParserTokenManager.java
SimpleCharStream.java
Token.java
TokenMgrError.java
Эти файлы можно включить в свой проект руками, но обычно этот процесс автоматизируют. Например, с помощью системы сборки Maven.
Проект Maven
Шаги, описанные выше могут быть включены в проект maven. Следует положить *.jj файл в поддиректорию $PROJECT_ROOT/src/main/javacc, где $PROJECT_ROOT - корневая директория проекта. В pom.xml следует включить такую секцию: