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!

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];
echo $firstToken->value; // hodnota tokenu: say
echo $firstToken->type; // hodnota T_STRING
echo $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í definujeme několik tříd jako příklad:

class Author
{
    public $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

class Package
{
    public $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

A 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 objektů. 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;
    }

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

        return new $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! Nyní můžeme pro tuto konkrétní anotaci vytvořit instanci třídy a předat jí zanalyzovanou hodnotu. Proměnná $content pravděpodobně bude obsahovat bílé znaky, takže je musíme oříznout.

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 => Author
   |  name => "David Grudl"
   |
   1 => Package
      name => "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.