6 листопада 2024 р.

Методи регулярних виразів та рядків

В цій статті ми детально розглянемо різні методи для роботи з регулярними виразами.

str.match(regexp)

Метод str.match(regexp) знаходить збіги для regexp в рядку str.

Він має 3 режими:

  1. Якщо regexp не має прапору g, тоді він повертає перший збіг у вигляді масиву з групами захоплення та властивостями index (позиція збігу), input (введений рядок, дорівнює str):

    let str = "Я люблю JavaScript";
    
    let result = str.match(/Java(Script)/);
    
    alert( result[0] );     // JavaScript (повний збіг)
    alert( result[1] );     // Script (перша група захоплення)
    alert( result.length ); // 2
    
    // Додаткова інформація:
    alert( result.index );  // 7 (позиція збігу)
    alert( result.input );  // Я люблю JavaScript (вихідний рядок)
  2. Якщо regexp має прапор g, тоді він повертає масив всіх збігів у вигляді рядків, без груп захоплення та інших деталей.

    let str = "Я люблю JavaScript";
    
    let result = str.match(/Java(Script)/g);
    
    alert( result[0] ); // JavaScript
    alert( result.length ); // 1
  3. Якщо збігів нема, повертається null, незалежно від наявності прапору g.

    Це важливий нюанс. Якщо збігів нема, ми отримаємо не порожній масив, а null. Легко помилитись, забувши про це:

    let str = "Я люблю JavaScript";
    
    let result = str.match(/HTML/);
    
    alert(result); // null
    alert(result.length); // Error: Cannot read property 'length' of null

    Якщо ми хочемо, аби результат був масивом, ми можемо написати:

    let result = str.match(regexp) || [];

str.matchAll(regexp)

Нещодавнє доповнення
Це нещодавнє доповнення до мови. У старих браузерах може бути потрібен поліфіл.

Метод str.matchAll(regexp) – це “новіший, покращений” варіант str.match.

В основному, його використовують для пошуку всіх збігів з усіма групами.

Існує 3 відмінності від match:

  1. Він повертає ітерований об’єкт із збігами замість масиву. Ми можемо отримати з нього звичайний масив за допомогою Array.from.
  2. Кожен збіг повертається у вигляді масиву з групами захоплення (той самий формат, що й str.match без прапору g).
  3. Якщо результатів нема, метод повертає порожній ітерований об’єкт замість null.

Приклад використання:

let str = '<h1>Вітаю, світе!</h1>';
let regexp = /<(.*?)>/g;

let matchAll = str.matchAll(regexp);

alert(matchAll); // [object RegExp String Iterator], не масив, але ітерований

matchAll = Array.from(matchAll); // тепер масив

let firstMatch = matchAll[0];
alert( firstMatch[0] );  // <h1>
alert( firstMatch[1] );  // h1
alert( firstMatch.index );  // 0
alert( firstMatch.input );  // <h1>Вітаю, світе!</h1>

Якщо ми використаємо for..of для циклічного проходження збігами matchAll, тоді ми більше не потребуємо Array.from.

str.split(regexp|substr, limit)

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

Можна використовувати split з рядками, як-то:

alert('12-34-56'.split('-')) // array of ['12', '34', '56']

Але ми так само можемо ділити за допомогою регулярного виразу:

alert('12, 34, 56'.split(/,\s*/)) // array of ['12', '34', '56']

str.search(regexp)

Метод str.search(regexp) повертає позицію першого збігу або -1, якщо нічого не знайдено:

let str = "Чорнил краплина – мільйонів думок причина";

alert( str.search( /ink/i ) ); // 8 (позиція першого збігу)

Важливе обмеження: search знаходить лише перший збіг.

Якщо нам потрібні позиції подальших збігів, нам слід пошукати інші варіанти, як-то повний пошук за допомогою str.matchAll(regexp).

str.replace(str|regexp, str|func)

Це загальний метод для пошуку та заміни, один з найбільш корисних. «Швейцарський ніж» подібних операцій.

