ANTLR

ANTLR – ANother Tool for Language Recognition

Na pewno spotkałeś się kiedyś z niestandardowymi strukturami danych, które ciężko przetworzyć w konwencjonalny sposób. W tym wpisie postaram się pokazać jak możesz poradzić sobie z takim problemem.

Czym jest ANTLR?

ANTLR – to generator parsera do czytania, wykonywania lub tłumaczenia binarnych oraz tekstowych struktur danych. Standardowy projekt procesowania danej struktury danych składa się z pliku Grammar zawierającego reguły dla Lexera oraz Parsera, które zaś używane są do tokenizacji oraz przetwarzania danych według wcześniej stworzonych reguł.

Ciekawostka

Zastanawiałeś się kiedyś jak działa analiza składni, dla przykładu w Netbeans? Jeśli tak, to pewnie nie zaskoczę Cię informacją, że używany jest tam między innymi ANTLR do analizy składni języka C++.

Ponad to, znany pewnie wszystkim Twitter, używa ANTLR do przetwarzania zapytań dla wyszukiwania. Są to ponad 2 miliardy zapytań na dzień. Ilość robi wrażenie? Na mnie tak.

Do czego możemy użyć ANTLR w praktyce?

Do wielu rzeczy, począwszy od przetwarzania niestandardowych strukturalnie plików konfiguracyjnych, przez konwertery tzw. ,,legacy code”, po różnego rodzaju parsery, renderowanie znaczników i tym podobne. Możliwości jest na prawdę sporo.

W jakich językach programowania można tego użyć?

W zasadzie we wszystkich najpopularniejszych językach: Java, C#, C++, Python, Swift, Go (po za PHP, chociaż swego czasu widziałem gdzieś, zrobiony port dla tego języka). Dla każdego z tych języków ,,kompilator” gramatyczny wygeneruje parser oraz lexer do przetwarzania zdefiniowanej przez nas struktury danych.

Przykład użycia

Na potrzeby tego wpisu, językiem programowania przy użyciu, którego będę prezentować możliwości ANLTR to C# (miłośnicy Javy muszą mi wybaczyć 🙂 ). Natomiast samo narzędzie ANLTR wykorzystam w jego najnowszej wersji, na dzień tworzenia tego wpisu czyli V4.

*Procesowanie przykładowej prostej struktury danych:

bq. # Comment
TypeId = 860
Name        = "Example Item"
Description = "Description of example item"
Attributes  = {Waypoint=0, Class=1}
Flags       = {Block, Pickable}

Struktura dosyć prosta, zapewne można by bez większego problemu przetworzyć takie dane przy użyciu choćby regexów. Jednak, chciałbym na tym dosyć trywialnym przykładzie pokazać jak działa ANTLR.

Plik grammar (.g4) wygląda w ten sposób:

grammar Objects;
 
root:
    items+
    ;
 
items:
    ('TypeId' '=' typeId) ('Name' '=' name) ('Description' '=' description)? ('Attributes' '=' attributes)? ('Flags' '=' flags)?
    ;
typeId:
     NUMBER
    ;
name:
    TEXT
    ;
description:
    TEXT
    ;
flags:
    ('{') flag+ ('}')
    ;
flag:
    STRING
    ;
attributes:
    ('{')  attribute+ ('}')
    ;
attribute:
    attributeName STRING?'=' attributeValue
    ;
attributeName:
    STRING;
attributeValue:
    NUMBER;
 
fragment DIGIT:
   ('-')? '0'..'9'
;
 
TEXT:
    '"' .*? '"'
;
 
STRING:
    ('a'..'z' | 'A'..'Z')+
;
 
NUMBER:
    DIGIT+
;
 
WHITESPACE:
    ' ' -> skip
;
 
COMMA_SEPARATOR :
    ',' -> skip
;
 
NEWLINE :
    ('\n' | '\r' | '\r\n') -> skip
;
 
COMMENT     :
    '#' ~( '\r' | '\n' )* -> skip
    ;

