Преобразование одного формата запроса в другой

Я в растерянности. Я пытался заставить это работать несколько дней. Но с этим я никуда не деться, поэтому я решил проконсультироваться с вами, ребята, и посмотреть, сможет ли кто-нибудь мне помочь!

Я использую pyparsing в попытке разобрать один формат запроса в другой. Это не простое преобразование, но на самом деле требует мозгов:)

Текущий запрос выглядит следующим образом:

("breast neoplasms"[MeSH Terms] OR breast cancer[Acknowledgments] 
OR breast cancer[Figure/Table Caption] OR breast cancer[Section Title] 
OR breast cancer[Body - All Words] OR breast cancer[Title] 
OR breast cancer[Abstract] OR breast cancer[Journal]) 
AND (prevention[Acknowledgments] OR prevention[Figure/Table Caption] 
OR prevention[Section Title] OR prevention[Body - All Words] 
OR prevention[Title] OR prevention[Abstract])

И с помощью pyparsing я смог получить следующую структуру:

[[[['"', 'breast', 'neoplasms', '"'], ['MeSH', 'Terms']], 'or',
[['breast', 'cancer'], ['Acknowledgments']], 'or', [['breast', 'cancer'],
['Figure/Table', 'Caption']], 'or', [['breast', 'cancer'], ['Section', 
'Title']], 'or', [['breast', 'cancer'], ['Body', '-', 'All', 'Words']], 
'or', [['breast', 'cancer'], ['Title']], 'or', [['breast', 'cancer'], 
['Abstract']], 'or', [['breast', 'cancer'], ['Journal']]], 'and', 
[[['prevention'], ['Acknowledgments']], 'or', [['prevention'], 
['Figure/Table', 'Caption']], 'or', [['prevention'], ['Section', 'Title']], 
'or', [['prevention'], ['Body', '-', 'All', 'Words']], 'or', 
[['prevention'], ['Title']], 'or', [['prevention'], ['Abstract']]]]

Но сейчас я в растерянности. Мне нужно отформатировать вышеприведенный вывод в поисковый запрос lucene. Вот краткий пример необходимых преобразований:

"breast neoplasms"[MeSH Terms] --> [['"', 'breast', 'neoplasms', '"'], 
['MeSH', 'Terms']] --> mesh terms: "breast neoplasms"

Но я застрял прямо там. Мне также нужно иметь возможность использовать специальные слова AND и OR.

итоговый запрос может быть таким: сеточные термины: "новообразования молочной железы" и профилактика

Кто может мне помочь и подсказать, как это решить? Любая помощь будет оценена.

Так как я использую pyparsing, я склонен к python. Я вставил приведенный ниже код, чтобы вы могли поиграть с ним и не начинать с 0!

Большое спасибо за помощь!

def PubMedQueryParser():
    word = Word(alphanums +".-/&§")
    complex_structure = Group(Literal('"') + OneOrMore(word) + Literal('"')) + Suppress('[') + Group(OneOrMore(word)) + Suppress(']')
    medium_structure = Group(OneOrMore(word)) + Suppress('[') + Group(OneOrMore(word)) + Suppress(']')
    easy_structure = Group(OneOrMore(word))
    parse_structure = complex_structure | medium_structure | easy_structure
    operators = oneOf("and or", caseless=True)
    expr = Forward()
    atom = Group(parse_structure) + ZeroOrMore(operators + expr)
    atom2 = Group(Suppress('(') + atom + Suppress(')')) + ZeroOrMore(operators + expr) | atom
    expr << atom2
    return expr

1 ответ

Решение

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

Когда вы начинаете с такого проекта, напишите BNF синтаксиса, который вы хотите проанализировать. Это не должно быть супер строгим, на самом деле, это начало, основанное на том, что я вижу из вашего примера:

word :: Word('a'-'z', 'A'-'Z', '0'-'9', '.-/&§')
field_qualifier :: '[' word+ ']'
search_term :: (word+ | quoted_string) field_qualifier?
and_op :: 'and'
or_op :: 'or'
and_term :: or_term (and_op or_term)*
or_term :: atom (or_op atom)*
atom :: search_term | ('(' and_term ')')

