CSV-Beispiel#

[1]:
import pandas as pd

Nach dem Import von pandas lesen wir zunächst mit read_csv eine CSV-Datei ein:

[2]:
pd.read_csv("https://raw.githubusercontent.com/veit/python-basics-tutorial-de/main/docs/save-data/books.csv")
[2]:
Python basics en Veit Schiele BSD-3-Clause 2021-10-28
0 Jupyter Tutorial en Veit Schiele BSD-3-Clause 2019-06-27
1 Jupyter Tutorial de Veit Schiele BSD-3-Clause 2020-10-26
2 PyViz Tutorial en Veit Schiele BSD-3-Clause 2020-04-13

Wie ihr seht, hat diese Datei keine Kopfzeile. Um dem DataFrame eine Kopfzeile zu geben, habt ihr mehrere Möglichkeiten. Ihr könnt pandas erlauben, Standard-Spaltennamen zuzuweisen, oder ihr könnt die Namen auch selbst festlegen:

[3]:
pd.read_csv(
    "https://raw.githubusercontent.com/veit/python-basics-tutorial-de/main/docs/save-data/books.csv",
    header=None,
)
[3]:
0 1 2 3 4
0 Python basics en Veit Schiele BSD-3-Clause 2021-10-28
1 Jupyter Tutorial en Veit Schiele BSD-3-Clause 2019-06-27
2 Jupyter Tutorial de Veit Schiele BSD-3-Clause 2020-10-26
3 PyViz Tutorial en Veit Schiele BSD-3-Clause 2020-04-13
[4]:
pd.read_csv(
    "https://raw.githubusercontent.com/veit/python-basics-tutorial-de/main/docs/save-data/books.csv",
    names=[
        "Titel",
        "Sprache",
        "Autor*innen",
        "Lizenz",
        "Veröffentlichungsdatum",
    ],
)
[4]:
Titel Sprache Autor*innen Lizenz Veröffentlichungsdatum
0 Python basics en Veit Schiele BSD-3-Clause 2021-10-28
1 Jupyter Tutorial en Veit Schiele BSD-3-Clause 2019-06-27
2 Jupyter Tutorial de Veit Schiele BSD-3-Clause 2020-10-26
3 PyViz Tutorial en Veit Schiele BSD-3-Clause 2020-04-13

Angenommen, ihr möchtet, dass die Spalte Autor*innen der Index des zurückgegebenen DataFrame ist. Ihr könnt entweder angeben, dass ihr die Spalte bei Index 3 oder mit dem Namen Autor*innen haben möchtet, indem ihr das Argument index_col verwendet:

[5]:
pd.read_csv(
    "https://raw.githubusercontent.com/veit/python-basics-tutorial-de/main/docs/save-data/books.csv",
    index_col=["Autor*innen"],
    names=[
        "Titel",
        "Sprache",
        "Autor*innen",
        "Lizenz",
        "Veröffentlichungsdatum",
    ],
)
[5]:
Titel Sprache Lizenz Veröffentlichungsdatum
Autor*innen
Veit Schiele Python basics en BSD-3-Clause 2021-10-28
Veit Schiele Jupyter Tutorial en BSD-3-Clause 2019-06-27
Veit Schiele Jupyter Tutorial de BSD-3-Clause 2020-10-26
Veit Schiele PyViz Tutorial en BSD-3-Clause 2020-04-13

Für den Fall, dass ihr einen hierarchischen Index aus mehreren Spalten bilden wollt, übergebt eine Liste von Spaltennummern oder -namen:

[6]:
pd.read_csv(
    "https://raw.githubusercontent.com/veit/python-basics-tutorial-de/main/docs/save-data/books.csv",
    index_col=[2, 0],
    names=[
        "Titel",
        "Sprache",
        "Autor*innen",
        "Lizenz",
        "Veröffentlichungsdatum",
    ],
)
[6]:
Sprache Lizenz Veröffentlichungsdatum
Autor*innen Titel
Veit Schiele Python basics en BSD-3-Clause 2021-10-28
Jupyter Tutorial en BSD-3-Clause 2019-06-27
Jupyter Tutorial de BSD-3-Clause 2020-10-26
PyViz Tutorial en BSD-3-Clause 2020-04-13