Kompilator na podstawie gramatyki zawartej w pliku grammar utworzy w docelowym języku (w naszym przypadku C#) parser oraz lexer przy użyciu których możemy skorzystać z możliwości przetworzenia wskazanych danych.

t1

Powstałe drzewo wygląda dosyć prosto i przejrzyście. Oczywiście w przypadku większej ilości danych, drzewo rozszerzy się o kolejne elementy.

t2

Jak z wydajnością takiego rozwiązania? To zależy od skomplikowania danych, ale mogę z czystym sumieniem powiedzieć, że w większości przypadków, przy odpowiednio stworzonej gramatyce, wydajność jest na zadowalającym poziomie.

Przykładowa klasa Reader

using Antlr4.Runtime;
using Objects.Antlr;
using Antlr.Visitor;
using Objects.Domain;
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Objects
{
    class ObjectsReader
    {
        private readonly string objectsFilePath;
 
        public ObjectsReader(string objectsFilePath)
        {
            this.objectsFilePath = objectsFilePath;
        }
 
        public List Read()
        {
            var objectsFileContent = System.IO.File.ReadAllText(objectsFilePath);
 
            var inputStream = new AntlrInputStream(objectsFileContent);
            var objectsLexer = new ObjectsLexer(inputStream);
            var commonTokenStream = new CommonTokenStream(objectsLexer);
 
            var objectsParser = new ObjectsParser(commonTokenStream);
            var itemsContext = objectsParser.root().items();
 
            var visitor = new ItemVisitor();
 
            foreach(var itemContext in itemsContext)
            {
                visitor.Visit(itemContext);
            }
 
            // Do something with parsed item's
        }
    }
}

Klasa Visitor z implementacją interfejsu z biblioteki ANTLR. Visitor służy do procesowania określonej części powstałego drzewa, danej struktury danych.

using Antlr4.Runtime.Misc;
using Objects.Domain;
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Antlr.Visitor
{
    class ItemVisitor : ObjectsBaseVisitor
    {
        public override Item VisitItems([NotNull] ObjectsParser.ItemsContext context)
        {
            var typeId = context.typeId().GetText();
            var name = context.name().GetText();
            var flags = context.flags();
            var description = context.description();
            var attributes = context.attributes();
 
            // Container
            var Item = new Item {
                Id = Convert.ToInt32(typeId),
                Name = name
            };
 
            if (null != description)
            {
                var descriptionText = description.GetText();
                Item.Description = descriptionText;
            }
 
            if (null != flags)
            {
                var _flags = flags.flag();
 
                foreach (var _flag in _flags)
                {
                    var flagText = _flag.GetText();
 
                    try
                    {
                        ItemFlag itemFlag = (ItemFlag)Enum.Parse(typeof(ItemFlag), flagText);
                        Item.AddFlag(itemFlag);
                    }
                    catch(ArgumentException)
                    {
                        Console.WriteLine("Undefined flag: {0}", flagText);
                    }
                }
            }
 
            if (null != attributes)
            {
                var _attributes = attributes.attribute();
 
                foreach(var _attribute in _attributes)
                {
                    var attributeType = _attribute.attributeName().GetText();
                    var attributeValue = _attribute.attributeValue().GetText();
 
                    try
                    {
                        ItemAttributeType itemAttributeType = (ItemAttributeType)Enum.Parse(typeof(ItemAttributeType), attributeType);
                        Item.AddAttribute(new ItemAttribute(itemAttributeType, Convert.ToInt32(attributeValue)));
                    }
                    catch (ArgumentException)
                    {
                        Console.WriteLine("Undefined attribute: {0}", attributeType);
                    }
                }
            }
 
            // Do something with parsed data
 
            return base.VisitItems(context);
        }
    }
}

Kod w klasach jest jedynie przykładem użycia parsera niżeli działającą wersją.

Po więcej informacji zapraszam na stronę projektu: http://www.antlr.org/index.html

Autorem tekstu jest Miłosz Lenczewski.

Dodaj komentarz

Please Login to comment

Zobacz również artykuły o podobnej tematyce

ORI – czy Twój biznes jest gotowy na omnichannel?

Pojęcie omnichannelu znamy już właściwie wszyscy, nie tylko w teorii, ale również w praktyce. Taka synergia wszystkich, wykorzystywanych przez markę...

Efekt ROPO – co może zagwarantować Twojej firmie?

Relacje między marką a klientem zmieniają się bardzo dynamicznie. Współcześnie są one zupełnie inne niż 5, 10, czy 20 lat...

Magia e-commerce, czyli ludzka psychika a wydatki

Żyjemy w czasach, gdzie półki uginają się pod ciężarem wyłożonych na nich towarów, a każdy produkt jest w zasadzie “na...

Zobacz więcej wpisów