Преобразование одного формата запроса в другой
Я в растерянности. Я пытался заставить это работать несколько дней. Но с этим я никуда не деться, поэтому я решил проконсультироваться с вами, ребята, и посмотреть, сможет ли кто-нибудь мне помочь!
Я использую 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']]]]]]]
На самом деле, очень похоже на результаты вашего парсера. Теперь мы можем пройти через эту структуру и создать новую строку запроса, но я предпочитаю делать это, используя проанализированные объекты, созданные во время анализа, определяя классы как контейнеры токенов вместо Group
s, а затем добавить поведение к классам, чтобы получить желаемый результат. Различие состоит в том, что наши контейнеры токенов анализируемого объекта могут иметь поведение, специфичное для того типа выражения, которое было проанализировано.
Мы начнем с базового абстрактного класса, ParsedObject
, что примет анализируемые токены в качестве своей инициализирующей структуры. Мы также добавим абстрактный метод, queryString
, который мы реализуем во всех производных классах для создания желаемого результата:
class ParsedObject(object):
def __init__(self, tokens):
self.tokens = tokens
def queryString(self):
'''Abstract method to be overridden in subclasses'''
Теперь мы можем наследовать этот класс, и любой подкласс может использоваться в качестве действия разбора при определении грамматики.
Когда мы делаем это, Group
s, которые были добавлены для структурного вида, встают на нашем пути, поэтому мы переопределим оригинальный синтаксический анализатор без них:
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
методы прикрепленных классов.
В качестве дополнительного упражнения к постеру: добавьте поддержку логического оператора НЕ на вашем языке запросов.