In manchen Fällen hat eine Tabelle kein festes Trennzeichen, sondern verwendet mehrere Leerzeichen oder ein anderes Muster zur Trennung von Feldern. Angenommen, eine Datei sieht folgendermaßen aus:

[7]:
list(open("books.txt"))
[7]:
['   Titel             Sprache  Autor*innen   Lizenz        Veröffentlichungsdatum\n',
 '1  Python basics     en       Veit Schiele  BSD-3-Clause  2021-10-28\n',
 '2  Jupyter Tutorial  en       Veit Schiele  BSD-3-Clause  2019-06-27\n',
 '3  Jupyter Tutorial  de       Veit Schiele  BSD-3-Clause  2020-10-26\n',
 '4  PyViz Tutorial    en       Veit Schiele  BSD-3-Clause  2020-04-13\n']

In solchen Fällen könnt ihr einen regulären Ausdruck als Trennzeichen für read_csv übergeben. Dies kann durch den regulären Ausdruck \s\s+ ausgedrückt werden, also haben wir dann:

[8]:
pd.read_csv("books.txt", sep="\s\s+", engine="python")
[8]:
Titel Sprache Autor*innen Lizenz Veröffentlichungsdatum
1 Python basics en Veit Schiele BSD-3-Clause 2021-10-28
2 Jupyter Tutorial en Veit Schiele BSD-3-Clause 2019-06-27
3 Jupyter Tutorial de Veit Schiele BSD-3-Clause 2020-10-26
4 PyViz Tutorial en Veit Schiele BSD-3-Clause 2020-04-13

Da es einen Spaltennamen weniger als die Anzahl der Datenzeilen gab, folgert read_csv, dass in diesem Fall die erste Spalte der Index des DataFrame sein sollte.

Die Parser-Funktionen haben viele zusätzliche Argumente, die euch helfen, die große Vielfalt der auftretenden Ausnahmedateiformate zu handhaben. So könnt ihr beispielsweise mit skiprows einzelne Zeilen einer Datei überspringen:

[9]:
pd.read_csv(
    "https://raw.githubusercontent.com/veit/python-basics-tutorial-de/main/docs/save-data/books.csv",
    skiprows=[2],
    names=[
        "Titel",
        "Sprache",
        "Autor*innen",
        "Lizenz",
        "Veröffentlichungsdatum",
    ],
)
[9]:
Titel Sprache Autor*innen Lizenz Veröffentlichungsdatum
0 Python basics en Veit Schiele BSD-3-Clause 2021-10-28
1 Jupyter Tutorial en Veit Schiele BSD-3-Clause 2019-06-27
2 PyViz Tutorial en Veit Schiele BSD-3-Clause 2020-04-13

Der Umgang mit fehlenden Werten ist ein wichtiger und häufig komplizierter Teil beim Parsen von Daten. Fehlende Daten sind normalerweise entweder nicht vorhanden (leerer String) oder durch einen Platzhalter gekennzeichnet. Standardmäßig verwendet Pandas eine Reihe von häufig vorkommenden Platzhalter, wie NA und NULL:

[10]:
df = pd.read_csv(
    "https://raw.githubusercontent.com/veit/python-basics-tutorial-de/main/docs/save-data/books.csv",
    names=[
        "Titel",
        "Sprache",
        "Autor*innen",
        "Lizenz",
        "Veröffentlichungsdatum",
        "doi",
    ],
)

df
[10]:
Titel Sprache Autor*innen Lizenz Veröffentlichungsdatum doi
0 Python basics en Veit Schiele BSD-3-Clause 2021-10-28 NaN
1 Jupyter Tutorial en Veit Schiele BSD-3-Clause 2019-06-27 NaN
2 Jupyter Tutorial de Veit Schiele BSD-3-Clause 2020-10-26 NaN
3 PyViz Tutorial en Veit Schiele BSD-3-Clause 2020-04-13 NaN
[11]:
df.isna()
[11]:
Titel Sprache Autor*innen Lizenz Veröffentlichungsdatum doi
0 False False False False False True
1 False False False False False True
2 False False False False False True
3 False False False False False True

Die Option na_values kann entweder eine Liste oder eine Reihe von Strings annehmen, um fehlende Werte zu berücksichtigen:

[12]:
df = pd.read_csv(
    "https://raw.githubusercontent.com/veit/python-basics-tutorial-de/main/docs/save-data/books.csv",
    na_values=["BSD-3-Clause"],
    names=[
        "Titel",
        "Sprache",
        "Autor*innen",
        "Lizenz",
        "Veröffentlichungsdatum",
        "doi",
    ],
)

df
[12]:
Titel Sprache Autor*innen Lizenz Veröffentlichungsdatum doi
0 Python basics en Veit Schiele NaN 2021-10-28 NaN
1 Jupyter Tutorial en Veit Schiele NaN 2019-06-27 NaN
2 Jupyter Tutorial de Veit Schiele NaN 2020-10-26 NaN
3 PyViz Tutorial en Veit Schiele NaN 2020-04-13 NaN

Die häufigsten Argumente der Funktion read_csv:

Argument

Beschreibung

path

Zeichenfolge, die den Speicherort im Dateisystem, eine URL oder ein dateiähnliches Objekt angibt

sep oder delimiter

Zeichenfolge oder regulärer Ausdruck zum Trennen der Felder in jeder Zeile

header

Zeilennummer, die als Spaltennamen zu verwenden ist; Standardwert ist 0, also die erste Zeile, sollte aber None sein, wenn es keine Kopfzeile gibt

index_col

index_col Zeilennummern oder -namen, die als Zeilenindex im Ergebnis verwendet werden sollen; kann ein einzelner Name/eine einzelne Zahl oder eine Liste von ihnen für einen hierarchischen Index sein

names

Liste der Spaltennamen

skiprows

Anzahl der zu ignorierenden Zeilen am Anfang der Datei oder Liste der Zeilennummern beginnend bei 0, die übersprungen werden sollen

na_values

Folge von Werten, die durch NA ersetzt werden sollen

comment

Zeichen, um Kommentare vom Zeilenende abzutrennen

parse_dates

Versuch, Daten mit datetime zu parsen; standardmäßig False. Wenn True, wird versucht, alle Spalten zu parsen. Andernfalls kann eine Liste von Spaltennummern oder -namen angegeben werden, die analysiert werden sollen. Wenn das Element der Liste ein Tupel oder eine Liste ist, werden mehrere Spalten miteinander kombiniert und in ein Datum umgewandelt, z.B. wenn Datum und Uhrzeit auf zwei Spalten aufgeteilt sind

keep_date_col

wenn Spalten zum Parsen des Datums kombiniert werden, werden die kombinierten Spalten beibehalten; Standardeinstellung: False.

converters

Dict, das die Spaltennummer der Namen enthält, die auf Funktionen abgebildet werden, z.B. würde {'Titel': f} die Funktion f auf alle Werte in der Spalte Titel anwenden

dayfirst

beim Parsen potenziell mehrdeutiger Datumsangaben als internationales Format behandeln, z.B. 28/6/202128. Juni 2021; standardmäßig False

date_parser

zu verwendende Funktion zum Parsen von Datumsangaben

nrows

Anzahl der zu lesenden Zeilen vom Anfang der Datei

iterator

Rückgabe eines TextFileReader-Objekts zum stückweisen Einlesen der Datei; dieses Objekt kann auch mit der with-Anweisung verwendet werden

chunksize

Für die Iteration die Größe der Datenblöcke.

skip_footer

Anzahl der Zeilen, die am Ende der Datei ignoriert werden sollen

verbose

gibt verschiedene Informationen zur Parser-Ausgabe aus, z.B. die Anzahl der fehlenden Werte in nicht-numerischen Spalten.

encoding

Textkodierung für Unicode, z.B. utf-8 für UTF-8-kodierten Text

squeeze

wenn die geparsten Daten nur eine Spalte enthalten, wird eine Serie zurückgegeben

thousands

Trennzeichen für Tausender, z.B. , oder .

Stückweises Einlesen von Textdateien#

Wenn ihr sehr große Dateien verarbeiten wollt, könnt ihr auch nur einen kleinen Teil einer Datei einlesen oder durch kleinere Teile einer Datei iterieren.

Bevor wir uns eine große Datei ansehen, verringern wir mit options.display.max_rows die Anzahl der angezeigten Zeilen:

[13]:
pd.options.display.max_rows = 10
[14]:
pd.read_csv("example.csv")
[14]:
Date Mon. Tues. Wed. Thurs. Fri. Sat. Sun.
0 1996-01-01 0.129453 -0.023836 1.121460 1.698286 -0.598506 1.042221 -0.726412
1 1996-01-02 -0.094021 -0.727942 0.698641 -1.198040 1.927505 1.147445 -1.134103
2 1996-01-03 -0.560857 0.145222 -0.990202 1.200214 0.717339 1.117095 -1.793565
3 1996-01-04 -0.169755 -0.677391 -1.533519 -0.343477 -0.109705 1.038236 -0.799088
4 1996-01-05 1.344705 -1.817261 0.460991 -0.839633 0.265814 0.477659 0.636383
... ... ... ... ... ... ... ... ...
9127 2020-12-27 -0.881800 -0.074270 -0.351769 1.381641 -0.049548 1.664180 -1.032204
9128 2020-12-28 -0.143386 0.198217 -1.243861 1.196576 1.338166 -0.212333 -0.023131
9129 2020-12-29 0.398787 -0.848786 1.791707 -1.167592 -0.033881 -0.285559 -0.323477
9130 2020-12-30 0.587846 0.411580 1.150380 0.444638 -1.093577 0.605456 1.463345
9131 2020-12-31 0.736350 0.436292 -0.260171 -0.066066 -0.328324 -0.586792 -1.204582

9132 rows × 8 columns

Wenn ihr nur eine kleine Anzahl von Zeilen lesen wollt (ohne die gesamte Datei zu lesen), könnt ihr dies mit nrows angeben:

[15]:
pd.read_csv("example.csv", nrows=7)
[15]:
Date Mon. Tues. Wed. Thurs. Fri. Sat. Sun.
0 1996-01-01 0.129453 -0.023836 1.121460 1.698286 -0.598506 1.042221 -0.726412
1 1996-01-02 -0.094021 -0.727942 0.698641 -1.198040 1.927505 1.147445 -1.134103
2 1996-01-03 -0.560857 0.145222 -0.990202 1.200214 0.717339 1.117095 -1.793565
3 1996-01-04 -0.169755 -0.677391 -1.533519 -0.343477 -0.109705 1.038236 -0.799088
4 1996-01-05 1.344705 -1.817261 0.460991 -0.839633 0.265814 0.477659 0.636383
5 1996-01-06 -0.354445 -0.065182 -1.244963 -0.559732 0.042362 -0.303712 0.067632
6 1996-01-07 1.460922 0.164412 0.883960 -0.833642 0.001582 1.138469 0.561618

Um eine Datei stückweise zu lesen, könnt ihr mit chunksize die Anzahl der Zeilen angeben:

[16]:
pd.read_csv("example.csv", chunksize=1000)
[16]:
<pandas.io.parsers.readers.TextFileReader at 0x13295f2d0>

Das von read_csv zurückgegebene TextFileReader-Objekt ermöglicht die Iteration über Teile der Datei entsprechend der chunksize. Zum Beispiel können wir über die example.csv-Datei iterieren und die Anzahl der Werte in der Spalte Date wie folgt aggregieren:

[17]:
chunks = pd.read_csv("example.csv", chunksize=1000)

serie = pd.Series([], dtype="float64")
for chunk in chunks:
    values = serie.add(chunk["Date"].value_counts(), fill_value=0)

sorted_values = values.sort_values(ascending=False)
[18]:
sorted_values[:10]
[18]:
Date
2020-08-22    1.0
2020-09-07    1.0
2020-08-24    1.0
2020-08-25    1.0
2020-08-26    1.0
2020-08-27    1.0
2020-08-28    1.0
2020-08-29    1.0
2020-08-30    1.0
2020-08-31    1.0
dtype: float64

TextFileReader verfügt außerdem über eine get_chunk-Methode, mit der ihr Stücke beliebiger Größe lesen könnt.

DataFrame und Series als csv-Datei schreiben#

Daten können auch in ein mit Trennzeichen versehenes Format exportiert werden. Mit der Methode pandas.DataFrame.to_csv können wir die Daten in eine kommagetrennte Datei schreiben:

[19]:
df.to_csv("out.csv")

Natürlich können auch andere Begrenzungszeichen verwendet werden, z.B. zum Schreiben nach sys.stdout, so dass das Textergebnis auf der Konsole und nicht in einer Datei ausgegeben wird:

[20]:
import sys
[21]:
df.to_csv(sys.stdout, sep="|")
|Titel|Sprache|Autor*innen|Lizenz|Veröffentlichungsdatum|doi
0|Python basics|en|Veit Schiele||2021-10-28|
1|Jupyter Tutorial|en|Veit Schiele||2019-06-27|
2|Jupyter Tutorial|de|Veit Schiele||2020-10-26|
3|PyViz Tutorial|en|Veit Schiele||2020-04-13|

Fehlende Werte erscheinen in der Ausgabe als leere Zeichenketten. Möglicherweise möchtet ihr sie durch einen anderen Platzhalter kennzeichnen:

[22]:
df.to_csv(sys.stdout, na_rep="NaN")
,Titel,Sprache,Autor*innen,Lizenz,Veröffentlichungsdatum,doi
0,Python basics,en,Veit Schiele,NaN,2021-10-28,NaN
1,Jupyter Tutorial,en,Veit Schiele,NaN,2019-06-27,NaN
2,Jupyter Tutorial,de,Veit Schiele,NaN,2020-10-26,NaN
3,PyViz Tutorial,en,Veit Schiele,NaN,2020-04-13,NaN

Wenn keine anderen Optionen angegeben sind, werden sowohl die Zeilen- als auch die Spaltenbeschriftungen geschrieben. Beides kann deaktiviert werden:

[23]:
df.to_csv(sys.stdout, index=False, header=False)
Python basics,en,Veit Schiele,,2021-10-28,
Jupyter Tutorial,en,Veit Schiele,,2019-06-27,
Jupyter Tutorial,de,Veit Schiele,,2020-10-26,
PyViz Tutorial,en,Veit Schiele,,2020-04-13,

Ihr könnt auch nur eine Teilmenge der Spalten schreiben, und zwar in einer von euch gewählten Reihenfolge:

[24]:
df.to_csv(
    sys.stdout,
    index=False,
    columns=["Titel", "Sprache", "Autor*innen", "Veröffentlichungsdatum"],
)
Titel,Sprache,Autor*innen,Veröffentlichungsdatum
Python basics,en,Veit Schiele,2021-10-28
Jupyter Tutorial,en,Veit Schiele,2019-06-27
Jupyter Tutorial,de,Veit Schiele,2020-10-26
PyViz Tutorial,en,Veit Schiele,2020-04-13

Arbeiten mit dem csv-Modul von Python#

Die meisten Formen von Tabellendaten können mit Funktionen wie pandas.read_csv geladen werden. In einigen Fällen kann jedoch auch eine manuelle Bearbeitung erforderlich sein. Es ist nicht ungewöhnlich, eine Datei mit einer oder mehreren fehlerhaften Zeilen zu erhalten, die read_csv zum Scheitern bringen. Für jede Datei mit einem einstelligen Begrenzungszeichen könnt ihr das in Python eingebaute csv-Modul verwenden. Um es zu verwenden, übergebt eine offene Datei oder ein dateiähnliches Objekt an csv.reader:

[25]:
import csv


f = open("out.csv")
reader = csv.reader(f)

for line in reader:
    print(line)
['', 'Titel', 'Sprache', 'Autor*innen', 'Lizenz', 'Veröffentlichungsdatum', 'doi']
['0', 'Python basics', 'en', 'Veit Schiele', '', '2021-10-28', '']
['1', 'Jupyter Tutorial', 'en', 'Veit Schiele', '', '2019-06-27', '']
['2', 'Jupyter Tutorial', 'de', 'Veit Schiele', '', '2020-10-26', '']
['3', 'PyViz Tutorial', 'en', 'Veit Schiele', '', '2020-04-13', '']

Dialekte#

csv-Dateien gibt es in vielen verschiedenen Varianten. Das Python csv-Modul kommt bereits mit drei verschiedenen Dialekten:

Parameter

excel

excel-tab

unix

delimiter

','

'\t'

','

quotechar

'"'

'"'

'"'

doublequote

True

True

True

skipinitialspace

False

False

False

lineterminator

'\r\n'

'\r\n'

'\n'

quoting

csv.QUOTE_MINIMAL

csv.QUOTE_MINIMAL

csv.QUOTE_ALL

escapechar

None

None

None

Ihr könnt damit auch euer eigenes Format definieren mit einem anderen Trennzeichen, einer anderen Zeichenkettenkonvention oder einem anderen Zeilenendezeichen. Hierfür empfiehlt sich die Registrierung eines eigenen Dialekts. Mögliche Optionen und Funktionen von csv.register_dialect sind:

