Правильный способ выделения текста с помощью QTextCursor (проблема с производительностью)

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

from PyQt6 import QtWidgets, QtGui
import time


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        # Create a plain text edit widget and add some text to it
        self.text_edit = QtWidgets.QPlainTextEdit()
        text = ''
        for i in range(10000):
            text+ = "2022 something or the other\n2022 some other test\n"
        self.text_edit.setPlainText(text)
        self.setCentralWidget(self.text_edit)

        # Create a button and connect its clicked signal to the select_text function
        self.button = QtWidgets.QPushButton("Change Text")
        self.button.clicked.connect(self.select_text)
        toolbar = self.addToolBar("Toolbar")
        toolbar.addWidget(self.button)

    def select_text(self):
        old = ["2022"] * 20000
        new = ["2023"] * 20000
        start_time = time.perf_counter()
        cursor = self.text_edit.textCursor()
        cursor.beginEditBlock()

        for i in range(self.text_edit.document().blockCount()):
            block = self.text_edit.document().findBlockByNumber(i)
            # Search for a specific string within the block
            block_cursor = QtGui.QTextCursor(block)
            # Get a QTextCursor object for the block
            while not block_cursor.atEnd() and block_cursor.block() == block:
                # block_cursor.movePosition(QtGui.QTextCursor.Right, QtGui.QTextCursor.KeepAnchor, 4)
                block_cursor = block.document().find(old[i], block_cursor)


                if not block_cursor.isNull():
                    block_cursor.insertText(new[i])
        cursor.endEditBlock()
        end_time = time.perf_counter()
        elapsed_time = end_time - start_time
        print(f"Elapsed time: {elapsed_time:.2f} seconds")





if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

Это мой текущий код, работающий по мере необходимости, но я надеюсь, что выбор с перемещением позиции вместо поиска ускорит его.

Редактировать: На основе ответа musicamante улучшенный код:

    def select_text(self):
        old = ["2022"] * 20000
        new = ["2023"] * 20000
        start_time = time.perf_counter()
        cursor = self.text_edit.textCursor()
        cursor.beginEditBlock()
        i=0
        doc = self.text_edit.document()
        find_cursor = QtGui.QTextCursor(doc.begin())
        while True:
            find_cursor = doc.find(old[i], find_cursor)
            if not find_cursor.isNull():
                find_cursor.insertText(new[i])
                i=i+1
                if i == 20000:
                    break
            else:
                break
        cursor.endEditBlock()
        end_time = time.perf_counter()
        elapsed_time = end_time - start_time
        print("Elapsed time: {:.2f} seconds".format(elapsed_time))

Редактировать2: Цель приложения состоит в том, что у меня есть временные метки с неизвестными настройками часового пояса, и вместо того, чтобы запрашивать часовой пояс, я добавил возможное смещение UTC, чтобы сместить время на UTC (когда \, если это необходимо). Я переработал пример и подход:

from PyQt6 import QtWidgets, QtGui
import time
import datetime


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        # Create a plain text edit widget and add some text to it
        self.text_edit = QtWidgets.QPlainTextEdit()
        text = "2022-08-02T15:41:05.000  something or the other\n2022-08-02T15:41:06.000  Some parts may contain timestamps 2021-08-02T15:42:06.000 or\u2028New lines within a block\n2022-08-02T15:42:06.000  some other test"
        self.text_edit.setPlainText(text)
        self.setCentralWidget(self.text_edit)


        # Create a button and connect its clicked signal to the select_text function
        self.button = QtWidgets.QPushButton("Change Text")
        self.button.clicked.connect(self.shift_timezone)
        toolbar = self.addToolBar("Toolbar")
        toolbar.addWidget(self.button)

    def shift_timezone(self):
        text_timestamps = [1659447665, 1659447666, 1659447726]
        start_time = time.perf_counter()
        cursor = self.text_edit.textCursor()
        cursor.movePosition(cursor.MoveOperation.Start, cursor.MoveMode.MoveAnchor)
        cursor.beginEditBlock()
        i=0
        while i<self.text_edit.document().blockCount():
            dt = datetime.datetime.fromtimestamp(text_timestamps[i]+3600)
            iso_string = dt.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
            cursor.movePosition(cursor.MoveOperation.Right, cursor.MoveMode.KeepAnchor, 24)
            cursor.insertText(iso_string)
            cursor.movePosition(cursor.MoveOperation.NextBlock, cursor.MoveMode.MoveAnchor)
            i= i+1
        cursor.endEditBlock()
        end_time = time.perf_counter()
        elapsed_time = end_time - start_time
        print(f"Elapsed time: {elapsed_time:.2f} seconds")





