Kurs
Es gibt viele Situationen, in denen du große Dateien schnell analysieren, ändern und verarbeiten musst – sowohl viele Dateien als auch sehr große. Oft handelt es sich dabei um textbasierte CSV-Dateien (Comma Separated Values). Öffnest du sie mit Standard-Tabellenkalkulationen wie Excel, LibreOffice oder OpenOffice, sprengen sie schnell den Arbeitsspeicher deines Rechners. Auch die Stapelverarbeitung einer großen Anzahl von Dateien bricht nach ein paar Stunden oft wegen eines unerwarteten Dateifehlers ab.
Du verbringst dann zwangsläufig viel Zeit mit eingefrorenen Bildschirmen, Neustarts und Warterei.
Viele dieser Aufgaben lassen sich jedoch mit wenigen Zeilen Code erledigen. Noch besser: Wenn du ein paar einfache Shell-Befehle beherrschst, sparst du enorm Zeit und Nerven.
In diesem Beitrag bekommst du einen Überblick über Shell-Befehle, die ich fast täglich nutze. Ich hoffe, sie sind auch für dich hilfreich.
Kurznotizen
Beachte, dass es verschiedene Shell-Typen gibt (bash, zsh, ...). Bash ist am weitesten verbreitet, weil es die Standardshell auf macOS und den meisten Linux-Distributionen ist. Zsh (und Oh My Zsh) ist eine beliebte und leistungsstarke Alternative. Die in diesem Blogpost gezeigten Shell-Befehle wurden unter bash auf OS X (macOS) getestet und sollten auch in anderen Shells und Umgebungen funktionieren.
Alle folgenden Beispiele basieren auf dem Adult-Datensatz aus dem UCI Machine Learning Repository, auch als „Census Income“-Datensatz bekannt und hier verfügbar. Er wird häufig genutzt, um anhand von Zensusdaten vorherzusagen, ob das Einkommen \$50K/Jahr übersteigt. Mit 48.842 Zeilen und 14 Attributen ist er keineswegs groß, aber ideal, um die Beispiele zu illustrieren. Obwohl die Daten die Erweiterung .data tragen, handelt es sich um eine sauber formatierte CSV-Datei. Lade dir den Datensatz gern herunter und mach mit!
Zählen mit wc
Du hast eine neue Textdatei und willst wissen, wie viele Zeilen sie enthält. Das erledigt der Word-Count-Befehl wc -l:
$ wc -l adult.data
32562 adult.data
Das zeigt dir, dass die Datei adult.data 32.562 Zeilen enthält. Das Flag -l weist wc an, die Zeilen zu zählen. Mit dem Flag -w kannst du stattdessen die Wörter zählen.
$ wc -w adult.data
488415 adult.data
Die Datei adult.data enthält knapp 500.000 Wörter.
Mit wc kannst du auch die Anzahl von Dateien in einem Verzeichnis ermitteln, indem du die Ausgabe eines einfachen ls -l als Eingabe an wc übergibst. Die Weitergabe der Ausgabe eines Befehls an einen anderen mit dem Pipe-Symbol | ist ein nützliches Shell-Muster namens Pipelining. Du wirst es in mehreren Beispielen in diesem Beitrag sehen.
Angenommen, du hast nun einen Ordner mit vielen Dateien. Der folgende Befehl sagt dir genau, wie viele Dateien er enthält:
$ ls -l <folder> | wc -l
508
Mit Wildcards und Unterordnern gibt es noch viele weitere Anwendungsfälle für wc.
Zurück zu deiner Datei adult.data: Schau dir mit dem Befehl head die ersten Zeilen an. Standardmäßig gibt head 10 Zeilen aus, mit dem Flag -n begrenzt du die Ausgabe, hier auf die ersten 2 Zeilen.
$ head -n 2 adult.data
39, State-gov, 77516, Bachelors, 13, Never-married, Adm-clerical, Not-in-family, White, Male, 2174, 0, 40, United-States, <=50K
50, Self-emp-not-inc, 83311, Bachelors, 13, Married-civ-spouse, Exec-managerial, Husband, White, Male, 0, 0, 13, United-States, <=50K
Du siehst, dass die Datei keine Kopfzeile hat. Die Spaltennamen stehen nicht in der Datei. Du kannst diese Kopfzeile hinzufügen, indem du zwei Dateien mit dem Befehl cat zusammenfügst.
Dateien zusammenfügen mit cat
Der Befehl cat gibt den Inhalt einer Datei auf der Standardausgabe (deinem Terminal) aus. Er kann auch zum Zusammenfügen mehrerer Dateien genutzt werden. Der Befehl cat file_1.csv file_2.csv > target_file.csv vereint den Inhalt von file_1.csv und file_2.csv in target_file.csv, wobei file_2.csv an das Ende von file_1.csv angehängt wird.
Die Header-Datei ist nicht im Originaldatensatz enthalten und muss erstellt werden. Dazu schreibst du die durch Kommas getrennte Liste der Spaltennamen mit echo in eine Datei header.csv.
$ echo "age,workclass,fnlwgt,education,education-num,marital-status,occupation,relationship,race,sex,capital-gain,capital-loss,native-country,class" > header.csv
In diesem Beispiel hast du die Shell-Umleitung > verwendet, um die Ausgabe von cat in die Datei adult.csv zu schreiben. > erstellt eine Datei oder überschreibt ihren Inhalt vollständig, wenn sie bereits existiert. Mit der doppelten Umleitung >> wird der neue Inhalt an eine bestehende Datei angehängt, ohne sie zu löschen.
Fügen wir nun die Datei header.csv an den Anfang von adult.data an. Gleichzeitig benennst du adult.data in adult.csv um, da es sich ja um eine CSV-Datei handelt.
$ cat header.csv adult.data > adult.csv
Prüfe, ob die erste Zeile von adult.csv die Spaltennamen enthält:
$ head -n 1 adult.csv
age,workclass,fnlwgt,education,education-num,marital-status,occupation,relationship,race,sex,capital-gain,capital-loss,native-country,class
39, State-gov, 77516, Bachelors, 13, Never-married, Adm-clerical, Not-in-family, White, Male, 2174, 0, 40, United-States, <=50K
Dateien ändern mit sed
Ein weiterer häufiger Fall in der Datenaufbereitung: Eine Datei ist beschädigt oder schlecht formatiert, zum Beispiel mit Nicht-UTF-8-Zeichen oder einem verrutschten Komma. Solche Fehler kannst du korrigieren, ohne die Datei zu öffnen – mit sed.
Das generische sed-Muster lautet
$ sed "s/<string to replace>/<string to replace it with>/g" <source_file> > <target_file>.
Im Datensatz adult.csv steht das Zeichen ? für fehlende Werte. Du kannst es mit sed durch einen besser geeigneten Standardwert ersetzen. Die leere Zeichenkette ist als Kennzeichnung für fehlende Werte vorzuziehen, weil sie beim Einlesen der Daten in einen Pandas DataFrame als NaN interpretiert wird.
$ grep ", ?," adult.csv | wc -l
2399
Das liefert dir die Anzahl von 2.399 Zeilen mit mindestens einer Spalte, in der ? einen fehlenden Wert markiert. Der folgende Befehl ersetzt alle Spaltenwerte ? durch eine leere Zeichenkette. Alle Zellen, die nur aus ? gefolgt von einem Leerzeichen bestehen, sind danach wirklich leer.
$ sed "s/, ?,/,,/g" adult.csv > adult.csv
Beachte, dass du im Such- und Ersetzungsmuster den Spaltentrenner , nutzt, um zu vermeiden, dass legitime Fragezeichen an anderer Stelle im Datensatz ersetzt werden.
Große Dateien ausschnittweise verarbeiten
Stell dir vor, du arbeitest mit einer Datei mit über 30 Millionen Zeilen. Während du dein Python- oder R-Skript erstellst, willst du deinen Workflow zunächst an einem Teil des Originaldatensatzes testen und nicht alles in den Speicher laden. Mit einer Kombination aus head und tail erzeugst du eine Stichprobe aus dem Original.
headgibt den Anfang von Dateien aus;tailgibt das Ende von Dateien aus
Wie gesehen, nehmen diese Befehle das Flag -n (--lines), um die Zeilenanzahl der Ausgabe zu begrenzen. tail unterstützt außerdem -f (--follow) und gibt angehängte Daten aus, sobald sie ans Dateiende geschrieben werden. Das ist besonders praktisch zum Überwachen von Logdateien während deine Skripte laufen, zum Beispiel mit tail -f <logfile>.
In Kombination mit dem Pipe-Symbol | kannst du mit head und tail einen bestimmten Zeilenbereich aus einer Quelldatei extrahieren und in eine Teilmenge schreiben. Um beispielsweise 20 Zeilen ab Zeile 100 zu extrahieren:
$ head -n 120 adult.csv | tail -n 20 > adult_sample.csv
Wichtig: Zuerst nimmst du mit head erste_zeile + anzahl_zeilen und danach mit tail die anzahl_zeilen. Der generische Befehl lautet:
$ head -n <total_lines> <source_file> | tail -n <number_of_lines> > <target_file>
wobei total_lines = first_line + number_of_lines. Deine neue Stichprobe enthält jedoch keine Kopfzeile. Wir wissen bereits, wie wir den Header wieder hinzufügen: mit cat die Stichprobe und die zuvor erstellte header.csv zusammenfügen:
$ cat adult_sample.csv header.csv > adult_sample_with_header.csv
Beachte, dass du adult\_sample.csv nicht als Zieldatei verwendet, sondern eine neue Datei namens adult\_sample\_with\_header.csv erstellt hast. Wenn Quell- und Zieldatei beim Einsatz von cat denselben Namen haben, führt das zu unerwartetem Inhalt. Erstelle besser eine neue Ausgabedatei.
Duplikate finden mit uniq
Mit dem Befehl uniq findest du benachbarte, wiederholte Zeilen in einer Datei. uniq unterstützt mehrere nützliche Flags:
uniq -c: Fügt jeder Zeile die Anzahl der Wiederholungen hinzu;uniq -d: Gibt nur doppelte Zeilen aus; unduniq -u: Gibt nur eindeutige Zeilen aus.
Allerdings ist uniq nicht „smart“: Wiederholungen werden nur erkannt, wenn sie direkt aufeinander folgen. Daher musst du die Datei zuerst mit sort sortieren. Dieser Befehl zählt die Anzahl der doppelten Zeilen in adult.csv:
$ sort adult.csv | uniq -d | wc -l
23
und zeigt, dass es 23 Duplikate gibt. Der nächste Befehl nimmt alle Zeilen mit hinzugefügten Zählungen, sortiert absteigend und gibt die ersten 3 Duplikate aus:
$ sort adult.csv | uniq -c | sort -r | head -n 3
3 25, Private, 195994, 1st-4th, 2, Never-married, ...
2 90, Private, 52386, Some-college, 10, Never-married, ...
2 49, Self-emp-not-inc, 43479, Some-college, 10, Married-civ-spouse, ...
Durch verschiedene Flags in Kombination mit sort und uniq ergeben sich viele mächtige Optionen. Sieh dir man sort und man uniq an, um mehr zu entdecken.
Spalten auswählen mit cut
Das Schöne an CSV-Dateien und Shell-Befehlen: Du kannst auch auf Spaltenebene arbeiten, indem du mit cut bestimmte Spalten auswählst. cut nutzt dabei zwei Haupt-Flags: -d für das Spaltentrennzeichen und -f für die gewünschten Spalten. Im folgenden Beispiel verwendest du cut, um die Anzahl der unterschiedlichen Werte der kategorialen Variablen workclass (Spalte 2) zu ermitteln.
Wähle zunächst die Spalte workclass und leite sie an head weiter, um zu prüfen, dass du die richtige Spalte hast:
$ cut -d "," -f 2 adult.csv | head -3
workclass
State-gov
Self-emp-not-inc
Um nun die Ausprägungen zu zählen, sortierst du die Ausgabe von cut und leitest sie an uniq -c weiter:
$ cut -d "," -f 2 adult.csv | sort | uniq -c
1837
960 Federal-gov
2093 Local-gov
7 Never-worked
22696 Private
1116 Self-emp-inc
2541 Self-emp-not-inc
1298 State-gov
14 Without-pay
1 workclass
Das zeigt dir zum Beispiel, dass es 1.837 leere Werte gibt und die häufigste Klasse mit großem Abstand Private ist – mit 22.969 Vorkommen.
Die obige Befehlskette entspricht dem value_counts()-Aufruf auf einer DataFrame-Spalte aus den adult.csv-Daten.
Schleifen
Bis hierhin hast du meist mit einer Datei gearbeitet. Wenn du viele Dateien umbenennen, verarbeiten oder übertragen willst, gehören Schleifen in deinen Shell-Werkzeugkasten. Das generische Format einer Schleife in bash ist:
while true; do
_do something_ ;
done
Setzen wir das um. Stell dir vor, du hast 1.000 Dateien mit Leerzeichen im Dateinamen und willst jedes Leerzeichen durch einen Unterstrich „_" ersetzen.
replace_source=' '
replace_target='_'
for filename in ./*.csv; do
new_filename=${filename//$replace_source/$replace_target}
mv "$filename" "$new_filename"
done
In diesem Beispiel:
- Definierst du zuerst zwei Variablen:
replace_sourceundreplace_targetals Platzhalter für Leerzeichen bzw. Unterstrich - Iterierst du über alle
*.csv-Dateien im aktuellen Ordner - Erstellst du für jeden
filenameeinennew_filename, indem du jedes Leerzeichen durch einen Unterstrich ersetzt - Bennennst du die Datei mit
mvvom aktuellen in den neuen Dateinamen um
Tatsächlich hast du damit nicht nur über Dateien in einem Verzeichnis iteriert, sondern auch Variablen verwendet.
Variablen
Zum Abschluss dieser kleinen Einführung in Shell-Befehle noch ein Wort zu Variablen. Variablen legst du in der Shell so an:
$ varname='<a string>'
$ varname=a number
Zum Beispiel:
$ varname='Hello world'
$ varname=123.4
Beachte, dass um das Gleichheitszeichen keine Leerzeichen stehen dürfen. var = '<a string>' würde nicht funktionieren. Um den Variablenwert auszugeben, verwendest du einfach echo:
$ echo $varname
123.4
Und um ihn in einem Skript zu nutzen, setzt du ihn – wie oben gesehen – in Anführungszeichen:
$ mv "$filename" "$new_filename"
Mit Schleifen und Variablen öffnest du die Tür zu komplexeren Dateimanipulationen – ein Einstieg ins Shell-Scripting, der über diesen Artikel hinausgeht. Fang klein an und erweitere Schritt für Schritt, dann ist Shell-Scripting gut machbar.
Fazit
Shell-Befehle sind im Alltag von Data Scientists extrem nützlich. Es gibt unzählige weitere Beispiele und Anwendungsfälle. Und oft führen verschiedene Wege zum Ziel – dank der Vielzahl an Shell-Befehlen und Flags. In solchen Fällen gilt: Einfach halten gewinnt.
Hoffentlich helfen dir die gezeigten Beispiele, Shell-Befehle in deinen Datenverarbeitungs-Workflow zu integrieren. Teile gern deine Shell-Tricks mit mir auf Twitter: @alexip.
Sieh dir auch unser Tutorial Richte dir eine Data-Science-Umgebung auf deinem Rechner ein an.