Как проанализировать ввод ключевых слов поисковой системы

Я реализую инструмент, который позволяет пользователям искать термины в текстах. В настоящее время я сосредоточен на обработке более сложных результатов поиска.

Операторы, которых я хочу поддержать:

  • | = ИЛИ
  • & = И
  • ^ = НЕ
  • " " = Кавычки для экранирования всего в последовательности
  • ( ) = круглые скобки для придания приоритета инкапсулированному

Этот код должен попасть в серверную часть Python, которая формирует запрос к ядру базы данных, поэтому мне нужен способ анализа запроса для преобразования соответствующих частей. Есть ли модули, которые позволят мне это сделать?

Я попытался просмотреть пакет логики NLTK, но, похоже, он делает слишком много вещей, и мне неясно, как свести его к этим функциям. Я думаю, мне нужно что-то вроде грамматических деревьев NLTK, но все, что я нашел, это пакеты, которые добавляют грамматические теги и, таким образом, привязаны к языковой модели.

Python Lex-Yacc может помочь вам понять, как построить парсер (если вы не используете его для создания собственного, я его не использовал).
Daviid 14.06.2024 09:13

Я согласен с @Daviid - я бы использовал PLY или SLY (один и тот же автор для обоих, но PLY использует функции, а SLY использует классы)

furas 14.06.2024 14:26
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
2
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Для его анализа я бы использовал PLY (Python Lex-Yacc) или SLY.

(PLY и SLY имеют одного и того же автора, но PLY использует функции, а SLY использует классы)

Я взял пример calc.py из SLY и создал код, который преобразует запрос типа

^(abc & def) | xyz

во вложенный список

['OR', ['NOT', ['AND', 'abc', 'def']], 'xyz']

который должно быть легко использовать для генерации SQL-запроса

Другой пример(ы):
(A&B)|(^C&D) ==> ['OR', ['AND', 'A', 'B'], ['AND', ['NOT', 'C'], 'D']]


Я изменил TEXT, чтобы он мог ловить A B C как одну строку без использования " ".
И если вы используете "A B C", он возвращает его как одну строку без " ".
Но позволяет использовать " внутри текста (если нет " в начале и в конце).

