Мой первый Pet-проект - Regexoop

Ссылка на проект Regexoop

Немного о себе. Я всегда был противником бесплатного труда, меня так воспитали. Работая только за деньги и только с чужими проектами я погорел. Это ужасное чувство тошноты. Я пробовал делать проекты для себя, но ни с кем не делился. Мне казалось, мою идею украдут, хотя идеи были на уровне игр про караваны. Я пытался делить шкуру неубитого медведя со своим эго. Оно побеждало.

Спустя некоторое время я увидел интересный метод "12 проектов за 12 месяцев". Суть метода состоит в том, что ты каждый месяц делаешь 1 продукт и показываешь его людям. Неважно насколько он готов. Ты можешь эти продукты выпускать как хочешь, хочешь за деньги, хочешь бесплатно. Я выбрал путь для этого проекта - бесплатно.

Суть проекта

Regexoop - переосмысление подхода написания regex. 30 сентября я подумал - "Хм, regex такой сложный. НУ ПОЧЕМУ? Может его можно сделать как-то проще?". Начав ровно 1 октября, я 3 дня писал документацию. Этот объем документации написанной за 3 дня, я разрабатываю уже 3 недели и не дошёл даже до половины. Чего уже говорить о настоящей мощи regex.

Концепт кода:

var rules = new { 
    pattern = "{second_level}{first_level}",
    start = "end",
    first_level = new {
        pattern = "[a-z]",
        minLength = 2,
        maxLength = 3,
    },
    second_level = new {
        pattern = "[a-zA-Z0-9-].", //а вот тут ошибка, но это ведь набросок
        minLength = 2,
        maxLength = 63,
        maxRepeat = 5,
        minRepeat = 0,
        symbolsRules = new {
            "-" = new { repeat: 0 }
        }
    }
};

var er = new Regexoop(rules).Input("https://docs.microsoft.com").Find()
Console.WriteLine(er); //docs.microsoft.com

Настоящий шаблон regex представляет собой мешанину из текста с параметрами и то, что показано в концепте в настоящем regex выглядит примерно так:

^(?!-)[A-Za-z0-9-]+([\\-\\.]{1}[a-z0-9]+)*\\.[A-Za-z]{2,6}$"

Это ещё простой шаблон валидации доменов. Бывают сложнее.

Техническая реализация

Несмотря на долгий опыт в PHP, я выбрал C# как основной язык для этого проекта. В текущем состоянии .net на голову выше экосистемы PHP. Ты просто садишься, открываешь Visual Studio 2019 и всё как по масти работает. Банальный отлов ошибок при компиляции вырезает 90% глупых ошибок.

Библиотека работает на C# под .net 5.

Cценарий работы c библиотекой

  1. Создаёте новый объект правил BasicRule и заполняете публичные свойства правила. Правила можно складывать в друг-друга как матрёшку, но дочерние правила перестают быть самостоятельными и свой результат отдают родительскому правилу;

  2. Создаёте объект Regexoop и помещаете в конструктор свои правила;

  3. Вызываете метод Input() для помещения исходного текста для поиска;

  4. Вызываете один из трех режимов работы методов для поиска текста.

Rule test = new BasicRule()
{
    Name = "root",
    Pattern = "Hello",
    Start = Rule.Direction.start
};
List res = new Regexoop.Regexoop(test).Input("Hello World").Find();
//res: Hello

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

Принцип разбора текста

Исходный текст обрабатывается посимвольно согласно шаблону правил. Вот кусок кода текущего роутера для метода Find()

while(_input.IsComplete() == false) //тут думаю ясно что это исходный текст
{
    _input.MoveCursor(1); //курсор передвинется только в том случае если был запрошен исходный текст
    Rule.Status stepResult = _rule.Peek().ParseSymbol(_input); 
    ...
}

Обработка правил является стеком вызовов правил. Вы спросите зачем, вот пример.

Rule test = new BasicRule()
{
    Name = "root",
    Pattern = "Hello {world} Hello {world}",
    Start = Rule.Direction.start,
    Variables = new List {
                                new BasicRule() { 
                                        Name = "world",
                                        Pattern = "World"
                                    }
                            }

};
List res = new Regexoop.Regexoop(test).Input("Hello World Hello World").Find();
//res: Hello World Hello World

Встречая команду {world} при парсинге шаблона, правило помечает необходимость переадресации на другое правило и помещает в специально отведённое место объект с необходимым правилом, а роутер считывая необходимость переадресации, замораживает текущее исполнение вызова правила и помещает в стек вызовов новое правило. Когда новое правило завершит свою работу со статусом Complete, результат дочернего правила будет присвоено родительскому правилу и родительское правило продолжит работу с места остановки. В общем простая работа стековой машины вызовов.

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

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

Pet-проект это тоже работа

Несмотря на получаемое удовольствие от проекта. Я не могу сказать что это просто. Если на обычной работе программистом ты делаешь только свою часть и в 18:00 уходишь в закат, то здесь всё не так. Даже не представляю сколько времени данный проект

