Samstag, 27. Juni 2009

Parser für Komma-separierte Daten

Komma-separierte Daten (CSV) stellen für den Datenaustausch häufig eine einfache, platzsparende Alternative zu XML dar. Da das Format in Spreadsheet-Applikationen wie Excel unterstützt wird, kann es mit PC-Anwendern oft problemlos als Bezugsformat vereinbart werden. Einzige Voraussetzung ist eine einfache, nicht verschachtelte Recordstruktur. CSV ist wie XML textbasiert, lässt sich also mit einem beliebigen Texteditor erstellen oder nachbearbeiten.

Obwohl - oder gerade weil - das Format so einfach ist - und obwohl ein eigener MIME-Type text/csv und eine klare Spezifikation im RFC 4180 existieren, gibt es in der Praxis viele unterschiedliche Ausprägungen:

  • Der RFC 4180 erlaubt in einem durch Doppelhochkommata umschlossenen Feld nicht nur Kommata, sondern auch Zeilenumbrüche. In der Praxis werden die Zeilenumbrüche meist nur als Recordbegrenzer eingesetzt und können in den Daten selbst nicht verwendet werden.

  • Der RFC 4180 verlangt, dass jede Zeile gleich viele Felder enthält. Das wird in der Praxis oft nicht eingehalten. Konvention ist, dass ab dem letzten aufgeführten Feld alle nachfolgenden, im Record nicht mehr enthaltenen Felder als leer gelten.

  • Das Trennzeichen der Felder selbst muss laut RFC 4180 das Komma sein. Excel verwendet aber das Komma nur, wenn das Gebietsschema das Komma nicht als Dezimaltrennzeichen benutzt. Ersatzweise wird das Semikolon oder im Unicodefall sogar das Tabulatorzeichen als csv-Feldtrennzeichen verwendet.

  • Laut RFC 4180 muss ein im Feld selbst vorkommendes Doppelhochkomma durch die Sequenz "" maskiert werden. Ausserdem muss dann das ganze Feld in Doppelhochkommata eingeschlossen werden. Viele CSV-Reader vor allem im Unix-Bereich gehen aber davon aus, dass die Sequenz \" das Doppelhochkomma maskiert.



Die Regeln von csv lassen sich in Form von acht Tests in einem Perl-Programm wie folgt formulieren, wobei für jeden Test der erste Ausdruck die zu parsende Zeile angibt, während die darauf folgende Listreferenz die erwarteten Einzelfelder nach dem Parsing enthält:
# Testsuite für csv-Parser
use Test::More tests => 8;

