Русский
Русский
English
Статистика
Реклама

Да хватит уже писать эти регулярки

Здравствуйте, меня зовут Дмитрий Карловский и раньше я тоже использовал Perl для разработки фронтенда. Только гляньте, каким лаконичным кодом можно распарсить, например, имейл:


/^(?:((?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}(?:\.(?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}){0,})|("(?:((?:(?:([\u{1}-\u{8}\u{b}\u{c}\u{e}-\u{1f}\u{21}\u{23}-\u{5b}\u{5d}-\u{7f}])|(\\[\u{1}-\u{9}\u{b}\u{c}\u{e}-\u{7f}]))){0,}))"))@(?:((?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}(?:\.(?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}){0,}))$/gsu

Тут, правда, закралось несколько ошибок. Ну ничего, пофиксим в следующем релизе!


Шутки в сторону



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



А с внедрением новых фичей, они теряют и лаконичность:


/(?<слово>(?<буквица>\p{Script=Cyrillic})\p{Script=Cyrillic}+)/gimsu

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


/\t//\ci//\x09//\u0009//\u{9}/u

В JS у нас есть интерполяция строк, но как быть с регулярками?


const text = 'lol;)'// SyntaxError: Invalid regular expression: /^(lol;)){2}$/: Unmatched ')'const regexp = new RegExp( `^(${ text }){2}$` )

Ну, или у нас есть несколько простых регулярок, и мы хотим собрать из них одну сложную:


const VISA = /(?<type>4)\d{12}(?:\d{3})?/const MasterCard = /(?<type>5)[12345]\d{14}/// Invalid regular expression: /(?<type>4)\d{12}(?:\d{3})?|(?<type>5)[12345]\d{14}/: Duplicate capture group nameconst CardNumber = new RegExp( VISA.source + '|' + MasterCard.source )

Короче, писать их сложно, читать невозможно, а рефакторить вообще адски! Какие есть альтернативы?


Свои регулярки с распутным синтаксисом


Полностью своя реализация регулярок на JS. Для примера возьмём XRegExp:


  • API совместимо с нативным.
  • Можно форматировать пробелами.
  • Можно оставлять комментарии.
  • Можно расширять своими плагинами.
  • Нет статической типизации.
  • Отсутствует поддержка IDE.

В общем, всё те же проблемы, что и у нативных регулярок, но втридорога.


Генераторы парсеров


Вы скармливаете им грамматику на специальном DSL, а они выдают вам JS код функции парсинга. Для примера возьмём PEG.js:


  • Наглядный синтаксис.
  • Каждая грамматика вещь в себе и не компонуется с другими.
  • Нет статической типизации генерируемого парсера.
  • Отсутствует поддержка IDE.
  • Минимум 2 кб в ужатопережатом виде на каждую грамматику.

Пример в песочнице.


Это решение более мощное, но со своими косяками. И по воробьям из этой пушки стрелять не будешь.


Билдеры нативных регулярок


Для примера возьмём TypeScript библиотеку $mol_regexp:


  • Строгая статическая типизация.
  • Хорошая интеграция с IDE.
  • Композиция регулярок с именованными группами захвата.
  • Поддержка генерации строки, которая матчится на регулярку.

Это куда более легковесное решение. Давайте попробуем сделать что-то не бесполезное..


Номера банковских карт


Импортируем компоненты билдера


Это либо функции-фабрики регулярок, либо сами регулярки.


const {    char_only, latin_only, decimal_only,    begin, tab, line_end, end,    repeat, repeat_greedy, from,} = $mol_regexp

Ну или так, если вы ещё используете NPM


import { $mol_regexp: {    char_only, decimal_only,    begin, tab, line_end,    repeat, from,} } from 'mol_regexp'

Пишем регулярки для разных типов карт


// /4(?:\d){12,}?(?:(?:\d){3,}?){0,1}/gsuconst VISA = from([    '4',    repeat( decimal_only, 12 ),    [ repeat( decimal_only, 3 ) ],])// /5[12345](?:\d){14,}?/gsuconst MasterCard = from([    '5',    char_only( '12345' ),    repeat( decimal_only, 14 ),])

