さて、あなたはまともなスタートを切りました。しかし、ここからは、パーサーの微調整の詳細に行き詰まりやすく、何日もそのモードにいる可能性があります。元のクエリ構文から始めて、問題を順を追って説明しましょう。
このようなプロジェクトを開始するときは、解析したい構文の 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 ')')
これはかなり近いです。「and」と「or」は単語の定義と一致するため、and と andword
の式の間にあいまいさが生じる可能性があるため、わずかな問題があります。実装時にこれを強化する必要があります。「癌または癌腫またはリンパ腫または黒色腫」が 1 つの大きな用語だけでなく、「または」で区切られた 4 つの異なる検索用語として読み取られるようにする必要があります (これが現在の検索用語です)。パーサーが行います)。また、演算子の優先順位を認識するという利点もあります - 厳密には必要ないかもしれませんが、今はそれで行きましょう。and_op
or_op
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
「or」と「and」のあいまいさに対処するために、単語の先頭に否定的な先読みを置きます。
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']]]]]]]
実際、パーサーの結果とかなり似ています。この構造を再帰して新しいクエリ文字列を作成することもできますが、私は、クラスを s ではなくトークン コンテナーとして定義Group
し、クラスに動作を追加して、解析時に作成された解析済みオブジェクトを使用してこれを行うことを好みます。出力。違いは、解析されたオブジェクト トークン コンテナーが、解析された式の種類に固有の動作を持つことができることです。
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
andor_term
式を実装します。両方とも、出力クエリの結果の演算子文字列のみが異なる二項演算子であるため、1 つのクラスを定義するだけで、それぞれの演算子文字列にクラス定数を提供できます。
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 or b) or c" としてではなく、"a or b or c" を単一の式として照合します。したがって、ステッピング スライスを使用してすべての項を再結合する必要があります[0::2]
。
最後に、すべての expr を () でラップすることにより、ネストを反映する parse アクションを追加します。
class Expr(ParsedObject):
def queryString(self):
return '(%s)' % self.tokens[0].queryString()
expr.setParseAction(Expr)
便宜上、パーサー全体を 1 つのコピー/貼り付け可能なブロックにまとめました。
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 ブール演算子のサポートを追加します。