嗯,你已经开始得不错了。但是从这里开始,很容易陷入解析器调整的细节中,你可能会在那种模式下度过几天时间。让我们从原始查询语法开始逐步解决你的问题。
当你开始一个这样的项目时,请编写一个你想要解析的语法的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
表达式之间的歧义,因为“and”和“or”符合单词的定义。在实现时,我们需要加强限制,确保“癌症或癌肉瘤或淋巴瘤或黑色素瘤”被视为4个由“or”分隔的不同搜索项,而不是一个大搜索项(我认为您当前的解析器会这样做)。我们还可以获得识别运算符优先级的好处——可能不是严格必要的,但现在就这样吧。
转换为 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())
gives:
[[[[[[['"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']]]]]]]
实际上,与您的解析器的结果非常相似。我们现在可以通过这个结构进行递归,并构建您的新查询字符串,但我更喜欢使用已解析的对象来完成这个任务,在解析时定义类作为令牌容器,然后向这些类添加行为以获得我们想要的输出。不同之处在于我们已解析的对象令牌容器可以具有特定于已解析表达式类型的行为。
我们将从一个基本的抽象类`ParsedObject`开始,它将把已解析的令牌作为其初始化结构。我们还将添加一个抽象方法`queryString`,我们将在所有派生类中实现该方法以创建您所需的输出:
class ParsedObject(object):
def __init__(self, tokens):
self.tokens = tokens
def queryString(self):
'''Abstract method to be overridden in subclasses'''
现在我们可以从这个类中派生,任何子类都可以用作解析操作来定义语法。
当我们这样做时,添加了结构的“Group”会有所妨碍,因此我们将重新定义没有它们的原始解析器。
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
方法即可。
作为对发帖者的额外练习:在查询语言中添加 NOT 布尔运算符的支持。