Я пытаюсь заменить даты в начале каждого блока моего документа, и в настоящее время у меня есть работающий 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 раз больше времени выполнения, чем просто удаление документа и создание нового с обновленными временными метками). Поскольку я не контролирую содержимое, оно может содержать точно такие же временные метки, как и у меня, а также может иметь разрывы строк внутри блока.
Что я знаю, так это то, что все блоки будут начинаться с временной метки, я могу управлять форматом временной метки, и у меня есть источник с временной меткой и содержимое в другой переменной, что позволяет отбрасывать все.
Я делаю здесь какую-то явно интенсивную ошибку?
Я на 100% уверен. Начало блока — это метка времени, которой я могу манипулировать, это то, что представляет собой повторяющийся список. Я стандартизировал на 2023-04-01T22:21:05.002 Это означает, что поиск не обязательно является хорошей идеей, хорошим решением будет выбрать начало блока и идти вправо len(2023-04-01T22:21: 05.002) символов.
Извините, но ваш комментарий еще больше сбивает с толку. Повторяющийся список не имеет смысла, так как их содержимое одинаково. И теперь вы говорите нам, что вам действительно нужно иметь дело с временными метками, но неясно, что вам нужно заменить, это только год, полную временную метку или только ее часть (например, изменить дату, но оставить время нетронутый). Мы не сможем вам помочь, если вы предоставите неполную информацию. Нам нужен практический и концептуально достоверный пример (минимальный воспроизводимый пример ), чтобы понять контекст вопроса и его проблему. В любом случае, смотрите мой обновленный ответ.
Я немного больше работал с кодом edit2, но, похоже, он настолько хорош, насколько я могу его получить. Лучше ли каким-то образом сохранить форматирование текста и воссоздать его?
Как уже говорилось, см. мой обновленный ответ: поскольку вы используете обычный текст, нет никакой реальной выгоды в использовании интерфейса QTextDocument для такого сложного требования, которое потребовало бы многократного поиска и замены: используйте регулярные выражения, а затем установите окончательный результат для содержимого виджета.
Итерация по всем блокам неэффективна и, кроме того, использование 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)
Прошу прощения, вы ответили на мой вопрос, но я задал не тот вопрос. У меня есть список со строками поиска и замены (они разные для каждого блока), поэтому я перебирал блоки. Отредактировал код примера.
Я понял, что ваше улучшение в отношении цикла может быть добавлено с помощью: 'while block.isValid(): (та же функция) block = block.next() i= i+1'. Это экономит около 10% времени выполнения.
Вы абсолютно уверены, что год всегда будет стоять в начале каждого блока? Так же не понятно ваше обновление, какой смысл искать по списку одинаковых предметов? Может быть, у вас действительно есть словарь замен? А что, если замена станет матчем для следующей замены? Например, если вы хотите заменить каждый 2021 на 2022 и каждый 2022 на 2023?