I am writing a fuzzy search application which finds words in text even if words have mistakes. I wrote gui form but it freezed because of heavy calculation. So I created class inherited from QThread and emitted signals from it to gui so that progress bar started to work and gui form wasn't freezed anymore. BUT I should also create console version of this application where I don't need gui form but I need methods that written in class inherited from QThread. But it's impossible without using PyQT library which strange to use in console version. So I don't know how to solve this problem. My teacher recommended to use threading but I didn't find how to emit signals from Thread class as I colud do in QThread.
That's a QThread class
import text_methods
from PyQt5.QtCore import pyqtSignal, QThread
class FuzzySearch(QThread):
sig_words_count = pyqtSignal(int)
sig_step = pyqtSignal(int)
sig_done = pyqtSignal(bool)
sig_insertions = pyqtSignal(str)
sig_insertions_indexes = pyqtSignal(list)
def __init__(self, text, words, case_sensitive):
super().__init__()
self.text = text
self.words = words
self.case_sensitive = case_sensitive
self.insertions_indexes = {}
self.text_dict = {}
def run(self):
self.get_insertions_info(self.text, self.words)
def find_insertions_of_word(self, word, word_number):
word_insertions = {}
for textword in self.text_dict.keys():
if text_methods.is_optimal_distance(word, textword):
word_insertions[textword] = self.text_dict[textword]
for index in self.text_dict[textword]:
self.insertions_indexes[index] = index + len(textword)
self.sig_step.emit(word_number)
return word_insertions
'''Get information about insertions of words in the text'''
def find_insertions(self, text, words):
word_number = 1
insertions = {}
self.text_dict = text_methods.transform_text_to_dict(text, self.case_sensitive)
words_list = text_methods.transform_words_to_list(words, self.case_sensitive)
self.sig_words_count.emit(len(words_list))
for word in words_list:
print(word_number)
insertions[word] = self.find_insertions_of_word(word, word_number)
word_number += 1
self.insertions_indexes = sorted(self.insertions_indexes.items())
return insertions
'''Get information about insertions of words in the text in special format'''
def get_insertions_info(self, text, words):
insertions = self.find_insertions(text, words)
insertions_info = ''
for word in insertions.keys():
insertions_info += 'Вы искали слово "' + word + '"\n'
if len(insertions[word]) == 0:
insertions_info += ' По этому запросу не было найдено слов\n'
else:
insertions_info += ' По этому запросу были найдены слова:\n'
for textword in insertions[word].keys():
insertions_info += ' "' + textword + '" на позициях: '
insertions_info += ", ".join([str(i) for i in insertions[word][textword]])
insertions_info += '\n'
self.sig_done.emit(True)
self.sig_insertions.emit(insertions_info)
self.sig_insertions_indexes.emit(self.insertions_indexes)
self.quit()
As you see there are a lot of emitted signals that I transfer to gui module where they connect to slots in class FindButton in method find_insertions:
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QApplication, QWidget,\
QLabel, QPushButton, QTextEdit, QFileDialog,\
QMessageBox, QProgressBar, QCheckBox
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from fuzzysearch import FuzzySearch
import sys
class OpenButton(QPushButton):
def __init__(self, name, font, textedit):
super().__init__(name, font=font)
self.textedit = textedit
self.clicked.connect(self.open_dialog)
def open_dialog(self):
fname = QFileDialog.getOpenFileName(self, 'Open file', '/home')
if fname[0]:
with open(fname[0], 'r') as f:
data = f.read()
self.textedit.setText(data)
class FindButton(QPushButton):
def __init__(self, name, font, text, words, result, window):
super().__init__(name, font=font)
self.window = window
self.textedit = text
self.wordsedit = words
self.resultedit = result
self.checkbox = window.case_sensitive_checkbox
self.clicked.connect(self.find_insertions)
def find_insertions(self):
text = self.textedit.toPlainText()
words = self.wordsedit.toPlainText()
if text == '':
QMessageBox.information(self, 'Нет текста',
'Текст не был введен. \nВведите текст.')
elif words == '':
QMessageBox.information(self, 'Нет слов',
'Слова не были введены. \nВведите слова через запятую.')
else:
self.setDisabled(True)
self.text_editor = TextEditor(text, self.textedit)
self.fuzzy_search = FuzzySearch(text, words, self.checkbox.checkState())
self.fuzzy_search.sig_words_count.connect(self.window.progress_bar.setMaximum)
self.fuzzy_search.sig_step.connect(self.window.progress_bar.setValue)
self.fuzzy_search.sig_done.connect(self.setEnabled)
self.fuzzy_search.sig_insertions.connect(self.resultedit.setText)
self.fuzzy_search.sig_insertions_indexes.connect(self.text_editor.mark)
self.fuzzy_search.start()
class TextEditor:
def __init__(self, text, textedit):
self.text = text
self.textedit = textedit
def mark(self, to_mark):
self.textedit.clear()
current_index = 0
for item in to_mark:
self.write_not_marked_text(self.text[current_index:item[0]])
self.write_marked_text(self.text[item[0]:item[1]])
current_index = item[1]
self.write_not_marked_text(self.text[current_index:])
def write_not_marked_text(self, text):
font = QFont("Times", 10)
font.setItalic(False)
font.setBold(False)
self.textedit.setCurrentFont(font)
self.textedit.setTextColor(Qt.black)
self.textedit.insertPlainText(text)
def write_marked_text(self, text):
font = QFont("Times", 10)
font.setItalic(True)
font.setBold(True)
self.textedit.setCurrentFont(font)
self.textedit.setTextColor(Qt.red)
self.textedit.insertPlainText(text)
class Window(QWidget):
def __init__(self, font):
super().__init__()
self.standard_font = font
self.text_edit_font = QFont("Times", 10)
text_label = QLabel("Введите или откройте текст",
font=self.standard_font)
words_label = QLabel("Введите или откройте слова (через запятую)",
font=self.standard_font)
result_label = QLabel("Результат",
font=self.standard_font)
text_edit = QTextEdit(font=self.text_edit_font)
words_edit = QTextEdit(font=self.text_edit_font)
result_edit = QTextEdit(font=self.text_edit_font)
self.case_sensitive_checkbox = QCheckBox('Учитывать регистр')
self.case_sensitive_checkbox.setFont(self.standard_font)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
open_btn1 = OpenButton("Открыть", self.standard_font, text_edit)
open_btn2 = OpenButton("Открыть", self.standard_font, words_edit)
find_btn = FindButton("Найти слова в тексте", self.standard_font,
text_edit, words_edit, result_edit, self)
text_label_box = QHBoxLayout()
text_label_box.addWidget(text_label, alignment=Qt.AlignLeft)
text_label_box.addWidget(open_btn1, alignment=Qt.AlignRight)
words_label_box = QHBoxLayout()
words_label_box.addWidget(words_label, alignment=Qt.AlignLeft)
words_label_box.addWidget(open_btn2, alignment=Qt.AlignRight)
words_box = QVBoxLayout()
words_box.addLayout(words_label_box)
words_box.addWidget(words_edit)
result_box = QVBoxLayout()
result_box.addWidget(result_label, alignment=Qt.AlignLeft)
result_box.addWidget(result_edit)
bottom_box = QHBoxLayout()
bottom_box.addLayout(words_box)
bottom_box.addLayout(result_box)
find_and_progress_box = QHBoxLayout()
find_and_progress_box.addWidget(find_btn, alignment=Qt.AlignLeft)
find_and_progress_box.addWidget(self.case_sensitive_checkbox)
find_and_progress_box.addWidget(self.progress_bar)
main_box = QVBoxLayout()
main_box.addLayout(text_label_box)
main_box.addWidget(text_edit)
main_box.addLayout(bottom_box)
main_box.addLayout(find_and_progress_box)
self.setLayout(main_box)
self.setGeometry(300, 300, 1100, 700)
self.setWindowTitle('Нечеткий поиск')
self.show()
def start_application():
app = QApplication(sys.argv)
w = Window(QFont("Times", 12))
sys.exit(app.exec_())
And it works perfectly. But it will not work in console version because without QEventLoop QThread will not work:
import fuzzysearch
class ConsoleVersion():
def __init__(self, text, words):
self.text = text
self.words = words
def search_words_in_text(self):
with self.text:
with self.words:
self.f = fuzzysearch.FuzzySearch(self.text.read(), self.words.read(), False)
self.f.sig_insertions.connect(self.get_insertions)
self.f.start()
def get_insertions(self, insertions):
print(insertions)
and in main file I wrote parsing arguments and choise between two versions
import argparse
import gui
import console_version
def parse_args():
parser = argparse.ArgumentParser(description='Fuzzy search in text')
parser.add_argument('-g', '--graphics', help='graphical version', action='store_true')
parser.add_argument('-c', '--console', help='console version', nargs=2, type=argparse.FileType('r'), metavar=('TEXTFILE', 'WORDSFILE'))
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
if args.graphics:
gui.start_application()
if args.console:
cv = console_version.ConsoleVersion(args.console[0], args.console[1])
cv.search_words_in_text()
and module text_methods:
from re import split, sub
def transform_text_to_dict(text, case_sensitive):
text_dict = {}
index = 0
if case_sensitive:
splitted_text = split("[^'а-яА-ЯA-Za-z0-9_-]", text)
else:
splitted_text = split("[^'а-яА-ЯA-Za-z0-9_-]", text.lower())
for element in splitted_text:
if element not in text_dict:
text_dict[element] = []
text_dict[element].append(index)
index += len(element) + 1
return text_dict
def transform_words_to_list(words, case_sensitive):
words = sub("^\s+|\n|\r|\s+$", '', words)
if case_sensitive:
return split(' *, *', words)
else:
return split(' *, *', words.lower())
'''Damerau-Levenstein'''
def find_distance(word1: str, word2: str):
len1, len2 = len(word1), len(word2)
if len1 > len2:
word1, word2 = word2, word1
len1, len2 = len2, len1
current_row = range(len1 + 1)
previous_row = range(len1 + 1)
pre_previous_row = range(len1 + 1)
for i in range(1, len2 + 1):
if i == 1:
previous_row, current_row = current_row, [i] + [0] * len1
else:
pre_previous_row, previous_row, current_row = previous_row, current_row, [i] + [0] * len1
for j in range(1, len1 + 1):
add = previous_row[j] + 1
delete = current_row[j - 1] + 1
change = previous_row[j - 1]
if word1[j - 1] != word2[i - 1]:
change += 1
if word1[j - 1] == word2[i - 2] and word1[j - 2] == word2[i - 1]:
transpose = pre_previous_row[j - 2] + 1
current_row[j] = min(add, delete, change, transpose)
else:
current_row[j] = min(add, delete, change)
return current_row[len1]
def is_optimal_distance(word1 : str, word2 : str):
distance = find_distance(word1, word2)
l = min(len(word1), len(word2))
return distance <= l // 4
so what can you advise me?
Qt so that to handle the tasks always needs a loop that is created internally for it one must construct an object of the type QCoreApplication
, QGuiApplication
or QApplication
, this needs it for example for this case for QThread
that is not a thread but a thread handler for that is monitoring the state of the thread, if you do not place the application is closed immediately since the run method is not executed in the main thread.
if args.console:
app = QCoreApplication(sys.argv)
cv = console_version.ConsoleVersion(args.console[0], args.console[1])
cv.search_words_in_text()
sys.exit(app.exec_())