Pyparsing un formato de consulta a otro

Estoy en una pérdida. Llevo días intentando que esto funcione. Pero no estoy llegando a ninguna parte con esto, ¡así que pensé en consultarles aquí y ver si alguien puede ayudarme!

Estoy utilizando el uso de pyparsing en un bash de analizar un formato de consulta a otro. Esto no es una transformación simple pero en realidad toma algunos cerebros 🙂

La consulta actual es la siguiente:

("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]) 

Y al usar pyparsing he podido obtener la siguiente estructura:

 [[[['"', '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']]]] 

Pero ahora, estoy en una pérdida. Necesito formatear la salida anterior a una consulta de búsqueda lucene. Aquí hay un breve ejemplo de las transformaciones requeridas:

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

Pero estoy atrapado allí mismo. También necesito poder hacer uso de las palabras especiales AND y OR.

por lo que una consulta final podría ser: términos de malla: “neoplasias de mama” y prevención

¿Quién puede ayudarme y darme algunos consejos sobre cómo resolver esto? Se agradece cualquier tipo de ayuda.

Ya que estoy usando pyparsing, estoy a favor de python. He pegado el código a continuación para que pueda jugar con él y no tenga que empezar en 0.

¡Muchísimas gracias por la ayuda!

 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 

Bueno, te has llevado a un comienzo decente. Pero a partir de aquí, es fácil atascarse en los detalles de los ajustes del analizador, y podría estar en ese modo durante días. Revisemos su problema comenzando con la syntax de la consulta original.

Cuando comiences con un proyecto como este, escribe un BNF de la syntax que deseas analizar. No tiene que ser súper riguroso, de hecho, aquí hay un comienzo basado en lo que puedo ver en su muestra:

 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 ')') 

Eso está bastante cerca: tenemos un pequeño problema con alguna posible ambigüedad entre la word y las expresiones and_op y or_op , ya que ‘y’ y ‘o’ coinciden con la definición de una palabra. Necesitaremos ajustar esto en el momento de la implementación, para asegurarnos de que “cáncer o carcinoma o linfoma o melanoma” se lean como 4 términos de búsqueda diferentes separados por ‘or’, no solo un gran término (que creo que es lo que su actual Parser haría). También obtenemos el beneficio de reconocer la precedencia de los operadores, tal vez no sea estrictamente necesario, pero vamos a hacerlo por ahora.

La conversión a pyparsing es bastante simple:

 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 

Para abordar la ambigüedad de 'o' y 'y', ponemos un lookahead negativo al principio de la palabra:

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

Para dar algo de estructura a los resultados, envuélvalos en clases de 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 

Ahora analizando su texto de muestra con:

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

da:

 [[[[[[['"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']]]]]]] 

En realidad, bastante similar a los resultados de su analizador. Ahora podríamos responder a través de esta estructura y construir su nueva cadena de consulta, pero prefiero hacer esto usando objetos analizados, creados en el momento del análisis definiendo clases como contenedores de token en lugar de Group , y luego agregando comportamiento a las clases para obtener nuestra salida deseada. La distinción es que nuestros contenedores de token de objetos analizados pueden tener un comportamiento que es específico para el tipo de expresión que se analizó.

Comenzaremos con una clase abstracta de base, ParsedObject , que tomará los tokens analizados como su estructura de inicialización. También agregaremos un método abstracto, queryString , que implementaremos en todas las clases derivadas para crear el resultado deseado:

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

Ahora podemos derivar de esta clase, y cualquier subclase puede usarse como una acción de análisis en la definición de la gramática.

Cuando hacemos esto, los Group que se agregaron para la estructura se interponen en nuestro camino, así que redefiniremos el analizador original sin ellos:

 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 

Ahora implementamos la clase para search_term , usando self.tokens para acceder a los bits analizados encontrados en la cadena de entrada:

 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) 

A continuación, implementaremos las expresiones and_term y or_term . Ambos son operadores binarios que se diferencian solo en su cadena de operador resultante en la consulta de salida, por lo que solo podemos definir una clase y dejar que proporcionen una constante de clase para sus cadenas de operador respectivas:

 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) 

Tenga en cuenta que el uso de pyparsing es un poco diferente al de los analizadores tradicionales: nuestra operación binaria coincidirá con "a or by c" como una expresión única, no como los pares nesteds "(aob) o c". Así que tenemos que volver a unir todos los términos usando la sección de pasos [0::2] .

Finalmente, agregamos una acción de análisis para reflejar cualquier anidamiento envolviendo todos los exprs en ():

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

Para su comodidad, aquí está el analizador completo en un bloque de copia / pastable:

 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() 

Lo que imprime lo siguiente:

 ((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)) 

Supongo que necesitarás ajustar algo de esta salida (los nombres de las tags lucene se ven muy ambiguos) simplemente estaba siguiendo tu muestra publicada. Pero no debería tener que cambiar mucho el analizador, solo ajuste los métodos de consulta de las clases adjuntas.

Como un ejercicio agregado al póster: agregue soporte para el operador NO booleano en su idioma de consulta.