assert_fields_on_parse(
q(1,2), ['1', '2' ],
'Werte werden durch Komma getrennt',
q(,), ['', '' ],
'Leere Spalten sind erlaubt',
q(,,1), ['', '', '1'],
'Mix leerer mit gefüllten Spalten',
q('a',2), [q('a'), '2' ],
'Einfaches Hochkomma hat keine Sonderbedeutung',
q("1","2"), ['1', '2' ],
'Einschliessende " gehören nicht zum Feldwert',
q("1,2",3), ['1,2', '3' ],
'Komma in einschliessenden " ist kein Feldtrenner',
q("""1",2), ['"1', '2' ],
'"" maskiert "',
q("1), [undef],
'" als Zeichen MUSS eingeschlossen werden',
);

sub assert_fields_on_parse {
my ($test,$exp,$msg,@act);
while (@_) {
($test,$exp,$msg) = splice(@_, 0, 3);
@act = parse_csv($test);
is_deeply( \@act, $exp, $msg);
}
}

Fügt man die folgende, etwas naive Implementierung eines csv-Parsers hinzu:
sub parse_csv {
return split( /,/, shift );
}

so werden nur drei der acht Tests erfüllt. Die Failures kommen daher, dass bei diesem einfachen Split die Sonderzeichen , und " in Feldwerten nicht korrekt interpretiert werden.

Auch die im Perl-Kochbuch von Christiansen und Torkingten [1] angegeben Routine (die nebenbei ein Sternchen für besonders gut lesbares Perl bekommen sollte) besteht die Testsuite leider nicht:
sub parse_csv {
my $text = shift; # Eine csv-Zeile
my @fields = ();
my $field;
while ( $text =~ /
([^"',]+) # Zeichenfolge ohne Komma und Hochkomma
|
"( # Anführendes " nicht merken
(?:
[^"] | # Beliebige Zeichen ausser "
"" # "" Fluchtsequenz für "
)*
)" # Abschliessendes " nicht merken
/gx ) {
if (defined $1) { $field = $1 }
else { ( $field = $2 ) =~ s/""/"/g }
push @fields, $field;
}
return @fields;
}

Diese Implementierung besteht nur vier der acht Tests: Sie kann leere Spalten nicht erkennen und verlangt seltsamerweise, dass auch einfache Hochkommata in " eingeschlossen sein müssen, obwohl einfache Hochkommata in CSV keine Sonderbedeutung haben. Dass leere Spalten nicht erkannt werden, liegt übrigens an der Entwurfsidee des verwendeten regulären Ausdrucks, die als Feldbegrenzer verwendeten Kommata beim Einlesen zu ignorieren.

Es ist natürlich immer am besten, eine Bibliotheksfunktion zu verwenden - vorausgesetzt, sie erfüllt die eigene Testsuite und das Modul ist im aktuellen Kontext verfügbar. Für CSV gibt es im CPAN das Modul Text::CSV von Alan Cittermann. Wenn man es einsetzt, reduziert sich die Routine parse_csv auf einen Dreizeiler:
sub parse_csv {
my $csv = Text::CSV->new();
$csv->parse( shift );
return $csv->fields();
}

Die Bibliotheksfunktion scheint brauchbar zu sein - sie erfüllt alle acht oben aufgeführten Tests. Die Instanz $csv kann übrigens nach Erzeugung für weitere Parsingvorgänge wiederverwendet werden. Um Testfälle möglichst isoliert zu durchlaufen, habe ich mir aber angewöhnt, für jeden Testfall neue Instanzen zu bilden. Daher ist $csv hier nur eine lokale Variable, die bei jedem Aufruf mit einem neuen Exemplar von Text::CSV belegt wird. Bei Bedarf kann man ja weitere Tests hinzufügen, die eine Instanz wiederverwenden und nur sicherstellen sollen, dass es keine Seiteneffekte gibt.

Doch nun zur ABAP-Welt: Zu einem Parser gehört nicht nur die blosse Erkennung der Datenstruktur, sondern auch die Validierung und Übernahme der Daten in Datenobjekte der Hostsprache. Ich habe bereits im Jahre 2004 einen Funktionsbaustein Z_READ_CSV verfasst, der dies leistet. Seitdem wurde er immer wieder einmal verbessert, so dass er mittlerweile in praktisch allen csv-Kontexten verwendbar ist. Der Baustein wandelt die Zeilen einer csv-Datei in Zeilen einer internen Tabelle, wobei jedes Feld typgerecht in die entsprechende Komponente der DDIC-Zeilenstruktur konvertiert wird. Menge und Reihenfolge der zu lesenden Komponenten kann über einen Importparameter it_fieldnames mit Feldnamen gesteuert werden. So kann die interne Tabelle auch einen breiten Zeilentyp haben, ohne dass alle Komponenten im csv angegeben werden müssen. Auch das Trennzeichen ist flexibel: Über die Schnittstelle kann angegeben werden, ob etwa ein Komma, ein Semikolon oder das Tabulatorzeichen als Feldtrennzeichen zu verwenden ist.

Ein Beispiel: Nehmen wir an, wir hätten eine Tabelle von Kundenauftragsköpfen
  types: ty_test_tab type standard table of vbak.

zu füllen. Nehmen wir weiter an, wir benötigen aus einer csv-Datei pro Beleg nur die Komponenten Verkaufsbelegsnummer (vbeln) und Auftragsdatum (audat). Die Datei hätte zum Beispiel folgendes Aussehen:



In der Subroutine eines Testreports können wir den Funktionsbaustein parametrisiert aufrufen und uns das Ergebnis ansehen:
  data:   lt_test   type ty_test_tab,
lt_return type bapirettab,
lt_fields type alfieldnames.

* Spaltenmenge einschränken
append: 'VBELN' to lt_fields,
'AUDAT' to lt_fields.

* Aufruf des Konvertierungsbausteins
call function 'Z_READ_CSV'
exporting
iv_filename = p_file
iv_collect_errors = p_coll
iv_from = p_from
iv_delimiter = p_sep
it_fieldname = lt_fields
importing
et_data = lt_test[]
et_return = lt_return
exceptions
io_error = 1
data_error = 2
others = 3.
if sy-subrc <> 0.
message id sy-msgid type 'I' number sy-msgno
with sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
endif.

* --> Tabelle lt_test[] ansehen:
break-point.


Wir starten das Testprogramm und belegen die Parameter wie folgt vor:



Am Breakpoint nach dem Aufruf der Z_READ_CSV ist die Tabelle lt_test dann wie folgt gefüllt. Beachten Sie, dass die Belegnummer wegen des Konvertierungsexits ALPHA für den Datentyp VBELN mit führenden Nullen aufgefüllt wurde. Das AUDAT hat den elementaren Datentyp "Datum" (D) und wurde daher ins ABAP-interne Format gewandelt.



Das Parsing bricht bei ungültigen Daten entweder ab, oder es sammelt eine Fehlermeldung in einer Fehlertabelle vom Typ BAPIRETURN. In diesem Modus wird das Parsing nicht abgebrochen, sondern nur die aktuelle Komponente auf ihren Initialwert gesetzt. Das Protokoll enthält den Hinweis auf die betroffene Zeile und Spalte (Feldname). Wenn ich zum Beispiel die csv-Datei so abändere, dass in der ersten Zeile das Datum und in der zweiten Zeile die Auftragsnummer ungültig werden, so resultiert folgendes Protokoll:



Im Debugger sieht man, dass die fehlerhaften Komponenten einfach übergangen wurden, das Parsing der anderen Komponenten jedoch erfolgte:



Für den Fall, dass die csv-Datei nicht über das Filesystem eingelesen wird, sondern irgendwie anders gegeben ist, kann der Baustein Z_READ_CSV auch mit einer internen Tabelle it_data aufgerufen werden, die die csv-Eingaben enthält. Auch einzelne Datenzeilen lassen sich mit dem Funktionsbaustein Z_READ_CSV_ROW auswerten.

Schliesslich gibt es auch noch einen Baustein für die umgekehrte Datenflussrichtung: Eine ABAP-Tabelle mit einfach strukturiertem Zeilentyp kann mit dem Baustein Z_WRITE_CSV in das csv-Datenformat konvertiert werden. Für einzelne Datenzeilen gibt es wieder den Z_WRITE_CSV_ROW. Die Bausteine erzeugen für die gegebenen ABAP-Daten entsprechende Darstellungen im externen Format, wobei das ABAP-Statement write verwendet wird, wenn die Zuweisung mit move keine als Eingabe verwendbare externe Darstellung erzeugt. Z_READ_CSV ist linksinvers zu Z_WRITE_CSV, reproduziert also die ursprünglichen ABAP-Daten. Das bedeutet, dieses Paar von Bausteinen kann auch für die Serialisierung mit einem lesbaren Zwischenformat verwendet werden (im Gegensatz zum Paar import to data buffer / export from data buffer, das zwar eine viel leistungsfähigere Serialisierung bietet, da es für beliebig tiefe Datenstrukturen verwendet werden kann und effizienter arbeitet, aber kein lesbares Zwischenformat generiert, sondern einen xstring).

Noch eine Bemerkung zur Implementierung. Die zentrale Routine map_csv_single liest die Zeichen einzeln bis zum nächsten Feldtrennzeichen oder bis zum Zeilenende ein. Dabei merkt sie sich einen "Quote Mode", der als Kippschalter bei jedem aufgefundenen Doppelhochkomma seinen Zustand wechselt. Das Feldtrennzeichen beendet nur dann das Scannen des Feldes, wenn wir gerade nicht im "Quote Mode" sind.

Das funktioniert glücklicherweise so gut, weil dank der Maskierung "" der Quote Mode auch bei eingeschlossenen Doppelhochkommata konstant bleibt. (Er entspricht immer der Parität der Anzahl der bereits vorgefundenen Doppelhochkommata.)
*---------------------------------------------------------------------
* Suche nach nächstem Semikolon oder Zeilenende
*---------------------------------------------------------------------

while lv_pos < lv_length.

* Kippschalter für Zustand innerhalb/ausserhalb " ändern
if iv_data+lv_pos(1) eq '"'.
translate lv_quote_mode using ' XX '.
endif.

* Semikolon gefunden?
if lv_quote_mode eq space and
iv_data+lv_pos(1) eq iv_delimiter.
exit.
endif.

* Nächstes Zeichen
add 1 to lv_pos.

endwhile.

Nachtrag am 5. Dezember 2016 - zwei Versionen in JavaScript

Kürzlich stellte ich unter Verwendung der neuen Syntax-Features von JavaScript einen CSV-Zeilenparser, der sich an der Idee meiner obigen ABAP-Lösung Z_READ_CSV orientiert - also die Zeile zeichenweise durcharbeitet und die nächste Zelle meldet, wenn er auf das Trennzeichen trifft oder das Zeilenende erreicht hat. Dabei muss die Zustandsinformation verwaltet werden, ob man sich gerade innerhalb eines durch Anführungszeichen markierten Strings befindet oder nicht (denn auch das Trennzeichen verliert ja innerhalb eines von Anführungszeichen eingeschlossenen Strings seine Sonderbedeutung):
function parseCSV(row,sep=',') {
  var result = []
  var quoteMode = false, s = "", c
  for (c of row) {
    quoteMode = quoteMode != (c == '"')
    if (!quoteMode && c == sep) {
      addToResult(s)
      s = ""
    } else s+=c
  }
  if (s || c == sep) addToResult(s)
  return result
  function addToResult(cell) {
    result.push( 
      cell.replace(/(?!^)""/g,'"')
          .replace(/^"([\s\S]*)"$/,"$1")
      )
  }
}
Einige Bemerkungen zur Implementierung:
  • Von hinten beginnend, kann man in ES6 neu ein oder mehrere Funktionsargumente mit einem Defaultwert belegen. Hier belege ich das Trennzeichen sep mit dem Komma als Defaultwert.
  • Die for ( ... of ... )-Schleife ist auch ein neues JavaScript-Konstrukt, sie durchläuft ein beliebiges iterierbares Objekt: das kann ein Array, ein Objekt, ein Set, eine Map, die aktuellen arguments einer Funktion, eine Sammlung von DOM-Knoten, oder eben – wie hier – ein String sein, der dann zeichenweise durchlaufen wird.
  • Wenn nach dem letzten Vorkommen des Trennzeichens noch Zeichen folgten, müssen diese auch als Inhalt der letzten Zelle ausgegeben werden. Diesen Inhalt, ebenso wie das durchlaufene Zeichen, muss also einen Sichtbarkeitsbereich ausserhalb der Schleife haben, denn nach deren Durchlaufen muss noch der letzte gesammelte String zurückgemeldet werden.
  • Der quoteMode ist hier mit einem ausschliessenden Oder mit dem Wahrheitswert von c == '"' verknüpft (realisiert durch den Operator !=). Die Wirkung eine ausschliessenden Oder kann man schaltungstechnisch so interpretieren: liegt der zweite Eingang auf false, so wird der Wert des ersten Eingangs unverändert durchgereicht. Liegt der zweite Eingang dagegen auf true, so wirkt es auf das Signal am ersten Eingang wie eine Invertierung (Negation). Im Ergebnis behält der quoteMode hier im allgemeinen seinen Wert bei und ändert sich nur dann auf sein Gegenteil, wenn das gerade untersuchte Zeichen c ein Anführungszeichen ist. All dies leistet die eine Zeile
        quoteMode = quoteMode != (c == '"')
  • Nur wenn wir gerade nicht im quoteMode sind, wirkt das Zeichen sep als Zellbegrenzung, und der bis dahin gesammelte String wird - nach einigen Bereinigungen - in den Ergebnisarray result aufgenommen.
  • Die erste Überarbeitung ist der replace(/(?!^)""/g,'"'), wodurch jedes doppelt vorkommenden Anführungszeichen durch ein einzelnes ersetzt wird. Der negative Lookahead (?!^) ist dabei nötig, um zu verhindern, dass eine Zeile, die nur aus der Zeichenfolge "" besteht, auf ein einziges Anführungszeichen verkürzt wird.
  • Die zweite Überarbeitung entfernt dann Anführungszeichen, die den kompletten Inhalt umschliessen. Die Zeichenklasse [\s\S] unterscheidet sich von dem einfachen Punkt . für ein beliebiges Zeichen dadurch, dass sie auch Zeilenumbrüche umfasst. Das ist hier nötig, weil ja Zeilenumbrüche innerhalb von den ganzen Zellinhalt umschliessenden Anführungszeichen nach dem oben Gesagten in CSV erlaubt sind.
Die Lösung kann man auf diesem jsfiddle ausprobieren. Die Seite funktioniert auf allen aktuellen standardkonformen Browsern (also nicht auf dem Internet Explorer 11).

Hier noch eine etwas konservativere Lösung, die wieder dieselbe Idee implementiert, aber mit Arrayfunktionen arbeitet. Sie hat auch Chancen, auf altmodischeren Browsern zu funktionieren:

function parseCSV(row,sep) {
  if (!sep) sep = ','
  var len = row.length, 
      quoteMode = false, 
      prev = 0
  return (row+" ").split('').reduce(function(result,c,i) {
    quoteMode = quoteMode != (c == '"')
    if (!quoteMode && c == sep || i == len) {
      result.push(
        row.substring(prev,i)
           .replace(/(?!^)""/g,'"')
           .replace(/^"([\s\S]*)"$/,"$1")
       )
       prev = i+1    
    }  
    return result
  },[])


[1] Tom Christiansen und Nathan Torkingten: Das Perl-Kochbuch, Zweite Auflage, o'Reilly, Köln 2006, Rezept 1.15. Die Implementierung unterscheidet sich von der ersten, auch im Internet verfügbaren Auflage (http://docstore.mik.ua/orelly/perl/cookbook), die noch die Unix-Maskierung \" für " annimmt.

16 Kommentare :

Alex hat gesagt…

Hi Rüdiger,

perfekte Programmierarbeit! Leider habe auch nichts vergleichbares von der SAP gefunden.
Ich benötige dringend die Funktionalität von Z_READ_CSV.
An einem Punkt komme ich nicht weiter: wie sieht denn die Referenztabelle zsrs_ref aus?
Danke, Viele Grüße Alex

Rüdiger Plantiko hat gesagt…

Hallo Alex,

da hast Du Dir den Code aber schon sehr genau angeschaut! ZSRS_REF ist eine Struktur, die einen Namen und einen Zeiger auf ein beliebiges Datenobjekt enthält. In unserem System ist sie im DDIC definiert. Du kannst sie aber ersatzweise aber auch im ABAP-Code als Typ definieren wie folgt:

types: begin of zsrs_ref,
name type parname,
ref type ref to data,
end of zsrs_ref.

Die Funktionsgruppe enthält auch noch Code zum Einlesen einer nativen Excel-Datei (also xls). Diesen Teil würde ich an Deiner Stelle ignorieren - mach lieber eine Funktionsgruppe, die sich nur mit CSV auskennt.

Alex hat gesagt…

Hallo Rüdiger,

funktioniert super! Danke.

Viele Grüße,
Alex

Unknown hat gesagt…

Hallo Rüdiger,

könntest Du bitte noch die Strukturen ZEXCEL und ZEXCELCELL darstellen? Danke!

Gruß Alexej

Rüdiger Plantiko hat gesagt…

Hallo Alexej,

Gegenfrage: Wozu brauchst Du die? Die Struktur wird für den in diesem Blog beschriebenen Konverter CSV->ABAP nicht benötigt (sondern für einen anderen Funktionsbaustein der Funktionsgruppe).


Aber bitte, hier das ABAP-Äquivalent dieser DDIC-Strukturen:

types: begin of zexcel,
cells type zexcelcelltab,
strings type zexcelstringtab,
float type zexcelfloattab,
int type zexcelinttab,
end of zexcel,
begin of zexcelcell,
row(2) type x,
col(2) type x,
typ(1) type c,
ref(4) type x,
end of zexcelcell,
zexcelcelltab type hashed table of zexcelcell
with unique key row col,
zexcelstringtab type standard table of string,
zexcelfloattab type standard table of f,
zexcelinttab type standard table of int4.

Gruss,
Rüdiger

Unknown hat gesagt…
Dieser Kommentar wurde vom Autor entfernt.
Unknown hat gesagt…

Hallo Rüdiger,

danke für die schnelle Antwort.

Dein CSV-Parser läuft prima und ich wollte einfach gleich mal die andere Funktion zum Einlesen einer nativen Excel-Datei ausprobieren.

Ansonsten habe ich Deinen Blog sofort in meine Favoritenliste aufgenommen. :)


P.S.
Ich habe einen kleinen Verbesserungsvorschlag. Damit die Funktion Z_READ_CSV biliebig große Felder lesen kann, habe ich im LZUTIL_EXCELTOP Definition von ty_text1024_tab wie folgt abgeändert:

ty_text1024_tab TYPE STANDARD TABLE OF string

(richtigerweise sollte ty_text1024 natürlich umbenannt werden)

Gruß Alexej

Rüdiger Plantiko hat gesagt…

Hallo Alexej,

den Baustein zum nativen Einlesen einer Excel-Datei habe ich im September 2002 geschrieben. Er funktioniert für einfache, nicht zu grosse Exceldateien. Mit einem Unittest, der eine binäre Datei ( http://bsp.mits.ch/code/prog/ZUTIL_EXCEL_TESTDATA ) einliest, habe ich dies für immer festgehalten.

Die grossen Macros, die ich darin verwende, entsprechen nicht mehr meinem heutigen Programmiergeschmack. Ich hatte sie damals aus Performancegründen verwendet, um zu verhindern, dass für jeden Aufruf eine neue Stackebene aufgebaut wird. Aber Macros sollten nach meinem Geschmack nicht mehr als höchstens zehn Zeilen umfassen. Und bloss vermutete Performanceprobleme merkt man sich besser nur vor, um sich nach Fertigstellung der Funktion mit einer SE30-Analyse Klarheit darüber verschafft, wo wirklich die meiste Laufzeit verbraucht wird.

Da dieser Baustein - im Gegensatz zum CSV-Leserich - jedoch nie produktiv eingesetzt wurde, kann ich nicht sagen, ob er für neuere Excel-Versionen, die seitdem eingeführt wurden, auch noch funktioniert.

ty_text1024_tab musste ich nehmen, weil die Methode cl_gui_frontend_services=>gui_upload damals keine Tabellen mit "tiefen" Zeilenstrukturen akzeptierte, wozu auch der Datentyp String gehörte. Wenn Du es ausprobiert hast und es anscheinend mittlerweile geht, werde ich auch auf stringtab umstellen.

stringtab ist ein eingebauter DDIC-Typ für "standard table of string", Teil von SAP_BASIS, also auf jedem SAP-System verwendbar. So sparst Du Dir die Definition eines eigenen Typs komplett.

Jedenfalls danke für den Hinweis,
Gruss,
Rüdiger

Rüdiger Plantiko hat gesagt…

Es funktioniert (mittlerweile)! Habe den Datentyp ty_text1024_tab vollständig aus der Funktionsgruppe entfernt und durch stringtab ersetzt.

Unknown hat gesagt…

Hallo Rüdiger,

ich habe jetzt die Funktion Z_EXCEL_TO_INTERNAL_TABLE zum Laufen gebracht.

Ich musste im Makro _get_string eine IF-Abfrage einbauen und schon klappte es:

if lv_m_skip > 0.
concatenate lv_m_string lv_m_text50(lv_m_skip) into lv_m_string.
endif.

Eine ziemlich umfangreiche Excel 2003 Datei wurde fast fehlerfrei eingelesen. Lediglich Zellen mit bedingter Formatierung werden etwas falsch ausgelesen. Einfache XLS-Dateien ohne komplizierte Formatierungen werden fehlerfrei bearbeitet.

Also nochmals vielen Dank für die Hilfe!

Gruß,
Alexej

Rüdiger Plantiko hat gesagt…

Hallo Alexej,

freut mich zu lesen, dass es für Excel 2003 im wesentlichen funktioniert!

Danke für die Info und die (harmlose) Korrektur, die wohl in bestimmten Situationen einen Kurzdump für den "unzulässigen Teilfeldzugriff" verhindert.

Habe sie gleich übernommen.

Gruss,
Rüdiger

Anonym hat gesagt…

Hi Rüdiger,
über diesen Blog bin ich auf den FuBa Z_READ_CSV gestoßen - den ich produktiv nutze: Kompliment. An Stelle des Fuba Z_GET_FIELDNAMES setze ich folgendes Upro ein (läuft ab Rel. 4.6 C):
FORM get_fieldnames
USING fs_line TYPE any
CHANGING ft_fname TYPE ttfieldname.
DATA:
lo_dscr TYPE REF TO cl_abap_structdescr.
FIELD-SYMBOLS:
TYPE abap_compdescr.
lo_dscr ?= cl_abap_typedescr=>describe_by_data( fs_line ).
IF sy-subrc <> 0.
* ... 2 do E-Message
ENDIF.
LOOP AT lo_dscr->components ASSIGNING .
APPEND -name TO ft_fname.
ENDLOOP.
ENDFORM.
...

Anonym hat gesagt…

... viel Spaß noch beim Bloggen.
Viele Grüße Holgi

Rüdiger Plantiko hat gesagt…

Hallo Holgi,

schön, dass Dir der Z_READ_CSV gefällt.

Den Baustein Z_GET_FIELDNAMES hättest Du im übrigen in unserer Funktionsgruppe ZUTIL gefunden: http://bsp.mits.ch/code/fugr/zutil

Ach so, Deine Routine läuft in 4.6C? Schön - der Baustein Z_GET_FIELDNAMES läuft allerdings auch auf 3.0F ;-)

Gruss,
Rüdiger

Unknown hat gesagt…

Hello Rüdiger,

I'm trying to install the function group ZUTIL_EXCEL in our system (http://bsp.mits.ch/code/fugr/ZUTIL_EXCEL). In the function module Z_EXCEL_IMPORT I notice a reference to the function Z_EXCEL_IMPORT_WITH_ABAP2XLSX that does not seem to be present the FG ZUTIL_EXCEL. Could you please give a hint where I can find the mentioned module?

Thanks a lot!
Dmitry

Rüdiger Plantiko hat gesagt…

Hi Dimitry,

for parsing or creating CSV files, as described in this blog, you only need Z_READ_CSV, Z_WRITE_CSV (and Z...CSV_ROW to do it on single row level). You won't need the function module Z_EXCEL_IMPORT. Actually, this function module was not present back in 2009 when I wrote the above blog post. You will meet further dependencies like ZCL_XLSX_PARSER if you insist on having the function module Z_EXCEL_IMPORT. If you are interested in parsing or creating .xlsx files, you may use the package abap2xlsx as originated by Ivan Femia and maintained by the ABAP community (actually, the function module Z_EXCEL_IMPORT in which you are interested is just a wrapper for abap2xlsx, so you may also call the classes of that package directly), or you may use other classes like CL_XLSX_DOCUMENT.

Hope this helps,
Rüdiger