SAP: so liest du eine CSV-Datei in ABAP ein

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.

heiko

Dipl.-Ing. Heiko Evermann

Vorheriger Artikel