Argument

Beschreibung

delimiter

Ein-Zeichen-Zeichenfolge zur Trennung von Feldern; Standardwert ist ,.

lineterminator

Zeilenabschlusszeichen zum Schreiben; Standardwert ist \r\n. Reader ignoriert dies und erkennt plattformübergreifende Zeilenbegrenzer.

quotechar

Anführungszeichen für Felder mit Sonderzeichen (wie ein Trennzeichen); Standardwert ist ".

quoting

Zitier-Konvention. Zu den Optionen gehören csv.QUOTE_ALL – alle Felder zitieren, csv.QUOTE_MINIMAL – nur Felder mit Sonderzeichen wie dem Begrenzungszeichen, csv.QUOTE_NONNUMERIC und csv.QUOTE_NONE – keine Zitate. Der Standardwert ist QUOTE_MINIMAL.

skipinitialspace

Leerzeichen nach jedem Begrenzungszeichen ignorieren; Standardwert ist False.

doublequote

bei True werden Anfürhuntszeichen innerhalb eines Feldes verdoppelt.

escapechar

Zeichenkette, um das Trennzeichen zu umgehen, wenn quoting auf csv.QUOTE_NONE gesetzt ist; standardmäßig deaktiviert.

[26]:
csv.register_dialect(
    "my_csv_dialect",
    lineterminator="\n",
    delimiter=",",
    quotechar="'",
    quoting=csv.QUOTE_MINIMAL,
)

Nun kann die CSV-Datei geöffnet werden mit:

[27]:
with open("out.csv") as f:
    reader = csv.reader(f, "my_csv_dialect")
    for line in reader:
        print(line)
['', 'Titel', 'Sprache', 'Autor*innen', 'Lizenz', 'Veröffentlichungsdatum', 'doi']
['0', 'Python basics', 'en', 'Veit Schiele', '', '2021-10-28', '']
['1', 'Jupyter Tutorial', 'en', 'Veit Schiele', '', '2019-06-27', '']
['2', 'Jupyter Tutorial', 'de', 'Veit Schiele', '', '2020-10-26', '']
['3', 'PyViz Tutorial', 'en', 'Veit Schiele', '', '2020-04-13', '']

Dann können wir ein Dict mit Datenspalten erstellen, indem wir Dict Comprehensions verwenden und mit zip über die Werte aus Values iterieren. Beachtet dabei, dass dies bei großen Dateien viel Speicherplatz benötigt, da hierfürdie Zeilen in Spalten umgewandelt werden:

[28]:
with open("out.csv") as f:
    reader = csv.reader(f, "my_csv_dialect")
    lines = list(reader)
    header, values = lines[0], lines[1:]
    data_dict = {h: v for h, v in zip(header, zip(*values))}

data_dict
[28]:
{'': ('0', '1', '2', '3'),
 'Titel': ('Python basics',
  'Jupyter Tutorial',
  'Jupyter Tutorial',
  'PyViz Tutorial'),
 'Sprache': ('en', 'en', 'de', 'en'),
 'Autor*innen': ('Veit Schiele',
  'Veit Schiele',
  'Veit Schiele',
  'Veit Schiele'),
 'Lizenz': ('', '', '', ''),
 'Veröffentlichungsdatum': ('2021-10-28',
  '2019-06-27',
  '2020-10-26',
  '2020-04-13'),
 'doi': ('', '', '', '')}

Um Dateien mit Trennzeichen manuell zu schreiben, könnt ihr csv.writer verwenden. Er akzeptiert ein offenes, beschreibbares Dateiobjekt und die gleichen Dialekt- und Formatoptionen wie csv.reader:

[29]:
with open("new.csv", "w") as f:
    writer = csv.writer(f, "my_csv_dialect")
    writer.writerow(("", "Titel", "Sprache", "Autor*innen"))
    writer.writerow(("1", "Python basics", "en", "Veit Schiele"))
    writer.writerow(("2", "Jupyter Tutorial", "en", "Veit Schiele"))
[30]:
list(open("new.csv"))
[30]:
[',Titel,Sprache,Autor*innen\n',
 '1,Python basics,en,Veit Schiele\n',
 '2,Jupyter Tutorial,en,Veit Schiele\n']