Регулярні вирази

Зміст



Деякі люди, зіткнувшись з проблемою, думають: «О, а використовую-ка я регулярні вирази». Тепер у них є дві проблеми.
Джеймі Завински

Юан-Ма сказав: «Потрібна велика сила, щоб різати дерево поперек структури деревини. Потрібно багато коду, щоб програмувати поперек структури проблеми.
Майстер Юан-Ма, «Книга програмування»


Інструменти і техніки програмування виживають і поширюються хаотично-еволюційним способом. Іноді виживають не красиві і геніальні, а просто такі, які достатньо добре працюють у своїй галузі — приміром, якщо їх інтегрують в іншу успішну технологію.

У цій главі ми обговоримо такий інструмент — регулярні вирази. Це спосіб описувати шаблони строкових даних. Вони створюють невеликий окремий мову, який входить в JavaScript і в безліч інших мов та інструментів.

Регулярки одночасно дуже дивні і вкрай корисні. Їх синтаксис загадковий, а програмний інтерфейс JavaScript для них незграбний. Але це потужний інструмент для дослідження і обробки рядків. Розібравшись з ними, ви станете більш ефективним програмістом.

Створюємо регулярний вираз

Регулярка — тип об'єкта. Її можна створити, викликавши конструктор RegExp, або написавши потрібний шаблон, оточений слешами.

var re1 = new RegExp("abc");
var re2 = /abc/;


Обидва ці регулярні вирази представляють один шаблон: символ «a», за яким слідує символ «b», за яким слідує символ «c».

Якщо ви використовуєте конструктор RegExp, тоді шаблон записується як звичайна рядок, тому діють всі правила щодо зворотніх слешів.

Другий запис, де шаблон знаходиться між слешами, обробляє зворотні скісні риски по-іншому. По-перше, так як шаблон закінчується прямим слешем, то потрібно ставити зворотний слеш перед прямим слешем, який ми хочемо включити в наш шаблон. Крім того, зворотні скісні риски, які не є частиною спеціальних символів типу \n, будуть збережені (а не проігноровані, як у рядках), і змінять сенс шаблону. У деяких символів, таких, як знак питання або плюс, є особливе значення в регулярках, і якщо вам потрібно знайти такий символ, його також треба випереджати зворотним слешем.

var eighteenPlus = /eighteen\+/;


Щоб знати, які символи треба випереджати слешем, вам треба вивчити список всіх спеціальних символів у регулярках. Поки це нереально, тому в разі сумнівів просто не ставте зворотний слеш перед будь-яким символом, не є буквою, числом або пробілом.

Перевіряємо на збіг

У регулярок є кілька методів. Найпростіший — test. Якщо передати йому рядок, він поверне булевское значення, повідомляючи, містить рядок входження заданого шаблону.

console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false


Регулярка, що складається лише з неспеціальних символів, просто являє собою послідовність цих символів. Якщо abc є де-то в рядку, яку ми перевіряємо (не тільки на початку), test поверне true.

Шукаємо набір символів

З'ясувати, чи містить рядок abc, можна було б і за допомогою indexOf. Регулярки дозволяють пройти далі і складати більш складні шаблони.

Припустимо, нам треба знайти будь-який номер. Коли ми в регулярці поміщаємо набір символів у квадратні дужки, це означає, що ця частина виразу збігається з будь-яким із символів в дужках.

Обидва вирази знаходяться в рядках, що містять цифру.

console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true


У квадратних дужках тире між двома символами використовується для завдання діапазону символів, де послідовність задається кодування Unicode. Символи від 0 до 9 знаходяться там просто поспіль (коди з 48 до 57), тому [0-9] захоплює їх всі і збігається з будь-якою цифрою.

У декількох груп символів є свої вбудовані скорочення.

\d-Будь-яка цифра
\w Алфавітно-цифровий символ
\s Пробільний символ (пробіл, табуляція, переклад рядка, тощо)
\D не цифра
\W не алфавітно-цифровий символ
\S не пробільний символ
. будь-який символ, крім переведення рядка

Таким чином можна задати формат дати і часу начебто 30-01-2003 15:20 наступним виразом:

var dateTime = /\d\d\d\d\d\d\d\d \d\d\d\d/;
console.log(dateTime.test("30-01-2003 15:20"));
// → true
console.log(dateTime.test("30-jan-2003 15:20"));
// → false


Виглядає жахливо, чи не так? Занадто багато зворотніх слешів, які ускладнюють розуміння шаблону. Пізніше ми злегка покращувати його.