Ми можемо користуватись ним без регулярних виразів, шукаючи та замінюючи підрядок:

// заміна дефіс на двокрапку
alert('12-34-56'.replace("-", ":")) // 12:34-56

Але тут є своє підводне каміння.

Коли перший аргумент replace є рядком, він замінює лише перший збіг.

Ви можете побачити це в попередньому прикладі: лише перше "-" замінюється на ":".

Аби знайти всі дефіси, нам потрібно використати не рядок "-", а регулярний вираз /-/g, з обов’язковим прапором g:

// заміна всіх дефісів на двокрапку
alert( '12-34-56'.replace( /-/g, ":" ) )  // 12:34:56

Другий аргумент є рядком для заміни. В ньому можуть міститись спеціальні символи:

Символи Роль в рядку для заміни
$& вставляє повний збіг
$` вставляє частину рядку, що йшла перед збігом
$' вставляє частину рядку, що йшла після збігу
$n якщоn є одно-двоцифровим числом, вставляє вміст n-ної групи захоплення, детальніше тут Групи захоплення
$<name> вставляє вміст іменованих дужок name, детальніше тут Групи захоплення
$$ вставляє символ $

Для прикладу:

let str = "Іван Сірко";

// переставляє місцями ім’я та прізвище
alert(str.replace(/(іван) (сірко)/i, '$2, $1')) // Сірко, Іван

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

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

Функція містить аргументи func(match, p1, p2, ..., pn, offset, input, groups):

  1. match – збіг,
  2. p1, p2, ..., pn – вміст груп захоплення (якщо є в наявності),
  3. offset – позиція збігу,
  4. input – вихідний рядок,
  5. groups – об’єкт з іменованими групами.

Якщо в регулярному виразі нема дужок, тоді є лише 3 аргументи: func(str, offset, input).

Наприклад, приведемо всі збіги до верхнього регістру:

let str = "html та css";

let result = str.replace(/html|css/gi, str => str.toUpperCase());

alert(result); // HTML та CSS

Замінимо кожен збіг на його позицію в рядку:

alert("Хо-хо-хо".replace(/хо/gi, (match, offset) => offset)); // 0-3-6

В прикладі нижче, наявні дві дужки, тож функція заміни матиме 5 аргументів: перший є повним збігом, далі 2 дужки, після цього (не використані в прикладі) позиція збігу та вихідний рядок:

let str = "Іван Сірко";

let result = str.replace(/(\w+) (\w+)/, (match, name, surname) => `${surname}, ${name}`);

alert(result); // Сірко, Іван

Якщо груп багато, зручно використовувати залишкові параметри для доступу до них:

let str = "Іван Сірко";

let result = str.replace(/(\w+) (\w+)/, (...match) => `${match[2]}, ${match[1]}`);

alert(result); // Сірко, Іван

В іншому випадку, якщо ми використовуємо іменовані групи, тоді об’єкт groups завжди йде останнім після них, тож ми можемо отримати його наступним чином:

let str = "Іван Сірко";

let result = str.replace(/(?<name>\w+) (?<surname>\w+)/, (...match) => {
  let groups = match.pop();

  return `${groups.surname}, ${groups.name}`;
});

alert(result); // Сірко, Іван

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

str.replaceAll(str|regexp, str|func)

Цей метод, по суті, такий самий, що й str.replace, з двома значними відмінностями:

  1. Якщо перший аргумент є рядком, він замінює всі входження в рядку, тоді як replace замінює лише перше входження.
  2. Якщо перший аргумент є регулярним виразом без прапору g, виникне помилка. З прапором g, метод працюватиме аналогічно до replace.

Основний випадок використання replaceAll – заміна всіх входжень збігу в рядку.

Наприклад:

// замінює всі дефіси на двокрапку
alert('12-34-56'.replaceAll("-", ":")) // 12:34:56

regexp.exec(str)

Метод regexp.exec(str) повертає збіг для regexp в рядку str. На відміну від попередніх методів, він застосовується до регулярного виразу, а не рядку.

Його поведінка залежить від наявності в регулярному виразі прапору g.

Якщо нема прапору g, тоді regexp.exec(str) повертає перший збіг у вигляді str.match(regexp). Ця поведінка не додає нічого нового.

Але за наявності g:

  • Виклик regexp.exec(str) повертає перший збіг та зберігає позицію після нього всередині властивості regexp.lastIndex.
  • Наступний виклик починає пошук з позиції regexp.lastIndex, повертає наступний збіг та зберігає позицію після в regexp.lastIndex.
  • …І так далі.
  • Якщо збігів нема, regexp.exec повертає null та скидає regexp.lastIndex до 0.

Тож, повторювані виклики один за одним повертають всі збіги, використовуючи властивість regexp.lastIndex для відслідковування поточної позиції пошуку.

В минулому, коли метод str.matchAll ще не був доданий в JavaScript, виклики regexp.exec використовувались в циклі для отримання всіх збігів, разом з групами:

let str = 'Детальніше про JavaScript тут https://javascript.info';
let regexp = /javascript/ig;

let result;

while (result = regexp.exec(str)) {
  alert( `${result[0]} знайдений на позиції ${result.index}` );
  // JavaScript знайдений на позиції 11
  // javascript знайдений на позиції 33
}

Нині це також працює, хоча для новіших браузерів str.matchAll, зазвичай, зручніший.

Можна використовувати regexp.exec для пошуку із зазначеної позиції, вручну змінивши lastIndex.

Для прикладу:

let str = 'Вітаю, світе!';

let regexp = /\w+/g; // без прапору "g", властивість lastIndex ігнорується
regexp.lastIndex = 5; // пошук з п’ятої позиції (з коми)

alert( regexp.exec(str) ); // світе

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

У прикладі зверху, замінимо прапор g на y. Збігів не буде, бо на 5 позиції нема слова:

let str = 'Вітаю, світе!';

let regexp = /\w+/y;
regexp.lastIndex = 5; // пошук саме з 5 позиції

alert( regexp.exec(str) ); // null

Це зручно для ситуацій, коли нам потрібно “зчитати” щось з рядку регулярним виразом з чітко визначеної позиції, не шукаючи далі.

regexp.test(str)

Метод regexp.test(str) шукає збіг та повертає true/false в залежності від його наявності.

Для прикладу:

let str = "Я люблю JavaScript";

// ці два тести роблять одне й те саме
alert( /люблю/i.test(str) ); // true
alert( str.search(/люблю/i) != -1 ); // true

Приклад з негативним результатом:

let str = "Бла-бла-бла";

alert( /люблю/i.test(str) ); // false
alert( str.search(/люблю/i) != -1 ); // false

Якщо регулярний вираз має прапор g, тоді regexp.test проводить пошук, починаючи з regexp.lastIndex та оновлює цю властивість, аналогічно до regexp.exec.

Тож ми можемо використовувати його для пошуку з визначеної позиції:

let regexp = /люблю/gi;

let str = "Я люблю JavaScript";

// починаємо пошук з позиції 10:
regexp.lastIndex = 10;
alert( regexp.test(str) ); // false (збігу нема)
Один і той самий глобальний регулярний вираз, протестований багато разів на різних даних, може помилятись

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

Для прикладу, ми викликаємо regexp.test двічі для одного тексту, та другий раз повертається помилковий результат:

let regexp = /javascript/g;  // (новостворений регулярний вираз: regexp.lastIndex=0)

alert( regexp.test("javascript") ); // true (наразі regexp.lastIndex=10)
alert( regexp.test("javascript") ); // false

Проблема саме в regexp.lastIndex, що не є нульовим під час виконання другого тесту.

Оминути це можна, встановлюючи regexp.lastIndex = 0 перед кожним пошуком. Іншим рішенням є використання методів рядків str.match/search/... замість методів регулярних виразів, так як вони не використовують lastIndex.

Навчальна карта