if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

(Я намеренно добавил пробел в качестве заполнителя для Zulu в исходном отображаемом тексте, чтобы учесть несколько запусков функции)

Это отлично работает на функциональном уровне, но производительность ужасна (в 7 раз больше времени выполнения, чем просто удаление документа и создание нового с обновленными временными метками). Поскольку я не контролирую содержимое, оно может содержать точно такие же временные метки, как и у меня, а также может иметь разрывы строк внутри блока.

Что я знаю, так это то, что все блоки будут начинаться с временной метки, я могу управлять форматом временной метки, и у меня есть источник с временной меткой и содержимое в другой переменной, что позволяет отбрасывать все.

Я делаю здесь какую-то явно интенсивную ошибку?

Вы абсолютно уверены, что год всегда будет стоять в начале каждого блока? Так же не понятно ваше обновление, какой смысл искать по списку одинаковых предметов? Может быть, у вас действительно есть словарь замен? А что, если замена станет матчем для следующей замены? Например, если вы хотите заменить каждый 2021 на 2022 и каждый 2022 на 2023?

musicamante 01.04.2023 01:42

Я на 100% уверен. Начало блока — это метка времени, которой я могу манипулировать, это то, что представляет собой повторяющийся список. Я стандартизировал на 2023-04-01T22:21:05.002 Это означает, что поиск не обязательно является хорошей идеей, хорошим решением будет выбрать начало блока и идти вправо len(2023-04-01T22:21: 05.002) символов.

Balog Dániel 01.04.2023 14:51

Извините, но ваш комментарий еще больше сбивает с толку. Повторяющийся список не имеет смысла, так как их содержимое одинаково. И теперь вы говорите нам, что вам действительно нужно иметь дело с временными метками, но неясно, что вам нужно заменить, это только год, полную временную метку или только ее часть (например, изменить дату, но оставить время нетронутый). Мы не сможем вам помочь, если вы предоставите неполную информацию. Нам нужен практический и концептуально достоверный пример (минимальный воспроизводимый пример ), чтобы понять контекст вопроса и его проблему. В любом случае, смотрите мой обновленный ответ.

musicamante 02.04.2023 02:23

Я немного больше работал с кодом edit2, но, похоже, он настолько хорош, насколько я могу его получить. Лучше ли каким-то образом сохранить форматирование текста и воссоздать его?

Balog Dániel 04.04.2023 21:26

Как уже говорилось, см. мой обновленный ответ: поскольку вы используете обычный текст, нет никакой реальной выгоды в использовании интерфейса QTextDocument для такого сложного требования, которое потребовало бы многократного поиска и замены: используйте регулярные выражения, а затем установите окончательный результат для содержимого виджета.

musicamante 05.04.2023 01:30
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Итерация по всем блокам неэффективна и, кроме того, использование findBlock() неоптимально, так как QTextDocument уже предоставляет QTextDocument.begin() и QTextBlock.next().

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

