% Content-Encoding: UTF-8 \documentclass[a4paper,ngerman]{article} \usepackage[T1]{fontenc} \usepackage[utf8]{inputenc} \setcounter{secnumdepth}{0} \setcounter{tocdepth}{0} \usepackage{amsmath} \usepackage{amssymb} \usepackage{latexsym} \usepackage{multicol} \setlength{\parindent}{0pt} \begin{document} \title{CSV–Files lesen und verarbeiten} \author{Ulrich Hoffmann} \maketitle \newcommand{\q}{\char34} \begin{multicols}{2} \begin{abstract} Auch im Zeitalter des Datenaustausches über XML ist es immer noch wichtig, mit CSV-Files (Comma Separated Values) umgehen zu können. Dieser Artikel stellt einige Definitionen vor, die es erlauben, CSV-Files zu parsen und auf ihre Bestandteile zugreifen zu können. \end{abstract} Comma Separated Values sind ein minimales Datenformat, um Datensätze auszutauschen. Jeder Datensatz steht in einem CSV--File in einer eigenen Zeile. Die einzelnen Felder des Datensatzes sind durch Kommata getrennt. So weit, so einfach. Problematisch wird es, wenn ein Feld selbst ein Komma enthält. Dies darf nämlich nicht mit einem Feldtrennungs--Komma verwechselt werden. Felder dürfen daher in Anführungszeichen gesetzt werden, um kenntlich zu machen, dass die im Feld möglicherweise vorkommenden Kommata keine Feldtrenner sind. So weit, so einfach. Was aber, wenn ein Feld auch Anführungszeichen enthält. Nun --- um ein Anführungszeichen darzustellen, muss das Feld selbst in Anführungszeichen stehen und das Anführungszeichen doppelt geschrieben werden. Hmm --- nicht mehr so einfach. Ein paar Beispiele können das vielleicht erhellen: Die Zeile\\ \centerline{\tt eins, zwei, drei}\\ beschreibt einen Datensatz mit 3 Feldern, die die Werte {\tt eins}, {\tt zwei} und {\tt drei} haben. Soll nun das zweite Feld ein Komma enthalten, notieren wir\\ \centerline{\tt eins, \q zwei, zwei\q, drei}. Bei \\ \centerline{\tt eins, zwei, \q drei \q\q\ drei\q}\\ enthält das dritte Feld ein An\-füh\-rungs\-zeichen: Es hat den Wert {\tt drei \q\ drei}. Das ist eigentlich doch nicht so schwer. Leider immer noch zu schwer für einige Programme. So exportiert OpenOffice Dezimalzahlen ohne Anführungszeichen, auch wenn die deutsche Zahlenformatierung mit Dezimalkomma gewählt ist. Oops. CSV--Files sind nicht standardisiert und es gibt zahlreiche Dialekte. Statt des Kommas kann zum Beispiel als Feldtrenner auch ein beliebig anderes Zeichen, etwa ein Semikolon oder das Tab--Zeichen (Ctrl-I, 9), zum Einsatz kommen. Leider besitzen CSV--Files keine Redundanz, so dass Daten im falschen Format meist ohne Fehlermeldung eingelesen und verarbeitet werden. Häufig haben alle Datensätze innerhalb eines CSV--Files die gleiche Anzahl von Feldern. Das kann man für eine einfache Plausibilitätsprüfung ausnutzen. Aber verlassen kann man sich darauf im Allgemeinen nicht. Um CSV--Files mit Forth zu bearbeiten, lesen wir sie zeilenweise ein und zerlegen jeden Datensatz gemäß der obigen Regeln in Felder. Zunächst benötigen wir einige grundlegende Funktionen für die zeichenweise Bearbeitung von Strings, die wir Forth--typisch auf dem Stack durch ihre Anfangsadresse und ihre Länge darstellen. \begin{verbatim} : c++ ( caddr0 u0 -- caddr1 u1 ) swap char+ swap 1- ; : c@+ ( caddr0 u0 -- caddr1 u1 char ) over c@ >r c++ r> ; : cappend ( caddr0 u0 char -- caddr1 u1 ) >r 2dup chars + r> swap c! 1+ ; \end{verbatim} Das Wort \verb|c++| ändert die Stringadresse und Länge auf dem Stack so, dass ein String repräsentiert wird, bei dem gegenüber dem ursprünglichen String das erste Zeichen fehlt. Dazu muss der ursprüngliche String mindestens ein Zeichen lang sein. Mit \verb|c@+| kann ein String zeichenweise durchlaufen werden. \verb|c@+| liefert das erste Zeichen des Strings und Adresse und Länge des Reststrings ohne das erste Zeichen. Auch bei \verb|c@+| muss der String mindestens ein Zeichen enthalten. \verb|cappend| hängt ein Zeichen an einen String an. Natürlich muss an der betreffenden Stelle im Speicher Platz für das zusätzliche Zeichen sein. Gut --- nachdem wir das Grundsätzliche geklärt haben, definieren wir Variablen und Tests für den Feldtrenner \verb|sep| und das Anführungszeichen \verb|quot|: \begin{verbatim} Variable sep char , sep ! Variable quot char " quot ! : sep? ( char -- flag ) sep @ = ; : quot? ( char -- flag ) quot @ = ; : -quot? ( char -- char flag ) dup quot? 0= ; \end{verbatim} Nun widmen wir uns dem zeichenweisen Lesen von Strings, die möglicherweise Anführungszeichen, wie oben beschrieben, haben können. Hierfür ist eine Variante von \verb|c@+| nützlich, die auch mit leeren Strings umgehen kann. Dann soll das Wort \verb|?c@+| einen speziellen Wert \verb|#nochar| liefern, um anzuzeigen, dass kein Zeichen gelesen werden konnte: \begin{verbatim} -1 Constant #nochar : ?c@+ ( caddr0 u0 -- caddr1 u1 x ) dup IF c@+ EXIT THEN #nochar ; \end{verbatim} Für die Behandlung der Anführungszeichen und die spätere Erkennung von Feldtrennern ist es wichtig, ob Zeichen außerhalb der Anführungszeichen oder innerhalb gelesen werden. Deshalb hat das folgende Wort \verb|getc| das zusätzliche Flag \verb|?out| (outside) als Argument und Resultat. \begin{verbatim} : getc ( caddr0 u0 ?out -- caddr1 u1 ?out' c ) ( 1) >r ?c@+ -quot? IF r> swap EXIT THEN ( 2) drop r> IF 0 #nochar EXIT THEN ( 3) ?c@+ -quot? swap ; \end{verbatim} \verb|getc| versucht, das nächste Zeichen aus dem durch \verb|caddr0 u0| beschriebenen String zu lesen. Stößt es dabei auf ein Anführungszeichen oder das Ende des Strings, so kann es sein, dass kein nächstes Zeichen ermittelt werden kann und \verb|#nochar| als Resultat geliefert wird. \verb|getc| liefert außerdem den möglicherweise um das nächste Zeichen verkürzten String und ein eventuell modifiziertes \verb|?out|. Die drei Zeilen von \verb|getc| regeln dabei Folgendes: \begin{enumerate} \item Unabhängig, ob \verb/getc/ innerhalb oder außerhalb von Anführungszeichen liest, werden \emph{normale} Zeichen einfach extrahiert und \verb|?out| bleibt unverändert. Am Ende des Strings wird \verb|#nochar| geliefert. \item Behandelt das Auftreten eines Anführungszeichens, wenn Zeichen außerhalb von Anführungszeichen gelesen werden. Dann nämlich wechselt \verb|?out| in den Zustand \emph{innerhalb}. Ein Zeichen wird dann nicht geliefert. \item Behandelt das Auftreten eines Anführungszeichens, wenn Zeichen innerhalb von Anführungszeichen gelesen werden. Folgt ein weiteres Anführungszeichen, steht es also doppelt, so ist das als nächstes gelieferte Zeichen ein Anführungszeichen selbst. \verb|?out| bleibt dann im Zustand \emph{innerhalb}. Folgt aber kein weiteres Anführungszeichen, steht das ursprüngliche Anführungszeichen also allein, dann wechselt \verb|?out| nach \emph{außerhalb} und das folgende Zeichen wird als nächstes gelesenes Zeichen geliefert. Auch hier wird durch \verb|?c@+| die Behandlung des String--Endes berücksichtigt und möglichwerweise \verb|#nochar| geliefert. \end{enumerate} Puh --- ganz schön kompliziert, was sich in den drei Zeilen alles tut. Schauen wir uns lieber an, wie \verb/getc/ verwendet wird. Wir definieren das Wort \verb/next-csv/, das uns aus einem Datensatz (gegeben durch \verb|caddr0 u0|) das nächste Feld heraussucht und nach \verb|caddr1| legt. \begin{verbatim} : next-csv ( caddr0 u0 caddr1 -- caddr2 u2 caddr1 u1 ) 0 >r >r -1 ( caddr u ?outside ) BEGIN ( caddr u ?outside ) over WHILE ( caddr u ?outside ) getc ( caddr' u' ?outside char ) dup #nochar = IF drop ELSE 2dup sep? and \ outside and sep IF ( caddr' u' -1 sep ) 2drop r> r> EXIT THEN r> swap r> swap cappend >r >r THEN ( caddr' u' ?outside ) REPEAT ( caddr 0 ?outside ) drop r> r> ; \end{verbatim} Das herausgesuchte Feld hat die Länge \verb|u1|. Außerdem verkürzt \verb|next-csv| den Datensatz (\verb/caddr2 u2/). Um seine Aufgabe zu erledigen, liest \verb|next-csv| zeichenweise mit \verb|getc| den Datensatz. Das Ende des Feldes ist gefunden, wenn ein Separator (Komma) gefunden wird, der nicht innerhalb von Anführungszeichen steht. Die gelesenen Zeichen werden mit \verb|cappend| an den String bei \verb|caddr1| angehängt, das zwischenzeitlich mit \verb|u1| auf dem Returnstack geparkt wird. Wird wegen gelesener Anführungszeichen von \verb|getc| mal kein Zeichen geliefert, wird einfach weiter probiert. Ist der Datensatz erschöpft, so bricht die \verb|WHILE|--Schleife ab und der letzte Wert ist gefunden (\verb/u2/ ist dann null). So --- wie kann man also die einzelnen Felder eines Datensatzes ausdrucken? Als Beispiel hier das Wort \verb/print-fields/, das das erledigen kann: \begin{verbatim} : print-fields ( caddr u -- ) BEGIN dup WHILE ." - " pad next-csv ( trim ) 2dup . . ." >" type ." <" cr REPEAT 2drop ; \end{verbatim} Die einzelnen Werte werden nach \verb|pad| gelesen und dann zusammen mit ihrer Anfangsadresse und Länge von \verb/>/ und \verb/ <$> -- ) create [char] $ word count here over 1+ chars allot place ; \end{verbatim} \begin{verbatim*} $line: record eins, " zwei , zwei " , drei$ \end{verbatim*} \end{small} Ein \verb|record count print-fields| gibt damit schließlich so etwas aus wie: \begin{verbatim*} - 5 211492 > eins< - 21 211492 > zwei , zwei < - 5 211492 > drei< ok \end{verbatim*} Wir sehen also, dass die Leerzeichen innerhalb der Felder erhalten geblieben sind. Das mag nicht immer sinnvoll sein. \verb|print-fields| enthält dafür den auskommentierten Aufruf des Wortes \verb|trim|, das führende und abschließende Leerzeichen entfernt. \verb|trim| zu definieren, bleibt als Aufgabe für den interessierten Leser und führt hoffentlich zu inspirierenden Leserbriefen\ldots Fazit: Das Bearbeiten von CSV--Files mit Forth ist nicht wirklich schwer. Der Kern sind die beiden Worte \verb|getc| für die Behandlung der Anführungszeichen innerhalb der Felder und \verb|next-csv|, um die Felder aus einem Datensatz herauszulösen. Hat man die Felder erst einmal als Forth--String zu packen, kann man sie beliebig transformieren. Werkzeugkiste auf --- Definitionen rein --- Werkzeugkiste zu. \hfill $\Box$ \end{multicols} \end{document}