Export einer "rechteckigen" UTF-8-Textdatei
Hallo zusammen,
ich habe die Anforderung, einen UTF-8-enkodierten Datensatz mit nationalen Sonderzeichen in eine Textdatei zu exportieren. Die Textdatei muss 'fixed length' Felder enthalten. Das heißt, jede Variable muss in der Textdatei in bestimmten Spalten stehen. In einem Texteditor mit Unicode-Unterstützung muss die Datei korrekt angezeigt werden.
Was mir Sorge bereitet, ist die variable Buchstabengröße nationaler Sonderzeichen in UTF-8 und die richtige Definition der 'length' in SAS. Meine Metadaten entahlten die maximale Anzahl von Buchstaben für jede Variable. Nur lässt sich daraus ja nicht die Größe in Bytes ableiten, die ich über 'length' definieren müsste, weil die Buchstaben eben unterschiedlich viel Platz brauchen.
Eine Möglichkeit wäre den Speicherplatz für jede Variable als das Dreifache ihrer Buchstabenzahl zu definieren (weil angenommen werden kann, dass in meinen Daten keine 4-Byte-Buchstaben enthalten sind). Nur kann ich leider die Länge-in-Buchstaben in meiner Textdatei, also das Ausgabeformat, nicht auch verdreifachen.
Wie kriege ich den Export also hin? Würde man das möglicherweise mit besonderen Unicode-Formaten machen?
Vielen Dank im Voraus!
Alex
- Anmelden oder Registrieren um Kommentare zu schreiben

