Я реализую инструмент, который позволяет пользователям искать термины в текстах. В настоящее время я сосредоточен на обработке более сложных результатов поиска.
Операторы, которых я хочу поддержать:
Этот код должен попасть в серверную часть Python, которая формирует запрос к ядру базы данных, поэтому мне нужен способ анализа запроса для преобразования соответствующих частей. Есть ли модули, которые позволят мне это сделать?
Я попытался просмотреть пакет логики NLTK, но, похоже, он делает слишком много вещей, и мне неясно, как свести его к этим функциям. Я думаю, мне нужно что-то вроде грамматических деревьев NLTK, но все, что я нашел, это пакеты, которые добавляют грамматические теги и, таким образом, привязаны к языковой модели.
Я согласен с @Daviid - я бы использовал PLY
или SLY (один и тот же автор для обоих, но PLY
использует функции, а SLY
использует классы)
Для его анализа я бы использовал 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, поэтому добавленное вами поколение не соответствует тому, что мне нужно, но я, безусловно, могу использовать его как хорошую основу для того, что я хотел сделать. Спасибо