В данном проекте я директор, pr менеджер, pm, team leader, разработчик, тестировщик, системный администратор и сам себе кофе делаю.

Уже 3 недели я выкладываю исходники на Github и только после оформления как nuget пакет я увидел что кто-то качает библиотеку. Наверное боты. 100%. Поэтому без рекламы ваши проекты никому не интересны, даже бесплатно. Пилите караваны и не бойтесь что их кто-то украдёт.

Жду от сообщества критику и предложения по развитию библиотеки.

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


  1. leschenko
    03.11.2021 21:52
    +11

    Очередная попытка убить регулярные выражения очередным велосипедом, который и 1% от их возможностей не покрывает и ничуть не делает код проще.

    Не проще ли разобраться с регулярными выражениями, чтобы они вас больше не пугали?


    1. fiftin
      03.11.2021 22:19
      +5

      Все мы писали свои велосипеды, это часть обучения.


      1. TyVik
        04.11.2021 06:57
        +2

        Главное не тащить их потом в прод.


      1. K1aidy
        04.11.2021 10:54
        +1

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


        1. spaceatmoon Автор
          04.11.2021 10:56
          +1

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


    1. netch80
      03.11.2021 23:34
      +2

      Регэкспы в таком виде действительно тяжело читать. "/x" и "/xx" перлового движка придуманы для того, чтобы можно было описать каждую деталь в комментариях.
      В движках типа Nitra используется PEG для лексического анализа, что позволяет писать с заданием имён конкретным элементам выражения, комментариями, и возможностей в сумме таки больше, чем у самых выдающихся движков вроде того же перлового.
      То, что именно АС сделал движок с меньшими возможностями, показывает, что он не в курсе достижений индустрии — факт, но и само по себе намерение что-то сделать в этом — похвально.


    1. amarao
      04.11.2021 17:36
      +1

      Меня пугают регулярные выражения. Не смотря на то, что я их знаю и использую. Основная причина - регэксп - это код для state machine, подобие ассемблера. Там ничего не для человека. Это write only код, который можно глазами реверс-инженерить и только. Там нет ни имён, ни мест для синтакисческого ритма (отступы, семантические блоки).

      Человек, который написал длинный и суровый регэксп - справился. А как насчёт того, что будет в этом регэкспе багу ловить?


      1. netch80
        07.11.2021 11:25

        > Там нет ни имён, ни мест для синтакисческого ритма (отступы, семантические блоки).

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

        re_token="[A-Za-z0-9.!%*_\\+`'~-]+"
        re_quoted_string="\"([^\r\n\"\\\\]|\\\\[^\r\n])*\""
        re_display_name = "\\s*((?:%s(?:\\s+%s){0,})|%s)\\s*" % \
          (re_token, re_token, re_quoted_string)
        re_gen_value = "(?:%s|%s)" % (re_token, re_quoted_string)
        re_gen_param = "(?:%s(?:\\s*=\\s*%s)?)" % (re_token, re_gen_value)
        


        ну и так далее. Для парсера они предкомпилируется, и то, что у конкретного выражения может быть в итоге 1467 символов (исходник регэкспа для name-address) — уже неважно, структурно всё понятно, тестируется по кускам и так далее.

        PS: Ну да, дальше предлагают механизмы, которые всё это оформляют уже в виде функций/методов — что-то вроде классического parser combinators. Если они могут потом скомпилировать результат во что-то эффективное (один автомат, как у классических регулярок) — отлично. Просто на одну зависимость больше :)


    1. perevedko
      06.11.2021 11:23

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


  1. lair
    03.11.2021 23:57
    +1

    Просто сравните удобство использования: https://github.com/sprache/Sprache


  1. Oskard
    04.11.2021 01:04
    +4

    На эту же тему есть мультиязычный https://github.com/VerbalExpressions


    1. spaceatmoon Автор
      04.11.2021 10:59

      Выглядит здорово.


    1. amarao
      04.11.2021 17:39

      Спасибо и вам, и автору поста, который этот вопрос поднял. Выглядит (Rust-версия) как ровно то, что я хотел бы. Адекватную семантическую разметку вместо байт-кода для робота.


      1. Oskard
        04.11.2021 18:59

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


  1. saboteur_kiev
    04.11.2021 01:09
    +9

    "Хм, regex такой сложный. НУ ПОЧЕМУ?

    Ну почему он всем кажется сложный?
    Регексп ПРОСТОЙ! Нужно просто сесть, прочитать одну книжку по регекспам (1-2 вечера) и посидеть немного на стековерфлоу или unixexchange посматривая задачи по регекспам и парся их. ВСЕ. Все регекспы становятся достаточно простыми.
    Можно не стать супергуру, но как минимум 90% регекспов вы сможете читать сходу, и 60% из них писать сами. Как минимум если говорить по PCRE.


    1. jMas
      04.11.2021 02:45
      +1

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

      По поводу статьи: автор просто учится, думаю пет-проект просто поможет разобраться, но а ещё полезно все таки реальные примеры из жизни.


    1. sshikov
      04.11.2021 13:06

      Книжку и 1-2 вечера даже много, если не пытаться конечно освоить все и сразу (а так делать вообще вредно). Ведь в принципе, базовые вещи совсем простые, и их достаточно мало.

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

      ^(?!-)[A-Za-z0-9-]+([\\-\\.]{1}[a-z0-9]+)*\\.[A-Za-z]{2,6}$

      ^...$ — начало и конец строки, значит мы ищем совпадение не внутри строки, а со строкой целиком. Теперь про это забываем, и рассматриваем то что осталось по частям.

      [A-Za-z0-9-] — латинские большие и маленькие буквы и цифры, и минус
      [a-z0-9] — тоже самое маленькие буквы и цифры
      [A-Za-z] — большие и маленькие буквы, без цифр на этот раз
      Во всех случаях — одна штука

      []+ — одно или более повторений того что в скобках
      {2,6} — от 2 до 6 повторений
      {1} — ровно одно повторение

      ([\\-\\.]{1}[a-z0-9]+)* — а это комбинация из уже изученных вещей, как и вся регулярка в целом. Тут из нового круглые скобки, чтобы + или * применялись к группе как целому

      Ну и что тут сложного, если совсем немного подумать? После этого от регулярки остались рожки да ножки — можно посмотреть в справочник, и понять, что означают остальные конструкции. А по сути, мы изучили всего несколько базовых вещей: *, +, {} как повторение, [] как диапазон символов, () как группировка, ^ И $. Ну и \\ еще.

      И что я забыл, из нужного все время?


      1. saboteur_kiev
        06.11.2021 01:08

        И что я забыл, из нужного все время?

        Я регулярно использую lookahead и look behind, а также группы, чтобы потом можно было использовать back referenсe на сматченную группу.


        1. sshikov
          06.11.2021 09:37

          Я про каждый день. А это все же продвинутые фичи.

          Ну и главное — пока я уложился в одну страницу. То есть правил-то совсем немного.


          1. saboteur_kiev
            09.11.2021 19:41

            Так я тоже про каждый день.

            при помощи lookahead и lookbehind можно грепом вырезать не просто нужные строчки, а еще и нужные столбцы или значения из другого мусора. Одной командой.


    1. SadOcean
      04.11.2021 15:02

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

      В моем случае это приводит к том, что правила то Я знаю, но сталкиваюсь так не часто и разнообразно (то в пайтоне, то в баше, то в шарповом коде), что каждый раз приходится мучительно вспоминать, как там начать строку, какой значок для букв и цифр и т.д.
      Для моего случая verbal expression могут быть крайне удобен (да и на выходе он дает те же регекспы)


    1. eternum
      07.11.2021 18:43

      Он не сложный, он read only. Я не понимаю, кому вообще могла прийти идея в половине случаев экранировать обычные символы, а в половине - управляющие. Но даже если и пришла, для этого нужен был ещё один человек, который сказал "Мне норм. В продакшен".
      Не поймите неправильно, в случае работы и регэспы парсятся и по логам дебажится, но всё же.


      1. netch80
        08.11.2021 10:14

        > Он не сложный, он read only.

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

        О, идея: научить этому IDE. Пусть даже в отдельном окошке, но чтобы рисовал структуру, пояснял и делал фолдинг, где надо. Или уже есть плагин? Или все уже продумали и откинули эту идею?
        Про сборку из частей (своими литералами, PEG, ещё как-то) я уже писал.

        > Я не понимаю, кому вообще могла прийти идея в половине случаев экранировать обычные символы, а в половине — управляющие.

        Исторически. Экранировка управляющих это специфика basic syntax, а не extended syntax, который сейчас в основном развивается всеми движками. А в basic с самого начала выделили небольшой набор символов для меты (.*^$ — и всё?), а дальше развивали через \.

        На самом деле, если бы в базовых сделали _все_ меты через \, было бы системнее (и проще развивать). Но уже как получилось, так получилось — ломать миллионы мест уже никто не будет.

        > Но даже если и пришла, для этого нужен был ещё один человек, который сказал «Мне норм. В продакшен».

        Если посмотрите историю Unix, там таких промежуточных «мне норм, в продакшен», а через месяц «ой не то, надо править» было несколько штук в неделю, какие-то мелочи могли шлифоваться с десятками переделок. В то время была поговорка «типичный пользователь Unix не знает, как на этой неделе вызывается вывод на принтер».
        Через несколько лет это всё обтесалось до приемлемого состояния.



  1. QtRoS
    04.11.2021 08:51

    У Alex Aiken есть курс Compilers. В самом начале курса так фундаментально разбираются возможности конечных автоматов и соответствующих им регулярных выражений, что после изучения их уже нельзя забыть/не понимать/сторониться. Очень рекомендую.


  1. extempl
    04.11.2021 09:27

    Погодите, но регулярные выражения были созданы чтоб простыню кода по парсингу скомкать в компактную строку. Её нет никакого смысла раскрывать обратно, вместо этого вы могли бы взять аналогичную простыню кода и попробовать её упростить до, например, упомянутого выше, https://github.com/sprache/Sprache.