CSV-Dateien sind immer noch ein wichtiges Dateiformat. Aber wie kannst du so eine Datei in ABAP einlesen?
Das Rückgrat zum Verarbeiten einer CSV-Datei in ABAP ist der Funktionsbaustein RSDS_CONVERT_CSV. Dieser zerlegt eine einzelne Zeile einer CSV-Datei in ihre einzelnen Elemente. Insbesondere behandelt er die Anführungsstriche korrekt (Escape-Handling).
Zunächst zeige ich dir den kompletten Code des Programms. Anschließend schauen wir uns an, warum wir den Funktionsbaustein brauchen und warum es nicht reicht, die Zeilen mit einem Split-Befehl in ihre Felder zu zerlegen.
Außerdem erkläre ich dir auch noch, wie der Rahmen des Programms, also der Zugriff auf das Dateisystem funktioniert.
Hier ist der Programmcode zum Einlesen einer CSV-Datei
Hier ist der komplette Programmcode
*&---------------------------------------------------------------------* *& Report ZREADCSV *&---------------------------------------------------------------------* *& *&---------------------------------------------------------------------* REPORT zreadcsv. PARAMETERS: p_file TYPE string. TYPES: tt_string TYPE TABLE OF string. AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_file. PERFORM at_selection_screen_val_file CHANGING p_file. START-OF-SELECTION. " check, whether file file exists: DATA: lf_ok TYPE abap_bool. PERFORM check_file_exists USING p_file CHANGING lf_ok. IF lf_ok <> abap_true. WRITE: / 'file does not exist:', p_file. RETURN. ENDIF. PERFORM read_file USING p_file. "-------------------------- FORM at_selection_screen_val_file CHANGING cf_filename TYPE string. DATA: lt_filetable TYPE filetable, ls_filetable TYPE file_table, lf_action TYPE i, lf_return TYPE i. CALL METHOD cl_gui_frontend_services=>file_open_dialog EXPORTING * window_title = default_extension = 'CSV' file_filter = '*.CSV' * with_encoding = CHANGING file_table = lt_filetable rc = lf_return user_action = lf_action * file_encoding = EXCEPTIONS file_open_dialog_failed = 1 cntl_error = 2 error_no_gui = 3 not_supported_by_gui = 4 OTHERS = 5. IF sy-subrc <> 0. " error handling MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4. RETURN. ENDIF. " OK => use filename IF lf_action = cl_gui_frontend_services=>action_ok AND lf_return = 1. * Determine file name READ TABLE lt_filetable INTO ls_filetable INDEX 1. IF sy-subrc IS INITIAL. * take over file name cf_filename = ls_filetable-filename. ENDIF. ENDIF. ENDFORM. " AT_SELECTION_SCREEN_VAL_FILEI FORM check_file_exists USING uf_filename TYPE string CHANGING cf_ok TYPE abap_bool. DATA: lf_result TYPE c. " Check if file exists CALL METHOD cl_gui_frontend_services=>file_exist EXPORTING file = uf_filename RECEIVING result = lf_result EXCEPTIONS cntl_error = 1 error_no_gui = 2 wrong_parameter = 3 not_supported_by_gui = 4 OTHERS = 5. IF sy-subrc NE 0 OR lf_result NE abap_true. cf_ok = abap_false. ELSE. cf_ok = abap_true. ENDIF. ENDFORM. "at_selection_screen_pa_filei FORM read_file USING if_filename TYPE string. DATA: lt_lines TYPE tt_string, lf_line TYPE string, lf_linenum TYPE sy-index, lf_colnum TYPE sy-index, lt_columns TYPE tt_string, lf_column TYPE string. CALL METHOD cl_gui_frontend_services=>gui_upload EXPORTING filename = if_filename CHANGING data_tab = lt_lines. lf_linenum = 0. LOOP AT lt_lines INTO lf_line. lf_linenum = sy-tabix. CALL FUNCTION 'RSDS_CONVERT_CSV' EXPORTING i_data_sep = ',' i_esc_char = '"' i_record = lf_line i_field_count = 9999 IMPORTING e_t_data = lt_columns EXCEPTIONS escape_no_close = 1 escape_improper = 2 conversion_error = 3 OTHERS = 4. LOOP AT lt_columns INTO lf_column. lf_colnum = sy-tabix. WRITE: / 'row:', lf_linenum, 'col:', lf_colnum, 'content:', lf_column. ENDLOOP. ENDLOOP. ENDFORM.
Hier ist eine Eingabedatei:
Vorname,Nachname,Strasse,PLZ,Ort
Donald,Duck,"Gänseweg 7, Hinterhaus",25337,Entenhausen
Dagobert,Duck,Am Geldspeicher 1,25336,Entenhausen
Damit erhalten wir diese Ausgabe des Programms:
read csv file
row: 1 col: 1 content: Vorname
row: 1 col: 2 content: Nachname
row: 1 col: 3 content: Strasse
row: 1 col: 4 content: PLZ
row: 1 col: 5 content: Ort
row: 2 col: 1 content: Donald
row: 2 col: 2 content: Duck
row: 2 col: 3 content: Gänseweg 7, Hinterhaus
row: 2 col: 4 content: 25337
row: 2 col: 5 content: Entenhausen
row: 3 col: 1 content: Dagobert
row: 3 col: 2 content: Duck
row: 3 col: 3 content: Am Geldspeicher 1
row: 3 col: 4 content: 25336
row: 3 col: 5 content: Entenhausen
Der Aufbau einer CSV-Datei: Feldtrenner und Escape-Sequenzen
Die Grundidee einer CSV-Datei ist mit ihrem Namen beschrieben: comma-separated values: Werte, die mithilfe eines Kommas voneinander getrennt werden.
Schauen wir uns ein Beispiel an:
Donald,Duck,Gänseweg 7,25337,Entenhausen Dagobert,Duck,Am Geldspeicher 1,25336,Entenhausen
Die Datei besteht aus einzelnen Zeilen. Jede Zeile enthält einen Datensatz. Die einzelnen Felder des Datensatzes sind mit einem Komma voneinander getrennt. Anders als bei einer XML-Datei gibt es keine Angaben darüber, was in welchem Feld steht. Manchmal findet sich in der CSV-Datei noch eine Kopfzeile wie
Vorname,Nachname,Strasse,PLZ,Ort
Nun kann es vorkommen, dass in einem einzelnen Feld auch selbst ein Komma vorkommt. In diesem Fall wird das Feld zusätzlich mit Gänsefüßchen eingerahmt. Wir spielen als Beispiel mal mit einer Adresse:
Vorname,Nachname,Strasse,PLZ,Ort Donald,Duck,"Gänseweg 7, Hinterhaus",25337,Entenhausen Dagobert,Duck,Am Geldspeicher 1,25336,Entenhausen
Und weil es so schön ist, das zu markieren, werden CSV-Dateien auch gerne so erzeugt, dass alle Felder diese Gänsefüßchen verwenden.
CSV in ABAP: der naive Ansatz scheitert
Mal angenommen, wir haben die Zeile schon in einem Feld lf_line und wollen diese in ein lt_fields zerlegen:
DATA: lf_line type string, lt_fields type standard table of string.
Man könnte nun auf die Idee kommen, die Zeile einfach mittels SPLIT zu zerlegen und anschließend die Gänsefüßchen rauszulöschen.
SPLIT lf_line AT ',' INTO TABLE lt_fields. REPLACE ALL OCCURRENCES OF '"' IN TABLE lt_fields WITH space.
Das funktioniert aber nur, solange die Datenfelder selbst kein Komma enthalten und auch kein Gänsefüßchen. Falls doch, verschieben sich alle folgenden Datenfelder. Mit Glück bricht dein Programm ab, weil die weitere Verarbeitung aufschlägt. Vielleicht schreibst du einen Feldinhalt in ein numerisches Feld, aber nun steht dort leider der Ortsname. Dann bricht ABAP dein Programm hart ab.
Wenn du Pech hast, merkst du das aber nicht und schreibst stattdessen die Straße in die PLZ und die Postleitzahl in den Ort. Dann kann es sehr, sehr lange dauern, bis der Fehler einem Sachbearbeiter auffällt.
Daher ist es wichtig, die Escape-Sequenzen korrekt zu behandeln.
Die korrekte Verarbeitung der Escape-Sequenzen ist kompliziert, aber zum Glück brauchst du sie nicht neu zu schreiben, denn sie ist in SAP schon fertig. Der Funktionsbaustein heißt RSDS_CONVERT_CSV und er erledigt die Arbeit für dich. Schau gerne mal in den Quellcode von RSDS_CONVERT_CSV hinein. Daran würdest du selbst eine ganze Weile schreiben und debuggen.
CALL FUNCTION 'RSDS_CONVERT_CSV' EXPORTING i_data_sep = ',' i_esc_char = '"' i_record = lf_line i_field_count = 9999 IMPORTING e_t_data = lt_fields EXCEPTIONS escape_no_close = 1 escape_improper = 2 conversion_error = 3 OTHERS = 4. IF sy-subrc <> 0. MESSAGE 'Fehler beim Importieren der Datei' TYPE 'E'. ENDIF.
Bitte beachte: du kannst hier das Trennzeichen selbst festlegen. Meistens ist es das Komma. Aber manchmal finden sich auch andere, z.B. das Semikolon.
Als Escapezeichen habe ich bisher nur das Gänsefüßchen gesehen, aber im Prinzip könnte es auch jedes beliebige Zeichen sein.
Den Kern des Programms haben wir mit diesem Funktionsbaustein also schon beisammen. Jetzt brauchen wir nur noch den Rahmen darum: die Dateiauswahl, das Öffnen der Datei und den Loop über die Zeilen.
Die Auswahl einer Datei.
Die Eingabedatei ist Parameter des Reports. Um hier einen Dateiauswahldialog anbieten zu können, melden wir eine Eingabehilfe an:
AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_file. PERFORM at_selection_screen_val_file CHANGING p_file.
Kernstück ist der Aufruf des Popups.
CALL METHOD cl_gui_frontend_services=>file_open_dialog EXPORTING * window_title = default_extension = 'CSV' file_filter = '*.CSV' * with_encoding = CHANGING file_table = lt_filetable rc = lf_return user_action = lf_action * file_encoding = EXCEPTIONS file_open_dialog_failed = 1 cntl_error = 2 error_no_gui = 3 not_supported_by_gui = 4 OTHERS = 5.
Anschließend wird geprüft, ob der Dialog mit OK, oder mit Abbrechen verlassen wurde.
Prüfen, ob die Datei überhaupt existiert
Wichtig ist auch vor dem Öffnen der Datei kurz nachzuschauen, ob es die Datei überhaupt gibt. Falls nicht, kann der Anwender unmittelbar informiert werden.
FORM check_file_exists USING uf_filename TYPE string CHANGING cf_ok TYPE abap_bool. DATA: lf_result TYPE c. " Check if file exists CALL METHOD cl_gui_frontend_services=>file_exist EXPORTING file = uf_filename RECEIVING result = lf_result EXCEPTIONS cntl_error = 1 error_no_gui = 2 wrong_parameter = 3 not_supported_by_gui = 4 OTHERS = 5. IF sy-subrc NE 0 OR lf_result NE abap_true. cf_ok = abap_false. ELSE. cf_ok = abap_true. ENDIF. ENDFORM. "at_selection_screen_pa_filei
Lesen der Datei
Um die Datei zu lesen, gibt es wieder eine SAP-Standardfunktion:
FORM read_file USING if_filename TYPE string. DATA: lt_lines TYPE tt_string, lf_line TYPE string, lf_linenum TYPE sy-index, lf_colnum TYPE sy-index, lt_columns TYPE tt_string, lf_column TYPE string. CALL METHOD cl_gui_frontend_services=>gui_upload EXPORTING filename = if_filename CHANGING data_tab = lt_lines. lf_linenum = 0. LOOP AT lt_lines INTO lf_line. lf_linenum = sy-tabix. CALL FUNCTION 'RSDS_CONVERT_CSV' EXPORTING i_data_sep = ',' i_esc_char = '"' i_record = lf_line i_field_count = 9999 IMPORTING e_t_data = lt_columns EXCEPTIONS escape_no_close = 1 escape_improper = 2 conversion_error = 3 OTHERS = 4. LOOP AT lt_columns INTO lf_column. lf_colnum = sy-tabix. WRITE: / 'row:', lf_linenum, 'col:', lf_colnum, 'content:', lf_column. ENDLOOP. ENDLOOP.
Hier wird einfach die Datei in eine lokale String-Tabelle im Speicher (und nicht in der Datenbank) gelesen.
Anschließend kommt ein Loop und jede der Zeilen wird auseinandergenommen.
An dieser Stelle müsste in deinem Programm dann noch eine Logik implementiert werden, was du mit den Daten machen willst. Vielleicht gibt es einen Datentyp bestehend aus den einzelnen Feldern, du weist die Werte zu und sammelst sie in eine Tabelle lt_address type ts_address zu. Oder irgend was in dieser Art.
Zusammenfassung
Für das Zerlegen eines CSV-Strings gibt es einen Standardbaustein. Dieser heißt RSDS_CONVERT_CSV.
Auch Für das Drumherum gibt es Standardfunktionen, also zur Auswahl einer Datei und zum Prüfen auf Existenz und zum Lesen der Datei.
Mehr ABAP-Tipps findest du hier.