Это довольно близко - у нас есть небольшая проблема с некоторой возможной неопределенностью между word и and_op а также or_op выражения, так как "и" и "или" соответствуют определению слова. Нам нужно будет ужесточить это во время реализации, чтобы убедиться, что "рак, рак, лимфома или меланома" читаются как 4 различных поисковых запроса, разделенных "или", а не только одним большим термином (который, я думаю, является вашим текущим парсер бы сделал). Мы также получаем преимущество распознавания приоритета операторов - может быть, это не является строго необходимым, но давайте пока с этим поговорим.

Преобразование в pyparsing достаточно просто:

LBRACK,RBRACK,LPAREN,RPAREN = map(Suppress,"[]()")
and_op = CaselessKeyword('and')
or_op = CaselessKeyword('or')
word = Word(alphanums + '.-/&')

field_qualifier = LBRACK + OneOrMore(word) + RBRACK
search_term = ((Group(OneOrMore(word)) | quoted_string)('search_text') + 
               Optional(field_qualifier)('field'))
expr = Forward()
atom = search_term | (LPAREN + expr + RPAREN)
or_term = atom + ZeroOrMore(or_op + atom)
and_term = or_term + ZeroOrMore(and_op + or_term)
expr << and_term

Чтобы устранить неоднозначность "или" и "и", мы ставим негативный взгляд в начало слова:

word = ~(and_op | or_op) + Word(alphanums + '.-/&')

Чтобы придать некоторую структуру результатам, заверните Group классы:

field_qualifier = Group(LBRACK + OneOrMore(word) + RBRACK)
search_term = Group(Group(OneOrMore(word) | quotedString)('search_text') +
                          Optional(field_qualifier)('field'))
expr = Forward()
atom = search_term | (LPAREN + expr + RPAREN)
or_term = Group(atom + ZeroOrMore(or_op + atom))
and_term = Group(or_term + ZeroOrMore(and_op + or_term))
expr << and_term

Теперь анализ вашего образца текста с:

res = expr.parseString(test)
from pprint import pprint
pprint(res.asList())

дает:

[[[[[[['"breast neoplasms"'], ['MeSH', 'Terms']],
     'or',
     [['breast', 'cancer'], ['Acknowledgments']],
     'or',
     [['breast', 'cancer'], ['Figure/Table', 'Caption']],
     'or',
     [['breast', 'cancer'], ['Section', 'Title']],
     'or',
     [['breast', 'cancer'], ['Body', '-', 'All', 'Words']],
     'or',
     [['breast', 'cancer'], ['Title']],
     'or',
     [['breast', 'cancer'], ['Abstract']],
     'or',
     [['breast', 'cancer'], ['Journal']]]]],
  'and',
  [[[[['prevention'], ['Acknowledgments']],
     'or',
     [['prevention'], ['Figure/Table', 'Caption']],
     'or',
     [['prevention'], ['Section', 'Title']],
     'or',
     [['prevention'], ['Body', '-', 'All', 'Words']],
     'or',
     [['prevention'], ['Title']],
     'or',
     [['prevention'], ['Abstract']]]]]]]

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

Мы начнем с базового абстрактного класса, ParsedObject, что примет анализируемые токены в качестве своей инициализирующей структуры. Мы также добавим абстрактный метод, queryString, который мы реализуем во всех производных классах для создания желаемого результата:

class ParsedObject(object):
    def __init__(self, tokens):
        self.tokens = tokens
    def queryString(self):
        '''Abstract method to be overridden in subclasses'''

Теперь мы можем наследовать этот класс, и любой подкласс может использоваться в качестве действия разбора при определении грамматики.

Когда мы делаем это, Groups, которые были добавлены для структурного вида, встают на нашем пути, поэтому мы переопределим оригинальный синтаксический анализатор без них:

search_term = Group(OneOrMore(word) | quotedString)('search_text') + 
                    Optional(field_qualifier)('field')
atom = search_term | (LPAREN + expr + RPAREN)
or_term = atom + ZeroOrMore(or_op + atom)
and_term = or_term + ZeroOrMore(and_op + or_term)
expr << and_term

Теперь мы реализуем класс для search_term, с помощью self.tokens чтобы получить доступ к проанализированным битам, найденным во входной строке:

class SearchTerm(ParsedObject):
    def queryString(self):
        text = ' '.join(self.tokens.search_text)
        if self.tokens.field:
            return '%s: %s' % (' '.join(f.lower() 
                                        for f in self.tokens.field[0]),text)
        else:
            return text
search_term.setParseAction(SearchTerm)