Зворотні скісні риски можна використовувати і в квадратних дужках. Наприклад, [\d.] означає будь-яку цифру або точку. Зауважте, що точка всередині квадратних дужок втрачає своє особливе значення і перетворюється просто в точку. Те ж стосується і інших спеціальних символів типу +.

Інвертувати набір символів — тобто, сказати, що вам треба знайти будь-який символ, крім тих, що є в наборі — можна, поставивши знак ^ відразу після лівої квадратної дужки.

var notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110"));
// → false
console.log(notBinary.test("1100100010200110"));
// → true


Повторюємо частини шаблону

Ми знаємо, як знайти одну цифру. А якщо нам треба знайти число цілком — послідовність з одного чи більш цифр?

Якщо поставити після чого в регулярці знак"+", це буде означати, що цей елемент може бути повторений більш одного разу. /\d+/ означає одну або декілька цифр.

console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("""));
// → false
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("""));
// → true


У зірочкою * значення майже таке ж, але вона дозволяє шаблоном бути нуль разів. Якщо після чого коштує зірочка, то воно ніколи не перешкоджає знаходженню шаблону в рядку — воно просто знаходиться там нуль разів.

Знак питання робить частину шаблону необов'язковою, тобто вона може зустрітися нуль або один раз. У наступному прикладі символ u може зустрічатися, але шаблон співпадає і тоді, коли його немає.

var neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true


Щоб установити точну кількість разів, яка шаблон повинен зустрітися, використовуються фігурні дужки. {4} після елемента означає, що він повинен зустрітися у рядку 4 рази. Також можна задати проміжок: {2,4} означає, що елемент повинен зустрітися не менше 2 і не більше 4 разів.

Ще одна версія формату дати і часу, де дозволені дні, місяці і годинник з однієї або двох цифр. І ще вона трохи більше читане.

var dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("30-1-2003 8:45"));
// → true


Можна використовувати проміжки з відкритим кінцем, опускаючи одне з чисел. {,5} означає, що шаблон може зустрітися від нуля до п'яти разів, а {5,} — від п'яти і більше.

Угруповання подвираженій

Щоб використовувати оператори * або + на декількох елементах відразу, можна використовувати дужки. Частина регулярки, укладена в дужки, вважається одним елементом з точки зору операторів.

var cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true


Перший і другий плюси відносяться тільки до других букв в словах boo і hoo. Третій + відноситься до цілої групи (hoo+), знаходячи одну або кілька таких послідовностей.

Буква i в кінці виразу робить регулярку нечутливою до регістру символів — так, що B збігається з b.

Збіги і групи

Метод test — найпростіший метод перевірки регулярок. Він лише повідомляє, чи було знайдено збіг, чи ні. У регулярок є ще метод exec, який поверне null, якщо нічого не було знайдено, а в іншому випадку поверне об'єкт з інформацією про збіг.

var = match /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8


У повернутого exec об'єкта є властивість index, де міститься номер символу, з якого сталося збіг. А взагалі об'єкт виглядає як масив рядків, де перший елемент — рядок, яку перевіряли на збіг. У нашому прикладі це буде послідовність цифр, яку ми шукали.

У рядків є метод match, який працює приблизно так само.

console.log("one two 100".match(/\d+/));
// → ["100"]


Коли в регулярці містяться подвираженія, згруповані круглими дужками, текст, який співпав з цими групами, теж з'явиться в масиві. Перший елемент завжди збіг цілком. Другий — частина, яка збіглася з першою групою (тій, у кого круглі дужки зустрілися раніше всіх), потім з другою групою, і так далі.

var quotedText= /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "привіт"]


Коли група не знайдено взагалі (наприклад, якщо за нею стоїть знак питання), її позиція у масиві містить undefined. Якщо група збіглася кілька разів, то в масиві буде тільки останній збіг.

console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]


Групи корисні для вилучення частин рядків. Якщо нам не просто треба перевірити, чи є в рядку дата, а отримати її і створити представляє дату об'єкт, ми можемо укласти послідовності цифр у круглі дужки і вибрати дату з результату exec.

Але для початку невеликий відступ, в якому ми дізнаємося кращий спосіб зберігання дати та часу в JavaScript.

Тип дати

В JavaScript є стандартний тип об'єкта для дат — а точніше, моментів у часі. Він називається Date. Якщо просто створити об'єкт дати через new, ви отримаєте поточні дату і час.

console.log(new Date());
// → Sun Nov 09 2014 00:07:57 GMT+0300 (CET)


Також можна створити об'єкт, що містить заданий час

