Регулярные выражения
Содержание:
- МетаСимволы
- Советы и подсказки
- Создание простого регулярного выражения и флаги
- Назад к словам и строкам
- «Петя любит Дашу».replace(/Дашу|Машу|Сашу/, «Катю») ¶
- Запрет возврата
- Обратные символьные классы
- Экранирование внутри […]
- Используем объект регулярного выражения
- Статические свойства
- Metacharacters
- Создаем собственные регулярные выражения JavaScript
- Заключение
МетаСимволы
Для указания регулярных выражений используются метасимволы. В приведенном выше примере ( ) является метасимволом.
Метасимволы — это символы, которые интерпретируются особым образом механизмом RegEx. Вот список метасимволов:
[]. ^ $ * +? {} () \ |
— Квадратные скобки
Квадратные скобки указывают набор символов, которые вы хотите сопоставить.
Выражение | Строка | Совпадения |
---|---|---|
1 совпадение | ||
2 совпадения | ||
Не совпадает | ||
5 совпадений |
Здесь будет соответствовать, если строка, которую вы пытаетесь сопоставить, содержит любой из символов , или .
Вы также можете указать диапазон символов, используя дефис в квадратных скобках.
то же самое, что и .
то же самое, что и .
то же самое, что и .
Вы можете дополСтрока (инвертировать) набор символов, используя символ вставки в начале квадратной скобки.
означает любой символ, кроме или или .
означает любой нецифровой символ.
— Точка
Точка соответствует любому одиночному символу (кроме новой строки ).
Выражение | Строка | Совпадения |
---|---|---|
Не совпадает | ||
1 совпадение | ||
1 совпадение | ||
2 совпадения (содержит 4 символа) |
— Каретка
Символ каретки используется для проверки того, начинается ли строка с определенного символа.
Выражение | Строка | Совпадения |
---|---|---|
1 совпадение | ||
1 совпадение | ||
Не совпадает | ||
1 совпадение | ||
Нет совпадений (начинается с , но не сопровождается ) |
— доллар
Символ доллара используется для проверки того, заканчивается ли строка определенным символом.
Выражение | Строка | Совпадения |
---|---|---|
1 совпадение | ||
1 совпадение | ||
Не совпадает |
— Звездочка
Символ звездочки соответствует предыдущему символу повторенному 0 или более раз. Эквивалентно {0,}.
Выражение | Строка | Совпадения |
---|---|---|
1 совпадение | ||
1 совпадение | ||
1 совпадение | ||
Нет совпадений ( не следует ) | ||
1 совпадение |
— Плюс
Символ плюс соответствует предыдущему символу повторенному 1 или более раз. Эквивалентно {1,}.
Выражение | Строка | Совпадения |
---|---|---|
Нет совпадений (нет символа) | ||
1 совпадение | ||
1 совпадение | ||
Нет совпадений ( не следует ) | ||
1 совпадение |
— Вопросительный знак
Знак вопроса соответствует нулю или одному вхождению предыдущего символа. То же самое, что и {0,1}. По сути, делает символ необязательным.
Выражение | Строка | Совпадения |
---|---|---|
1 совпадение (0 вхождений а) | ||
1 совпадение | ||
Нет совпадений (более одного символа) | ||
Нет совпадений ( не следует ) | ||
1 совпадение |
— Фигурные скобки
Рассмотрим следующий код: . Это означает, по крайней мере , и не больше повторений предыдущего символа. При m=n=1 пропускается..
Выражение | Строка | Совпадения |
---|---|---|
Не совпадает | ||
1 совпадение (в ) | ||
2 совпадения (при и ) | ||
2 совпадения (при и ) |
Посмотрим еще один пример. Это RegEx соответствует как минимум 2 цифрам, но не более 4-х цифр.
Выражение | Строка | Совпадения |
---|---|---|
1 совпадение (совпадение в ) | ||
3 совпадения ( , , ) | ||
Не совпадает |
— Альтернация (или)
Альтернация (вертикальная черта) – термин в регулярных выражениях, которому в русском языке соответствует слово «ИЛИ». (оператор ).
Например: gr(a|e)y означает точно то же, что и gry.
Выражение | Строка | Совпадения |
---|---|---|
Не совпадает | ||
1 совпадение (совпадение в ) | ||
3 совпадения (в ) |
Здесь сопоставьте любую строку, содержащую либо, либо
Чтобы примеСтрока альтернацию только к части шаблона, можно заключить её в скобки:
- найдёт или .
- найдёт или .
— Скобочные группы
Круглые скобки используются для группировки подшаблонов. Так, например, соответствует любой строке, которая соответствует либо или или с последующим
Выражение | Строка | Совпадения |
---|---|---|
Не совпадает | ||
1 совпадение (совпадение в ) | ||
2 совпадения (в ) |
— обратная косая черта
Обратная косая черта используется для экранирования различных символов, включая все метасимволы. Например,
соответствует, если строка содержит , за которым следует . Здесь механизм RegEx не интерпретирует особым образом.
Если вы не уверены, имеет ли символ особое значение или нет, вы можете экранировать его косой чертой . Это гарантирует, что экранированный символ не будет компилироваться по-особенному.
Советы и подсказки
Если вы захотите найти символ слеша, нужно экранизировать его с помощью обратного слеша. То же самое верно для других символов, которые имеют особое значение, например, вопросительного знака. Вот JavaScript regexp пример того, как их искать:
var slashSearch = ///; var questionSearch = /?/;
- d – это то же самое, что и : каждая конструкция соответствует цифровому символу.
- w – это то же самое, что : оба выражения соответствуют любому одиночному алфавитно-цифровому символу или подчеркиванию.
Пример: добавляем пробелы в строки, написанные в «верблюжьем» стиле
В этом примере мы очень устали от «верблюжьего» стиля написания и нам нужен способ добавить пробелы между словами. Вот пример:
removeCc('camelCase') // => должен вернуть 'camel Case'
Существует простое решение с использованием регулярного выражения. Во-первых, нам нужно найти все заглавные буквы. Это можно сделать с помощью поиска набора символов и глобального модификатора.
Это соответствует символу «C» в «camelCase»
//g
Теперь, как добавить пробел перед «C»?
Нам нужно использовать захватывающие скобки! Они позволяют найти соответствие и запомнить его, чтобы использовать позже! Используйте захватывающие скобки, чтобы запомнить найденную заглавную букву:
/()/
Получить доступ к захваченному значению позднее можно так:
$1
Выше мы используем $1 для доступа к захваченному значению. Кстати, если бы у нас было два набора захватывающих скобок, мы использовали бы $1 и $2 для ссылки на захваченные значения и аналогично для большего количества захватывающих скобок.
Если вам нужно использовать скобки, но не нужно фиксировать это значение, можно использовать незахватывающие скобки: (?: x). В этом случае находится соответствие x, но оно не запоминается.
Вернемся к текущей задаче. Как мы реализуем захватывающие скобки? С помощью метода JavaScript regexp replace! В качестве второго аргумента мы передаем «$1»
Здесь важно использовать кавычки
function removeCc(str){ return str.replace(/()/g, '$1'); }
Снова посмотрим на код. Мы захватываем прописную букву, а затем заменяем ее той же самой буквой. Внутри кавычек вставим пробел, за которым следует переменная $1. В итоге получаем пробел после каждой заглавной буквы.
function removeCc(str){ return str.replace(/()/g, ' $1'); } removeCc('camelCase') // 'camel Case' removeCc('helloWorldItIsMe') // 'hello World It Is Me'
Пример: удаляем заглавные буквы
Теперь у нас есть строка с кучей ненужных прописных букв. Вы догадались, как их удалить? Во-первых, нам нужно выбрать все заглавные буквы. Затем используем поиск набора символов с помощью глобального модификатора:
//g
Мы снова будем использовать метод replace, но как в этот раз сделать строчной символ?
function lowerCase(str){ return str.replace(//g, ???); }
Подсказка: в методе replace() в качестве второго параметра можно указать функцию.
Мы будем использовать стрелочную функцию, чтобы не захватывать значение найденного совпадения. При использовании функции в методе JavaScript regexp replace эта функция будет вызвана после поиска совпадений, и результат функции используется в качестве замещающей строки. Еще лучше, если совпадение является глобальным и найдено несколько совпадений — функция будет вызвана для каждого найденного совпадения.
function lowerCase(str){ return str.replace(//g, (u) => u.toLowerCase()); } lowerCase('camel Case') // 'camel case' lowerCase('hello World It Is Me') // 'hello world it is me'
Пример: преобразуем первую букву в заглавную
capitalize('camel case') // => должен вернуть 'Camel case'
Еще раз воспользуемся функцией в методе replace(). Однако на этот раз нам нужно искать только первый символ в строке. Напомним, что для этого используется символ «^».
Давайте на секунду задержимся на символе «^». Вспомните пример, приведенный ранее:
console.log(/cat/.test('the cat says meow')); //верно
При добавлении символа «^» функция больше не возвращает значение true, поскольку слово «cat» находится не в начале строки:
console.log(/^cat/.test('the cat says meow')); //неверно
Мы хотим применить символ «^» к любому строчному символу в начале строки, поэтому мы поместим его перед набором символов . В этом случае JavaScript regexp будет направлено только на первый символ, если это строчная буква.
/^/
Обратите внимание, что мы больше не используем глобальный модификатор, так как нам нужно только одно совпадение. Теперь можно вставить регулярное выражение в метод replace и добавить стрелочную функцию в качестве второго аргумента:
function capitalize(str){ return str.replace(/^/, (u) => u.toUpperCase()); } capitalize('camel case') // 'Camel case' capitalize('hello world it is me') // 'Hello world it is me'
Создание простого регулярного выражения и флаги
Для тестирования и написания паттернов в режиме онлайн я обычно использую сервис https://regex101.com. Выбираете там Javascript и смотрите в риалтайме, как обрабатывается текст вашей регуляркой, плюс там есть подсказки и небольшой справочник.
Есть несколько способов задания регулярного выражения. Вот пример синтаксиса:
// Стандартный метод var re = new RegExp("паттерн", "флаги"); // Укороченная форма записи var re = /паттерн/; // без флагов var re = /паттерн/gmi; // с флагами gmi
Флаги — это параметры поиска, их всего несколько видов и вы можете использовать любой из них, или даже все сразу.i — ignore case, Если этот флаг есть, то регэксп ищет независимо от регистра, то есть не различает между А и а.g — global match, Если этот флаг есть, то регэксп ищет все совпадения, иначе – только первое.m — multiline, Многострочный режим.
Пример использования:
var str = 'Писать ботов на iMacros+JS очень круто!'; window.console.log(/imacros/i.test(str)); // true window.console.log(/imacros/.test(str)); // false
Назад к словам и строкам
В начальном примере, когда мы ищем слова по шаблону в строке вида , происходит то же самое.
Дело в том, что каждое слово может быть представлено как в виде одного , так и нескольких:
Человеку очевидно, что совпадения быть не может, так как эта строка заканчивается на восклицательный знак , а по регулярному выражению в конце должен быть символ или пробел . Но движок этого не знает.
Он перебирает все комбинации того, как регулярное выражение может «захватить» каждое слово, включая варианты как с пробелами , так и без (пробелы ведь не обязательны). Этих вариантов очень много, отсюда и сверхдолгое время выполнения.
«Петя любит Дашу».replace(/Дашу|Машу|Сашу/, «Катю») ¶
Не трудно догадаться, что результатом работы js-выражения выше будет текст . Даже, если Петя неровно дышит к Маше или Саше, то результат всё равно не изменится.
Рассмотрим базовые спец. символы, которые можно использовать в шаблонах:
Символ | Описание | Пример использования | Результат |
---|---|---|---|
\ | Символ экранирования или начала мета-символа | /путь\/к\/папке/ | Надёт текст |
^ | Признак начала строки | /^Дом/ | Найдёт все строки, которые начинаются на |
$ | Признак конца строки | /родной$/ | Найдёт все строки, которые заканчиваются на |
. | Точка означает любой символ, кроме перевода строки | /Петя ..бит Машу/ | Найдёт как , так и |
| | Означает ИЛИ | /Вася|Петя/ | Найдёт как Васю, так и Петю |
? | Означает НОЛЬ или ОДИН раз | /Вжу?х/ | Найдёт и |
* | Означает НОЛЬ или МНОГО раз | /Вжу*х/ | Найдёт , , , и т.д. |
+ | Означает ОДИН или МНОГО раз | /Вжу+х/ | Найдёт , , и т.д. |
Помимо базовых спец. символов есть мета-символы (или мета-последовательности), которые заменяют группы символов:
Символ | Описание | Пример использования | Результат |
---|---|---|---|
\w | Буква, цифра или _ (подчёркивание) | /^\w+$/ | Соответствует целому слову без пробелов, например |
\W | НЕ буква, цифра или _ (подчёркивание) | /\W\w+\W/ | Найдёт полное слово, которое обрамлено любыми символами, например |
\d | Любая цифра | /^\d+$/ | Соответствует целому числу без знака, например |
\D | Любой символ НЕ цифра | /^\D+$/ | Соответствует любому выражению, где нет цифр, например |
\s | Пробел или табуляция (кроме перевода строки) | /\s+/ | Найдёт последовательность пробелов от одного и до бесконечности |
\S | Любой символ, кроме пробела или табуляции | /\s+\S/ | Найдёт последовательность пробелов, после которой есть хотя бы один другой символ |
\b | Граница слова | /\bдом\b/ | Найдёт только отдельные слова , но проигнорирует |
\B | НЕ граница слова | /\Bдом\b/ | Найдёт только окночние слов, которые заканчиваются на |
\R | Любой перевод строки (Unix, Mac, Windows) | /.*\R/ | Найдёт строки, которые заканчиваются переводом строки |
Нужно отметить, что спец. символы \w, \W, \b и \B не работают по умолчанию с юникодом (включая кириллицу). Для их правильной работы нужно указывать модификатор . К сожалению, на окончание 2019 года JavaScript не поддерживает регулярные выражения для юникода даже с модификатором, поэтому в js эти мета-символы работают только для латиницы.
Ещё регулярные выражения поддерживают разные виды скобочек:
Выражение | Описание | Пример использования | Результат |
---|---|---|---|
(…) | Круглые скобки означают под-шаблон, который идёт в результат поиска | /(Петя|Вася|Саша) любит Машу/ | Найдёт всю строку и запишет воздыхателя Маши в результат поиска под номером 1 |
(?:…) | Круглые скобки с вопросом и двоеточием означают под-шаблон, который НЕ идёт в результат поиска | /(?:Петя|Вася|Саша) любит Машу/ | Найдёт только полную строку, воздыхатель останется инкогнито |
(?P<name>…) | Задаёт имя под-шаблона | /(?P<воздыхатель>Петя|Вася|Саша) любит Машу/ | Найдёт полную строку, а воздыхателя запишет в результат под индексом 1 и ‘воздыхатель’ |
Квадратные скобки задают ЛЮБОЙ СИМВОЛ из последовательности (включая спец. символы \w, \d, \s и т.д.) | /^+$/ | Соответствует любому выражению , но не | |
Если внутри квадратных скобок указать минус, то это считается диапазоном | /+/ | Аналог /\w/ui для JavaScript | |
Если минус является первым или последним символом диапазона, то это просто минус | /+/ | Найдёт любое целое числое с плюсом или минусом (причём не обязательно, чтобы минус или плюс были спереди) | |
Квадратные скобки с «крышечекой» означают любой символ НЕ входящий в диапазон | //i | Найдёт любой символ, который не является буквой, числом или пробелом | |
] | Квадратные скобки в квадратных скобках задают класс символов (alnum, alpha, ascii, digit, print, space, punct и другие) | /]+/ | Найдёт последовательность непечатаемых символов |
{n} | Фигурные скобки с одним числом задают точное количество символов | /\w+н{2}\w+/u | Найдёт слово, в котором две буквы н |
{n,k} | Фигурные скобки с двумя числами задают количество символов от n до k | /\w+н{1,2}\w+/u | Найдёт слово, в котором есть одна или две буквы н |
{n,} | Фигурные скобки с одним числом и запятой задают количество символов от n до бесконечности | /\w+н{3,}\w+/u | Найдёт слово, в котором н встречается от трёх и более раз подряд |
Запрет возврата
Переписывать регулярное выражение не всегда удобно, и не всегда очевидно, как это сделать.
Альтернативный подход заключается в том, чтобы запретить возврат для квантификатора.
Движок регулярных выражений проверяет множество вариантов, которые для человека являются очевидно ошибочными.
Например, в шаблоне для человека очевидно, что в не нужно «откатывать» . От того, что вместо одного у нас будет два независимых , ничего не изменится:
Если говорить об изначальном примере , то хорошо бы исключить возврат для . То есть, для нужно искать только одно слово целиком, максимально возможной длины. Не нужно уменьшать количество повторений , пробовать разбить слово на два , и т.п.
В современных регулярных выражениях для решения этой проблемы придумали захватывающие (possessive) квантификаторы, которые такие же как жадные, но не делают возврат (то есть, по сути, они даже проще, чем жадные).
Также есть «атомарные скобочные группы» – средство, запрещающее возврат внутри скобок.
К сожалению, в JavaScript они не поддерживаются, но есть другое средство.
Мы можем исключить возврат с помощью опережающей проверки.
Шаблон, захватывающий максимальное количество повторений без возврата, выглядит так: .
Расшифруем его:
- Опережающая проверка ищет максимальное количество , доступных с текущей позиции.
- Содержимое скобок вокруг не запоминается движком, поэтому оборачиваем внутри в дополнительные скобки, чтобы движок регулярных выражений запомнил их содержимое.
- …И чтобы далее в шаблоне на него сослаться обратной ссылкой .
То есть, мы смотрим вперед – и если там есть слово , то ищем его же .
Зачем? Всё дело в том, что опережающая проверка находит слово целиком, и мы захватываем его в шаблон посредством . Поэтому мы реализовали, по сути, захватывающий квантификатор . Такой шаблон захватывает только полностью слово , не его часть.
Например, в слове он не может захватить только , и оставить для совпадения с остатком шаблона.
Вот, посмотрите, сравнение двух шаблонов:
- В первом варианте сначала забирает слово целиком, потом постепенно отступает, чтобы попробовать найти оставшуюся часть шаблона, и в конце концов находит (при этом будет соответствовать ).
- Во втором варианте осуществляет опережающую проверку и видит сразу слово , которое целиком захватывает в совпадение, так что уже нет возможности найти .
Внутрь можно вместо вставить и более сложное регулярное выражение, при поиске которого квантификатор не должен делать возврат.
Больше о связи захватывающих квантификаторов и опережающей проверки вы можете найти в статьях Regex: Emulate Atomic Grouping (and Possessive Quantifiers) with LookAhead и Mimicking Atomic Groups.
Перепишем исходный пример, используя опережающую проверку для запрета возврата:
Здесь внутри скобок стоит вместо , так как есть ещё внешние скобки. Чтобы избежать путаницы с номерами скобок, можно дать скобкам имя, например .
Проблему, которой была посвящена эта глава, называют «катастрофический возврат» (catastrophic backtracking).
Мы разобрали два способа её решения:
- Уменьшение возможных комбинаций переписыванием шаблона.
- Запрет возврата.
Обратные символьные классы
Для каждого символьного класса существует «обратный класс», обозначаемый той же буквой, но в верхнем регистре.
«Обратный» означает, что он соответствует всем другим символам, например:
- Не цифра: любой символ, кроме , например буква.
- Не пробел: любой символ, кроме , например буква.
- Любой символ, кроме , то есть не буквы из латиницы, не знак подчёркивания и не цифра. В частности, русские буквы принадлежат этому классу.
Мы уже видели, как сделать чисто цифровой номер из строки вида : найти все цифры и соединить их.
Альтернативный, более короткий путь – найти нецифровые символы и удалить их из строки:
Экранирование внутри […]
Обычно, когда мы хотим найти специальный символ, нам нужно экранировать его, например . А если нам нужна обратная косая черта, тогда используем , т.п.
В квадратных скобках большинство специальных символов можно использовать без экранирования:
- Символы не нужно экранировать никогда.
- Тире не надо экранировать в начале или в конце (где оно не задаёт диапазон).
- Символ каретки нужно экранировать только в начале (где он означает исключение).
- Закрывающую квадратную скобку , если нужен именно такой символ, экранировать нужно.
Другими словами, разрешены без экранирования все специальные символы, кроме случаев, когда они означают что-то особое в наборах.
Точка внутри квадратных скобок – просто точка. Шаблон будет искать один из символов: точку или запятую.
В приведённом ниже примере регулярное выражение ищет один из символов :
…Впрочем, если вы решите экранировать «на всякий случай», то не будет никакого вреда:
Используем объект регулярного выражения
Создаем объект регулярного выражения
Этот объект описывает шаблон символов. Он используется для сопоставления шаблонов. Есть два способа сконструировать объект регулярного выражения.
Способ 1: используя литерал регулярного выражения, который состоит из шаблона, заключенного в слэши, например:
var reg = /ab+c/;
Литералы регулярных выражений запускают предварительную компиляцию регулярного выражения при анализе скрипта. Если регулярное выражение постоянно, то пользуйтесь им, чтобы увеличить производительность.
Способ 2: вызывая функцию-конструктор объекта RegExp, например:
var reg = new RegExp("ab+c");
Использование конструктора позволяет выполнить компиляцию регулярного выражения JS во время исполнения скрипта. Используйте данный способ, если регулярное выражение будет изменяться или не знаете шаблон заранее. Например, если вы получаете информацию от пользователя, который вводит поисковый запрос.
Методы объекта регулярного выражения
Давайте познакомимся с несколькими распространенными методами объекта регулярного выражения:
- compile() (устарел в версии 1.5) – компилирует регулярное выражение;
- exec() – производит сопоставление в строке. Возвращает первое совпадение;
- test() – производит сопоставление в строке. Возвращает значение true или false;
- toString() – возвращает строчное значение регулярного выражения.
Статические свойства
Ну и напоследок — еще одна совсем оригинальная особенность регулярных выражений.
Вот — одна интересная функция.
Запустите ее один раз, запомните результат — и запустите еще раз.
function rere() { var re1 = /0/, re2 = new RegExp('0') alert() re1.foo = 1 re2.foo = 1 }
rere()
В зависимости от браузера, результат первого запуска может отличаться от второго. На текущий момент, это так для Firefox, Opera. При этом в Internet Explorer все нормально.
С виду функция создает две локальные переменные и не зависит от каких-то внешних факторов.
Почему же разный результат?
Ответ кроется в стандарте ECMAScript, :
Цитата…
A regular expression literal is an input element that is converted to a RegExp object (section 15.10)
when it is scanned. The object is created before evaluation of the containing program or function begins.
Evaluation of the literal produces a reference to that object; it does not create a new object.
То есть, простыми словами, литеральный регэксп не создается каждый раз при вызове .
Вместо этого браузер возвращает уже существующий объект, со всеми свойствами, оставшимися от предыдущего запуска.
В отличие от этого, всегда создает новый объект, поэтому и ведет себя в примере по-другому.
Metacharacters
A metacharacter is simply an alphabetical character preceded by a backslash that acts to give the combination a special meaning.
For instance, you can search for a large sum of money using the ‘\d’ metacharacter: /(+)000/, Here \d will search for any string of numerical character.
The following table lists a set of metacharacters which can be used in PERL Style Regular Expressions.
Sr.No. | Character & Description |
---|---|
1 |
. a single character |
2 |
\s a whitespace character (space, tab, newline) |
3 |
\S non-whitespace character |
4 |
\d a digit (0-9) |
5 |
\D a non-digit |
6 |
\w a word character (a-z, A-Z, 0-9, _) |
7 |
\W a non-word character |
8 |
a literal backspace (special case). |
9 |
matches a single character in the given set |
10 |
matches a single character outside the given set |
11 |
(foo|bar|baz) matches any of the alternatives specified |
Создаем собственные регулярные выражения JavaScript
Существует два способа создания регулярного выражения: с использованием литерала регулярного выражения или с помощью конструктора регулярных выражений. Каждый из них представляет один и тот же шаблон: символ «c», за которым следует «a», а затем символ «t».
// литерал регулярного выражения заключается в слэши (/) var option1 = /cat/; // Конструктор регулярнго выражения var option2 = new RegExp("cat");
Как правило, если регулярное выражение остается константой, то есть не будет меняться, лучше использовать литерал регулярного выражения. Если оно будет меняться или зависит от других переменных, лучше использовать метод с конструктором.
Заключение
В предыдущих статьях, были рассмотрены основы регулярных выражений, а также некоторые более сложные выражения, которые могут оказаться полезными. В следующих двух статьях объясняется, как разные символы или последовательности символов работают в регулярных выражениях.
Если после прочтения приведённых выше статей вы все еще путаетесь в регулярных выражениях, советуем вам продолжить практиковаться на примерах того, как другие люди придумывают регулярные выражения.
Оригинал статьи: https://code.tutsplus.com/tutorials/a-simple-regex-cheat-sheet—cms-31278/
Перевод: Земсков Матвей