Далее мы реализуем and_term а также or_term выражения. Оба являются бинарными операторами, различающимися только в своей результирующей строке оператора в выходном запросе, поэтому мы можем просто определить один класс и позволить им предоставить константу класса для соответствующих им строк операторов:

class BinaryOperation(ParsedObject):
    def queryString(self):
        joinstr = ' %s ' % self.op
        return joinstr.join(t.queryString() for t in self.tokens[0::2])
class OrOperation(BinaryOperation):
    op = "OR"
class AndOperation(BinaryOperation):
    op = "AND"
or_term.setParseAction(OrOperation)
and_term.setParseAction(AndOperation)

Обратите внимание, что pyparsing немного отличается от традиционных парсеров - наш BinaryOperation будет соответствовать "a или b или c" как отдельное выражение, а не как вложенные пары "(a или b) или c". Таким образом, мы должны воссоединиться со всеми терминами, используя степпинг [0::2],

Наконец, мы добавляем действие синтаксического анализа, чтобы отразить любое вложение, оборачивая все выражения в ():

class Expr(ParsedObject):
    def queryString(self):
        return '(%s)' % self.tokens[0].queryString()
expr.setParseAction(Expr)

Для вашего удобства вот весь анализатор в одном копируемом / вставляемом блоке:

from pyparsing import *

LBRACK,RBRACK,LPAREN,RPAREN = map(Suppress,"[]()")
and_op = CaselessKeyword('and')
or_op = CaselessKeyword('or')
word = ~(and_op | or_op) + Word(alphanums + '.-/&')
field_qualifier = Group(LBRACK + OneOrMore(word) + RBRACK)

search_term = (Group(OneOrMore(word) | quotedString)('search_text') + 
               Optional(field_qualifier)('field'))
expr = Forward()
atom = search_term | (LPAREN + expr + RPAREN)
or_term = atom + ZeroOrMore(or_op + atom)
and_term = or_term + ZeroOrMore(and_op + or_term)
expr << and_term

# define classes for parsed structure
class ParsedObject(object):
    def __init__(self, tokens):
        self.tokens = tokens
    def queryString(self):
        '''Abstract method to be overridden in subclasses'''

class SearchTerm(ParsedObject):
    def queryString(self):
        text = ' '.join(self.tokens.search_text)
        if self.tokens.field:
            return '%s: %s' % (' '.join(f.lower() 
                                        for f in self.tokens.field[0]),text)
        else:
            return text
search_term.setParseAction(SearchTerm)

class BinaryOperation(ParsedObject):
    def queryString(self):
        joinstr = ' %s ' % self.op
        return joinstr.join(t.queryString() 
                                for t in self.tokens[0::2])
class OrOperation(BinaryOperation):
    op = "OR"
class AndOperation(BinaryOperation):
    op = "AND"
or_term.setParseAction(OrOperation)
and_term.setParseAction(AndOperation)

class Expr(ParsedObject):
    def queryString(self):
        return '(%s)' % self.tokens[0].queryString()
expr.setParseAction(Expr)


test = """("breast neoplasms"[MeSH Terms] OR breast cancer[Acknowledgments]  
OR breast cancer[Figure/Table Caption] OR breast cancer[Section Title]  
OR breast cancer[Body - All Words] OR breast cancer[Title]  
OR breast cancer[Abstract] OR breast cancer[Journal])  
AND (prevention[Acknowledgments] OR prevention[Figure/Table Caption]  
OR prevention[Section Title] OR prevention[Body - All Words]  
OR prevention[Title] OR prevention[Abstract])"""

res = expr.parseString(test)[0]
print res.queryString()

Который печатает следующее:

((mesh terms: "breast neoplasms" OR acknowledgments: breast cancer OR 
  figure/table caption: breast cancer OR section title: breast cancer OR 
  body - all words: breast cancer OR title: breast cancer OR 
  abstract: breast cancer OR journal: breast cancer) AND 
 (acknowledgments: prevention OR figure/table caption: prevention OR 
  section title: prevention OR body - all words: prevention OR 
  title: prevention OR abstract: prevention))

Я предполагаю, что вам нужно будет ужесточить некоторые из этих выводов - эти имена тегов lucene выглядят очень неоднозначно - я просто следил за вашим опубликованным образцом. Но вам не нужно сильно менять парсер, просто настройте queryString методы прикрепленных классов.

В качестве дополнительного упражнения к постеру: добавьте поддержку логического оператора НЕ на вашем языке запросов.

Другие вопросы по тегам