console.log(new Date(2015, 9, 21));
// → Wed Oct 21 2015 00:00:00 GMT+0300 (CET)
console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0300 (CET)


JavaScript використовує угоду, в якій номери місяців починаються з нуля, а номери днів — з одиниці. Це нерозумно і безглуздо. Побережіться.

Останні чотири аргументи (години, хвилини, секунди і мілісекунди) необов'язкові, і в разі відсутності прирівнюються до нуля.

Мітки часу зберігаються як кількість мілісекунд, що пройшли з початку 1970 року. Для часу до 1970 року використовуються від'ємні числа (це пов'язано з угодою Unix time, яке було створено приблизно в той час). Метод getTime об'єкта дати повертає це число. Воно, звичайно, велика.

console.log(new Date(2013, 11, 19).getTime());
// → 1387407600000
console.log(new Date(1387407600000));
// → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)


Якщо задати конструктору Date один аргумент, він сприймається як це кількість мілісекунд. Можна отримати поточне значення мілісекунд, створивши об'єкт Date і викликавши метод getTime, або ж викликавши функцію Date.now.

Об'єкту Date для вилучення його компонентів є методи getFullYear, getMonth, getDate, getHours, getMinutes, і getSeconds. Є також метод getYear, повертає досить марний двозначний код, типу 93 або 14.

Уклавши потрібні частини шаблону в круглі дужки, ми можемо створити об'єкт дати прямо з рядка.

function findDate(string) {
var dateTime = /(\d{1,2})-(\d{1,2})-(\d{4})/;
var = match dateTime.exec(string);
return new Date(Number(match[3]),
Number(match[2]) - 1,
Number(match[1]));
}
console.log(findDate("30-1-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)


Межі слова і рядка

На жаль, findDate так само радісно витягне безглузду дату 00-1-3000 з рядка «100-1-30000». Збіг може трапитися в будь-якому місці рядка, так що в даному випадку він просто почне з другого символу і закінчить на передостанньому.

Якщо нам треба примусити збіг взяти весь рядок, ми використовуємо мітки ^ і $. ^ збігається з початком рядка, а $ з кінцем. Тому /^\d+$/ збігається з рядком, що складається тільки з однієї або декількох цифр, /^!/ збігається з сторокой, що починається зі знаку оклику, а /x^/ не співпадає ні з якою рядком (перед початком рядка не може бути x).

Якщо, з іншого боку, нам просто треба переконатися, що дата починається і закінчується на кордоні слова, ми використовуємо мітку \b. Кордоном слова може бути початок чи кінець рядка, або будь-яке місце рядка, де з одного боку стоїть алфавітно-цифровий символ \w, а з іншого — не алфавітно-цифровий.

console.log(/cat/.test("concatenate"));
// → true
console.log(/\bcat\b/.test("concatenate"));
// → false


Зазначимо, що мітка кордону не представляє з себе символ. Це просто обмеження, що означає, що збіг відбувається тільки якщо виконується певна умова.

Шаблони з вибором

Припустимо, треба з'ясувати, чи містить текст не просто номер, а номер, за яким слід pig, cow, або chicken в однині або множині.

Можна було б написати три регулярки і перевірити їх по черзі, але є спосіб краще. Символ | означає вибір між шаблонами ліворуч і праворуч від нього. І можна сказати наступне:

var animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pigchickens"));
// → false


Дужки обмежують частину шаблона, до якої застосовується |, і можна поставити багато таких операторів один за одним, щоб позначити вибір з більш ніж двох варіантів.

Механізм пошуку

Регулярні вирази можна розглядати як блок-схеми. Наступна діаграма описує останній тваринницький приклад.



Вираз збігається з рядком, якщо можна знайти шлях з лівої частини діаграми праву. Ми запам'ятовуємо поточне положення в рядку, і кожен раз, проходячи прямокутник, перевіряємо, що частина рядка відразу за нашим становищем у ній збігається з вмістом прямокутника.

Отже, перевірка збігу нашої регулярки у рядку «the 3 pigs» при проходженні по блок-схемі виглядає так:

— на позиції 4 є межа слова, і проходимо перший прямокутник
— починаючи з 4 позиції знаходимо цифру, і проходимо другий прямокутник
— на позиції 5 один шлях замикається тому перед другим прямокутником, а другий проходить далі до прямокутника з пропуском. У нас пробіл, а не цифра, і ми вибираємо другий шлях.
— тепер ми на позиції 6, початок «pigs», і на потрійному розгалуженні шляхів. У рядку немає «cow» або «chicken», зате є «pig», тому ми вибираємо цей шлях.
— на позиції 9 після потрійного розгалуження, один шлях обходить «s» і прямує до останнього прямокутника з кордоном слова, а другий проходить через «s». У нас є «s», тому ми йдемо туди.
— на позиції 10 ми в кінці рядка, і збігтися може тільки межа слова. Кінець рядка вважається кордоном, і ми проходимо через останню прямокутник. І ось ми успішно знайшли наш шаблон.

В принципі, працюють регулярні вирази наступним чином: алгоритм починає початок рядка і намагається знайти збіг там. У нашому випадку там є межа слова, тому він проходить перший прямокутник — але там немає цифри, тому на другому прямокутнику він спотикається. Потім він рухається до другого символу в рядку, і намагається знайти збіг там… І так далі, поки він не знаходить збіг або не доходить до кінця рядка, в якому випадку збіг на знайдено.

Відкати

Регулярка /\b([01]+b|\d+|[\da-f]h)\b/ збігається або з двійковим числом, за яким слідує b, або з десятковим числом без суфікса, або шістнадцятковим (цифри від 0 до 9 або символи від a до h), за яким йде h. Відповідна діаграма:



У пошуках збігу може статися, що алгоритм пішов по верхньому шляху (двійкове число), навіть якщо в рядку немає такого числа. Якщо там є рядок «103», наприклад, зрозуміло, що тільки досягнувши цифри 3 алгоритм зрозуміє, що він на неправильному шляху. Взагалі рядок збігається з регуляркой, просто не в цій гілці.

Тоді алгоритм здійснює відкат. На роздоріжжі він запам'ятовує поточне положення (у нашому випадку, це початок рядка, відразу після кордону слова), щоб можна було повернутися назад і спробувати інший шлях, якщо обраний не спрацьовує. Для рядка «103» після зустрічі з трійкою він повернеться і спробує пройти шлях для десяткових чисел. Це спрацює, тому збіг буде знайдено.

Алгоритм зупиняється, як тільки знайде повне збіг. Це означає, що навіть якщо кілька варіантів можуть підійти, використовується тільки один з них (у тому порядку, в якому вони з'являються в регулярці).

Відкати трапляються при використанні операторів повторення, таких, як + і *. Якщо ви шукаєте /^.*x/ у рядку «abcxe», частина регулярки .* спробує поглинути всю сходинку. Алгоритм потім зрозуміє, що йому потрібен ще і «x». Так як ніякого «x» після кінця рядка немає, алгоритм спробує пошукати збіг, відкотившись на один символ. Після abcx теж ні x, тоді він знову повертається, вже до підрядку abc. І після рядка він знаходить x і доповідає про успішному збігу, на позиціях з 0 по 4.

Можна написати регулярку, яка призведе до множинним відкатах. Така проблема виникає, коли шаблон може збігтися з вхідними даними безліччю різних способів. Наприклад, якщо ми помилимося при написанні регулярки для двійкових чисел, ми можемо випадково написати щось на зразок /([01]+)+b/.



Якщо алгоритм буде шукати такий шаблон в довгому рядку з нулів і одиниць, не містить у кінці «b», він спочатку пройде внутрішньої петлі, поки у нього не закінчаться цифри. Тоді він помітить, що в кінці немає «b», зробить відкат на одну позицію, пройде зовнішньої петлі, знову здасться, спробує відкотитися на ще одну позицію внутрішньої петлі… І далі шукати таким чином, задіюючи обидві петлі. Тобто, кількість роботи з кожним символом рядка буде подвоюватися. Навіть для кількох десятків символів пошук збігу займе дуже довгий час.

Метод replace

У рядків є метод replace, який може замінювати частину рядка на інший.

console.log("тато".replace("п", "м"));
// → мапа


Перший аргумент може бути і регулярної, в якому випадку замінюється перше входження регулярки в рядку. Коли до регулярці додається опція «g» (global, загальний), замінюються всі входження, а не тільки перше

console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar


Мало б сенс передавати опцію «замінити все» через окремий аргумент, або через окремий метод типу replaceAll. Але на жаль, опція передається через саму регулярку.

Вся сила регулярок розкривається, коли ми використовуємо посилання на знайдені у рядку групи, задані в регулярці. Наприклад, у нас є рядок, що містить імена людей, одне ім'я на сходинку, у форматі «Прізвище, Ім'я». Якщо нам треба поміняти їх місцями і прибрати кому, щоб вийшло «Ім'я Прізвище», ми пишемо наступне:

console.log(
"Hopper, Grace\nMcCarthy, John\nRitchie, Dennis"
.replace(/([\w ]+), ([\w ]+)/g, "$2 $1"));
// → Grace Hopper
// John McCarthy
// Dennis Ritchie


$1 і $2 в рядку на заміну посилаються на групи символів, укладені в дужки. $1 замінюється текстом, який збігся з першою групою, $2 — з другою групою, і так далі, до $9. Всі збіг цілком міститься у змінній $&.

Також можна в якості другого аргументу передавати і функцію. Для кожної заміни буде викликана функція, аргументами якої будуть знайдені групи (і вся збігається частина рядка), а її результат буде вставлений в новий рядок.

Простий приклад:

var s = "the cia and fbi";
console.log(s.replace(/\b(fbi|cia)\b/g, function(str) {
return str.toUpperCase();
}));
// → the CIA and FBI


А ось більш цікавий:

var stock = "1 lemon, 2 cabbages, and 101 eggs";
function minusOne(match, amount, unit) {
amount = Number(amount) - 1;
if (amount == 1) // залишився тільки один, видаляємо 's' в кінці
unit = unit.slice(0, unit.length - 1);
else if (amount == 0)
amount = "ні";
return amount + " " + unit;
}
console.log(stock.replace(/(\d+) (\w+)/g, minusOne));
// → no lemon, 1 cabbage, and 100 eggs


Код приймає рядок, знаходить всі входження чисел, за якими йде слово, і повертає рядок, де кожне число зменшено на одиницю.

Група (\d+) потрапляє в аргумент amount, а (\w+) — unit. Функція перетворює amount в число — це завжди спрацьовує, тому що наш шаблон якраз \d+. І потім вносить зміни в слово, на випадок якщо залишився всього 1 предмет.

Жадібність

Нескладно за допомогою replace написати функцію, убирающую всі коментарі з коду JavaScript. Ось перша спроба:

function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*\*\//g "");
}
console.log(stripComments("1 + /* 2 */3"));
// → 1 + 3
console.log(stripComments("x = 10;// ten!"));
// → x = 10;
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 1


Частина перед оператором «або» збігається з двома слешами, за якими йдуть будь-яку кількість символів, крім символів переведення рядка. Частина, що прибирає багаторядкові коментарі, більш складна. Ми використовуємо [^], тобто будь-який символ, який не є порожнім, як спосіб знайти будь-який символ. Ми не можемо використовувати крапку, тому що блокові коментарі тривають і на новому рядку, а символ перекладу рядка не збігається з точкою.

Але висновок попереднього прикладу неправильний. Чому?

Частина [^]* спочатку спробує захопити стільки символів, скільки може. Якщо з-за цього наступна частина регулярки не знайде собі збіги, відбудеться відкат на один символ і спробує знову. У прикладі, алгоритм намагається захопити всю рядок, і потім повертається. Відкотившись на 4 символу тому, він знайде в рядку */ — а це не те, чого ми домагалися. Ми-то хотіли захопити тільки один коментар, а не пройти до кінця рядка і знайти останній коментар.

З-за цього ми говоримо, що оператори повторення (+, *, ?, and {}) жадібні, тобто вони спочатку захоплюють, скільки можуть, а потім йдуть назад. Якщо ви помістіть питання після такого оператора(+?, *?, ??, {}?), вони перетворяться в нежадібних, і почнуть знаходити найменші з можливих входжень.

І це те, що нам потрібно. Змусивши зірочку знаходити збігу в мінімально можливій кількості символів рядка, ми поглинаємо тільки один блок коментарів, і не більше того.

function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*?\*\//g "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1


Багато помилок виникає при використанні жадібних операторів замість нежадібних. При використанні оператора повтору спочатку завжди розглядайте варіант нежадного оператора.

Динамічне створення об'єктів RegExp

У деяких випадках точний шаблон невідомий під час написання коду. Наприклад, вам треба буде шукати ім'я користувача в тексті, і укладати його в підкреслення. Так як ви дізнаєтеся ім'я тільки після запуску програми, ви не можете використовувати запис з слешами.

Але ви можете побудувати рядок і використовувати конструктор RegExp. Ось приклад:

var name = "гаррі";
var text = "А у Гаррі на лобі шрам.";
var regexp = new RegExp("\\b(" + name + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → А у _Гарри_ на лобі шрам.


При створенні кордонів слова доводиться використовувати подвійні слеші, тому що ми пишемо їх в нормальній рядку, а не в регулярці з прямими слешами. Другий аргумент для RegExp містить опції для регулярок — в нашому випадку «gi», тобто глобальний і реєстро-незалежний.

Але що, якщо ім'я буде «dea+hl[]rd» (якщо користувач — кульхацкер)? В результаті ми отримаємо безглузду регулярку, яка не знайде в рядку збігів.

Ми можемо додати зворотніх слешів перед будь-яким символом, який нам не подобається. Ми не можемо додавати зворотні скісні риски перед буквами, тому що \b або \n — це спецсимволи. Але додавати скісні риски перед будь-якими не алфавітно-цифровими символами можна без проблем.

var name = "dea+hl[]rd";
var text = "Цей dea+hl[]rd всіх дістав.";
var escaped = name.replace(/[^\w\s]/g, "\\$&");
var regexp = new RegExp("\\b(" + escaped + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → _dea+hl[]rd_ всіх дістав.


Метод search

Метод indexOf не можна використовувати з регулярками. Зате є метод search, який якраз очікує регулярку. Як і indexOf, він повертає індекс першого входження, або -1, якщо його не сталося.

console.log(" word".search(/\S/));
// → 2
console.log(" ".search(/\S/));
// → -1


На жаль, ніяк не можна задати, щоб метод шукав збіг, починаючи з конкретного зсуву (як це можна зробити з indexOf). Це було б корисно.

Властивість lastIndex

Метод exec теж не дає зручного способу почати пошук з заданої позиції в рядку. Але незручний спосіб дає.

У об'єкта регулярок є властивості. Одне з них — source, що містить рядок. Ще одне — lastIndex, контролює, в деяких умовах, де почнеться наступний пошук входжень.

Ці умови включають необхідність присутності глобальної опції g, і те, що пошук повинен йти з застосуванням методу exec. Більш розумним рішенням було б просто допустити додатковий аргумент для передачі в exec, але розумність — не основна риса в інтерфейсі регулярок JavaScript.

var pattern = /y/g;
pattern.lastIndex = 3;
var = match pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5


Якщо пошук був успішним, виклик exec оновлює властивість lastIndex, щоб воно вказувало на позицію після знайденого входження. Якщо успіху не було, lastIndex встановлюється в нуль — як і lastIndex у тільки що створеного об'єкта.

При використанні глобальної змінної-регулярки і декількох викликів exec ці автоматичні оновлення lastIndex можуть призвести до проблем. Ваша регулярка може почати пошук з позиції, що залишилася з попереднього виклику.

var digit = /\d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null


Ще один цікавий ефект опції g в тому, що вона змінює роботу методу match. Коли він викликається з цією опцією, замість повернення масиву, схожого на результат роботи exec, він знаходить всі входження шаблону в рядку і повертає масив з знайдених підрядків.

console.log("Банан".match(/ан/g));
// → ["ан", "ан"]


Так що обережніше з глобальними змінними-регулярками. У випадках, коли вони необхідні — виклики replace або місця, де ви спеціально використовуєте lastIndex — мабуть, і всі випадки, в яких їх слід застосовувати.

Цикли з входженням



Типова задача — пройти по всіх входжень шаблону в рядок так, щоб мати доступ до об'єкта match у тілі циклу, використовуючи lastIndex і exec.

var input = "Рядок з 3 числами в ній... 42 88.";
var number = /\b(\d+)\b/g;
var match;
while (match = number.exec(input))
console.log("Знайшов ", match[1], " на ", match.index);
// → Знайшов на 14 3
// Знайшов 42 33
// Знайшов на 88 40


Використовується той факт, що значенням присвоєння є привласнюється значення. Використовуючи конструкцію match = re.exec(input) в якості умови в циклі while, ми здійснюємо пошук на початку кожної ітерації, зберігаємо результат в змінну, і закінчуємо цикл, коли всі знайдені збіги.

Розбір INI файли



На закінчення глави розглянемо задачу з використанням регулярок. Уявіть, що ми пишемо програму, яка збирає відомості про наших ворогів через інтернет в автоматичному режимі. (Всю програму писати не будемо, тільки ту частину, яка читає файл з налаштуваннями. Вибачте.) Файл виглядає так:

searchengine=http://www.google.com/search?q=$1
spitefulness=9.7

; перед коментарями ставиться крапка з комою
; кожна секція відноситься до окремого ворогові
[larry]
fullname=Larry Doe
type=бичара з дитсадка
website=http://www.geocities.com/CapeCanaveral/11451

[gargamel]
fullname=Gargamel
type=злий чарівник
outputdir=/home/marijn/enemies/gargamel


Точний формат файлу (який досить широко використовується, і зазвичай називається INI), наступний:

— порожні рядки, рядки, що починаються з крапки з комою, ігноруються
— рядки, укладені в квадратні дужки, починають нову секцію
— рядки, що містять алфавітно-цифровий ідентифікатор, за яким слідує =, додають налаштування в даній секції

Всі решта — неправильні дані.

Наше завдання — перетворити таку рядок в масив об'єктів, кожен з властивістю name масивом налаштувань. Для кожної секції потрібен один об'єкт, і ще один — для глобальних налаштувань зверху файлу.

Так як файл треба розбирати порядково, непогано почати з розбиття файлу на рядки. Для цього в розділі 6 ми використовували string.split("\n"). Деякі операційки використовують для перекладу рядка не один символ \n, а два — \r\n. Так як метод split приймає регулярки в якості аргументу, ми можемо ділити лінії за допомогою виразу /\r?\n/, дозволяючого і поодинокі \n \r\n між рядками.

function parseINI(string) {
// Почнемо з об'єкта, що містить налаштування верхнього рівня
var currentSection = {name: null, fields: []};
var categories = [currentSection];

string.split(/\r?\n/).forEach(function(line) {
var match;
if (/^\s*(;.*)?$/.test(line)) {
return;
} else if (match = line.match(/^\[(.*)\]$/)) {
currentSection = {name: match[1], fields: []};
categories.push(currentSection);
} else if (match = line.match(/^(\w+)=(.*)$/)) {
currentSection.fields.push({name: match[1],
value: match[2]});
} else {
throw new Error("Рядок '" + line + "' містить неправильні дані.");
}
});

return categories;
}


Код проходить всі рядки, оновлюючи об'єкт поточної секції «current section». Спочатку він перевіряє, чи можна ігнорувати рядок, за допомогою регулярки /^\s*(;.*)?$/. Міркуєте, як це працює? Частина між дужок збігається з коментарями, а? робить так, що регулярка співпаде і з рядками, що складаються з одних прогалин.

Якщо рядок не є коментарем, код перевіряє, чи починає вона нову секцію. Якщо так, він створює новий об'єкт для поточної секції, до якої додаються наступні налаштування.

Остання осмислена — рядок є звичайною налаштуванням, і в цьому випадку вона додається до поточного об'єкту.

Якщо жоден варіант не спрацював, функція видає помилку.

Зауважте, як часте використання ^ і $ піклується про те, що вираз збігається з усієї рядком цілком, а не з частиною. Якщо їх не використовувати, код у цілому буде працювати, але іноді буде видавати дивні результати, і таку помилку буде важко відстежити.

Конструкція if (match = string.match(...)) схожа на трюк, який використовує привласнення як умова в циклі while. Часто ви не знаєте, що виклик match буде успішним, тому ви можете отримати доступ до результирующему об'єкту тільки всередині блоку if, який це перевіряє. Щоб не розбивати красиву ланцюжок перевірок if, ми присвоюємо результат пошуку змінної, і одразу використовуємо це привласнення як перевірку.

Міжнародні символи

Із-за спочатку простий реалізації мови, і подальшої фіксації такої реалізації «в граніті», регулярки JavaScript туплять з символами, не зустрічаються в англійській мові. Наприклад, символ «літери» з точки зору регулярок JavaScript, може бути одним з 26 літер англійського алфавіту, і чомусь ще підкресленням. Літери типу é або β, однозначно є літерами, не збігаються з \w (і співпадуть з \W, тобто з буквою).

За дивним збігом обставин, історично \s (пробіл) збігається з усіма символами, які в Unicode вважаються пробельными, включаючи такі штуки, як нерозривний пробіл або монгольський роздільник голосних.

У деяких реалізацій регулярок в інших мовах є особливий синтаксис для пошуку спеціальних категорій символів Unicode, типу «всі прописні букви», «всі знаки пунктуації» або «керуючі символи». Є плани по додаванню таких категорій і JavaScript, але вони, мабуть, будуть реалізовані не скоро.

Підсумок



Регулярки — це об'єкти, що представляють шаблони пошуку в рядках. Вони використовують свій синтаксис для вираження цих шаблонів.

/abc/ Послідовність символів
/[abc]/ Будь-символ зі списку
/[^abc]/ Будь-який символ, крім символів зі списку
/[0-9]/ Будь-який символ з проміжку
/x+/ Одне або декілька входжень шаблону x
/x+?/ Одне або більше примірників, нежадное
/x*/ Нуль або більше примірників
/x?/ Нуль або одне входження
/x{2,4}/ Від двох до чотирьох входжень
/(abc)/ Група
/a|b|c/ Будь-який з декількох шаблонів
/\d/ Будь-яка цифри
/\w/ Будь-алфавітно-цифровий символ («буква»)
/\s/ Будь пробільний символ
/./ Будь-який символ, крім перекладів рядків
/\b/ Межа слова
/^/ Початок рядка
/$/ Кінець рядка

У регулярки є метод test, для перевірки того, чи шаблон у рядку. Є метод exec, що повертає масив, що містить всі знайдені групи. У масиву є властивість index, що показує, де почався пошук.

У рядків є метод match для пошуку шаблонів, і метод search, повертає тільки початкову позицію входження. Метод replace може заміняти входження шаблону на інший рядок. Крім цього, ви можете передати в replace функцію, яка буде будувати сходинку на заміну, грунтуючись на шаблоні і знайдених групах.

У регулярок є налаштування, які пишуть після закриває слешу. Опція i робить регулярку регистронезависимой, а опція g робить її глобальної, що, крім іншого, змушує метод replace замінювати всі знайдені входження, а не тільки перше.

Конструктор RegExp можна використовувати для створення регулярок з рядків.

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

Вправи

Неминуче при вирішенні завдань у вас виникнуть незрозумілі випадки, і ви можете іноді впадати у відчай, бачачи непередбачувану поведінку деяких регулярок. Іноді допомагає вивчити поведінку регулярки через онлайн-сервіс типу debuggex.com, де можна подивитися її візуалізацію і порівняти з бажаним ефектом.

Регулярний гольф
«Гольфом» у коді називають гру, де треба висловити задану програму мінімальною кількістю символів. Регулярний гольф — практичне вправу з написання найменших можливих регулярок для пошуку заданого шаблону, і тільки його.

Для кожної з подстрочек напишіть регулярку для перевірки їх знаходження в рядку. Регулярка повинна знаходити тільки ці зазначені підрядка. Не хвилюйтеся щодо меж слів, якщо це не згадано особливо. Коли у вас вийде працює регулярка, спробуйте її зменшити.

— car і cat
— pop і prop
— ferret, ferry, і ferrari
— Будь-яке слово, що закінчується на ious
— Пропуск, за яким йде крапка, кома, двокрапка або крапка з комою.
— Слово довше шести літер
— Слово без літер e

// Впишіть свої регулярки

verify(/.../,
["my car", "bad cats"],
["camper", "high art"]);

verify(/.../,
["pop culture", "mad props"],
["plop"]);

verify(/.../,
["ferret", "ferry", "ferrari"],
["ferrum", "transfer A"]);

verify(/.../,
["how delicious", "spacious room"],
["ruinous", "consciousness"]);

verify(/.../,
["bad punctuation ."],
["escape the dot"]);

verify(/.../,
["hottentottententen"],
["ні", "hotten totten tenten"]);

verify(/.../,
["red platypus", "wobbling nest"],
["earth bed", "learning ape"]);

function verify(regexp, yes, no) {
// Ignore unfinished exercises
if (regexp.source == "...") return;
yes.forEach(function(s) {
if (!regexp.test(s))
console.log("Не знайшлося '" + s + "'");
});
no.forEach(function(s) {
if (regexp.test(s))
console.log("Несподіване входження '" + s + "'");
});
}


Лапки в тексті
Припустимо, ви написали розповідь, і скрізь для позначення діалогів використовували одинарні лапки. Тепер ви хочете замінити лапки діалогів на подвійні, і залишити в одинарні скорочення слів типу aren't.

Придумайте шаблон, розрізняють два цих використання лапок, і напишіть виклик методу replace, який виробляє заміну.

Знову числа
Послідовності цифр можна знайти простий регуляркой /\d+/.

Напишіть вираз, що знаходить тільки числа, записані в стилі JavaScript. Воно повинно підтримувати можливий мінус або плюс перед числом, десяткову крапку, і експоненційну запис 5e-3 або 1E10 — знову-таки з можливими плюсом або мінусом. Також зауважте, що до або після точки не обов'язково можуть стояти цифри, але при цьому число не може складатися з однієї точки. Тобто, .5 або 5. — дійсні числа, а одна точка сама по собі — ні.

// Впишіть сюди регулярку.
var number = /^...$/;

// Tests:
["1", "-1", "+15", "1.55", ".5", "5.", "1.3e2", "1E-4",
"1e+12"].forEach(function(s) {
if (!number.test(s))
console.log("Не знайшла '" + s + "'");
});
["1a", "+-1", "1.2.3", "1+1", "1e4.5", ".5.", "1f5",
"."].forEach(function(s) {
if (number.test(s))
console.log("Неправильно прийняте '" + s + "'");
});


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.