Токенізація рядків в PHP

15. 11. 2022

Obsah článku

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

Коли комп'ютер обробляє вихідний код PHP, наприклад, він спочатку розбиває його на безліч дрібних частин, які несуть свій власний сенс. Ці частини називаються "токенами", і вони являють собою найменші самодостатні будівельні блоки мови.

Принцип розбору та токенізації рядка

Принцип обробки рядка/мови поділяється на кілька етапів:

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

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

Мотивація до токенізації

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

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

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

Як токенізувати в PHP

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

Для цілей цієї статті я підготував базову версію токенізатора на основі токенізатора Latte (Nette). Автором оригінальної реалізації є David Grudl, якому я хотів би подякувати за таку просту функцію, яка вирішує всі проблеми за вас.

final class Token
{
public string $value;
public int $offset;
public string $type;
}
final class Tokenizer
{
public const TokenTypes = [
'масив' => 'масив',
'<' => '\<',
'>' => '\>',
'{' => '\{',
'}' => '\}',
'або' => '\|',
'список' => '\[\]',
'тип' => '[a-zA-Z]+',
'простір' => '\s+',
'кома' => ',',
'інший' => '.+?',
];
/**
* @повертається масив<int, Token>
*/
public static function tokenize(string $haystack): array
{
$re = '~(' . implode(')|(', self::TokenTypes) . ')~A';
$types = array_keys(self::TokenTypes);
preg_match_all($re, $haystack, $tokenMatch, PREG_SET_ORDER);
$len = 0;
$count = count($types);
$tokens = [];
foreach ($tokenMatch as $match) {
$type = null;
for ($i = 1; $i <= $count; $i++) {
if (isset($match[$i]) === false) {
break;
}
if ($match[$i] !== '') {
$type = $types[$i - 1];
break;
}
}
$token = new Token;
$token->value = $match[0];
$token->offset = $len;
$token->type = (string) $type;
$tokens[] = $token;
$len += strlen($match[0]);
}
if ($len !== strlen($haystack)) {
$text = substr($haystack, 0, $len);
$line = substr_count($text, "\n") + 1;
$col = $len - strrpos("\n" . $text, "\n") + 1;
$token = str_replace("\n", '\n', substr($haystack, $len, 10));
throw new \LogicException(sprintf('Неочікувані "%s" в рядку %s, стовпці %s.', $token, $line, $col));
}
return $tokens;
}
}

Цей токенізатор може розібрати, наприклад, такий складний рядок (формат навмисно перемежовується пробілами, щоб показати, що токенізатор може обробляти великий діапазон випадків):

array<int, array<bool, array<string, float> >

Jan Barášek   Více o autorovi

Autor článku pracuje jako seniorní vývojář a software architekt v Praze. Navrhuje a spravuje velké webové aplikace, které znáte a používáte. Od roku 2009 nabral bohaté zkušenosti, které tímto webem předává dál.

Rád vám pomůžu:

Související články

1.
7.