Text-Länge in Zeichen in Unicode
Hallo Alex,
in Unicode (oder UTF-8) muß man zwischen der
Anzahl Byte eines Strings
und
der Anzahl Zeichen eines Strings
unterscheiden.
Für die Variablen-Definition mit
lengthist die Anzahl Byte wichtig.Für Ausgabe in Spalten mit fester Breite ist die Anzahl Zeichen wichtig, die wird mit
klengthermittelt.Wenn trotz Unicode in Spalten mit fester Breite ausgegebn werden soll ist m.E. Handarbeit angesagt!
Dann klappt kein
proc print, keinproc reportund auch keindata _null_; file print; put feld1 $10. feld2 $20. "...";.SAS unterscheidet (scheinbar) nicht zwischen Zeichen und Bytes bei der Berechnung der Spaltenbreite. Dadurch wird der Abstand zur Folge-Spalte schief wenn z.B. Umlaute in Feld1 ausgegeben werden.
Wir haben den Aufwand gescheut die Spaltenbreite und die Positionierung der Ausgabe und alles mögliche zu Fuß zu berechnen bzw. zu programmieren und diese Probleme mit der Umstellung auf CSV-Dateien umgangen.
Gruß
Hans Kneilmann, Schäfer Shop GmbH (SSI)
Hallo Herr Kneilmann, danke
Hallo Herr Kneilmann,
danke für die schnelle Antwort. Sind sie sicher, dass data _null_; file print; put... nicht funktioniert? Ich hatte gehofft, dass put-Statements in Verbindung mit Unicode-Formaten die Lösung sein könnten um eine fixe Anzahl Buchstaben in einer variablen Anzahl Bytes unterzubringen. CSV o.ä. ist leider keine Option für uns bzw. unsere Partner, die mit den Files arbeiten müssen.
Hat sonst noch jemand eine Idee?
Gruß
Alex
leider
Hallo Alex,
leider bin ich sicher .....
Nicht ohne Grund haben wir diverse Ausgabe-Schnioottstellen auf CSV-Format umgestellt :-(
Wenn kein CSV geht, dann ist m.E. Hand-Arbeit angesagt: Also voll dynamisch alles selbst ausrechnen und die Füll-Leerzeichen dynamisch selbst ausgeben.....
Gruß
Hans Kneilmann, Schäfer Shop GmbH (SSI)
Mist...
Hallo Herr Kneilmann,
danke nochmal. Ich habe es inzwischen auch kurz ausprobieren können. Beim Unicode-Format
steht das
offenbar für die Länge in Bytes, nicht in Zeichen. Bei einem fixed length file ist es natürlich relativ witzlos wenn die Spaltenbreiten jeweils von den enthaltenen Daten abhängen. Um das einzulesen bräuchte man ggf. für jede Datei gesonderte Metadaten, was für uns keine Alternative darstellt.
OK, dann muss ich mir was anderes einfallen lassen.
Vielen Dank nochmals!
Alex
Könnte dies eine Lösung sein?
Ich habe mal ein kleines Programm geschrieben, mit dem zumindest eine Variable an die korrekte Position geschrieben wird. Das ist für meine Datenmenge (ca. 2500 Variablen, 2000 Obs) weder hübsch, noch effizient. Allerdings scheint mir das eine Möglichkeit zu sein.
Kann jemand dazu eine Meinung abgeben?
Vielen Dank!
Alex
data have;
infile cards;
input UniqueID CharVar $ NumVar ;
cards;
1 äää 1
2 aaa 1
3 løøp 1
4 ыыы 1
;
run;
data _null_;
set have end = __Last;
if _n_=1 then call execute( 'data _null_; set have; file out;' );
LengthInLetters = 10;
LengthInBytes = length(CharVar) + ( LengthInLetters - klength(CharVar) );
call execute( catx( ' ', 'if UniqueID=', UniqueID, 'and CharVar =', quote(strip(CharVar)), 'then put UniqueID @2 CharVar @', LengthInBytes+1+1, ' NumVar ;') );
if __Last then call execute( 'run;' );
run;
Verbesserung
Hallo Alex,
Deine Lösung ist sehr interessant, deshalb habe ich sie rauskopiert, damit herumexperimentiert und sie analysiert (man will ja verstehen was da so passiert).
Das erste was mich sehr erstaunte ist, dass die obige Lösung in meiner NON-Unicode-PC-Session (NON-Unicode ist meine Standard- bzw. Brot-und-Butter-Einstellung) prima klappte und eine korrekte (Unicode-)Datei erzeugte .... !!!
Das einzige was mich irritierte war der LOG, in dem jede Zeile quasi gleich aussah:
NOTE: CALL EXECUTE generated line. 1 + data _null_; set have; file out; 2 + if UniqueID= 1 and CharVar = "äää" then put UniqueID @2 CharVar @ 12 NumVar ; 3 + if UniqueID= 2 and CharVar = "aaa" then put UniqueID @2 CharVar @ 12 NumVar ; 4 + if UniqueID= 3 and CharVar = "løøp" then put UniqueID @2 CharVar @ 12 NumVar ; 5 + if UniqueID= 4 and CharVar = "???" then put UniqueID @2 CharVar @ 12 NumVar ; 6 + run; NOTE: The file OUT is: Filename=C:\tmp\test.txt, RECFM=V,LRECL=1536,File Size (bytes)=0, Last Modified=19. August 2010 08.29 Uhr, Create Time=19. August 2010 08.29 Uhr NOTE: 4 records were written to the file OUT. The minimum record length was 12. The maximum record length was 12. NOTE: There were 4 observations read from the data set WORK.HAVE. NOTE: DATA statement used (Total process time): real time 0.09 seconds cpu time 0.00 secondsEigentlich hatt ich erwartet, dass diese Stelle
CharVar @ 12 NumVarnicht überall gleich aussieht...Als ich in der Unicode-PC-Session den Code ausführte wurde zum einen auch eine korrekte (Unicode-)Datei erzeugt und zum anderen sah im LOG diese Stelle
CharVar @ 12 NumVarjetzt nicht überall gleich aus!NOTE: CALL EXECUTE generated line. 1 + data _null_; set have; file out; 2 + if UniqueID= 1 and CharVar = "äää" then put UniqueID @2 CharVar @ 15 NumVar ; 3 + if UniqueID= 2 and CharVar = "aaa" then put UniqueID @2 CharVar @ 12 NumVar ; 4 + if UniqueID= 3 and CharVar = "løøp" then put UniqueID @2 CharVar @ 14 NumVar ; 5 + if UniqueID= 4 and CharVar = "???" then put UniqueID @2 CharVar @ 12 NumVar ; 6 + run; NOTE: The file OUT is: Filename=C:\tmp\test3.txt, RECFM=V,LRECL=1536,File Size (bytes)=0, Last Modified=19Aug2010:12:31:23, Create Time=19Aug2010:09:00:29 NOTE: 4 records were written to the file OUT. The minimum record length was 12. The maximum record length was 15. NOTE: There were 4 observations read from the data set WORK.HAVE. NOTE: DATA statement used (Total process time): real time 0.04 seconds cpu time 0.01 secondsSoweit so gut!
Als nächstes stolperte ich über die if-Bedingung (
if UniqueID= 2 and CharVar = "aaa" then, genauer überand CharVar = "aaa". Dieser Teil ist überflüssig, also raus und neuer Test: Code klappt immer noch, prima:set have end = __Last;
if _n_=1 then call execute( 'data _null_; set have; file out;' );
LengthInLetters = 10;
LengthInBytes = length(CharVar) + ( LengthInLetters - klength(CharVar) );
call execute( catx( ' ', 'if UniqueID=', UniqueID, 'then put UniqueID @2 CharVar @', LengthInBytes+1+1, ' NumVar ;') );
if __Last then call execute( 'run;' );
run;
Soweit so gut!
Als nächstes stolperte ich über das
call execute. Das ist eine geniale Technik, aber hier erschien mir die Lösung dadurch zu umständlich, unperformant und nicht erweiterbar. Im Hinterkopf hatte ich den Gedanken an einen produktiven Einsatz mit den üblichen ca. 20 bis 30 Feldern.Mein Ziel war das Problem direkt in einem Rutsch zu lösen. So sieht das Ergenis aus:
set have;
LengthInLetters = 10;
file out;
/* //--<<-- hier die gewünschte Spaltenbreite (LengthInLetters) */
/* || */
put UniqueID 1. CharVar +(10-klength(CharVar)-1) NumVar ;
run;
Die Ausgabe-Datei ist unverändert eine korrekte (Unicode-)Datei !
Diese Lösung erscheint mir handhabbar und damit kann ich mir vorstellen Unicode-Dateien mit festen Spaltenbreiten mit vertretbarem Aufwand zu produzieren.
Das gute alte (Papier-)Handbuch SAS Language leistete unschätzbare Dienste. Im Chapter 9 SAS Language Statements studierte ich den Abschnitt zum
put-Befehl und fand die Lösung unter Pointer Controls mit+numeric-variablebzw.+(expression).Gruß
Hans Kneilmann, Schäfer Shop GmbH (SSI)
P.S.:
Was ist der betriebliche Hintergrund für Deine Fragestellung?
Wo gilt meine Datenmenge (ca. 2500 Variablen ... ???
Woher kommen 2.500 Felder pro Datensatz ?
Mir kamen bisher unsere bis zu ca. 100 Felder pro Satz schon viel vor ...
Von dem Datensatz mit ca. 54 000 Spalten von Melanie reden wir später ...
Sehr interessant!
Hallo Hans,
danke zunächst für Deine ausführliche Beschäftigung mit meinem Problem! Dann werde ich Deinen Beitrag mal Stück für Stück durchgehen.
Dass das in der Nicht-Unicode-Umgebung so aussieht, finde ich erstmal nicht verwunderlich. Die Werte, die über
cardserzeugt werden, sind dann eben NICHT unicode-enkodiert, sondern alle 1 Byte groß, was den konstanten Offset von 12 erklärt (CharVar @ 12 NumVar). Übrigens: die drei Fragezeichen in meinen Testdaten waren ursprünglich kyrillische Buchstaben, die das Eingabefeld hier offenbar gefressen hat, weil die Seite nicht unicodefähig ist!In der Unicode-Umgebung ist dieser Offset eben variabel und enthält so viele Bytes mehr wie der String nationale Sonderzeichen enthält.
and CharVar = "aaa"erscheinen mir heute auch überflüssig. Da war ich wohl müde gestern Abend...Richtig großartig finde ich nun Deine Lösung mit der Pointerberechnung zur Laufzeit! Das kannte ich so noch nicht. Auf diese Art kann man wirklich auf call execute verzichten. Großartig!
Wobei: In unserem konkreten Umfeld wird es am Ende bestimmt doch darauf hinauslaufen. Der Gurnd ist die große Variablenmenge und damit verbunden das Auslesen der Variablennamen und der Länge in Zeichen aus unseren Metadaten. Ich hatte schonmal versucht mir vor diesem Data Step einfach einen ganz langen String mit Variablennamen und -formaten in eine einzige Makrovariable zu schreiben um die dann zu benutzen. Aber das ist mir angesichts des 32K-Limits zu risikoreich.
Womit wir schließlich bei der Variablenmenge und den Gründen dafür angekommen sind. Auch mir kommen die Tränen wenn ich Tabellen mit derartig vielen Spalten sehe. Es handelt sich hier aber um Umfragedaten aus der Bildungsforschung. Die Psychometriker und Sozialwissenschaftler, die damit arbeiten, sind es gewohnt ihre Daten in 'breiten' Analysedatensätzen zu erhalten. Deshalb kommen wir leider nicht drumherum uns darauf einzulassen. (Ich habe meine Zweifel, dass deren Programme mit Unicode-Fixed-Length-Textdateien umgehen können, aber das ist eine andere Geschichte...)
Viele Grüße und Danke nochmals!
Alex
Gerne und ...
Hallo Alex,
gern geschehen.
Mir hat die Beschäftigung mit Deinem Problem es auch weiter geholfen, denn unsere Adress-Datensätze z.B. um die Adressträger für den Katalog-Versand (SSI=Versandhandel) zu drucken enthalten auch einiges an Unicode-Zeichen und spätestens mit der Ausdehnung nach Ungarn hatten wir das Problem, dass der Westeuropäische Zeichensatz nicht ausreichte .....
Und hatten mit unserer Unicode-Umstellung genau Dein Problem.
Bisher hatte ich es immer mit der Umstelung auf CSV-Dateien gelöst, aber es schadet nichts für die andere Variante eine praktikale Lösung in der Hinterhand zu haben.
Zu Deinen Bedenken mit dem 32K-Limit bei Macro-Variablen:
Wenn Du 2.500 Felder hast hast Du im Prinzip 13 Zeichen pro Variable. Wenn das nicht reicht, dann bleibt abkürzen (technische Variablen-Namen autom. vergeben mit den langen Namen als Labels) oder man programmiert so, dass bei Bedarf die Matadaten in zwei oder drei oder ... Macro-Variablen gespeichert werden. Das ist relativ gut handhabbar ....
Du kennst die Möglichkeit Macro-Variablen zu indizieren? Z.B.
MacVar1
MacVar2
bzw
MacVar&II.
Und zum Schluß:
Schreib doch bei Deinen Redscope-User-Daten ein paar Worte zu Deiner Firma, dann muß der nächste der es wissen will nicht mühselig googeln was IEA DPC bedeutet ...
Gruß
Hans Kneilmann, Schäfer Shop GmbH (SSI)
Schön, dass es beiden etwas gebracht hat!
Hallo Hans,
es freut mich, dass Du auch etwas davon hattest. Generell ist CSV mit Sicherheit die bessere Lösung. Unsere Partner wollen das aber nicht, weil deren Tools damit nicht arbeiten können. Wie bereits angedeutet, kann ich mir nicht vorstellen, dass es Tools gibt, die so altmodisch sind, dass sie statt mit CSV nur mit Fixed-Length-Text-Files arbeiten, gleichzeitig aber Unicode unterstützen. Naja, Versuch macht kluch, sagen wir hier in Hamburg...
Abkürzungen der Variablennamen sind keine Alternative. Dadurch würden sicher Doubletten entstehen. Diese sogenannten Macro Arrays kenne ich, habe damit auch schon gearbeitet. Dann habe ich aber mindestens 2 Makroschleifen im Code (eine zum Initialisieren, mindestens eine zum Auslesen) plus Extracode um Anzahl und Länge der einzelnen Makrovariablen dynamisch zu steuern falls sich die Variablenanzahl erhöhen sollte. Da sehe ich keine Vorteile mehr gegenüber call execute().
Ich habe dann auch mal unsere Firmen-URL in das Homepage-Feld gesetzt. Das sollte doch reichen, denke ich.
Schöne Grüße
Alex
Meine Lösung
Mit Hans Kneilmanns Hilfe bin ich auf eine relativ simple Lösung gekommen (s.u.). Das hätte ich anfangs nicht für möglich gehalten. Danke nochmals!
Der Datensatz MetaData enthält eine Zeile pro Variable in den tatsächlichen Daten. In ihm sind VariableName, VariableWidth, VariableDecimals (nur für numerische) und IsChar (Boolean, 1 wenn es sich um eine Charactervariable handelt, sonst 0) enthalten.
Wenn keine Metadaten in ähnlicher Form zur Verfügung stehen oder es sich nur um sehr wenige Variablen handelt, die man in dem Programm "hart" kodieren kann, sollte man auf Hans Kneilmanns Lösung ein paar Beiträge weiter oben zurückgreifen.
Wichtig könnte allerdings das Statement "format _all_;" sein. Ohne es würden Charactervariablen mit eventuell zuvor zugewiesenen Formaten ausgedruckt, was problematisch sein kann. In meinem Fall bin ich darüber gestolpert, dass eine Variable das Format $4 zugewiesen hatte und 4 nationale Sonderzeichen enthielt, die jeweils 2 Bytes groß waren. Beim Rausschreiben mit dem Format wurden nur die ersten 2 Buchstaben (4 Bytes) gedruckt.
Beste Grüße
Alex
filename OutTXT "Test.txt" encoding = "utf-8" ;
data _null_;
set MetaData end = __Last ;
if _n_ = 1 then call execute( 'data _null_; set DataFile; file OutTXT; format _all_; put ' );
if IsChar then call execute( catx( ' ', VariableName, '+ (', VariableWidth, '- klength(', VariableName, ') -1 )' ) );
else call execute( catx( ' ', VariableName, cats( VariableWidth, '.', VariableDecimals ) ) );
if __Last then call execute( '; run;' );
run;
Bitte Beispiel
Hallo Alex,
prima, dass Du eine relativ simple Lösung gefunden hast.
Könntest Du ein vollständiges Beispiel mit Test-Daten, Meta-Daten, ... zusammenstellen, dann ist es (für mich) wesentlich einfacher Deine Lösung nachzuvollziehen. Und genau das will ich, denn einfache Lösungen sind immer gut und die hier besprochene Problematik kann bei uns jederzeit erneut auftreten ...
Danke und Gruß
Hans Kneilmann, Schäfer Shop GmbH (SSI)
Beispiel
Hallo Hans,
gern bastle ich ein Beispiel zusammen.
Voilá:
length CharVar1 $ 20
CharVar2 $ 20
;
input NumVar1 CharVar1 $ NumVar2 CharVar2 $ ;
format CharVar1 CharVar2 $10. ;
infile cards;
cards;
1 aaaaaaaaaa 1.123 bbbbbbbbbb
2 ääääääääää 1.1234 cccccccccc
3 dddddddddd 1.0 öööööööööö
4 üüüüüüüüüü 1 ääääääääää
;
run;
data MetaData ;
length VariableName $ 8 ;
input VariableName $ VariableWidth VariableDecimals IsChar ;
infile cards;
cards;
NumVar1 1 0 0
CharVar1 10 . 1
NumVar2 5 3 0
CharVar2 10 . 1
;
run;
options missing = ' ';
filename OutTXT "d:\temp\Test.txt" encoding = "utf-8" ;
data _null_;
set MetaData end = __Last ;
if _n_ = 1 then call execute( 'data _null_; set DataFile; file OutTXT; format _all_; put ' );
if IsChar then call execute( catx( ' ', VariableName, '+ (', VariableWidth, '- klength(', VariableName, ') -1 )' ) );
else call execute( catx( ' ', VariableName, cats( VariableWidth, '.', VariableDecimals ) ) );
if __Last then call execute( '; run;' );
run;
Es ist wichtig, dass dieses Beispielskript in einer Unicode-Session ausgeführt wird, damit die Umlaute mit 2 Bytes enkodiert werden. In einer englischen oder deutschen SAS-Session hätten die nur 1 Byte und das Beispiel wäre witzlos. Ich habe mich auf Umlaute beschränkt, weil das Eingabefeld auf dieser Seite offenbar nicht unicodefähig ist.
Die beiden Textvariablen in DataFile haben das Format $10. zugewiesen bekommen. Wenn man nun das "format _all_;" aus dem Data Step entfernt, kann man sehen, dass die aus Umlauten bestehenden Werte um die Hälfte gekürzt werden.
Ich hoffe, das hilft Dir, Hans, und anderen.
Schöne Grüße
Alex
Dank + noch eine Frage
Hallo Alex,
danke, mir hat es geholfen.
Es ist mit einem Beispiel einfacher leichter fremden Code zu verstehen.
Eine Frage noch: Was bedeutet/macht
format _all_;"?Gruß
Hans Kneilmann, Schäfer Shop GmbH (SSI)
P.S.:
Die Lösung ist ja (dank MetaData) dermaßen einfach (und elegant): Wahnsinn!
Und warum müssen wir das alleine austüfteln? Warum hat SAS an diesem Punkt keine Unterstützung, keine Muster-Lösung?
Hallo Hans, mit Deiner
Hallo Hans,
mit Deiner Anmerkung zu Beispieln im allgemeinen hast Du natürlich völlig Recht. Das sehe ich genauso. Da hätte ich schon vorher etwas ausführlicher werden können.
"format _all_;" entfernt sämtliche Formate für alle Variablen im Datensatz. Dies ist für Textvariablen wichtig, wie oben beschrieben.
Deine Frage im PS verstehe ich als rhetorische. ;-) SAS bleibt SAS. In diesem besonderen Fall allerdings kann ich mir vorstellen, dass wir hier an einer Lösung herumfuhrwerken, die man hinterher zu nichts gebrauchen kann. Was will man mit solchen Dateien anfangen? Ich habe ja weiter oben schon meine Zweifel darüber angedeutet, dass es Systeme gibt, die mit solchen Dateien etwas anfangen können und gleichzeitig keine einfacher zu erstellenden Formate wie CSV einlesen können. Aber wer weiß? Ich spekuliere nur.
So oder so hat mir diese kleine Tüftelei aber auch Spaß gemacht und ich habe einiges gelernt. Nicht zuletzt die Pointersteuerung im Put-Statement zur Laufzeit.
Schöne Grüße
Alex
Nutzen im Zusammenspiel mit SAP
Hallo Alex,
diese Lösung hätte ich vor ca. 3 bis 4 Wochen gut gebrauchen können.
Grund:
Wir haben hier SAP für das richtige Gescäft und SAS für unser DWH.
In dieser Konstellation ist natürlich heftiges Daten-Austauschen angesagt.
Das läuft soweit alles und es gibt einen gewissen Bestand an Programmen auf beiden Seiten, die zusammenarbeiten.
Jetzt hatten wir vor ca. 3 bis 4 Wochen auf der DWH-Seite etwas umgestellt und dadurch wurden die Daten die an SAP gehen sollten als UTF-8-Daten erzeugt...
Die Daten wurden schon immer als fester Satz geliefert ....
Für SAP ist UNICODE überhaupt kein Problem! Also im ABAP den Code an einer einzigen Stelle geändert und schon konnte der SAP-Abap die UTF-8-Daten lesen. Leider war die Ausgabe aus SAS nicht mehr korrekt, sondern bei jedem Umlaut oder Akzent (es waren schweizer Adressen mit vielen französischen Namen bzw. Strassen/Orte) gab es einen Verschieber im Satz!
Also hatten wir im SAP-Kundenstamm ein Problem (nein 1000 Probleme ....) und da ich nicht wußte wie man einen festen Satz sauber produziert mußte die Einlese-Routine im SAP-Abap auf CSV-Satz unmgestellt werden.
Das war für die SAP-Kollegen unnötiger Mehraufwand, denn es hätte genügt, wenn unsere Seite wie bisher die Daten im festen Satz korrekt geliefert hätte.
Ausserdem gab es bei dem betr. SAP-Kollegen noch das Extra-Problem: Das gewußt wie fehlte und mußte von dem der es wußte (aber keine Zeit hatte) beschafft werden. Das ergab dann noch eine saftige Verzögerung ....
Also:
Es gibt/gab (bei uns) schon Fälle in denen unsere Lösung nützlich ist/war!
Gruß
Hans Kneilmann, Schäfer Shop GmbH (SSI)
Danke für das Beispiel
Hallo Hans,
danke für das ausführliche Beispiel. Das ist ein absolut nachvollziehbares Szenario. Ich finde Eure CSV-Lösung zwar klarer und technisch einfacher, aber unter Euren Umständen ist 'fester Satz' offenbar wirklich die bessere Lösung wenn man Anpassungen möglichst auf eine Seite des Systems beschränken will.
Dann haben wir hier ja tatsächlich was Brauchbares geschaffen. Mit einem umso besseren Gefühl gehe ich jetzt in die Mittagspause.
Schöne Grüße
Alex