3. Python / ANTLR – vCard Reader#

In diesem Kapitel wird die Konvertierung einer VCF-Datei in CSV und zurück mittels eines Compiler-Tools (ANTLR) vorgestellt.

ANTLR (ANother Tool for Language Recognition) is a powerful parser generator

for reading, processing, executing, or translating structured text or binary files. It’s widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build and walk parse trees. (https://www.antlr.org/)

3.1. So geht’s#

Bemerkung

Der Konverter ist ein Python-Programm, dass in einer „shell“ ausgeführt wird.

Das „User-Interface“ ändert sich also nicht zum vorherigen Kapitel und damit auch nicht die Nutzung.

Die einzelnen Schritte.

  1. Exportiere die Kontaktdaten des Smartphones in eine VCF-Datei, z.B.:

    phone.vcf
    
  2. Erstelle aus phone.vcf eine Datei im CSV-Format, dass man mit Libreoffice oder Excel öffnen und bearbeiten kann

    $ vcf2csv.py --vcf_datei data/phone.vcf
    result: report.d/csv_ausgabe.csv
    

    Bemerkung

    Duplikate sind in der Ausgabe entfernt worden.

  3. Erstelle sicherheitshalber eine Arbeitskopie

    cp report.d/csv_ausgabe.csv phone_neu.csv
    
  4. Bearbeite die Arbeitskopie phone_neu.csv mit Libreoffice oder Excel.

  5. Erstelle aus der CSV-Datei eine neue VCV-Datei mit korrigierten Daten für den Import im neuen Smartphone oder per Thunderbird.

    $ ./csv2vcf.py --csv_datei report.d/phone_neu.csv
    result: report.d/vcf_ausgabe.vcf
    
  6. Importiere die neue, überarbeitete VCF-Datei vcf_ausgabe.vcf.

3.2. Wofür der Mehraufwand#

Die VCF -> CSV -> VCF Konvertierung wurde im letzten Kapitel erfolgreich implementiert.

Warnung

Die Implementierung per DEA stellt faktisch nur eine kleine Teilmenge des wirklichen Umfangs dar.

Ein Blick auf Wikipedia zeigt uns fehlende Funktionalitäten (https://de.wikipedia.org/wiki/VCard):

  1. Es wird bislang mit dem „Automaten“ nur ein Teil der möglichen Attribute unterstützt.

  2. Es wird nur Version 2.1 gelesen. Version 3 und 4 unterscheiden sich erheblich.

Wichtiger jedoch ist die Tatsache, dass schon in der Spezifikation der vCard (siehe Referenzen) eine „Grammatik“ (ansatzweise) definiert wird.

Texte auszuwerten (zu parsen) auf Basis einer Grammatik ist Aufgabe eines Compilers und nicht eines Automaten.

3.2.1. Objekt-Spezifikation 2.1#

Es soll hier der Darstellung in Wikipedia gefolgt werden:

Attribut

Muss

Beschreibung

Beispiel

ADR

n

Strukturierte Darstellung der physischen Anschrift des vCard-Objekts.

ADR;TYPE=home:;;Heidestrasse 17;Koeln;;51147;Germany

AGENT

n

Informationen über eine andere Person, die im Namen des vCard-Objekts handeln soll.

AGENT:http://de.wikipedia.org/wiki/007

BDAY

n

Geburtsdatum

BDAY:19640812

BEGIN

y

Jede vCard muss mit dieser Eigenschaft beginnen.

BEGIN:VCARD

CATEGORIES

n

Liste von Eigenschaften

CATEGORIES:swimmer,biker

EMAIL

n

E-Mail-Adresse

EMAIL:erika@mustermann.de

END

y

Jede vCard muss mit dieser Eigenschaft enden.

END:VCARD

FN

n

Formatierte Zeichenfolge mit dem vollständigen Namen

FN:Dr. Erika Mustermann

GEO

n

Längen- und Breitengrad.

GEO:50.858,7.0885

KEY

n

Öffentlicher Schlüssel

KEY;PGP:http://example.org/key.pgp ODER KEY;PGP;ENCODING=BASE64:[base64-data]

LABEL

n

Stellt den eigentlichen Adress-Text dar

LABEL;TYPE=HOME:Heidestrasse 17n51147 KoelnnDeutschland

LOGO

n

Logo der Organisation, mit der die Person in Beziehung steht

LOGO;PNG:http://example.org/logo.png ODER LOGO;PNG;ENCODING=BASE64:[base64-data]

MAILER

n

Genutztes E-Mail-Programm (Client)

MAILER:Thunderbird

N

y

Strukturierte Darstellung vom Namen der Person

N:Mustermann;Erika;;Dr.;

NOTE

n

Zusätzliche Informationen oder Kommentar

NOTE:Eine fiktive Person

ORG

n

Name und gegebenenfalls Abteilung der Organisation

ORG:Google;GMail Team;Spam Detection Squad

PHOTO

n

Bild

PHOTO;JPEG:http://example.org/photo.jpg ODER PHOTO;JPEG;ENCODING=BASE64:[base64-data]

PROFILE

n

Legt fest, dass die vCard eine vCard ist.

PROFILE:VCARD

REV

n

Zeitstempel der letzten Aktualisierung der vCard.

REV:20140301T221110Z

ROLE

n

Rolle oder Beruf

ROLE:Executive

SORT-STRING

n

Zeichenkette, die die Sortierreihenfolge beschreibt

SORT-STRING:Mustermann

SOUND

n

Gibt standardmäßig die Aussprache der FN-Eigenschaft an

SOUND;OGG:http://example.org/sound.ogg ODER SOUND;OGG;ENCODING=BASE64:[base64-data]

SOURCE

n

URL, die verwendet werden kann, um die neueste Version dieser vCard zu erhalten.

SOURCE:http://mustermann.de/vCard.vcf

TEL

n

Normalform einer numerischen Zeichenkette für eine Telefonnummer

TEL;TYPE=CELL,HOME:(0170) 1234567

TITLE

n

Angabe der Stellenbezeichnung, funktionellen Stellung

TITLE:V.P. Research and Development

TZ

n

Zeitzone

TZ:+0100

UID

n

UUID, die eine persistente, global eindeutige Kennung darstellt.

UID:urn:uuid:550e8400-e29b-11d4-a716-44665544ffff

URL

n

URL zu einer Website.

URL:http://www.mustermann.de

VERSION

y

Version der vCard-Spezifikation.

VERSION:2.1

Offensichtlich sind nur sehr wenige Eigenschaften / Attribute wirklich notwendig:

Attribut

Muss

Beschreibung

Beispiel

BEGIN

y

Jede vCard muss mit dieser Eigenschaft beginnen.

BEGIN:VCARD

END

y

Jede vCard muss mit dieser Eigenschaft enden.

END:VCARD

N

y

Strukturierte Darstellung vom Namen der Person

N:Mustermann;Erika;;Dr.;

VERSION

y

Version der vCard-Spezifikation.

VERSION:2.1

3.2.2. Was fehlt#

Des weiteren wird mit der Auflistung aller Attribute deutlich, welche Attribute in der ersten Version es Konverters erkannt werden und welche fehlen (in der folgenden Tabelle mit „n“ gekennzeichnet):

Attribut

DEA

ADR

y

AGENT

n

BDAY

n

BEGIN

y

CATEGORIES

y

EMAIL

y

END

y

FN

y

GEO

n

KEY

n

LABEL

n

LOGO

n

MAILER

n

N

y

NOTE

n

ORG

y

PHOTO

n

PROFILE

n

REV

n

ROLE

n

SORT-STRING

n

SOUND

n

SOURCE

n

TEL

y

TITLE

y

TZ

n

UID

n

URL

n

VERSION

y

3.3. Grammatik – Spezifikation#

3.3.1. versit Consortium#

In erster Näherung wird als Grundlage die Grammatik in „vCard – The Electronic Business vCard, Version 2.1; September 18, 1996“ gewählt (siehe Referenzen).

In Kapitel 2.9 wird eine formale Definition in Backus-Naur Notation (BNF) vorgestellt:

This syntax is written according to the form described in RFC 822, but it references just this small subset of RFC 822 literals:
  CR                        =  <ASCII CR, carriage return>  ; (     15,      13.)
  LF                        =  <ASCII LF, linefeed>         ; (     12,      10.)
  CRLF              =  CR LF
  SPACE             =  <ASCII SP, space>            ; (     40,      32.)
  HTAB              =  <ASCII HT, horizontal-tab>   ; (     11,       9.)

All literal property names are valid as upper, lower, or mixed case.
ws          = 1*(SPACE / HTAB)
  ; “whitespace,” one or more spaces or tabs

wsls                = 1*(SPACE / HTAB / CRLF)
  ; whitespace with line separators

word                = <any printable 7bit us-ascii except []=:., >
groups              = groups “.” word
    / word

vcard_file  = [wsls] vcard [wsls]

vcard               = “BEGIN” [ws] “:” [ws] “VCARD” [ws] 1*CRLF
    items *CRLF “END” [ws] “:” [ws] “VCARD”

items               = items *CRLF item
    / item
  ; these may be “folded”

item                = [groups “.”] name
      [params] “:” value CRLF
    / [groups “.”] “ADR”
      [params] “:” addressparts CRLF
    / [groups “.”] “ORG”
      [params] “:” orgparts CRLF
    / [groups “.”] “N”
      [params] “:” nameparts CRLF
    / [groups “.”] “AGENT”
      [params] “:” vcard CRLF
  ; these may be “folded”

name                = “LOGO” / “PHOTO” / “LABEL” / “FN” / “TITLE”
    / “SOUND” / “VERSION” / “TEL” / “EMAIL” / “TZ” / “GEO” / “NOTE”
    / “URL” / “BDAY” / “ROLE” / “REV” / “UID” / “KEY”
    / “MAILER” / “X-” word
  ; these may be “folded”

value               = 7bit / quoted-printable / base64
7bit                = <7bit us-ascii printable chars, excluding CR LF>
8bit                = <MIME RFC 1521 8-bit text>
quoted-printable = <MIME RFC 1521 quoted-printable text>
base64              = <MIME RFC 1521 base64 text>
  ; the end of the text is marked with two CRLF sequences
  ; this results in one blank line before the start of the next property

params              = “;” [ws] paramlist

paramlist   = paramlist [ws] “;” [ws] param
    / param

param               = “TYPE” [ws] “=“ [ws] ptypeval
    / “VALUE” [ws] “=“ [ws] pvalueval
    / “ENCODING” [ws] “=“ [ws] pencodingval
    / “CHARSET” [ws] “=“ [ws] charsetval
    / “LANGUAGE” [ws] “=“ [ws] langval
    / “X-” word [ws] “=“ [ws] word
    / knowntype

ptypeval    = knowntype / “X-” word
pvalueval   = “INLINE” / “URL” / “CONTENT-ID” / “CID” / “X-” word
pencodingval        = “7BIT” / “8BIT” / “QUOTED-PRINTABLE” / “BASE64” / “X-” word
charsetval  = <a character set string as defined in Section 7.1 of
    RFC 1521>

langval             = <a language string as defined in RFC 1766>
addressparts        = 0*6(strnosemi “;”) strnosemi
  ; PO Box, Extended Addr, Street, Locality, Region, Postal Code,
  Country Name

orgparts    = *(strnosemi “;”) strnosemi
  ; First is Organization Name, remainder are Organization Units.

nameparts   = 0*4(strnosemi “;”) strnosemi
  ; Family, Given, Middle, Prefix, Suffix.
  ; Example:Public;John;Q.;Reverend Dr.;III, Esq.

strnosemi   = *(*nonsemi (“\;” / “\” CRLF)) *nonsemi
  ; To include a semicolon in this string, it must be escaped
  ; with a “\” character.

nonsemi             = <any non-control ASCII except “;”>

knowntype   = “DOM” / “INTL” / “POSTAL” / “PARCEL” / “HOME” / “WORK”
    / “PREF” / “VOICE” / “FAX” / “MSG” / “CELL” / “PAGER”
    / “BBS” / “MODEM” / “CAR” / “ISDN” / “VIDEO”
    / “AOL” / “APPLELINK” / “ATTMAIL” / “CIS” / “EWORLD”
    / “INTERNET” / “IBMMAIL” / “MCIMAIL”
    / “POWERSHARE” / “PRODIGY” / “TLX” / “X400”
    / “GIF” / “CGM” / “WMF” / “BMP” / “MET” / “PMB” / “DIB”
    / “PICT” / “TIFF” / “PDF” / “PS” / “JPEG” / “QTIME”
    / “MPEG” / “MPEG2” / “AVI”
    / “WAVE” / “AIFF” / “PCM”
    / “X509” / “PGP”

3.3.2. ANTLR#

Die obige Grammatik hat zwei Arten von Regeln:

  1. lexikalische Regeln

    Das sind Regeln zur Definition einzelner „Worte“, Token die verwendet werden dürfen nach bestimmten Regeln.

    Beispiel

    CR                      =  <ASCII CR, carriage return>  ; (     15,      13.)
    LF                      =  <ASCII LF, linefeed>         ; (     12,      10.)
    CRLF            =  CR LF
    SPACE           =  <ASCII SP, space>            ; (     40,      32.)
    HTAB            =  <ASCII HT, horizontal-tab>   ; (     11,       9.)
    
    word            = <any printable 7bit us-ascii except []=:., >
    
  2. syntaktische Regeln

    Das sind Regeln mit denen Satzstrukturen beschrieben werden.

    Beispiel

    vcard           = “BEGIN” [ws] “:” [ws] “VCARD” [ws] 1*CRLF
        items *CRLF “END” [ws] “:” [ws] “VCARD”
    

Es soll zunächst der selbe Umfang realisiert werden, wie im vorigen Kapitel mit Hilfe des „Endlichen Automaten“. Eine Realisierung mit Compiler-Tools ist langfristig flexibler zu warten, als die erste Lösung.

Beginnen wir mit einem ersten Entwurf der elementaren „Token“, die erkannt sein wollen:

 1  lexer grammar QLexerRules;
 2
 3  BEGIN   : 'BEGIN' ;
 4  END     : 'END' ;
 5  VCARD   : 'VCARD' ;
 6  VERSION : 'VERSION' ;
 7
 8  PROP_N   : 'N' ;
 9  PROP_FN  : 'FN' ;
10  PROP_TEL : 'TEL' ;
11  PROP_CAT : 'CATEGORIES' ;
12  PROP_MAIL: 'EMAIL' ;
13  PROP_ORG : 'ORG' ;
14  PROP_TIT : 'TITLE' ;
15
16  BBS     : 'BBS' ;
17  CAR     : 'CAR' ;
18  CELL    : 'CELL';
19  CHARSET : 'CHARSET' ;
20  ENCODING: 'ENCODING' ;
21  FAX     : 'FAX' ;
22  HOME    : 'HOME' ;
23  INTERNET : 'INTERNET' ;
24  ISDN    : 'ISDN' ;
25  MODEM   : 'MODEM' ;
26  MSG     : 'MSG';
27  PAGER   : 'PAGER' ;
28  PREF    : 'PREF' ;
29  VIDEO   : 'VIDEO' ;
30  VOICE   : 'VOICE' ;
31  WORK    : 'WORK' ;
32
33  QUOTPRINT   : ('=' [0-9ABCDEF]*)+ ;
34  W7BIT       : ('-' | '+' | [a-zA-Z0-9_~., @#])+ ;  // alles Zeichen erlaubt; außer Operatoren
35
36  CRLF    : '\r'? '\n' ;

Auf der linken Seite (vor dem „:“) findet man einen Alias-Namen, des gleich bei der Definition der Grammatik verwendet wird.

Auf der rechten Seite ist die Zeichenkette definiert, die damit beim Lesen erfasst wird. Das sind einzelne Wörter wie „EMAIL“ oder Zeichenketten, die über reguläre Ausdrücke festgelegt werden.

Mit obiger Definition kann die vCard-Datei, die bislang als Aufgabe zu parsen war, gelesen werden.

Eine Grammatik, die diese Token verwendet kann wie folgt aussehen:

 1grammar QLang;
 2
 3prog:   CRLF*  (vcard)*            # vcf_prog
 4    ;
 5
 6vcard   : begin  version items  end     # vcf_card
 7        ;
 8
 9version : VERSION ':' '2.1' CRLF    # vcf_version
10        ;
11
12begin   : BEGIN ':' VCARD  CRLF     # vcf_begin
13        ;
14
15end     : END ':' VCARD  CRLF       # vcf_end
16        ;
17
18items   : (item)*
19        ;
20
21item    : item_rec  CRLF
22        | CRLF
23        ;
24
25item_rec    : name
26            | form_name
27            | telefon
28            | mail
29            | categorie
30            | org
31            | title
32            ;
33
34name        : PROP_N  (';' prop_param)*  ':'  name_rhs    # vcf_name
35            ;
36
37prop_param  : ENCODING  '='  pencodingval   # val_enc
38            | CHARSET  '=' charsetval       # val_char
39            ;
40
41name_rhs    : n_family? ';' n_given? ';' n_add? ';' n_prefix? ';' n_suffix?
42            ;
43
44n_family    : value ;
45n_given     : value ;
46n_add       : value ;
47n_prefix    : value ;
48n_suffix    : value ;
49
50value       : text | quotprint | ('-')+  ;
51text        : W7BIT ;
52quotprint   : QUOTPRINT ;
53
54form_name   : PROP_FN  (';' prop_param)* ':' value ;
55
56telefon     : PROP_TEL  (';' tel_param)* ':' value ;
57
58tel_param   :  prop_param
59  |  PREF
60  |  WORK
61  |  HOME
62  |  VOICE
63  |  FAX
64  |  MSG
65  |  CELL
66  |  PAGER
67  |  BBS
68  |  MODEM
69  |  CAR
70  |  ISDN
71  |  VIDEO
72  ;
73
74pencodingval    : '7BIT' | '8BIT' | 'QUOTED-PRINTABLE' | 'BASE64' ;
75charsetval      : W7BIT ;
76
77mail    :  PROP_MAIL  (';' mail_param)* ':' value    # vcf_mail
78        ;
79
80mail_param :  PREF
81  | WORK
82  | HOME
83  | INTERNET
84  ;
85
86categorie   : PROP_CAT ':' value  ;
87org         : PROP_ORG ':' value ;
88title       : PROP_TIT ':' value ;

Die Regeln haben wie bei der lexikalischen Analyse einen Alias (Name der Produktion) und eine oder mehrere Definitionen. Die Namen der Regeln sind klein geschrieben, die der Token sind in Großbuchstaben.

3.4. Test der Grammatik#

Ein wichtiges Merkmal von ANTLR4 ist die Abkehr vom Performance-Gedanken hin zu der Wartbarkeit des Codes. Schon die Grammatik unterscheidet sich erheblich von den klassischen Ansätzen [Dragon] und unterstützt den Entwickler.

Warnung

Im Umkehrschluss bedeutet die Priorisierung der Wartbarkeit, dass das Tool nicht optimal ist, wenn Performance im Vordergrund steht. Wenn Performance ein „Muss“ ist, sollte man sich doch mit lex/yacc, flex/bison beschäftigen.

Die Wartbarkeit des Codes wird unterstützt durch die Trennung von Grammatik und Source Code. Das Herzstück ist der AST (Abstract Syntax Tree), der zur Laufzeit erzeugt wird, und den man mit den generierten Methoden abarbeiten kann.

Dieser AST lässt sich bei Vorliegen der Grammatik visualisieren, ohne dass man eine Zeile Java/Python/… schreiben muss und zwar wie folgt:

  1. Es werden die Java-Klassen aus der Grammatik generiert, im Beispiel

    java -jar /opt/antlr/antlr4/antlr-4.9-complete.jar -visitor QLang.g4
    
  2. Die Klassen werden übersetzt

    javac  QLang*.java
    
  3. Das Testprogramm wird gestartet

    java org.antlr.v4.gui.TestRig QLang prog -gui
    

    Bemerkung

    Es empfiehlt sich einen Alias einzurichten, z.B.:

    grun=“java org.antlr.v4.gui.TestRig“

  4. Auf der Konsole werden die Testdaten eingegeben, z.B.

    BEGIN:VCARD
    VERSION:2.1
    N:Public;John;Quinlan;Mr.;Esq.
    END:VCARD
    
  5. Der Syntax-Baum erscheint:

    _images/antlr4_parse_tree_ct002.png

    Man sieht auf den ersten Blick, dass man bei der Traversierung dieses Baumes die Information der vCard im Detail erhält.

    Z.B. finden wir unter dem „name-Knoten“ alle Elemente des vCard „N-Items“ von „family“ bis „suffix“.

3.5. ANTLR Generatoren#

Auf Basis dieser Grammatik kann ANTLR (siehe Referenzen) Basis-Klassen für verschiedene Programmiersprachen generieren. Diese Basisklassen müssen nur angepasst werden, um den kompletten Parser zu erhalten.

Bemerkung

Es wird hier vorausgesetzt, dass antlr-4.9-complete.jar lokal installiert ist unter /opt/antlr/antlr4.

Um die Basisklassen für python3 zu erhalten, wird wie folgt vorgegangen:

  1. Die Grammatik wird in einer Datei mit Namen Qlang.g4 gespeichert.

  2. Es wird folgender Aufruf genutzt:

    java -jar /opt/antlr/antlr4/antlr-4.9-complete.jar -visitor -Dlanguage=Python3 QLang.g4
    

Anschliessend findet man die folgenden generierten python-Dateien vor

QLangLexer.py
QLangListener.py
QLangParser.py
QLangVisitor.py

Im Folgenden werden wir mit dem Listener-Konzept arbeiten, so dass ein genauerer Blick auf QLangListener.py geworfen werden muss.

Man findet eine Klasse vor, die für jede Produktion der Grammatik eine „enter“- und eine „exit“-Methode definiert:

# This class defines a complete listener for a parse tree produced by QLangParser.
class QLangListener(ParseTreeListener):

    # Enter a parse tree produced by QLangParser#vcf_prog.
    def enterVcf_prog(self, ctx:QLangParser.Vcf_progContext):
        pass

    # Exit a parse tree produced by QLangParser#vcf_prog.
    def exitVcf_prog(self, ctx:QLangParser.Vcf_progContext):
        pass

    # ...

Die Namen der Methoden enden entweder mit dem Namen der Regel oder mit einem selbst gewähten Namen am Ende der Regel (hier: „# vcf_prog“), der in der Grammatik angegeben ist – Beispiel:

prog:   CRLF*  (vcard)*            # vcf_prog

erzeugt genau die obigen Methodennamen.

3.6. Fleisch ans Skelett#

Der wesentliche Teil der Implementierung des Parsers besteht nun darin, eine abgeleitete Klasse von QLangListener zu erstellen, die die Daten bei dem Durchlaufen des AST „einsammelt“.

In unserem Beispiel kann die wie folgt aussehen:

  1class CVcardListener(QLangListener):
  2    def __init__(self):
  3        super(CVcardListener, self).__init__()
  4        self.log = get_logger()
  5        self._result = None
  6        self._adr_lst = []  # [CDcVcard]
  7        self.adr_hash = {}  # key == CDcVcard.n.__repr__; value == CDcVcard
  8
  9        self._encoding = None
 10        self._is_quotprint = False
 11        self._is_base64 = False
 12        self._text = None
 13        self._name_obj = empty_n()
 14        self._vcard_obj = empty_vcard()
 15
 16    @property
 17    def result(self):
 18        return self._result
 19
 20    def gen_adresses(self) -> CDcVcard:
 21        for k, ao in self.adr_hash.items():
 22            yield ao
 23
 24    # Exit a parse tree produced by QLangParser#prog.
 25    def exitVcf_prog(self, ctx: QLangParser.ProgContext):
 26        self._result = 'IO'
 27        self.log.debug('prog')
 28        pass
 29
 30    # Exit a parse tree produced by QLangParser#vcf_card.
 31    def exitVcf_card(self, ctx: QLangParser.Vcf_cardContext):
 32        pass
 33
 34    # Exit a parse tree produced by QLangParser#vcf_begin.
 35    def exitVcf_begin(self, ctx: QLangParser.Vcf_beginContext):
 36        self.log.debug('begin')
 37
 38    # Exit a parse tree produced by QLangParser#vcf_end.
 39    def exitVcf_end(self, ctx: QLangParser.Vcf_endContext):
 40        self._vcard_obj.n = self._name_obj
 41        k = self._name_obj.__repr__()
 42        if k not in self.adr_hash:
 43            self.adr_hash[k] = self._vcard_obj
 44            self.log.info(f'  {self._vcard_obj}')
 45        self._vcard_obj = empty_vcard()
 46        self._name_obj = empty_n()
 47        self.log.debug('end')
 48
 49    def enterItem(self, ctx: QLangParser.ItemContext):
 50        self._encoding = None
 51        self._is_quotprint = False
 52        self._is_base64 = False
 53
 54    def enterQuotprint(self, ctx: QLangParser.QuotprintContext):
 55        self._is_quotprint = True
 56
 57    def exitValue(self, ctx: QLangParser.ValueContext):
 58        self._text = ctx.getText() if ctx.getText() else ''
 59        if self._is_quotprint:
 60            self._text = to_str(decodestring(self._text))
 61        pass
 62
 63    def exitN_add(self, ctx: QLangParser.N_addContext):
 64        self._name_obj.middle = self._text
 65
 66    def exitN_family(self, ctx: QLangParser.N_familyContext):
 67        self._name_obj.family = self._text
 68
 69    def exitN_given(self, ctx: QLangParser.N_givenContext):
 70        self._name_obj.given = self._text
 71
 72    def exitN_prefix(self, ctx: QLangParser.N_prefixContext):
 73        self._name_obj.prefix = self._text
 74
 75    def exitN_suffix(self, ctx: QLangParser.N_suffixContext):
 76        self._name_obj.suffix = self._text
 77
 78    # Exit a parse tree produced by QLangParser#vcf_name.
 79    def exitName_rhs(self, ctx: QLangParser.Name_rhsContext):
 80        self.log.debug(f'N:  {self._name_obj}')
 81
 82    def exitForm_name(self, ctx: QLangParser.Form_nameContext):
 83        prop = self._text
 84        self.log.debug(f'FN: {prop}')
 85        self._vcard_obj.fn = prop
 86
 87    def exitTelefon(self, ctx: QLangParser.TelefonContext):
 88        prop = self._text
 89        self.log.debug(f'TEL: {prop}')
 90        self._vcard_obj.tel.append(prop)
 91
 92    def exitVcf_mail(self, ctx: QLangParser.Vcf_mailContext):
 93        prop = self._text
 94        self.log.debug(f'MAIL: {prop}')
 95        self._vcard_obj.mail = prop
 96
 97    def exitCategorie(self, ctx: QLangParser.CategorieContext):
 98        prop = self._text
 99        self.log.debug(f'CATEGORIES: {prop}')
100        self._vcard_obj.categorie = prop
101
102    def exitTitle(self, ctx: QLangParser.TitleContext):
103        prop = self._text
104        self.log.debug(f'TITLE: {prop}')
105        self._vcard_obj.title = prop
106
107    def exitOrg(self, ctx: QLangParser.OrgContext):
108        prop = self._text
109        self.log.debug(f'ORG: {prop}')
110        self._vcard_obj.org = prop

Zum Verständnis, wie in dem Hash in Zeile 8 die Daten gesammelt werden, müssen noch zwei „Daten-Container-Klassen“ vorgestellt werden:

 1# Property: N
 2#  Family, Given, Middle, Prefix, Suffix.
 3#  Example:Public;John;Q.;Reverend Dr.;III, Esq.
 4@dataclass
 5class CDcN:
 6    family: str
 7    given: str
 8    middle: str
 9    prefix: str
10    suffix: str
11
12    @property
13    def n_name(self):
14        rv = f'{self.family} {self.given} {self.middle} {self.prefix} {self.suffix}'
15        return rv
16
17
18@dataclass
19class CDcVcard:
20    n: CDcN
21    fn: str
22    tel: []
23    mail: str
24    categorie: str
25    org: str
26    title: str
27
28    def __lt__(self, other):
29        return self.n.n_name < other.n.n_name
30
31
32def empty_n():
33    rv = CDcN(
34        '',
35        '',
36        '',
37        '',
38        '',
39    )
40    return rv
41
42
43def empty_vcard():
44    rv = CDcVcard(empty_n(), '', [], '', '', '', '')
45    return rv

Man sieht, es wird für die gesamte vCard ein Container (CDcVcard) definiert und ein separater Container für den Namen (N-Tag) CDcN.

Beim Durchlaufen des AST werden die einzelnen Datenfelder gefüllt und in am Ende einer vCard (exitVcf_end, ab Zeile 40) in den Sammel-Hash eingetragen.

Duplikate werden ignoriert, wobei die „Gleichheit“ über den Namen verglichen wird.

3.7. Das Hauptprogramm#

Jetzt fehlt nur noch das Rahmenprogramm, um unsere Parser-Ableitung mit Daten zu füttern.

 1class CqLangParser:
 2    def __init__(self):
 3        self.log = get_logger()
 4        msg = 'QLang-Parser is starting.'
 5        self.log.info(msg)
 6        self.vcf_listener = CVcardListener()
 7
 8    def parse_vcf(self, src_txt):
 9        _input = InputStream(src_txt)
10        lexer = QLangLexer(_input)
11        stream = CommonTokenStream(lexer)
12        parser = QLangParser(stream)
13
14        tree = parser.prog()
15        walker = ParseTreeWalker()
16        walker.walk(self.vcf_listener, tree)
17        return self.vcf_listener.result

Das Vorgehen ist mit ANTLR dabei immer dasselbe:

  1. Der Source-Text wird in einen InputStream gewandelt (Zeile 10).

  2. Der InputStream wird anschliessend in ein Lexer-Objekt überführt (Zeile 11).

  3. Aus dem Lexer-Objekt wird eine Tokenstream generiert (Zeile 12).

  4. Dieser Tokenstream dient dem Parser als Input (Zeile 13).

  5. In Zeile 14 wird eine AST-Instanz erzeugt.

  6. Ein AST-Durchlaufobjekt in Zeile 15 inspiziert mit seiner walk-Methode in Zeile 16 schließlich alle Knoten und ruft die entsprechenden Methoden unseres Listener-Objektes (Zeile 7) auf.

  7. Am Ende enthält die Listener-Instanz den Hash mit allen vCard-Objekte.

3.8. Test Code#

Natürlich schreibt sich auch die einfache Ableitung der Parser-Klasse in der Regel nicht ohne anfängliche Fehler.

Es empfiehlt sich deshalb, parallel zur Klassen-Implementfierung Unit-Tests zu schreiben, z.B. mit einer Datei ./tests/test_vcf_simple.py:

 1import unittest
 2
 3from qlib.qparser_vcf import CqLangParser
 4
 5CT001 = """BEGIN:VCARD
 6VERSION:2.1
 7END:VCARD
 8"""
 9
10CT002 = """BEGIN:VCARD
11VERSION:2.1
12N:Public;John;Quinlan;Mr.;Esq.
13END:VCARD
14"""
15
16
17class TestVcfParserMethods(unittest.TestCase):
18    def setUp(self):
19        pass
20
21    def test_vcf01(self):
22        p = CqLangParser()
23        self.assertEqual(p.parse_vcf(src_txt=CT001), 'IO')
24
25    def test_vcf02(self):
26        p = CqLangParser()
27        self.assertEqual(p.parse_vcf(src_txt=CT002), 'IO')
28
29    def tearDown(self):
30        pass
31
32
33if __name__ == '__main__':
34    unittest.main()

Ein Testlauf liefert folgendes Ergebnis

$ python -m tests.test_vcf_simple -v
test_vcf01 (__main__.TestVcfParserMethods) ... ok
test_vcf02 (__main__.TestVcfParserMethods) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.006s

3.9. DB-Import#

Wenn man sich die Grammatik der vCard ansieht (siehe die Spezifikation des versit Consortium), wird klar, dass es sich bei einer solchen Visitenkarte eigentlich um ein „Objekt“ handelt, dass mehr als 20 Entitäten haben kann und davon jeweils gleich mehrere, z.B. gibt es oft mehrere Telefonnummern oder Email-Adressen zu einer vCard.

Diese Tatsache führte zu dem „Workaround“ bei den Telefonnummern in den einzeiligen CSV-Einträgen.

Um ein Objekt mit beliebig vielen Eigenschaften zu speichern, bietet sich der Einsatz einer relationalen Datenbank an.

Glücklichwerweise braucht man weder ein „Oracle“ noch „Mircosoft“ oder AWS, um ein „state-of-the-art“ Datenbanksystem für diesen Zweck einzusetzen.

PostgreSQL ist ein Datenbanksystem, das nicht nur „mit allen Wassern gewaschen ist“ sondern auf Jahrzehnte „gereift“ ist und damit ideal für diesen Einsatz.

Bevor das Projekt „vCard-Import“ in ein ein RDBMS (PostgreSQL) gestartet wird, soll aber noch ein Kapitel über „Multi-Release“, „Multi-Cluster“ PostgreSQL geschrieben werden, welches dann die Basis für die Implementierung wird (voraussichtlich werden wir auch nicht um SQLAlchemy „herumkommen“).

3.10. Referenzen#

  1. Referencen

    [Dragon]

    Compilers: Principles, Techniques, and Tools; https://suif.stanford.edu/dragonbook/

  2. URLs

    1. https://de.wikipedia.org/wiki/VCard

    2. https://en.wikipedia.org/wiki/VCard

    3. https://tools.ietf.org/html/rfc2425 [V 2.1]

    4. http://jpim.sourceforge.net/contacts/specifications/vcard-21.pdf

    5. https://tools.ietf.org/html/rfc2426 [V 3.0]

    6. https://tools.ietf.org/html/rfc6350 [V 4.0]

    7. https:www.antlr.org

    8. https://www.postgresql.org/

    9. https://www.sqlalchemy.org/