Почему эта замена регулярного выражения с использованием захвата зависает в этом быстром коде?

Пытаясь одновременно освоить некоторые элементы 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).

Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
73
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Несколько наблюдений:

  1. Неэкранированный . в регулярном выражении будет соответствовать любому символу (включая запятую!). Если . можно избежать с помощью обратной косой черты в targetPattern, будет сопоставлен только символ точки:

    let targetPattern = /(?<timeStamp>\d\d:\d\d:\d\d)\./
    

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

  2. Ваш код вызывает 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). Для коротких строк разница будет скромной, но для действительно длинных строк она действительно будет суммироваться.

  3. Более того, replace(_:maxReplacements:with:) полностью устраняет необходимость в цикле for. Он может заменить все совпадения в одном вызове API:

    let targetPattern = /(?<timeStamp>\d\d:\d\d:\d\d)\./
    var vtt = vttText.replacing(targetPattern) { $0.timeStamp + "," }
    

    Цикл for не нужен.

  4. Лично я бы вместо использования именованной группы захвата использовал группу без захвата (?:…) для части hh:mm:ss строки. На самом деле вам вообще не нужно заменять эту часть (и, следовательно, вам не нужно ее захватывать; вас интересует только замена символов ., которым предшествуют строки hh:mm:ss):

    let targetPattern = /(?:\d\d:\d\d:\d\d)\./
    var vtt = vttText.replacing(targetPattern, with: ",")
    

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

Ответ Роба полезен и решил мою проблему. То есть я чему-то научился. Однако пункт 4. не сохраняет остальную часть моей записи, поэтому мне понадобился захват. Пункт 3. делает свое дело! (Я пробовал что-то подобное, но все равно думал, что мне нужен цикл.)

rbkopelman 26.08.2024 13:20

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