Как стать автором
Обновить

Комментарии 47

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

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

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

Есть ощущения, что один раз их понял (как один товарищ абсолютно понял мир), и можешь работать с любыми текстами.

Ну... Я когда-то, пробуя Перл после Си (до этого была куча и других языков), тоже активно использовал строковые функции. Но потом втянулся, прочитал man perlre и пару перловых книжек — теперь спокойно пользуюсь регулярными выражениями. Правда, не везде их синтаксис одинаков.

Регулярка вроде как медленнее работает.

  • Лучше не использовать квантификаторы * и +, по моему опыту, они работают гораздо дольше квантификаторов с установленными границами {,}

Что-то мне подсказывает, что это оттого что вы использовали greedy квантификаторы.

Модно воспользоваться *?

(?<=text) и (?<!text)

Zero-lookahead и ко вообще нужны только в очень крайних случаях. Вместо этого лучше сгруппировать и достать группу. Работает быстрее как правило всегда.

r"(?<=Pick_something().{,1000}(?<!))"

кстати тут быстрее ещё будет greedy позитиынй запрос. То есть:

r"Pick_something\(([^,]*)\)"

Огромное спасибо!

Все замечания прочекаю, попробую отредактировать статью)

Может я глупый или искать инфу нормально не умею, но вот не мог нормальные примеры (особенно для 3-ей задачи) найти и все.. А вы сразу практически все объяснили) Как-то даже немного "стремно" от того, что чтобы начинающему "специалисту" найти человека, который разбирается и подскажет какой-то вопрос, нужно написать статью на хабре..)

Раньше нужно было попасть в FIDO

Эх, опередили. Добавлю только, что в варианте автора сматчится самая длинная подстрока, заканчивающаяся start_of_app_here, тогда как в варианте с .*? - самая короткая.

Самая длинная до 1000 символов. С учетом /m модификатора без /s под точку не попадает перевод строки, так что в данном случае эти регексы более-менее эквивалентны.

3-ёх

Я думал, это просто анекдот.
Это Вам просто «2-ва» еще не попадалось никогда.

2-вух*

Падеж не тот)

Меня удивляет, что редко в коде встретишь, чтобы человек декомпозировал регулярку. При том, что обычно в длинном выражении четко видны куски, на которое оно распадается. Вот зачем нужен вот этот write-only код?

^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.]((19|20)\d\d)$

Куда уж проще написать

"^" + day + delimiter + month + delimiter + year + "$"

Интересно, что в деле разработки ПО есть несколько таких слепых зон, где многие толковые программисты берут и забивают на все правила. И если за однострочик вроде такого тебе сразу оторвут руки:

TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
    return root1&&root2 ? new TreeNode(root1->val+root2->val,mergeTrees(root1->left,root2->left),mergeTrees(root1->right,root2->right)):root1?root1:root2;
}

... то приведенная выше регулярка зачастую пройдет код ревью.

Длинные регулярки и детсадовский код в юнит тестах приходят на ум мгновенно. Еще, пожалуй, скрипты, но если они совсем маленькие и их немного - то ладно.

У меня сложилось впечатление, что если регулярка длиннее ~30 символов — надо её не разбивать на кусочки с комментариями, а упрощать и заменять средствами самого языка. Например, для вашего примера с датой я бы написал что-то вроде
matches = regex.parse("^(\d{1,2})[- /.](\d{1,2})[- /.](\d{4})$", input);
month = matches[1]; day = matches[2]; year = matches[3];
if (day<1 || day > 31) || (month<1 || month>12) || (year<1960 || year>2050) {/*обработка ошибки*/};
// полезный код

Так можно не только сразу получить переменные со значениями, но и проверить более хитрые сценарии (например, ваша регулярка радостно примет 31-е февраля, что не очень корректно).
Либо регуляркой искать общий шаблон, а затем пробовать парсить в нормальный тип (вроде Date.parse(matches[0])).

А что до примеров из статьи — сложную регулярку из 3-го примера можно заменить двумя простыми: первой доставать циферку QuantityInputCommand и сохранять в переменную, а второй искать Pick_something, и просто брать ту самую переменную.

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

В моей практике был такой случай: медленный огромный динамический запрос в БД, из-за которого вешалась одна из подсистем сервиса, и который не удалось победить в рамках SQL или кеширования, но в итоге удалось забороть именно гибридным подходом, с промежуточными вычислениями в коде.

