Inhaltsverzeichnis

6.13 Klasse CsvFile

Die Klasse CsvFile (gb.util) kann eingesetzt werden, um eine CSV-Datei zu lesen und den Inhalt zu dekodieren. Achtung: Es wird davon ausgegangen, dass in der ersten Zeile der CSV-Datei notwendigerweise die Feldnamen stehen. Sind nicht alle Feld-Namen angegeben, dann wird der fehlende Feldname durch ein #-Zeichen ersetzt, dem der Index des Feldes in der imaginären Feldliste folgt. Generell gilt: Die Klasse CsvFile kann nicht mit CSV-Dateien umgehen, die keine (erste) Zeile mit den Feld-Namen (Header) besitzen, obgleich der Header mit den Feldnamen nach RFC 4180 optional ist!

Sie können ein Objekt der Klasse erzeugen. Es wird eine CSV-Datei zum Lesen geöffnet:

Dim hCsvFile As CsvFile 
hCsvFile = New CsvFile ( Path As String [ , Separator As String, Escape As String ] )

6.13.1 Eigenschaften

Die Klasse CsvFile verfügt über drei Eigenschaften:

EigenschaftDatentypBeschreibung
EofBooleanEs wird True zurückgegeben, wenn das Ende der CSV-Datei beim Lesen erreicht wurde.
FieldsString[ ]Zurückgeben wird eine Liste der Feld-Namen als String-Array.
LineIntegerGibt die Nummer der zuletzt gelesenen Zeile zurück.

Tabelle 6.13.1.1 : Eigenschaften der Klasse CsvFile

Alle Feldnamen in der Liste der Feld-Namen werden nach dem folgenden Algorithmus normiert:

6.13.2 Methoden

Die Klasse CsvFile verfügt nur über diese eine Methode:

Function Read() As Collection

Die Funktion liest eine CSV-Datei und gibt sie als Collection von Feld-Werten zurück, die durch ihre Feld-Namen indiziert sind: Eigenschaft CsvFile.Fields. Die Feld-Werte werden nach dem folgenden Algorithmus normiert:

6.13.3 Hinweise

Die folgenden Hinweise sollten Sie kennen und beachten:

Die korrekte Konvertierung der Zeilen in einer CSV-Datei zum Beispiel nach Variant[] statt nach Collection würde so aussehen, wobei Sie alle Feld-Namen über die Eigenschaft CsvFile.Fields erhalten:

Private Function GetData() As Variant[] 
  Dim aFieldList As Variant[] 
  Dim cLine As Collection 
  Dim sField As String, aFields As String[] 
 
  aFieldList = New Variant[] 
 
  While Not hCSVFile.Eof 
    cLine = hCSVFile.Read() 
    aFields = New String[] 
    For Each sField In hCSVFile.Fields 
      aFields.Add(cLine[sField]) 
    Next 
    aFieldList.Add(aFields) 
  Wend 
 
  Return aFieldList 
 
End

Wenn Sie zum Beispiel wissen wollen, ob das Feld mit dem Feld-Namen „ort“ in der aktuellen Zeile gesetzt ist, dann müssen Sie die Exist()-Methode für die Collection einsetzen:

Dim cLine As Collection 
... 
cLine = hCSVFile.Read() 
If Not cLine.Exist("ort") Then ...

Hier sind weitere Hinweise für die Erzeugung und Bearbeitung von CSV-Dateien - gefunden auf der Website https://www.thoughtspot.com/blog/6-rules-creating-valid-csv-files - von Jon Avrach, deren Beachtung vor allem beim Einsatz der CSV-Dateien beim Daten-Import in Datenbanken Probleme minimieren:

Für weitere Details zu diesen Hinweisen können Sie sich auf Wikipedia mit passenden Suchkriterien und in den RFC 4180 (CSV-Spezifikation) gut informieren.

Bei den Projekten zum Thema CSV wurden 2 Fälle unterschieden. Im ersten Fall geht es um das Schreiben von CSV-Dateien mit Daten aus unterschiedlichen Quellen. Die Quellen waren Daten in Steuerelementen wie zum Beispiel GridView oder TextArea oder XML-Dateien oder Datenbank-Tabellen. Als Quellen für den zweiten Fall dienten CSV-Dateien, deren Inhalt ausgelesen wird und in unterschiedlicher Weise verarbeitet, abgespeichert oder angezeigt wird. Nur für diesen Fall kann die Klasse CsvFile aus der Komponente gb.util eingesetzt werden.