В фабрику можно передавать:


  • Строку и тогда она будет заэкранирована.
  • Число и оно будет интерпретировано как юникод кодепоинт.
  • Другую регулярку и она будет вставлена как есть.
  • Массив и он будет трактован как последовательность выражений. Вложенный массив уже используется для указания на опциональность вложенной последовательности.
  • Объект означающий захват одного из вариантов с именем соответствующим полю объекта (далее будет пример).

Компонуем в одну регулярку


// /(?:(4(?:\d){12,}?(?:(?:\d){3,}?){0,1})|(5[12345](?:\d){14,}?))/gsuconst CardNumber = from({ VISA, MasterCard })

Строка списка карт


// /^(?:\t){0,}?(?:((?:(4(?:\d){12,}?(?:(?:\d){3,}?){0,1})|(5[12345](?:\d){14,}?))))(?:((?:\r){0,1}\n)|(\r))/gmsuconst CardRow = from(    [ begin, repeat( tab ), {CardNumber}, line_end ],    { multiline: true },)

Сам список карточек


const cards = `    3123456789012    4123456789012    551234567890123    5512345678901234`

Парсим текст регуляркой


for( const token of cards.matchAll( CardRow ) ) {    if( !token.groups ) {        if( !token[0].trim() ) continue        console.log( 'Ошибка номера', token[0].trim() )        continue    }    const type = ''        || token.groups.VISA && 'Карта VISA'        || token.groups.MasterCard && 'MasterCard'    console.log( type, token.groups.CardNumber )}

Тут, правда, есть небольшое отличие от нативного поведения. matchAll с нативными регулярками выдаёт токен лишь для совпавших подстрок, игнорируя весь текст между ними. $mol_regexp же для текста между совпавшими подстроками выдаёт специальный токен. Отличить его можно по отсутствию поля groups. Эта вольность позволяет не просто искать подстроки, а полноценно разбивать весь текст на токены, как во взрослых парсерах.


Результат парсинга


Ошибка номера 3123456789012Карта VISA 4123456789012Ошибка номера 551234567890123MasterCard 5512345678901234

Заценить в песочнице.


E-Mail


Регулярку из начала статьи можно собрать так:


const {    begin, end,    char_only, char_range,    latin_only, slash_back,    repeat_greedy, from,} = $mol_regexp// Логин в виде пути разделённом точкамиconst atom_char = char_only( latin_only, "!#$%&'*+/=?^`{|}~-" )const atom = repeat_greedy( atom_char, 1 )const dot_atom = from([ atom, repeat_greedy([ '.', atom ]) ])// Допустимые символы в закавыченном имени сендбоксаconst name_letter = char_only(    char_range( 0x01, 0x08 ),    0x0b, 0x0c,    char_range( 0x0e, 0x1f ),    0x21,    char_range( 0x23, 0x5b ),    char_range( 0x5d, 0x7f ),)// Экранированные последовательности в имени сендбоксаconst quoted_pair = from([    slash_back,    char_only(        char_range( 0x01, 0x09 ),        0x0b, 0x0c,        char_range( 0x0e, 0x7f ),    )])// Закавыченное имя сендборксаconst name = repeat_greedy({ name_letter, quoted_pair })const quoted_name = from([ '"', {name}, '"' ])// Основные части имейла: доменная и локальнаяconst local_part = from({ dot_atom, quoted_name })const domain = dot_atom// Матчится, если вся строка является имейломconst mail = from([ begin, local_part, '@', {domain}, end ])

Но просто распарсить имейл эка невидаль. Давайте сгенерируем имейл!


//  SyntaxError: Wrong param: dot_atom=foo..barmail.generate({    dot_atom: 'foo..bar',    domain: 'example.org',})

Упс, ерунду сморозил Поправить можно так:


// foo.bar@example.orgmail.generate({    dot_atom: 'foo.bar',    domain: 'example.org',})

Или так:


// "foo..bar"@example.orgmail.generate({    name: 'foo..bar',    domain: 'example.org',})

Погонять в песочнице.


Роуты


