Java, плохая производительность регулярных выражений с ленивыми выражениями

Код фактически находится на Scala (Spark / Scala), но библиотека scala.util.matching.Regex, согласно документации, делегирует java.util.regex.

Код, по сути, считывает множество регулярных выражений из файла конфигурации, а затем сопоставляет их с журналами, подаваемыми в приложение Spark / Scala. Все работало нормально, пока я не добавил регулярное выражение для извлечения строк, разделенных вкладками, где вкладка была сглажена до "# 011" (с помощью rsyslog). Поскольку в строках могут быть пробелы, мое регулярное выражение выглядит так: (.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)#011(.+?)

В тот момент, когда я добавляю это регулярное выражение в список, приложению требуется вечность, чтобы завершить обработку журналов. Чтобы дать вам представление о величине задержки, типичный пакет из миллиона строк занимает менее 5 секунд для сопоставления / извлечения в моем кластере Spark. Если я добавлю выражение выше, партия займет час!

В моем коде я пробовал несколько способов сопоставить регулярное выражение:

  1. if ( (regex findFirstIn log).nonEmpty ) { do something }

  2. val allGroups = regex.findAllIn(log).matchData.toList if (allGroups.nonEmpty) { do something }

  3. if (regex.pattern.matcher(log).matches()){do something}

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

Вопрос / ответ, помеченный как дубликат, содержит ссылку что мне трудно уследить. Было бы легче следить за текстом, если бы упомянутое программное обеспечение, regexbuddy, было бесплатным или, по крайней мере, работало на Mac.

Я пробовал отрицательный просмотр вперед, но не могу понять, как отрицать строку. Вместо /(.+?)#011/, что-то вроде /([^#011]+)/, но в нем просто написано отрицание "#", "0" или "1". Как мне отрицать "# 011"? Даже после этого я не уверен, решит ли отрицание мою проблему с производительностью.


person Joe Nate    schedule 15.05.2015    source источник
comment
Почему вы повторяете # 011 (. +?) Несколько раз?   -  person JFPicard    schedule 15.05.2015
comment
AFAIK, регулярное выражение медленное не из-за ленивых / неохотных квантификаторов, а из-за того, что базовая реализация обратного отслеживания по своей сути медленная. Настоящее долгосрочное решение - заменить движок Java на сопоставитель автоматов NFA. А пока, как обходной путь к проблемам с регулярным выражением, как насчет использования String.split () на # 011 и выполнения обработки на его основе?   -  person Nayuki    schedule 15.05.2015
comment
Для справки: swtch.com/~rsc/regexp/regexp1.html (экспоненциальный по сравнению с полиномиальным временем для регулярных выражений со многими кванторами)   -  person Nayuki    schedule 15.05.2015
comment
@NayukiMinase Невозможно использовать split (), потому что код не привязан к конкретному журналу / строке. Код / приложение принимает список регулярных выражений и для каждой строки журнала, передаваемой в приложение, пытается сопоставить регулярное выражение из списка. Один из подходов, которые я рассматриваю, - это написать функцию регулярного выражения на C и вызвать ее из Scala.   -  person Joe Nate    schedule 15.05.2015
comment
Я снова открыл этот вопрос, потому что не думаю, что проблема в отсутствии жадности. Вы говорите, что применяете регулярное выражение к одной строке за раз; почему вы не используете якоря (^ и $)? Кроме того, есть ли еще какие-нибудь # символы, кроме тех, что на сглаженных TAB? Может быть, вы могли бы использовать ([^#]++) вместо (.+?).   -  person Alan Moore    schedule 15.05.2015
comment
Вам действительно нужны все группы матчей? Если нет, возможно, это регулярное выражение немного ускорит процесс: (?:(?:(?!#011).)++#011){34}(?:.+?)   -  person Bart Kiers    schedule 15.05.2015
comment
@JoeNate Как вы думаете, почему функция на C будет быстрее? Это будет быстрее, если вы получите лучший движок регулярных выражений, и медленнее, если вы получите худший. Но это всегда будет намного сложнее и к тому же намного медленнее, чем использование правильного регулярного выражения (которое требует всего несколько десятков циклов на символ).   -  person maaartinus    schedule 15.05.2015
comment
@AlanMoore Я бы также снова открыл этот вопрос, поскольку он гораздо более конкретен, а связанный общий ответ довольно нетривиален. Здесь лень приводит к катастрофическому возврату, но это может происходить и без лени.   -  person maaartinus    schedule 15.05.2015


Ответы (1)


Самый простой способ - разделить на #011. Если вам нужно регулярное выражение, вы действительно можете отрицать строку, но это сложно. Я бы пошел на атомную группу

(?>(.+?)#011)

После сопоставления больше не будет возврата. Готово и с нетерпением жду следующей группы.

Отрицание строки

Дополнение #011 - это все, что не начинается с #, не начинается с # и за которым не следует 0, или начинается с двух и не следует ... вы знаете. Я добавил пробелы для удобочитаемости:

 ((?: [^#] | #[^0] | #0[^1] | #01[^1] )+) #011

Довольно ужасно, не правда ли? В отличие от вашего исходного выражения, оно соответствует символам новой строки (вы не указали их конкретно).

Альтернативой является использование отрицательного просмотра вперед: (?!#011) соответствует, если следующие символы не #011, но ничего не съедают, поэтому мы используем ., чтобы съесть один символ:

 ((?: (?!#011). )+)#011

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

Оптимизация

Из моих вышеупомянутых регулярных выражений первое лучше. Однако, как писали Казимир и Ипполит, есть возможности для улучшений (коэффициент 1,8).

( [^#]*+ (?: #(?!011) [^#]* )*+ ) #011

Это не так сложно, как кажется. Сначала сопоставьте любое число (включая ноль) не-# атомарно (завершающее +). Затем сопоставьте # без 011 и снова любое количество не #. Повторите последнее предложение любое количество раз.

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

person maaartinus    schedule 15.05.2015
comment
Регулярное выражение для атомарной группы, похоже, не работает. Попробуйте сопоставить / извлечь эту строку (пустая) # 011. - person Joe Nate; 15.05.2015
comment
@JoeNate Спасибо, исправлено. Проблема заключалась в '‹' (чепуха, похожая на заглядывание назад) вместо '›'. - person maaartinus; 15.05.2015
comment
Вы спасаете жизнь, спасибо большое :) Пробовал ^. Теперь противопехотная мина исчезла! :) Разместите в подарок ссылку на пиво, если она у вас есть: D - person Joe Nate; 15.05.2015
comment
Когда вы используете расширенный механизм регулярных выражений, нет необходимости использовать подшаблоны, разработанные для posix, например: (([^#] | #[^0] | #0[^1] | #01[^1] )+) #011 или этот неэффективный обходной путь: ((?: (?!#011). )+)#011 (обратите внимание на количество бесполезных тестов с этим способом). Вы можете использовать что-то вроде этого: [^#]*+(?:#(?!011)[^#]*)*+, который намного эффективнее. Однако первый способ может быть полезен и хорошо известен для mysql, sed или grep. - person Casimir et Hippolyte; 16.05.2015
comment
@CasimiretHippolyte Я не вижу количества бесполезных тестов, вы можете уточнить? Я вижу, что если я не смотрю на знак решетки, то съедаю чеканку или делаю что-то более сложное, что должно быть довольно быстро. +++ Я также потерялся в отношении гораздо более производительного решения, поскольку вместо этого я выбрал бы свое самое первое регулярное выражение (моя причина для раздела отрицания на самом деле предполагала, что я не могу использовать атомарные группы). - person maaartinus; 16.05.2015
comment
@maaartinus: Взгляните сюда: regex101.com/r/fN8xN8/1 (см. версии с 1 по 4, посмотрите на количество шагов и используйте отладчик, чтобы увидеть, что происходит). Очевидно, что количество шагов является только ориентировочным, но если вы сравните эти шаблоны с большими строками (или повторите их много раз), вы увидите реальную разницу. - person Casimir et Hippolyte; 16.05.2015
comment
@CasimiretHippolyte Действительно хороший сайт! На самом деле, я знал, что вы использовали (simple*+ (complex simple*+)*), но надеялся, что мое простое решение будет конкурентоспособным. Что касается количества шагов, то вы выигрываете почти в 2 раза, и это неплохо! - person maaartinus; 16.05.2015
comment
Притяжательные квантификаторы *+ используются только для предотвращения обратного отслеживания, когда нет #011 после, но основная идея состоит в том, чтобы ограничить использование просмотра вперед (только когда это необходимо, то есть только после #). - person Casimir et Hippolyte; 16.05.2015