Im Kapitel → https://www.gambas-buch.de/doku.php?id=k17:k17.7:k17.7.6:start finden Sie ein Projekt zum Export von ausgewählten Daten in eine CSV-Datei und im Kapitel unter https://www.gambas-buch.de/doku.php?id=k17:k17.7:k17.7.7:start ein Projekt zum Daten-Import aus einer CSV-Datei. Interessant ist in beiden Fällen die Möglichkeit sowohl das Separator-Zeichen als auch das Escape-Zeichen anzugeben. Sie können auch festlegen, ob die Feldnamen als Header mit gespeichert werden sollen oder in einer Vorschau feststellen, ob eine (erste) Zeile mit den Feld-Namen als Header existiert.

B1

Abbildung 6.13.3.1: Daten-Export

B2

Abbildung 6.13.3.2: Daten-Import

Die Archive für diese beiden überarbeiteten Projekte finden Sie im Downloadbereich.

6.13.4 Projekt 1 – CSV2XML

Das erste Projekt liest zuerst im Dialog eine CSV-Datei ein. Dann wird der Inhalt der CSV-Datei so präpariert, dass

Der so präparierte Inhalt wird unter dem originalen Datei-Namen abgespeichert. Anschließend wird der Inhalt der präparierten CSV-Datei unter Angabe des Feld-Trennzeichens (Separator) und des Escape-Zeichens mit einem neuen CsvFile-Objekt dekodiert und in ein XML-Dokument (Kapitel 27.2.1) geschrieben. Abschließend wird das XML-Dokument abgespeichert. Eine Prüfung, ob die CSV-Datei in der ersten Zeile eine Liste der Feld-Namen enthält, erfolgt nicht. Ein Header wird einfach erwartet, weil sonst der Einsatz der Klasse CsvFile nicht möglich ist!

Abbildung 6.13.4.1: Inhalt der präparierten CSV-Datei

Abbildung 6.13.4.2: Ausschnitt aus dem Inhalt der XML-Datei

Der Quelltext wird vollständig angegeben und kommentiert:

[1] ' Gambas class file
[2] 
[3] Private $sSeparator As String
[4] Private $sEscape As String
[5] Public hXmlWriter As XmlWriter
[6] Public sFilePath As String
[7] Public hCSVFile As CsvFile
[8] 
[9] Public Sub Form_Open()
[10]   FMain.Center
[11]   FMain.Resizable = True
[12]   FMain.Caption = "CSV2XML | Gambas-Version: " & System.FullVersion
[13]   
[14]   $sSeparator = "," 
[15]   $sEscape = "\"" ' Alternative: Chr(34) => "
[16]   
[17]   btnConvertAndSave.Enabled = False
[18]   
[19] End ' Form_Open()
[20] 
[21] Public Sub btnPrepareAndSave_Click()
[22]   sFilePath = FileOpen()  
[23]   If sFilePath Then
[24]      txaPrepare.Text = Prepare(sFilePath)
[25]      txaPrepare.Pos = 0
[26]      File.Save(sFilePath, Prepare(sFilePath))
[27]      Wait
[28]      btnPrepareAndSave.Enabled = False
[29]      btnConvertAndSave.Enabled = True
[30]   Else
[31]     Return
[32]   Endif  
[33] End
[34] 
[35] Public Sub btnConvertAndSave_Click()
[36]   WriteXML()
[37]   btnPrepareAndSave.Enabled = True
[38]   btnConvertAndSave.Enabled = False
[39] End
[40] 
[41] Public Sub btnClose_Click()
[42]   FMain.Close()
[43] End ' Close
[44] 
[45] Private Function Prepare(sFilePath As String) As String
[46]   
[47]   Dim sCSVContent, sCSVChanged, sLine, sField, sNewLine As String
[48]   
[49]   sCSVContent = File.Load(sFilePath) 
[50]   
[51]   For Each sLine In Split(sCSVContent, gb.NewLine, $sEscape, False, True) 
[52]     sLine = Trim(sLine)
[53]     If Not sLine Then Continue ' Leerzeilen werden ignoriert
[54]     sNewLine = ""
[55]     For Each sField In Split(sLine, $sSeparator, $sEscape, False, True) 
[56]       If sField Not Begins $sEscape And If sField Not Ends $sEscape Then
[57]          sField = $sEscape & sField & $sEscape
[58]       Endif
[59]       If sField = $sEscape & $sEscape Or If sField = $sEscape & " " & $sEscape Then
[60]        ' sField = $sEscape & "" & $sEscape ' Fall 1
[61]          sField = "NULL"                   ' Fall 2
[62]       Endif
[63]       sNewLine &= sField & $sSeparator
[64]     Next    
[65]      If sNewLine Ends $sSeparator Then sNewLine = Left(sNewLine, -1)
[66]      sCSVChanged &= sNewLine & gb.NewLine
[67]   Next
[68]   
[69]   sCSVChanged = Left(sCSVChanged, -1) ' Zeilenende-Zeichen der *letzten* Zeile wird entfernt
[70]   
[71]   Return sCSVChanged
[72]   
[73] End ' Prepare(...)
[74] 
[75] Private Sub WriteXML()
[76]   
[77]   Dim k As Integer
[78]   Dim cLine As Collection 
[79]   Dim sField As String 
[80] 
[81]   hCSVFile = New CsvFile(sFilePath, $sSeparator, $sEscape) 
[82] 
[83]   hXmlWriter = New XmlWriter 
[84]   hXmlWriter.Open(Null, True, "UTF-8") 
[85]   
[86]   hXmlWriter.PI("Document", "version=\"" & Format(Now, "dd.mm.yyyy-hh:nn") & "\"")
[87]   
[88]   hXmlWriter.StartElement("root")
[89]   hXmlWriter.Comment("DataBase: " & File.Name(sFilePath))
[90]   
[91]   While Not hCSVFile.Eof 
[92]     hXmlWriter.StartElement("item")  
[93]     cLine = hCSVFile.Read() 
[94]     k = 0
[95]     For Each sField In hCSVFile.Fields 
[96]       If sField = "null" Then 
[97]          hXmlWriter.Element("field_" & CStr(k + 1), cLine[sField]) 
[98]       Else 
[99]          hXmlWriter.Element(sField, cLine[sField]) 
[100]     Endif 
[101]     Inc k 
[102]    Next 
[103]    hXmlWriter.EndElement
[104]  Wend
[105]     
[106]   hXmlWriter.EndElement ' root
[107]   hXmlWriter.EndDocument  
[108]   txaPrepare.Text = hXmlWriter.Data
[109]   txaPrepare.Pos = 0  
[110]   File.Save(File.Dir(sFilePath) &/ File.BaseName(sFilePath) & ".xml", hXmlWriter.Data) 
[111]   
[112] End
[113] 
[114] Private Function FileOpen() As String  
[115]   Dialog.Title = "Importieren Sie eine csv-Datei!"
[116]   Dialog.Filter = ["*.csv", "csv-Dateien"]
[117]   Dialog.Path = Application.Path &/ "CSV"  
[118]   If Dialog.OpenFile(False) = True Then ' Multiselect=False (Standard)
[119]      Message.Info("Das Öffnen der csv-Datei wurde abgebrochen!")
[120]      Return 
[121]   Else
[122]      Return Dialog.Path
[123]   Endif
[124] End ' FileOpen()

Kommentar:

6.13.5 Exkurs – CSV2XML

In diesem Exkurs wird Ihnen ein (Bash-)Skript vorgestellt, mit dem Sie ebenso den Inhalt einer CSV-Datei auf die Schnelle in den Inhalt einer XML-Datei transformieren. Das ausführbare Skript erwartet genau 3 Parameter:

den Datei-Namen der CSV-Datei, das Feld-Trennzeichen und das das Escape-Zeichen:

hans@mint71 ~/GB3BUCH/.../Projekte/CSV_READ/SH-Skript $ ./test.sh "db.csv" "," "\""

Ist die Anzahl der Parameter nicht 3, dann gibt es eine Fehlermeldung mit einem Hinweis auf die korrekte Syntax:

Syntax: ./test.sh 'CSV-File-Path' 'Field separator' 'Escape character'
# hans@mint71 ~/GB3BUCH/.../Projekte/CSV_READ/SH-Skript $ ./test.sh "db.csv" ","

Wenn das der Inhalt der CSV-Datei ist:

"Vorname","Alter","Wohnort"
"Hans","68","Pößneck"
"Peter","56","Cork"
"Anna","18","Jena"

dann sehen Sie hier das Ergebnis in der XML-Datei:

<?xml version="1.0" encoding="UTF-8"?> 
<Data> 
  <Record> 
    <Vorname>Hans</Vorname> 
    <Alter>68</Alter> 
    <Wohnort>Pößneck</Wohnort> 
  </Record> 
  <Record> 
    <Vorname>Peter</Vorname> 
    <Alter>56</Alter> 
    <Wohnort>Cork</Wohnort> 
  </Record> 
  <Record> 
    <Vorname>Anna</Vorname> 
    <Alter>18</Alter> 
    <Wohnort>Jena</Wohnort> 
  </Record> 
</Data>

Der erprobte Quelltext vom (Bash-)Skript ist gut dokumentiert und wird vollständig angegeben:

#!/bin/bash # She-Bang Zeile → Bash-Interpreter
 
  if [ $# -ne 3 ] # Wenn die Anzahl der Parameter (= $#) ungleich 3 ist ...
  then 
    # FARB-WERTE: 
    RO="\033[31m"    # rot 
    NO="\033[0m"     # normal   
    echo -e "${RO}Syntax: $0 'CSV-File-Path' 'Field separator' 'Escape character'" 
    exit 1 
  fi 
  # Sicherung der Variablen IFS in der Variablen backIFS | IFS = Internal Field Separator 
  backIFS=$IFS 
 
  file_in=$1 # Parameter 1
  echo $file_in 
  # Dateiname für file_out (XML-Datei) erzeugen
  filebasename=${file_in%.*} 
  fileextension=".xml" 
  file_out=$filebasename$fileextension 
  echo $file_out 
 
  separator=$2 # Parameter 2
  escape=$3    # Parameter 3
 
  # 1. Zeile (= Liste der Feld-Namen) aus CSV-Datei lesen 
  read header_line < "$file_in" 
  IFS=$separator 
  # Liste der Feld-Namen im Array 'header' speichern -> Operator =() 
  header=($header_line) 
 
  IFS='*' 
 
  # Inhalt der CSV-Datei zeilenweise einlesen und im Array 'content' speichern 
  i=0 
  while read content[$i] 
  do 
    # echo "Zeile $i: ${content[i]}" 
    ((i++)) 
  done < $1 
 
  # 1. Zeile im Array 'content' löschen  
  unset content[0] 
  # Array kopieren - aber ohne das jetzt leere erste Element 
  content=(${content[*]}) 
 
  # Inhalt der XML-Datei schreiben 
  #------------------------------------------------------------------------------- 
  # XML-Prolog 
  echo '<?xml version="1.0" encoding="UTF-8"?>' > $file_out 
  # XML-Root-Element 
  echo '<Data>' >> $file_out 
  # XML-Elemente
  #------------------------------------------------------------------------------- 
  for record in "${content[@]}" 
  do 
  echo '  <Record>' >> $file_out 
    index=0 
    #............................................................................. 
 
    IFS=$separator list=($record) 
 
    for (( c=0; c<=${#header[@]}-1; c++ )) 
    do 
      tag=${header[$c]#$escape} 
      tag=${tag%$escape} 
      value=${list[$c]#$escape} 
      value=${value%$escape} 
      echo '    <'${tag}'>'${value}'</'${tag}'>' >> $file_out 
    done 
 
    #............................................................................. 
    echo '  </Record>' >> $file_out 
    ((index++)) 
  done 
  #------------------------------------------------------------------------------- 
  echo '</Data>' >> $file_out 
 
  IFS=$backIFS 
 
  exit 0 

6.13.6 Projekt 2 – CSV2GRID

Im zweiten Projekt wird eine CSV-Datei gelesen. Die mit der CsvFile-Read-Methode dekodierten Daten werden in einer GridView angezeigt.

Quelltext:

' Gambas class file
 
Public Sub btnReadCSV_Click()
 
  Dim i, iVal As Integer
  Dim sField As New String[]
  Dim sKey, sSeparator, sEscape As String
  Dim hCsvFile As CsvFile
  Dim cLine As Collection
  sSeparator = "," 
  sEscape = "\"" ' Chr$(34) 
' Open CSV file
  Try hCsvFile = New CsvFile(FileOpen(), sSeparator, sEscape)
  If Error Then Return
  GridView1.Columns.Count = 0
' Prepare header
  GridView1.Header = True
  sField = hCsvFile.Fields
  GridView1.Columns.Count = sField.Count
  For i = 0 To sField.Count - 1
    GridView1.Columns[i].Title = UCase(sField[i])
  Next
' Read CSV data 
  While Not hCsvFile.Eof 
    cLine = hCsvFile.Read()
    If cLine.Count > 0 Then ' Collection not empty?
       Inc GridView1.Rows.Count
       iVal = 0
       For Each sKey In hCsvFile.Fields
         Gridview1[GridView1.Rows.Count - 1, iVal].text = cLine[sKey]
         Inc iVal
       Next
    Endif
  Wend
 GridView1.MoveTo(0, 0)
 GridView1.Columns.Width = -1 ' Adjust column width automatically
End
 
Private Function FileOpen() As String
 
  Dialog.Title = ("Import csv-File!")
  Dialog.Filter = ["*.csv", "csv-Files"]
  Dialog.Path = Application.Path &/ "Files"
  If Dialog.OpenFile(False) = True Then ' Multiselect=False (Standard)
     Message.Info(("Opening the csv file has been canceled!"))
     Return "break"
  Else
     Return Dialog.Path
  Endif
 
End ' FileOpen()

B5

Abbildung 6.13.6.1: Daten aus einer CSV-Datei – angezeigt in einer GridView

6.13.7 Projekt 3 – QSLITE2CSV

Das Projekt 3 widmet sich dem Fall, dass Daten aus einer Datenbank-Tabelle in einer CSV-Datei gespeichert werden. Achtung: Bei den Datenbank-Tabellen bieten MyQSL, PostgreSQL oder SQlite Dump- oder Copy-Methoden an, mit denen Daten direkt in eine CSV-Datei abgespeichert werden können. Deshalb wird nur die Methode vorgestellt, in der die selektierten Daten aus einer DB-Tabelle in einen String konvertiert werden, der den Inhalt einer CSV-Datei repräsentiert:

Private Function GetCSVData() As String
 
  Dim sCSVLine, sRequest, sSeparator, sEscape As String
  Dim hRequest As SQLRequest
  Dim dbResult As Result
  Dim rField As ResultField
 
  hRequest = hConnection.SQL.Select("").From(cmbDBTabellen.Text)
  sRequest = hRequest.Get()
  dbResult = hConnection.Exec(sRequest)
 
  If dbResult.Count = 0 Then 
     Message(("The number of selected records is zero!"))
     Return 
  Endif
 
  FMain.Caption = "Daten-Export aus Datenbank: '"
  FMain.Caption &= File.Name(sDBFilePath) & "' " 
  FMain.Caption &= " Tabelle: '" & cmbDBTabellen.Text
  FMain.Caption &= "' in CSV-Datei"
 
  sSeparator = ","  ' Feld-Trenn-Zeichen
  sEscape = "\""    ' Text-Trennzeichen
 
' Field list
  For Each rField In dbResult.Fields
    sCSVLine &= sEscape & Upper(rField.Name) & sEscape & sSeparator
  Next
  sCSVLine = Left(sCSVLine, -1) ' Last field name WITHOUT field separator
  sCSVLine &= gb.NewLine
 
' Data lines
  For Each dbResult
    For Each rField In dbResult.Fields
      If dbResult[rField.Name] = "" Then
         sCSVLine &= sEscape & "NULL" & sEscape & sSeparator  
      Else
         sCSVLine &= sEscape & dbResult[rField.Name] & sEscape & sSeparator  
      Endif
    Next    
    sCSVLine = Left(sCSVLine, -1) ' Last field name WITHOUT field separator
    sCSVLine &= gb.NewLine
  Next
 
  sCSVLine = Left(sCSVLine, -1) ' Last line WITHOUT gb.NewLine
  Return sCSVLine
 
End

B6

Abbildung 6.14.7.1: Daten-Export DB-Daten nach CSV-Datei In der TextArea sehen Sie einen Ausschnitt aus dem Inhalt der CSV-Datei.

Hinweis:
Im Kapitel https://www.gambas-buch.de/doku.php?id=k27:k27.6:start#projekt_2xml_csv wird ein Projekt vorgestellt, bei dem die Transformation XML → CSV umgesetzt wird.

Download