es.davy.ai

Preguntas y respuestas de programación confiables

¿Tienes una pregunta?

Si tienes alguna pregunta, puedes hacerla a continuación o ingresar lo que estás buscando.

Cómo crear consultas dinámicas con objetos Q de Django desde paréntesis dentro de una cadena.

No sé si el título de la pregunta está formado lo suficientemente bien. Pero esencialmente quisiera poder hacer algo así desde el front-end:

(nombre="abc" OR nombre="xyz") AND (estado="activo" OR (estado="disponible" AND edad=30))

Quiero que el usuario envíe esta cadena. Lo analizaré en el backend y formaré una consulta.

He visto esta respuesta y esta, pero no pude averiguar cómo resolver los paréntesis aquí.

Estoy pensando en usar una pila (la forma en que resolvemos expresiones infijas) para hacer esto, pero no quiero ir por ese camino largo a menos que esté seguro de que no hay otra solución disponible. Si alguien puede hacer esto con ese método, también sería genial.

Tags:  , , , ,

Answer

  1. Avatar for davy.ai

    Para analizar una cadena como (name="abc" OR name="xyz") AND (status="active" OR (status="available" AND age=30)) a un queryset de Django, puedes seguir estos pasos generales:

    1. Tokenizar la cadena – Dividir la cadena en tokens individuales como (, name="abc", OR, name="xyz", ), AND, (, ... etc. Esto se puede hacer fácilmente utilizando expresiones regulares.
    2. Analizar los tokens utilizando un analizador de descenso recursivo – Empezando con el operador de mayor precedencia, es decir, los paréntesis, evalúa recursivamente las subexpresiones. Es buena idea utilizar una pila para implementar esto.
    3. Traducir la expresión analizada a un queryset de Django – Una vez que tienes un árbol de expresión bien formado, puedes construir un queryset de Django construyendo de manera recursiva objetos Q y operadores |/&.

    Aquí hay un código de Python de muestra que debería ayudarte a empezar con el análisis:

    import re
    
    class Token:
        # Tipos de token
        LPAREN = '('
        RPAREN = ')'
        OR = 'OR'
        AND = 'AND'
        NAME = 'NAME'
        VALUE = 'VALUE'
        OPERATOR = 'OPERATOR'
    
        def __init__(self, type, value=None):
            self.type = type
            self.value = value
    
        def __repr__(self):
            return f'{self.type}("{self.value}")'
    
    def tokenize(query):
        # Definir expresiones regulares para cada tipo de token
        token_specification = [
            (Token.LPAREN, r'\('),
            (Token.RPAREN, r'\)'),
            (Token.OR, r'OR'),
            (Token.AND, r'AND'),
            (Token.NAME, r'[a-zA-Z_][a-zA-Z0-9_]*'),
            (Token.VALUE, r'"[^"]+"'),
            (Token.OPERATOR, r'='),
        ]
        tok_regex = '|'.join(f'(?P<{type}>{regex})' for type, regex in token_specification)
    
        # Tokenizar la cadena de consulta
        tokens = []
        for match in re.finditer(tok_regex, query):
            type = match.lastgroup
            value = match.group(type)
            tokens.append(Token(type, value))
        return tokens
    
    def parse_query(tokens):
        def parse_expr():
            nonlocal pos
            lhs = parse_term()
            while pos < len(tokens):
                op = tokens[pos]
                if op.type in (Token.OR, Token.AND):
                    pos += 1
                    rhs = parse_term()
                    lhs = (op.type, lhs, rhs)
                else:
                    break
            return lhs
    
        def parse_term():
            nonlocal pos
            if tokens[pos].type == Token.LPAREN:
                pos += 1
                expr = parse_expr()
                if tokens[pos].type != Token.RPAREN:
                    raise ValueError(f"Se esperaba ')' en la posición {pos}")
                pos += 1
                return expr
            else:
                lhs = parse_factor()
                if pos < len(tokens) and tokens[pos].type == Token.OPERATOR:
                    op = tokens[pos]
                    pos += 1
                    rhs = parse_factor()
                    return (op.value, lhs, rhs)
                else:
                    return lhs
    
        def parse_factor():
            nonlocal pos
            if tokens[pos].type == Token.NAME:
                name = tokens[pos].value
                pos += 1
                if pos < len(tokens) and tokens[pos].type == Token.OPERATOR:
                    op = tokens[pos]
                    pos += 1
                    if tokens[pos].type == Token.VALUE:
                        value = tokens[pos].value[1:-1]
                        pos += 1
                        return (f'{name}__{op.value}', value)
                    else:
                        raise ValueError(f"Se esperaba valor en la posición {pos}")
                else:
                    return name
            else:
                raise ValueError(f"Token inesperado {tokens[pos]} en la posición {pos}")
    
        pos = 0
        return parse_expr()
    
    # Ejemplo de uso
    cadena_consulta = '(name="abc" OR name="xyz") AND (status="active" OR (status="available" AND age=30))'
    tokens = tokenize(cadena_consulta)
    consulta_analizada = parse_query(tokens)
    print(consulta_analizada)  # imprime ('AND', ('OR', ('name__exact', 'abc'), ('name__exact', 'xyz')), ('OR', ('status', 'active'), ('AND', ('status', 'available'), ('age__exact', '30'))))
    

    Esto debería darte un árbol de expresión bien formado que puedes traducir a un queryset de Django utilizando objetos Q y operadores |/&.

Comments are closed.