Ребята в течении трех лет несколько раз пытались оптимизировать проблемный запрос, но ничего сделать не смогли, и в итоге просто сдались, отказавшись от любых дальнейших попыток. Но в рамках не связанной напрямую с проблемой задачи эта проблема случайно досталась и мне, в стиле "просто посмотреть, вдруг что-то можно сделать". И за пару дней мозгового штурма и анализа мне тоже не удалось его существенно ускорить в рамках SQL, хотя запрос и был препарирован до мельчайших частей, тщательно изучен, многократно проанализирован и пересобран около 2-3 десятков раз в различных вариантах, разными методами и способами, от базовых до самых извращенных - это уже был спортивный интерес. В итоге только подтвердил выводы ребят: в рамках SQL и ограничений системы тут ничего не сделать, в любом случае каждый раз БД приходится перемалывать огромные объемы данных, откуда и идет большая задержка, и для функционала запроса нужен весь этот объем данных.

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

Мне же пришло в голову декомпозировать не сам запрос, а задачу решаемую им в целом - там и удалось нащупать гибридное решение. Причем сначала просто как идею, а потом ее удалось развить и в полноценное решение проблемы.

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

Но тут как с денормализацией БД: иногда нужно сделать шаг назад, чтобы прыгнуть вперед. Из-за того, что появились статические условия, пусть и в рамках ограниченного набора условий, появилась возможность начальные части запроса поместить в кеши - теперь для этого не нужны бесконечные кеши под каждый вызов, теперь это всего несколько десятков вариантов наборов условий, которые отлично кешируются. Конечно кеширование тоже пришлось оптимизировать - объем данных там хоть и относительно небольшой, но заметный, пришлось выгружать два примитивных поля в отдельные кеши, с промежуточным сжатием, т.к. на таких объемах без сжатия задержку начинает давать уже кеш. А динамическая часть запроса при этом никуда не делась, просто теперь она дополняется кешированными предвычесленными данными, и по БД фактически остается перебрать совсем немного данных.

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

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

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

Еще как вариант - есть разные ситуации когда денормализация данных хранимых в БД бывает очень полезна.

Решения с хранимыми процедурами по производительности сильно зависят от возможности масштабирования самой СУБД (а они обычно не очень). Поэтому со временем набрали популярность трехзвенки, ведь application сервера масштабируется гораздо проще.

Комментарий интересный, конечно, но не очень понимаю как он к статье относится.

Может будет эффективнее перейти на статью с похожей тематикой или самому описать? Вроде, интересующихся вашим кейсом немало, возможно, могли бы какие-то ещё идеи найти)

Звучит как тизер к статье)

Очень классный комментарий, совершенно с вами согласен. Не имеет смысла использовать текстовые методы для верификации чисел

Разве то, что могут идти две QuantityInputCommand подряд не помешает предложенной вашей логике?

Если мы раскидываем переменные по спискам, то их длинны необязательно будут одинаковыми.

Храните только последнее значение — тогда перед Pick_something будет именно самое последнее встретившееся QuantityInputCommand. Если вы складываете в список пары (QuantityInputCommand, Pick_something) — формируйте их прям в момент складывания.
Единственный нюанс: если не перед каждым Pick_something есть свой QuantityInputCommand — тогда надо будет, например, класть в QuantityInputCommand Null и проверять на «нормальное» значение перед использованием.

Не совсем понял...

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

QuantityInputCommand: 10
Pick_something:(args_1)
QuantityInputCommand: 1
QuantityInputCommand: 2
Pick_something:(args_2)

Нужно, чтобы получилось:

["10, args_1", "2, args_2"]

Если что, я ищу через findall

Первая версия алгоритма была: было 2 регулярки, одна на Pick..., другая на Quantity..., через (?:Pick...|Quantity...) искалось все подряд, добавлялось в Series, потом была проверка на существование Pick... после Quantity..., соответсвенно, если проверка не проходила либо удалял не подходящий Quantity, либо в другой Series сохранял те, что проверку прошли, не помню уже. Вы что-то подобное имеете ввиду?

Поскольку для третьего варианта не указано, сколько записей лога может быть между Quantity и Pick_something, один regex на "все сразу" вызовет необходимость считывать все содержимое целиком, хотя задача явно потоковая, надо просто хранить стейт.

  1. Находим значение Quantity простой регуляркой и всегда перезаписываем текущим значением.

  2. Находим Pick_something как и раньше и выдаём на выход Quantity и аргументы (тут же можно встроить проверки данных при необходимости).

  3. Обнуляем Quantity и найденные аргументы и наяинаем с пункта 1 со следующей строки лога, пока не дойдем до конца.

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

А вот в перле, например, можно в регулярных выражениях использовать модификатор x — он позволяет игнорировать пробелы (точнее пробельные символы сами по себе, а не \s), записывать выражение в несколько строк и добавлять комментарии. Получается достаточно удобно (пример из Mojo::Date):

my $RFC3339_RE = qr/
  ^(\d+)-(\d+)-(\d+)\D+(\d+):(\d+):(\d+(?:\.\d+)?)   # Date and time
  (?:Z|([+-])(\d+):(\d+))?$                          # Offset
/xi;

или даже так (из Mojo::DOM::HTML):

