Tokenizer: tokenizace řetězců

Tokenizer je velmi jednoduchý nástroj, který používá regulární výrazy k rozdělení řetězce do tokenů. K čemu je to užitečné? Můžete si vytvořit vlastní jazyky.

(Pro zkušenější programátory)

Instalace:

composer require nette/tokenizer

Tokenizace řetězce

Vytvoříme jednoduchý tokenizer, který odděluje řetězce od čísel, mezery a písmen.

$tokenizer = new Nette\Tokenizer\Tokenizer([
	T_DNUMBER => '\d+',
	T_WHITESPACE => '\s+',
	T_STRING => '\w+',
]);

V případě, že jste zvědaví, odkud pocházejí konstanty T_, jsou to interní typy, které se používají pro analýzu kódu. Pokrývají většinu běžných názvů tokenů, které obvykle potřebujeme. Mějte na paměti, že jejich hodnota není zaručena, takže pro srovnání nepoužívejte čísla.

Když mu nyní předáme řetězec, vrátí nám stream tokenů:

$stream = $tokenizer->tokenize("say \n123");

Výsledné pole tokenů $stream->tokens vypadá takto.

[
	new Token('say', T_STRING, 0),
	new Token(" \n", T_WHITESPACE, 3),
	new Token('123', T_DNUMBER, 5),
]

Můžete přístupovat k jednotlivým vlastnostem tokenů:

$firstToken = $stream->tokens[0];
$firstToken->value; // hodnota tokenu: say
$firstToken->type; // hodnota T_STRING
$firstToken->offset; // pozice v řetězci: 0

Jednoduché, ne?

Zpracování tokenů

Nyní víme, jak vytvořit tokeny z řetězce. Efektivně je zpracujeme pomocí Stream. Má spoustu opravdu úžasných metod, pokud potřebujete procházet tokeny!

Pokusme se analyzovat jednoduchou anotaci z PHPDoc a vytvořit z ní objekt. Jaké regulární výrazy potřebujeme pro tokeny? Všechny anotace začínají znakem @, pak následuje jejich jméno, mezery a jejich hodnota.

  • @ pro začátek anotace
  • \s+ pro bílé znaky
  • \w+ pro řetězce

Nikdy nepoužívejte v regulárních výrazech Tokenizeru zachytávací podřetězce jako '(ab)+c', použijte jejich nezachytávací varianty '(?:ab)+c'.

To by mělo fungovat na jednoduchých anotacích, ne? Nyní si ukažme vstupní řetězec, který se pokusíme analyzovat.

$input = '
	@author David Grudl
	@package Nette
';

Vytvořme třídu Parser, která přijme řetězec a vrátí pole dvojic [název, hodnota]. Bude to velmi naivní a jednoduché.

use Nette\Tokenizer\Tokenizer;
use Nette\Tokenizer\Stream;

class Parser
{
	const T_AT = 1;
	const T_WHITESPACE = 2;
	const T_STRING = 3;

	/** @var Tokenizer */
	private $tokenizer;

	/** @var Stream */
	private $stream;

	public function __construct()
	{
		$this->tokenizer = new Tokenizer([
			self::T_AT => '@',
			self::T_WHITESPACE => '\s+',
			self::T_STRING => '\w+',
		]);
	}

	public function parse(string $input): array
	{
		$this->stream = $this->tokenizer->tokenize($input);

		$result = [];
		while ($this->stream->nextToken()) {
			if ($this->stream->isCurrent(self::T_AT)) {
				$result[] = $this->parseAnnotation();
			}
		}

		return $result;
	}

	private function parseAnnotation(): array
	{
		$name = $this->stream->joinUntil(self::T_WHITESPACE);
		$this->stream->nextUntil(self::T_STRING);
		$content = $this->stream->joinUntil(self::T_AT);

		return [$name, trim($content)];
	}
}
$parser = new Parser;
$annotations = $parser->parse($input);

Takže co dělá metoda parse()? Prochází přes tokeny a hledá @, což je symbol začátku anotace. Volání nextToken() přesune kurzor na další token. Metoda isCurrent() zkontroluje, zda aktuální token na kurzoru je daný typ. Poté, pokud je nalezen @, metoda parse() volá parseAnnotation(), která očekává, že anotace budou ve velmi specifickém formátu.

Nejprve pomocí metody joinUntil() přesouvá kurzor a spojuje řetězec tokenů do vyrovnávací paměti, dokud nenajde token požadovaného typu, a pak se zastaví a vrátí celý řetězec. Vzhledem k tomu, že v daném pozici je pouze jeden token typu T_STRING, bude v proměnné $name řetězec 'name'.

Metoda nextUntil() je podobná jako joinUntil(), ale bez vyrovnávací paměti. Přesune pouze kurzor, dokud nenarazí na očekávaný token. Takže toto volání jednoduše přeskočí všechny mezery po názvu anotace.

A pak je další joinUntil(), který hledá další @. Toto konkrétní volání vrátí "David Grudl\n ".

A máme to, analyzovali jsme jednu celou anotaci! Proměnná $content pravděpodobně bude obsahovat bílé znaky, takže je musíme oříznout. Nyní tuto konkrétní anotaci vrátíme jako dvojici [$name, $content].

Zkuste si kopírovat kód a spustit jej. Pokud vypíšete proměnnou $annotations, uvidíte nějaký podobný výstup.

array (2)
   0 => array (2)
   |  0 => 'author'
   |  1 => 'David Grudl'
   1 => array (2)
   |  0 => 'package'
   |  1 => 'Nette'

Metody třídy Stream

Stream může vrátit aktuální token pomocí metody currentToken() nebo pouze jeho hodnotou pomocí currentValue().

nextToken() přesune kurzor a vrací token. Pokud mu nedáte žádné argumenty, jednoduše se vrátí další token.

nextValue() je totéž jako nextToken(), ale pouze vrací hodnotu tokenu.

Většina metod také přijímá několik argumentů, takže můžete vyhledávat více typů najednou.

// hledá, dokud nenalezne řetězec nebo prázdný znak, a poté vrátí následující token
$token = $stream->nextToken(T_STRING, T_WHITESPACE);

// dejte mi další token
$token = $stream->nextToken();

Můžete také vyhledat tokeny podle hodnoty.

// přesouvej kurzor, dokud nenajdeš token '@', poté zastav a vrať jej
$token = $stream->nextToken('@');

nextUntil() přesune kurzor a vrací pole všech tokenů, které najde, dokud nenalezne požadovaný token, před kterým zastaví. Může přijmout více argumentů.

joinUntil() je podobný nextUntil(), ale vrátí spojený řetězec ze všech tokenů, které prošel.

joinAll() jednoduše zřetězí všechny zbývající hodnoty tokenů a vrátí jej. Přesune kurzor na konec streamu tokenu.

nextAll() je stejné jako joinAll(), ale vrací pole tokenů.

isCurrent() zkontroluje, zda se aktuální token nebo hodnota tokenu rovná jednomu z argumentů.

// je aktuální token '@' nebo typ T_AT?
$stream->isCurrent(T_AT, '@');

isNext() je stejné jako isCurrent(), ale kontroluje následující token.

isPrev() je stejné jako isCurrent(), ale kontroluje předchozí token.

Poslední metoda reset() vrací kurzor na začátek, takže můžete opakovat průchod tokeny.