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

Суть - у нас есть лог (покажу самую интересную часть*), в котором много много разной информации (~100k-700k строк). Из этого лога нам нужно ~3% символов (именно, даже не строк). Затем, сделать таблички по полученным данным и визуализировать это всё. Делал я всё это на python, поэтому и регулярки написаны под python (спойлер: здесь только про регулярки).

Фактически, вся работа с логом сводилась к решению 3-ёх случаев:

  1. Достать строку, в которой есть определенные слова.

  2. Нам нужно найти конкретный реквест и достать данные из его аргументов.

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

Для проверок наших регулярок, я использовал regex101.com, соответственно, настраиваем его под python.

Итак, часть Лога

Наш любимый лог
Наш любимый лог

Задача №1:

Нужно достать данные, когда запустилось приложение.

Мы знаем:
1) Строка начинается с даты, то есть с цифры.
2) Из документации (и здравого смысла, после просмотра лога),
что нужная нам строчка, содержит фразу "start_of_app_here".

Я рассчитываю, что вы уже немножко понимаете в регулярках, поэтому не буду расписывать, какой символ за что отвечает.
В итоге получаем такую регулярку:
r"^\d.{,1000}start_of_app_here"

Проверяем.

Итоговая регулярка 1-ой задачи
Итоговая регулярка 1-ой задачи

Всё работает, но стоит добавить:

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

  • Не забыть, при использовании метода findall третьим аргументов указать re.M, в противном случае, ^ будет восприниматься как начало текста, а не начало строки.
    Пример: re.findall(r"your_reg", data, re.M)

Задача №2:

Достать аргументы запроса Pick_something.

Здесь немного поинтереснее и самое простое было бы достать всё, что начинается с Pick_something. У нас получилось бы что-то вроде:
r"Pick_something\(.{,1000}\)"
Но после этого нужно было бы избавляться от самого названия запроса и скобок.

Первая попытка решения 2-ой задачи
Первая попытка решения 2-ой задачи

Я предлагаю, сразу доставать только то, что внутри скобок. Для этого нам надо узнать, что делают (?<=text) и (?<!text). (?<=) - ищет text и не включает находку в вывод. (?<!) - тоже самое, только наоборот.
Из этого можем построить шаблон:
(?<=начало_искомого)что_нужно_достать(?<!конец_искомого).

В нашем случае:
r"(?<=Pick_something\().{,1000}(?<!\))"

Итоговая регулярка 2-ой задачи
Итоговая регулярка 2-ой задачи

Задача №3:

Получаем от коллег по "самой лучшей работе в мире"* новую вводную, что в аргументах не вся информация, что нам нужна.
Обновленная задача звучит как: достать аргументы запроса и циферку команды QuantityInputCommand, которая идёт до запроса.

Первое, что я попробовал, используя прошлые "наработки" достать инфу также, только обернуть нужную мне информацию в группировочные скобки - ()
То есть:
r"(?<=QuantityInputCommand:\s)(\d{,10})(?:\s|\S)+Pick_something((.{,200})(?<!))"

Первая попытка решения 3-ей задачи (сильно неудачная)
Первая попытка решения 3-ей задачи (сильно неудачная)

Нам нужна цифра, которая идёт непосредственно перед нашим запросом (Pick_something), то есть такая регулярка не отработала от слова совсем.
Есть вариант доставать всё подряд, и на пост обработке, когда уже будет список всех значений, сделать проверку на последовательность, то есть "если за цифрой из QuantityInputCommand идёт что-то начинающееся с ", то оставляем, в противном случае, удаляем".
Выглядит примерно так:
r"(?<=QuantityInputCommand:\s)\d{,10}|(?<=Pick_something().{,200}(?<!))"

Вторая попытка решения 3 задачи (рабочая, но требует пост обработки)
Вторая попытка решения 3 задачи (рабочая, но требует пост обработки)

В итоге, мы можем указать на проверку отсутствие повтора, который нам всё ломает. Просто используем (?<!).
В нашей задаче решение* выглядит так:
r"(?<=QuantityInputCommand:\s)(\d{,10})(?:\s|\S(?<!QuantityInputCommand:))+(?<=Pick_something()(.{,200})(?<!))"

Смотрим, стоит ли слева QuantityInputCommand ((?<=QuantityInputCommand:\s)), забираем цифру и группируем её ((\d{,10})), проверяем, не стоит ли после какого-либо текста (\s|\S) еще одна команда QuantityInputCommand ((?<!QuantityInputCommand:))+) и дальше, по старой схеме, ищем нужный нам запрос и группируем и забираем его аргументы ((?<=Pick_something()(.{,200})(?<!)))

Получаем:

Итоговая регулярка 3-ей задачи
Итоговая регулярка 3-ей задачи