Поскольку find(str) автоматически учитывает текущую позицию данного курсора и выполняет поиск по всему документу (в отличие от функций, основанных на регулярных выражениях), вы можете использовать гораздо более простой цикл while:

    def select_text(self):
        start_time = time.perf_counter()
        cursor = self.text_edit.textCursor()
        cursor.beginEditBlock()

        doc = self.text_edit.document()
        find_cursor = QtGui.QTextCursor(doc.begin())
        while True:
            find_cursor = doc.find("2022", find_cursor)
            if not find_cursor.isNull():
                find_cursor.insertText("2023")
            else:
                break

        cursor.endEditBlock()
        end_time = time.perf_counter()
        elapsed_time = end_time - start_time
        print("Elapsed time: {:.2f} seconds".format(elapsed_time))

Тем не менее, поскольку вы используете простой текстовый виджет, не имеет большого смысла использовать интерфейс QTextDocument для обработки основных замен, а использование регулярных выражений будет чрезвычайно быстрым, основанным на простом и быстром re. функция sub().

Учитывая приведенный выше базовый случай, это будет просто вопрос использования регулярного выражения, которое соответствует шаблону в начале строки.

Вам нужно только преобразовать новые символы строки (которые в Qt становятся Unicode Paragraphj Separator (U+2029)), чтобы вы могли использовать идентификатор ^ для начала каждой строки:

        cursor.setPosition(0)
        cursor.movePosition(cursor.End, cursor.KeepAnchor)
        text = cursor.selectedText().replace('\u2029', '\n')

        text = re.sub(r'^2022', '2023', text, flags=re.MULTILINE)
        cursor.insertText(text)

Это также может быть достигнуто с помощью нескольких замен, например, с помощью простого словаря:

    substitutions = {
        '2020': '2021', 
        '2022': '2023', 
    }
    for x, y in substitutions.items():
        text = re.sub(r'^{}'.format(x), y, text, flags=re.MULTILINE)

Но есть одна загвоздка.
Что делать, если эти пары несовместимы? Возьмем, к примеру, следующий словарь:

    substitutions = {
        '2020': '2021', 
        '2022': '2023', 
        '2021': '2022', 
    }

Учитывая порядок вставки, введенный в Python 3.7, все совпадения 2020 будут заменены на 2022 вместо 2021. И если у вас нет прямого контроля над порядком вставки (или вы используете Python <3.7), вы также можете заменить все записи на 2023.

Имея это в виду, возможное решение состоит в том, чтобы использовать временные заполнители, а затем заменить их фактической заменой.

В следующем примере я использую символ @, чтобы определить (и позже идентифицировать) временные заполнители: если 2020 должен стать 2021, а любой предыдущий 2021 должен быть 2022, они будут заменены на @2021@ и @2022@ соответственно; таким образом, любое предыдущее появление 2021 (без символов @) не будет перепутано с @2021@.

В редких случаях в исходном тексте встречается символ @<match>@, добавляется еще один символ @ до тех пор, пока не будет найдено существующее совпадение.

    placeholder = '@'
    while True:
        for k in substitutions.keys():
            if re.search(re.compile('^' + placeholder + k + placeholder), text):
                placeholder += '@'
                break # restart search from the beginning
        else:
            break # no match with placeholders, yay!

    final = {}
    for x, y in substitutions.items():
        repl = placeholder + y + placeholder
        text = re.sub(r'^{}'.format(x), repl, text, flags=re.MULTILINE)
        final[repl] = y

    for x, y in final.items():
        text = re.sub('^' + x, y, text, flags=re.MULTILINE)

Прошу прощения, вы ответили на мой вопрос, но я задал не тот вопрос. У меня есть список со строками поиска и замены (они разные для каждого блока), поэтому я перебирал блоки. Отредактировал код примера.

Balog Dániel 01.04.2023 00:38

Я понял, что ваше улучшение в отношении цикла может быть добавлено с помощью: 'while block.isValid(): (та же функция) block = block.next() i= i+1'. Это экономит около 10% времени выполнения.

Balog Dániel 01.04.2023 00:47

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