A"B"C & "F G H" & I J K ==> ['AND', ['AND', 'A"B"C', 'F G H'], 'I J K'] `

Но он не позволяет использовать символы &|^!~() внутри текста. Потребуются некоторые изменения в TEXT и функциях.

"A&B" ==> ['AND', 'A', 'B']


я добавил

  • ! как NOT, потому что Python использует != как not equal, поэтому я автоматически писал ! в запросе
  • ~ как NOT, потому что я часто использую его в pandas для отрицаний, поэтому иногда я писал ~ автоматически в запросе

from sly import Lexer, Parser
import readline  # it allows to use keys `arrow up` `arrow down` to see previous queries in current session

class QueryLexer(Lexer):
    tokens = { TEXT }
    ignore = ' \t'
    literals = { '&', '|', '^', '!', '~', '(', ')'}

    # Tokens
    #TEXT = r'[a-zA-Z_][a-zA-Z0-9_]*'
    TEXT = r'[^&|^!~()]{1,}'  # at least one char which is not in `literals`

    @_(r'\n+')
    def newline(self, t):
        self.lineno += t.value.count('\n')

    def error(self, t):
        print("Illegal character '%s'" % t.value[0])
        self.index += 1

class QueryParser(Parser):
    tokens = QueryLexer.tokens

    precedence = (
        ('left', '&', '|', '!', '~'),
        ('right', '^'),
    )

    @_('expr')
    def statement(self, p):
        return p.expr

    @_('expr "&" expr')
    def expr(self, p):
        return ['AND', p.expr0, p.expr1]

    @_('expr "|" expr')
    def expr(self, p):
        return ['OR', p.expr0, p.expr1]

    @_('"^" expr', '"!" expr', '"~" expr')
    def expr(self, p):
        return ['NOT', p.expr]

    @_('"(" expr ")"')
    def expr(self, p):
        return p.expr

    @_('TEXT')
    def expr(self, p):
        return p.TEXT.strip(' ').strip('"')

if __name__ == '__main__':
    lexer = QueryLexer()
    parser = QueryParser()
    while True:
        try:
            text = input('query > ')
        except EOFError:
            break
        except KeyboardInterrupt:
            break
        if text:
            result = parser.parse(lexer.tokenize(text))
            if isinstance(result, str):
               print(f'text: >{result}<')  # I uses `> <` to see if there are spaces
            else:
               print(f'list: {result}')
               #import json
               #print(json.dumps(result, indent=1))

Здесь версия с большим количеством модификаций

Исходная версия преобразует A & B & C & D в ['AND', ['AND', ['AND', 'A', 'B'], 'C'], 'D'], а новая версия может использовать функцию flatten() для создания ['AND', 'A', 'B', 'C', 'D']

Если вы отправите .flat или .notflat в качестве запроса, он переключит переменную FLATTEN, которая решит, следует ли использовать flatten().

Я также добавил функцию, которая пытается преобразовать его в SQL-запрос с помощью [VAR] LIKE "%text%"
и для списка ['AND', 'A', 'B', 'C', 'D'] выдается строка

([VAR] LIKE "%A%" AND [VAR] LIKE "%B%" AND [VAR] LIKE "%C%" AND [VAR] LIKE "%D%") 

Поэтому вам нужно только заменить [VAR] на свою переменную.

Но я не знаю, должен ли я добавлять % автоматически или пользователь должен спросить A% & %B, потому что это позволяет проверить, начинается ли текст с A и заканчивается B

В конце концов пользователь сможет использовать A* & *B & C, и код должен преобразовать его в A% AND %B AND %C% (добавьте % автоматически, если с обеих сторон нет *.

Я также добавил функцию, которая сохраняет историю в файл и читает ее при следующем запуске.

Я также добавил json для отображения данных в виде

[
 "AND",
 "A",
 [
  "OR",
  "B",
  "C",
  "D"
 ],
 [
  "NOT",
  "X"
 ]
]

Я добавил функцию .history (и ярлык .h), чтобы просмотреть историю.

Я также добавил функцию выбора поля, например day:*h*day & title:Hello
что дает (day LIKE "%h%day" AND title LIKE "%Hello%")


from sly import Lexer, Parser
import readline
import atexit
import os
import json

#COLOR = '\x1b[1;31m' # red
COLOR = '\x1b[1;32m' # green
RESET = '\x1b[m'

BASE = os.path.dirname(os.path.abspath(__file__))
HISTORY_PATH = os.path.join(BASE, ".history")

MAKE_FLAT = True
DEFAULT_VAR = '[VAR]'

class QueryLexer(Lexer):
    tokens = { TEXT }
    ignore = ' \t'
    literals = { '&', '|', '^', '!', '~', '(', ')'}

    # Tokens
    #WORD   = r'[^&|^!~()"]{1,}'      # at least one char which is not in `literals`
    #PHRASE = r'"[^"]*"'          
    TEXT = r'"[^"]*"|[^&|^!~()]{1,}'  # at least one char which is not in `literals`

    @_(r'\n+')
    def newline(self, t):
        self.lineno += t.value.count('\n')

    def error(self, t):
        print("Illegal character '%s'" % t.value[0])
        self.index += 1

class QueryParser(Parser):
    tokens = QueryLexer.tokens

    precedence = (
        ('left', '&', '|', '!', '~'),
        ('right', '^'),
    )

    @_('expr')
    def statement(self, p):
        return p.expr

    @_('expr "&" expr')
    def expr(self, p):
        result = ['AND', p.expr0, p.expr1]
        if MAKE_FLAT:
            result = flatten(result)
        return result

    @_('expr "|" expr')
    def expr(self, p):
        result = ['OR', p.expr0, p.expr1]
        if MAKE_FLAT:
            result = flatten(result)
        return result

    @_('"^" expr', '"!" expr', '"~" expr')
    def expr(self, p):
        return ['NOT', p.expr]

    @_('"(" expr ")"')
    def expr(self, p):
        return p.expr

    @_('TEXT')
    def expr(self, p):
        return p.TEXT.strip(' ')


def flatten(data):
    key = data[0]
        
    values = []
    
    for item in data[1:]:
        if isinstance(item, list) and item[0] == key:
            values.extend(item[1:])
        else:
            values.append(item)
        
    return [key, *values]

def generate(data):
    if isinstance(data, str):
        text = data
        var  = DEFAULT_VAR

        if not text.startswith('"') and (':' in text):
            var, text = text.split(':', 1)
            print(var, text)
            
        text = text.strip('"')
        
        if '*' in text:
           text = text.replace('*', '%')
        
        if '%' not in text:
           text = f'%{text}%'
           
        return f'{var} LIKE "{text}"'

    key = data[0]
    values = data[1:]
    
    if key in ['NOT']:
        text = data[1]
        var  = DEFAULT_VAR
        
        if not text.startswith('"') and (':' in text):
            var, text = text.split(':', 1)
            print(var, text)

        text = text.strip('"')
        
        if '*' in text:
            text = text.replace('*', '%')
           
        if '%' not in text:           
            text = f'%{text}%'
           
        return f'{var} NOT LIKE "{text}"'
    
    if key in ['AND', 'OR']:
        key = f' {key} '  # add spaces
        
        # convert to `var LIKE value`
        values = [generate(item) for item in values]
        # join using AND, OR
        text = key.join(values)
    else:
        text = str(data)
        
    return f'({text})'  # add ( )
    
if __name__ == '__main__':

    # read history at the beginning    
    try:
        readline.read_history_file(HISTORY_PATH)
        readline.set_history_length(1000)
    except FileNotFoundError:
        pass
    # write history at the end
    atexit.register(readline.write_history_file, HISTORY_PATH)
        
    lexer = QueryLexer()
    parser = QueryParser()

    number = 0    
    while True:
        try:
            number += 1
            text = input(f'[{number}] {COLOR}query >{RESET} ')
        except EOFError:
            break
        except KeyboardInterrupt:
            break
        if text:
            result = parser.parse(lexer.tokenize(text))
            if isinstance(result, str):
                print(f'text: >{result}<')  # I uses `> <` to see if there are spaces
                cmd = result.split(' ')
                if cmd[0] in ('.history', '.h') :
                    for index in range(1, readline.get_current_history_length()+1):
                        print(f'[{index}]', readline.get_history_item(index))
                elif cmd[0] in ('.flat', '.f'):
                    MAKE_FLAT = True
                    print('MAKE_FLAT:', MAKE_FLAT)
                elif cmd[0] == ('.notflat', '.nf'):
                    MAKE_FLAT = False
                    print('MAKE_FLAT:', MAKE_FLAT)
                elif cmd[0] in ('.var', '.v'):
                    if len(cmd) > 1:
                        DEFAULT_VAR = cmd[1]
                    print('DEFAULT_VAR:', DEFAULT_VAR)
                else:
                    print(f'data: {result}')
                    print(json.dumps(result, indent=1))
                    print(generate(result))
            else:
                print(f'data: {result}')
                print(json.dumps(result, indent=1))
                print(generate(result))

.не плоская

.плоский

Ну, это не для SQL, поэтому добавленное вами поколение не соответствует тому, что мне нужно, но я, безусловно, могу использовать его как хорошую основу для того, что я хотел сделать. Спасибо

Equino 17.06.2024 09:25

Другие вопросы по теме