Есть немного лишнего текста, но в группах у нас только нужные данные.
Я остановился на этом варианте, данные нужные получены, по скорости меня удовлетворило решение, наверняка, можно придумать что-то поэлегантнее, но в регулярках главное вовремя остановиться, доделывать их можно вечно*.

Итог

Основное, что я хотел показать, это 3-я регулярка, возможно, кому-то поможет и станет неким шаблоном. Довольно долго искал, где бы в лоб сказали: "делай вот так и будут тебе 1) нужные данные 2) по твоему условию 3) без лишних повторов".

Мне показалось, такая потребность может встречаться часто при работе с текстовыми данными, но в статьях про "Основы regex" подобного не нашел, есть подозрения, что такая штука должна в них быть, но мне так и не попалась.

Надеюсь, был кому-то полезен, заранее благодарю за оценку и\или комментарий)
Возможно, напишу небольшое продолжение, по дальнейшей обработке и визуализации в Jupyter Notebook на манер книги "Storytelling with data", если кому-то интересно, дайте знать в комментариях, пожалуйста :-)

P.S. * - по мнению автора.

Комментарии (43)


  1. jaiprakash
    16.10.2022 14:03
    +14

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


    1. tuxi
      16.10.2022 14:25
      +1

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


    1. spectr100101 Автор
      16.10.2022 21:38
      +1

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

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


    1. shoorick
      16.10.2022 23:29
      +1

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


  1. datacompboy
    16.10.2022 14:08
    +11

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

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

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


    1. datacompboy
      16.10.2022 14:11
      +3

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

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

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

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

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


      1. spectr100101 Автор
        16.10.2022 21:19
        +1

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

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

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


    1. martin_wanderer
      16.10.2022 15:55

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


      1. datacompboy
        16.10.2022 17:16

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


  1. torbasow
    16.10.2022 14:32
    +23

    3-ёх

    Я думал, это просто анекдот.


    1. Firz
      16.10.2022 19:03
      +7

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


      1. spectr100101 Автор
        16.10.2022 19:18
        +2

        2-вух*

        Падеж не тот)


  1. ganqqwerty
    16.10.2022 15:11
    +7

    Меня удивляет, что редко в коде встретишь, чтобы человек декомпозировал регулярку. При том, что обычно в длинном выражении четко видны куски, на которое оно распадается. Вот зачем нужен вот этот 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;
    }

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

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


    1. DirectoriX
      16.10.2022 16:41
      +4

      У меня сложилось впечатление, что если регулярка длиннее ~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, и просто брать ту самую переменную.


      1. 0x131315
        16.10.2022 17:50
        +11

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

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

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

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

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

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

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

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

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


        1. Politura
          16.10.2022 20:53

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

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


          1. funca
            16.10.2022 21:38
            -1

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


        1. spectr100101 Автор
          16.10.2022 21:44

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

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


        1. jaiprakash
          17.10.2022 09:24

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


      1. ganqqwerty
        16.10.2022 18:24
        +1

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


      1. spectr100101 Автор
        16.10.2022 21:22

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

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


        1. DirectoriX
          16.10.2022 21:31

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


          1. spectr100101 Автор
            16.10.2022 22:07

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

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

            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 сохранял те, что проверку прошли, не помню уже. Вы что-то подобное имеете ввиду?


    1. shoorick
      16.10.2022 23:41

      А вот в перле, например, можно в регулярных выражениях использовать модификатор 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;
      


      1. ganqqwerty
        17.10.2022 00:20
        +1

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


    1. ganqqwerty
      17.10.2022 00:23

      Зафигачу-ка я по этому поводу статью... (опубликуется утром, вдруг за ночь что-то еще в голову придёт).


  1. vasyakolobok77
    16.10.2022 15:51
    +10

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

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

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


    1. spectr100101 Автор
      16.10.2022 21:33
      +1

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

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

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


  1. FilimoniC
    16.10.2022 17:43

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


    1. datacompboy
      16.10.2022 17:52
      +2

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

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


      1. funca
        16.10.2022 21:30

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


  1. miga
    16.10.2022 19:13
    +2

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


    1. Moskus
      16.10.2022 20:54
      +1

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


      1. spectr100101 Автор
        16.10.2022 21:50
        +2

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


        1. Moskus
          16.10.2022 23:42
          +1

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


      1. FruTb
        17.10.2022 13:36
        +1

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


  1. funca
    16.10.2022 21:19

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

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


    1. spectr100101 Автор
      16.10.2022 21:47
      +1

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

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

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


      1. funca
        16.10.2022 22:22
        +1

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


        1. spectr100101 Автор
          16.10.2022 22:25

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


          1. funca
            16.10.2022 22:35

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


            1. spectr100101 Автор
              16.10.2022 22:59

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

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


  1. 7313
    17.10.2022 12:45
    -1

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