Tokenizer: tokenizace řetězců
Tokenizer je velmi jednoduchý nástroj, který používá regulární výrazy k rozdělení řetězce do tokenů. Tato knihovna se již nevyvíjí.
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;
private Tokenizer $tokenizer;
private Stream $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.