Представим, что сеошник поймал вас в тёмном переулке и заставил сделать ему "человекопонятные" урлы вида /snjat-dvushku/s-remontom/v-vihino. Не делайте резких движений, а медленно соберите ему регулярку:


const translit = char_only( latin_only, '-' )const place = repeat_greedy( translit )const action = from({ rent: 'snjat', buy: 'kupit' })const repaired = from( 's-remontom' )const rooms = from({    one_room: 'odnushku',    two_room: 'dvushku',    any_room: 'kvartiru',})const route = from([    begin,    '/', {action}, '-', {rooms},    [ '/', {repaired} ],    [ '/v-', {place} ],    end,])

Теперь подсуньте в неё урл и получите структурированную информацию:


// `/snjat-dvushku/v-vihino`.matchAll(route).next().value.groups{    action: "snjat",    rent: "snjat",    buy: "",    rooms: "dvushku",    one_room: "",    two_room: "dvushku",    any_room: "",    repaired: "",    place: "vihino",}

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


// /kupit-kvartiru/v-moskveroute.generate({    buy: true,    any_room: true,    repaired: false,    place: 'moskve',})

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


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


Как это работает?


Нативные именованные группы, как мы выяснили ранее, не компонуются. Попадётся вам 2 регулярки с одинаковыми именами групп и всё, поехали за костылями. Поэтому при генерации регулярки используются анонимные группы. Но в каждую регулярку просовывается массив groups со списком имён:


// time.source == "((\d{2}):(\d{2}))"// time.groups == [ 'time', 'hours', 'minutes' ]const time = from({    time: [        { hours: repeat( decimal_only, 2 ) },        ':',        { minutes: repeat( decimal_only, 2 ) },    ],)

Наследуемся, переопределям exec и добавляем пост-процессинг результата с формированием в нём объекта groups вида:


{    time: '12:34',    hours: '12,    minutes: '34',}

И всё бы хорошо, да только если скомпоновать с нативной регуляркой, содержащей анонимные группы, но не содержащей имён групп, то всё поедет:


// time.source == "((\d{2}):(\d{2}))"// time.groups == [ 'time', 'minutes' ]const time = wrong_from({    time: [        /(\d{2})/,        ':',        { minutes: repeat( decimal_only, 2 ) },    ],)

{    time: '12:34',    hours: '34,    minutes: undefined,}

Чтобы такого не происходило, при композиции с обычной нативной регуляркой, нужно "замерить" сколько в ней объявлено групп и дать им искусственные имена "0", "1" и тд. Сделать это не сложно достаточно поправить регулярку, чтобы она точно совпала с пустой строкой, и посчитать число возвращённых групп:


new RegExp( '|' + regexp.source ).exec('').length - 1

И всё бы хорошо, да только String..match и String..matchAll клали шуруп на наш чудесный exec. Однако, их можно научить уму разуму, переопределив для регулярки методы Symbol.match и Symbol.matchAll. Например:


*[Symbol.matchAll] (str:string) {    const index = this.lastIndex    this.lastIndex = 0    while ( this.lastIndex < str.length ) {        const found = this.exec(str)        if( !found ) break        yield found    }    this.lastIndex = index}

И всё бы хорошо, да только тайпскрипт всё равно не поймёт, какие в регулярке есть именованные группы:


interface RegExpMatchArray {    groups?: {        [key: string]: string    }}

Что ж, активируем режим обезьянки и поправим это недоразумение:


interface String {    match< RE extends RegExp >( regexp: RE ): ReturnType<        RE[ typeof Symbol.match ]    >    matchAll< RE extends RegExp >( regexp: RE ): ReturnType<        RE[ typeof Symbol.matchAll ]    >}

Теперь TypeScript будет брать типы для groups из переданной регулярки, а не использовать какие-то свои захардкоженные.


Ещё из интересного там есть рекурсивное слияние типов групп, но это уже совсем другая история.


Напутствие



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


Источник: habr.com
К списку статей
Опубликовано: 08.06.2021 16:18:29
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Ненормальное программирование

Javascript

Совершенный код

Регулярные выражения

Typescript

$mol

Regexp

Builder

Грамматики

Парсеры

Построители

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru