Что такое регулярные выражения в программировании
Перейти к содержимому

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

  • автор:

Уроки по Регулярным Выражениям

Регулярные выражения (они же «регулярки» или «RegExp», от англ. «Regular Expressions») — это шаблоны, используемые для сопоставления последовательностей символов. Они являются особенностью многих программ и почти всех существующих языков программирования. Возможно, даже сейчас вы используете инструменты, в арсенале которых есть регулярные выражения, и как только вы освоите их лучше — ваши возможности возрастут.

Когда вы впервые начинаете изучать регулярные выражения и играться с ними, то, скорее всего, вы будете много ошибаться, у вас будет что-то не получаться и т.д. И это нормально, не волнуйтесь, ведь это всего лишь часть пути. Чем больше у вас будет проблем, тем солиднее будет ваш опыт (при условии, что вы будете решать проблемы). А в этом вам помогут данные уроки по регулярным выражениям.

Теория подкрепляется практическими заданиями, есть встроенный редактор регулярных выражений, где можно сразу же наблюдать результат выполнения написанных вами «регулярок». От вас требуется только желание учиться!

Регулярные выражения для простых смертных

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

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

Зачем нужны регулярные выражения?

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

  • Сравнение с шаблоном: Регулярные выражения отлично помогают определять, соответствует ли строка тому или иному формату – например, телефонному номеру, адресу электронной почты или номеру кредитной карты.
  • Замена: При помощи регулярных выражений легко находить и заменять шаблоны в строке. Так, выражение text.replace(/\s+/g, » «) заменяет все пробелы в text, например, » \n\t » , одним пробелом.
  • Извлечение: При помощи регулярных выражений легко извлекать из шаблона фрагменты информации. Например, name.matches(/^(Mr|Ms|Mrs|Dr)\.?\s/i)[1] извлекает из строки обращение к человеку, например, «Mr» из «Mr. Schropp» .
  • Портируемость: Почти в любом распространенном языке программирования есть своя библиотека регулярных выражений. Синтаксис в основном стандартизирован, поэтому вам не придется переучиваться регулярным выражениям при переходе на новый язык.
  • Код: Когда пишете код, можно пользоваться регулярными выражениями для поиска информации в файлах; так, в Atom для этого предусмотрен find and replace, а в командной строке — ack.
  • Четкость и лаконичность: Если вы с регулярными выражениями на «ты», то сможете выполнять весьма нетривиальные операции, написав минимальный объем кода.

Как писать регулярные выражения

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

   input:not([data-validation="valid"]) ~ label.valid, input:not([data-validation="invalid"]) ~ label.invalid < display: none; >$("input").on("input blur", function(event) < if (isPhoneNumber($(this).val())) < $(this).attr(< "data-validation": "valid" >); return; > if (event.type == "blur") < $(this).attr(< "data-validation": "invalid" >); > else < $(this).removeAttr("data-validation"); >>); 

Теперь, если человек введет или вставит в поле валидный номер, то отобразится галочка. Если пользователь уберет курсор из поля ввода, а в поле при этом останется недопустимое значение, то отобразится крестик.
Поскольку вы знаете, что телефонные номера состоят из десяти цифр, первым делом проверяете, чтобы isPhoneNumber выглядел так:

function isPhoneNumber(string) < return /\d\d\d\d\d\d\d\d\d\d/.test(string); >

В этой функции между символами / содержится регулярное выражение с десятью \d’ , то есть, символами-цифрами. Метод test возвращает true , если регулярное выражение соответствует строке, в противном случае – false . Если выполнить isPhoneNumber(«5558675309») , метод вернет true ! Ура!

Однако, писать десять \d – слегка муторная работа. К счастью, то же самое можно сделать и при помощи фигурных скобок.

function isPhoneNumber(string) < return /\d/.test(string); > 

Иногда, вводя телефонный номер, человек начинает с ведущей 1. Правда было бы неплохо, если бы ваше регулярное выражение обрабатывало и такие случаи? Это можно сделать при помощи символа?.

function isPhoneNumber(string) < return /1?\d/.test(string); > 

Символ ? означает «ноль или единица», поэтому теперь isPhoneNumber возвращает true как для «5558675309», так и для «15558675309»!

Пока isPhoneNumber вполне хороша, но мы упускаем одну ключевую деталь: регулярные выражения сплошь и рядом могут совпадать не со строкой, а с частью строки. Оказывается, isPhoneNumber(«555555555555555555») возвращает true, поскольку в этой строке десять цифр. Проблему можно решить, воспользовавшись якорями ^ и $.

function isPhoneNumber(string) < return /^1?\d$/.test(string); > 

Грубо говоря, ^ соответствует началу строки, а $ — концу строки, поэтому теперь ваше регулярное выражение совпадет с целым телефонным номером.

Серьезный пример

Релиз страницы состоялся, она пользуется бешеным успехом, но есть существенная проблема. В США телефонный номер можно записать разными способами:

  • (234) 567-8901
  • 234-567-8901
  • 234.567.8901
  • 234/567-8901
  • 234 567 8901
  • +1 (234) 567-8901
  • 1-234-567-8901

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

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

function isPhoneNumber(string) < return /^1?\d$/.test(string.replace(/\D/g, "")); > 

Функция replace заменяет пустой строкой символ \D , соответствующий любым символам кроме цифр. Глобальный флаг g приказывает функции заменить на регулярное выражение все совпадения, а не только первое.

Еще более серьезный пример

Ваша страница с телефонными номерами всем нравится, в офисе вы – король кулера. Однако, такие профессионалы как вы не останавливаются на достигнутом, поэтому вы хотите сделать страницу еще лучше.
North American Numbering Plan – это стандарт по составлению телефонных номеров, используемый в США, Канаде и еще 23 странах. В этой системе есть несколько простых правил:

  1. Телефонный номер ((234) 567-8901) делится на три части: региональный код (234), код АТС (567) и номер абонента (8901).
  2. В региональном коде и коде АТС первая цифра может быть любой от 2 до 9, а вторая и третья цифры – от 0 до 9.
  3. В коде АТС 1 не может быть третьей цифрой, если вторая цифра – это 1.

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

Номер абонента прост, он состоит всего из четырех цифр

Региональный код немного сложнее. Нас интересует цифра от 2 до 9, за которой идут еще две цифры. Для этого можно использовать символьное множество! Символьное множество позволяет задать группу символов, из которых затем можно выбирать.

Отлично, но мы устанем вручную вводить все символы от 2 до 9. Сделаем код еще чище при помощи символьного диапазона.

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

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

Итак, [2-9]\d\d содержится в группе, а указывает, что эта группа должна фигурировать дважды.

Вот и все! Рассмотрим окончательный вариант функции
isPhoneNumber :

function isPhoneNumber(string) < return /^1?([2-9]\d\d)\d$/.test(string.replace(/\D/g, "")); > 

Когда лучше обходиться без регулярных выражений

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

Не будьте слишком строги. Нет никакого смысла проявлять чрезмерную строгость, когда пишешь регулярные выражения. В случае с телефонными номерами, даже если мы учтем все правила из документа NANP, все равно невозможно определить, реален ли данный телефонный номер. Если я заделаю номер(555) 555-5555, то он совпадет с шаблоном, но ведь такого телефонного номера не существует.

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

Не используйте их с очень сложными строками. Полное регулярное выражение для работы с электронной почтой состоит из 6 318 символов. Простое и приблизительное выглядит так: /^[^@]+@[^@]+\.[^@\.]+$/ . Общее правило таково: если у вас получается регулярное выражение длиннее одной строки кода, то, возможно, стоит поискать другое решение.

  • регулярные выражения
  • программирование
  • Блог компании Издательский дом «Питер»
  • Программирование
  • Регулярные выражения

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

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

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

00:00 – 02:37 Вступление. Повестка и цели вебинара.
02:38 – 04:35 История возникновения регулярных выражений
04:36 – 10:54 Что такое регулярные выражения? Определение. Пример пасинга лога
10:55 – 14:37 Синтаксис регулярных выражений. Обычные (литералы) и специальные символы. Экранирование специальных символов.
14:38 – 17:09 Наборы символов (классы); [0-9], [Aa]
17:10 – 17:54 Специальные метасимволы-сокращения. \d \D \s \S \w \W
17:55 – 20:00 Указание позиции в поиске: начало и конец строки, начало и конец слова
20:01 – 20:24 Группировка a(bc|b|x)cc
20:25 – 27:30 Последовательности . *, +, ?
27:32 – 30:29 Перебор “(a|c|z)”
30:30 – 34:07 Практические примеры. Как прочитать регулярное выражение?
34:07 – 35:59 Инструменты проверки регулярных выражений
36:00 – 42:19 Простые задачи, которые можно решить при помощи регулярных выражений: парсинг телефонных номеров, email, html тэгов
42:20 – 47:36 Примеры практического применения регулярных выражений. Найти все запущенные процессы по шаблону, все email адреса в файле.
47:37 – 50:33 Регулярные выражения в Notepad++ и Total Commander
50:34 – 56:59 Использование регулярных выражений в программировании: Python, Java, JavaScript
57:00 – 59:14 Применение регулярных выражений в автоматизации тестирования
59:15 – 1:01:31 Итоги.
1:01:32 – 1:09:45 Ответы на вопросы

Я для себя выписал на память кое-то в виде неупорядоченных записей.

Регулярные выражения — это язык поиска и манипуляций с подстроками в тексте. По сути это строка-образец, состоящая из символов и метасимволов и задающая правило поиска.

Порой сложно задать нужную строку для поиска. Склонения, окончания, регистр, изменяемая часть выражения и другие причины.

Удобно работать с большим текстом.

В http://regexr.com/ пишем онлайн регулярное выражение, и нам сразу показывается, где эти строки.

 (WARN|ERROR) 
 ^.*(WARN|ERROR).*\((.*):(\d=)\)$ $1 in class $2 in line $33 

Просто этим строчками меняем текст логов в удобный текст.

Обычный текст ищем как есть.

Есть спецсимволы, например, @, который также доступен для поиска.

Экранирование

Есть специальные символы, которые используются в регулярных выражениях, поэтому их не получится искать. К ним относятся

Их следует экранировать, т.е. поставить обратный слеш (\).

Для чего же он нужен? Рассмотрим простой пример. У нас есть строка с несколькими предложениями, в которых нам необходимо найти точки. Если вы попробуете это сделать, то будут выделены все символы. Дело в том, что точка является специальным метасимволом, которая заменяет любой символ в регулярном выражении, кроме переноса строки (см. ниже).

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

Метасимвол точка

Точка заменяет любой символ в регулярном выражении. Если задать точку в регулярном выражении, то будут выделены все символы: буквы, цифры, пробел, точка, запятая и т.д. При этом точка совпадает с одним символом, но благодаря модификатору g поиск не ограничивается первым совпадением и продолжается дальше.

Создайте пример из трёх строк. Если мы добавим квантификатор + после точки (.+), то тем самым зададим поиск совпадающих символов от одного до бесконечности.

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

Поскольку точка не совпадает с символом перевода строки, шаблон найдет совпадение до конца строки.

Мы знаем, что можно экранировать метасимвол точки, чтобы найти обычную точку.

Есть и другой способ — поместить точку в символьный класс, где она рассматривается как обычный символ: [.]+.

Использовать метасимвол точки полезно, когда есть строка с динамической составляющей, которая меняется. http://example.com/pictures/123/

Тогда для строки /pictures/123/, где числа 123 могут меняться, применим паттерн:

 \/pictures\/. \/ 

Мы экранировали слеши, ищем слово pictures, а также любые три символа (обязательно) после этого слова и слеша.

Наборы символов [Квадратные скобки]

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

Такой пример \/pictures\/[0-9]\/ будет находить строку /pictures/1/ (с одним символом), но не найдет /pictures/123/

Можно указать буквы [a-z] — любой символ алфавита.

Символ ^ означает НЕ — [^ae]

Можно указывать не диапазон, а просто указать нужные символы. Например, [19] (напоминаю, только один символ)

Тогда \/pictures\/[19]\/ найдет варианты /pictures/1/ и /pictures/9/

Есть готовые сокращения для диапазонов

 \9 [0-9] цифры \D [^\d] не цифры \s [\f\n\r\t\v] пробельный символ \S [^\s] не пробельный символ \w [\da-zA-Z_] буквенный символ \W [^\w] не буквенный символ 

Позиция

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

 Рыжик - это кот. Рыжик - это кот кот по имени Рыжик. 

Тогда выражение кот$ найдет слово только во второй строке. В первой в конце есть точка. В третьей слово начинается, а не заканчивается.

Для поиска строк, которые начинаются на определенное слово используется символ ^. Выражение ^кот найдет слово в третьей строке.

Символ \b определяет границу слова, \B — не границу слова.

Например, выражение [A-Z] будет находить все заглавные буквы. В том числе по два символа в словах TextView, StatusBar, EditText

Если использовать \b[A-Z] то ищутся символы, которые является границей слова. Т.е. будет находить первые заглавные буквы в указанных словах. Соответственно, \B[A-Z] наоборот найдет вторые заглавные буквы в этих словах.

Группировка (Круглые скобки)

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

В основном удобна для замены. Допустим мы ищем какое-то слово. Помещаем его в круглые скобки, таким образом мы определили группу. Это простейший вариант.

Допустим, нас интересует слово gray. Можно просто написать это слово целиком в качестве шаблона. Однако слово gray часто пишут несколько как grey. Это также допустимый вариант.

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

 gr(a|e)y 

Мы можем таким образом создать несколько групп. При этом они нумеруются. Первая группа будет доступна в переменной под номером один — $1, вторая — $2 и т.д.

Создадим две группы. Теперь мы можем манипулировать этими группами. Для этого используем $1 и $2 для ссылки к этим группам. Тогда запись $2 $1 поменяет слова местами в тексте (режим Replace).

Для примера возьмём строку.

 Кот, который гулял сам по себе 
 (гулял) (сам по себе) 

В режиме Replace пишем.

Теперь строка должна быть:

 Кот, который сам по себе гулял 

Можно использовать back reference, если мы знаем, что текст в группе повторяется. Тогда можно не писать (cat)(cat), а заменить на (back)\1

Последовательности

Последовательности задаются фигурными скобками.

Если использовать [A-Z] то найдутся большие заглавные буквы, но один раз. А выражение [A-Z] указывает, что нужно показать все повторяющиеся символы от 1 до 4 раз. Можно указать точное значение [A-Z] — найдет слова, где идут именно три заглавные буквы подряд.

До четырех символов [A-Z]
От четырех символов [A-Z]

Существует сокращения для них *, +, ?

* — ноль или более символов
+ — один или более символов
? — ноль или один символ

^.*слово$ — такое выражение найдет целую строку, которое заканчивается на слово.

Перебор

TExT|TEXT — найдет два варианта слова, которые написаны в разных регистрах. | — это или. Как вариант TE(x|X)T — третий символ мы определили в двух вариантах.

Читать регулярные выражения

 ([0-9]+)\s.*\s(\d\d\d\d)$ 

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

 044 - 1234 asdf 123 asdf 1234 ^[tT]ele\d$ 

Крышка — начало строки. Далее маленькая или большая буква T. Дальше три литерала. В конце строки должна быть цифра. Пример.

 Tele2 tele1 
 ([mM]y name is|I am)\s[Mm]ykhailo [Pp]oliarush 
 My name is Mykhailo Poliaruhs I am mykailo Poliaruhs 

Задачи

Телефонный номер 066.225-23-23

Три цифры, точка обязательно, три цифры, тире, две цифры, тире, две цифры.

Как вариант, можно было написать

 \d\.\d-\d-\d

parse http: http://host.com/parameters

 [htps]+://\w.\w+/\w+ 

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

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

Наше недавнее исследование вакансий показало: знание регулярных выражений и навык работы с ними требуется в 24% вакансий продуктовых компаний для фронтенд-разработчиков с опытом больше двух лет.

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

Зачем нужны регулярные выражения

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

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

Самое очевидное решение — использовать прямую замену, применив встроенную в JavaScript функцию:

'Быстрее всего мы догоним ее на машине'.replace('ее', 'его'); 

Однако мы получим ожидаемо неверный результат: ее является также окончанием слова Быстрее , а .replace() заменит первое вхождение подстроки. Поэтому необходимо проверить строку на наличие символа, стоящего перед ее : если это пробел, можно делать замену.

В задаче могут возникнуть и другие условия. Например, мы не знаем, в каком регистре написаны слова ( ее , Ее , ЕЕ ) и используется ли буква ё . Если добавить их в функцию, она станет слишком большой и сложной. Это может привести к другим ошибкам.

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

�� Чтобы углубиться в тему, пройдите курс «Регулярные выражения для фронтенда». Он научит вас составлять регулярные выражения, чтобы писать меньше кода и работать быстрее.

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

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

  • Поиск или замена подстроки в строке с «плавающими» (неизвестными) данными. Самая распространённая задача — найти в тексте ссылки и адреса электронной почты и сделать их кликабельными.
  • Валидация данных формы и ограничение ввода. Например, валидация номера телефона, электронной почты, данных паспорта гражданина РФ и другой информации.
  • Получение части строки или формирование новых структур данных из строк. Например, нужно найти количество вхождений ключевых слов в тексте без учёта падежных окончаний, составить из них массив с данными для дальнейшего использования.

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

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

Когда нет смысла применять регулярные выражения

В некоторых случаях этот инструмент усложняет реализацию или время выполнения кода. Вам не нужны регулярные выражения, если стандартные функции JavaScript справляются с задачей сами. Вот ещё пара ситуаций:

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

Разумеется, это общие примеры, и научиться подбирать правильное решение можно только с опытом. Главное, чтобы оно позволяло тратить на задачу меньше ресурсов и повышало производительность.

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

Регулярные выражения могут облегчить работу фронтендеру не только при работе с кодом, но и при его написании.

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

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

Какие типовые задачи решаются регулярными выражениями:

  • Поиск и гибкая замена в коде.
  • Подготовка и обработка данных. Когда вы выносите предварительные данные в текстовый редактор и готовите их для следующих операций.
  • Написание кода с большим количеством одинаковых конструкций.

Регулярные выражения вне фронтенда

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

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

Выводы

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

Больше статей

  • HTML-шаблонизаторы
  • Что такое и зачем нужны алгоритмы
  • Что такое CMS и как под них верстать

«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *