Пытаясь одновременно освоить некоторые элементы SwiftUI и Regex, я подумал, что перевести файл VTT в файл SRT будет достаточно простой задачей. В этом переводе отметка времени, включающая точку, преобразуется в такую же отметку времени с запятой.
Я думал, что это будет аккуратно обработано с помощью выражения регулярного выражения с использованием метки времени. Я закодировал это, и кажется, что все работает нормально при тестировании на выборке из нескольких записей. Однако когда я записываю весь файл VTT в виде строки и пробую его, функция просто зависает. Я думаю, проблема в количестве замен.
Файл VTT типичного голливудского фильма содержит около 1500 записей, каждая из которых имеет временные метки, требующие двух переводов. Временная метка выглядит следующим образом: 00:00:48.757 --> 00:00:52.970
, и единственное, что нужно сделать, это превратить точки в запятые, например: 00:00:48,757 --> 00:00:52,970
.
Я не могу просто выполнить глобальную замену, потому что другие точки будут преобразованы в запятые, и мне нужно это только для временных меток. Следующий код правильно выполняет переводы, но зависает для больших файлов:
func VTT2SRT() {
var vtt$ = vttText // preserve vttText from views
let targetPattern = /(?<timeStamp>\d\d:\d\d:\d\d)./
let matchArray = vtt$.matches(of: targetPattern)
for s$ in matchArray {
vtt$ = vtt$.replacing(s$.0, with: s$.timeStamp + ",")
}
[other stuff that works just fine]
srtText = srt$ // pass it back to views
}
Я не могу понять, почему это зависает или занимает так много времени. Может ли кто-нибудь указать мне правильное направление или предложить альтернативный подход? Примечание. Я пытаюсь это сделать в macOS 14.6.1 (23G93), используя Xcode версии 15.4 (15F31d).
Несколько наблюдений:
Неэкранированный .
в регулярном выражении будет соответствовать любому символу (включая запятую!). Если .
можно избежать с помощью обратной косой черты в targetPattern
, будет сопоставлен только символ точки:
let targetPattern = /(?<timeStamp>\d\d:\d\d:\d\d)\./
Если не использовать .
, строка замены будет соответствовать строке поиска. Теперь ваш пример не выполняет повторный поиск, но если бы это было так, вы могли бы получить бесконечный цикл, если бы вам не удалось выйти из .
.
Ваш код вызывает replacing
внутри цикла for
. Это означает, что на каждой итерации цикла for
он повторно сканирует всю строку в поисках совпадений, что приводит к производительности, схожей с производительностью O(n²). Кроме того, на каждой итерации будет создаваться новая копия полной строки, что очень неэффективно.
Вместо повторного сканирования строки в цикле for
вы можете просто заменить соответствующую подстроку:
var vtt = vttText
let targetPattern = /(?<timeStamp>\d\d:\d\d:\d\d)\./
let matches = vtt.matches(of: targetPattern)
for match in matches.reversed() {
vtt.replaceSubrange(match.range, with: match.timeStamp + ",")
}
Вызов replaceSubrange
вместо replacing
позволит избежать повторного сканирования строки для каждой итерации цикла for
.
(Обратите внимание: если вам интересно, почему я перевернул массив совпадений, это надежный способ убедиться, что замена некоторой подстроки не делает недействительными индексы ранее найденных совпадений позже в строке. В данном случае это не является необходимым (поскольку длина замены всегда равна длине заменяемой строки), но это общий шаблон.)
Приведенный выше фрагмент будет гораздо более производительным: вам понравится производительность O(n). Для коротких строк разница будет скромной, но для действительно длинных строк она действительно будет суммироваться.
Более того, replace(_:maxReplacements:with:) полностью устраняет необходимость в цикле for
. Он может заменить все совпадения в одном вызове API:
let targetPattern = /(?<timeStamp>\d\d:\d\d:\d\d)\./
var vtt = vttText.replacing(targetPattern) { $0.timeStamp + "," }
Цикл for
не нужен.
Лично я бы вместо использования именованной группы захвата использовал группу без захвата (?:…)
для части hh:mm:ss
строки. На самом деле вам вообще не нужно заменять эту часть (и, следовательно, вам не нужно ее захватывать; вас интересует только замена символов .
, которым предшествуют строки hh:mm:ss
):
let targetPattern = /(?:\d\d:\d\d:\d\d)\./
var vtt = vttText.replacing(targetPattern, with: ",")
Документы говорят нам, что группы без захвата более эффективны, чем группы захвата, и интуитивно кажется более логичным просто заменить один рассматриваемый символ, а именно .
, а не всю строку временной метки.