my $TOKEN_RE = qr/
  ([^<]+)?                                            # Text
  (?:
    <(?:
      !(?:
        DOCTYPE(
        \s+\w+                                        # Doctype
        (?:(?:\s+\w+)?(?:\s+(?:"[^"]*"|'[^']*'))+)?   # External ID
        (?:\s+\[.+?\])?                               # Int Subset
        \s*)
      |
        --(.*?)--\s*                                  # Comment
      |
        \[CDATA\[(.*?)\]\]                            # CDATA
      )
    |
      \?(.*?)\?                                       # Processing Instruction
    |
      \s*([^<>\s]+\s*(?:(?:$ATTR_RE){0,32766})*+)     # Tag
    )>
  |
    (<)                                               # Runaway "<"
  )??
/xis;

Да у вас внутри регулярки можно и комменты писать, круто!

Не примите за личное, но неужели ваши "потуги" достойны отдельной статьи? Если пишите про регулярки, то будьте добры описать что такое квантификаторы, жадность, классы символов, look-ahead / look-behind запросы, модификаторы.

Вот даже вас кейс с поиском внутри скобок решает через пару look-behind + look-ahead:

(?<=Prefix)(inner)(?=Postfix)

Вопрос интересный про "достойно для отдельной статьи" и, скорее всего, ответ будет неоднозначным.. С одной стороны, действительно, Америку я тут не открываю, с другой, я бы не против наткнуться на решении 3 задачи, когда пытался что-то подобное найти..)

Но после публикации появился другой фактор, пришёл datacompboy в комментарии и кратко, понятно и доходчиво объяснил, что я неправильно понимаю и как мою регулярки улучшить (те что в статье и будущие), а это точно стоило публикации этой статьи)

Насчёт того, что нужно было описать что такое квантификатор и т.д. не могу согласится. Это решение конкретно моего кейса и пример решения задачи, которого сам я найти не смог,. Опять же, по себе сужу, все части с очередным объяснением что такое квантификатор, жадность и т.д., скорее всего, пролистал бы и сначала пошёл смотреть задачи, совпадают ли они с моими. К тому же, не думаю, что человек вводит в поисковике "Основы regex" и ему первой ссылкой советуют эту статью, все же рассчитываю на то, что человек уже немножко понимает, а если не понимает, сможет решить свою задачу.

На больших объемах строк регулярка всегда проигрывает по скорости "классичесим" методам, причем в сотни раз. Не рекомендую использовать регулярки для фильтрации больших объемов строк - сперва объем надо сократить, а уже потом можно на него натравливать регулярку

А мы покупаем или продаём? perl как раз специализировался на процессинге больших текстов, регулярками.

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

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

Может быть году в 2030м до людей все-таки дойдет польза структурированных логов…

Это было бы чудесно. Но пока есть системы и устройства, где логи формируются непонятно чем (навскидку - DPRINT в контроллерах станков, чья выдача по serial2ip отправляется на сервер), такой ужас будет жить.

И пока логику сбора логов пишет не тот, кто потом с этими логами работает)

В приведенном мной примере, мне лично гораздо проще сделать на порядок более сложную логику reader-а, чем возиться с writer-ом.

Так берем тот-же логстеш или грейлог и настраиваем обработчики. Те логсервер уже обладает возможностью сам структурировать любой неструктурированный текс через, к примеру, горк

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

Если же речь о продакшне, то уже с первой задачи стоит начинать с архитектуры вашего решения, и смотреть в сторону нормальных агрегатов логов типа Splunk, ELK и т.п.

А если уже рабочая задача, тебя не спрашивают, что ты думаешь о логах, тебе просто скидывают файл и говорят: "Мне нужно, чтобы ты посчитал сколько за день таких-то реквестов"

Что-то вроде - один раз сделал и больше к этому не возвращался.

Я это к тому, что теория это хорошо, но иногда нужно решение, а не понимание.

Если бы статья называлась в духе "как решать одноразовые рабочие задачи с помощью регулярок, ни чего в них не понимая", то ни каких вопросов бы не возникло. Это тоже опыт, которым наверное стоит делиться. Все обычно начинают с каких-то своих экспериментов и кастылей. Но давать советы как писать эти самые регулярки на этом этапе я бы не стал.

С одной стороны резонно, с другой почему нет, если если задача решена и решается за приемлимое время?

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

Век живи, век учись писать статьи на хабре)

Попробую поредачить, но концепт оставлю. Все ещё думаю, что должно быть красивое решение 3-ей задачи с нормальным шаблоном, должен же кто-то его написать хехе

Умоляю! Не используйте древнепрограммистское слово «регулярки» в одном предложении с новопи нововыпендрёжным «кейсы»

Просмотрел комменты. Складывается ощущение, что я один в детстве читал Mastering Regular Expressions. Пожалуйста, скажите, что я не прав...

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории