Mit dem Programm join können Daten aus zwei Dateien verknüpft werden. Dabei werden Zeilen mit gleichen Daten in einem bestimmten Feld zusammengefügt. Das Ganze funktioniert also ähnlich wie ein SQL-JOIN.
Ähnlich wie bei anderen Tools müssen die Daten bereits sortiert sein. Hierbei hilft wie immer sort. Außerdem arbeitet join auch mit Feldern, die durch ein Trennzeichen getrennt sind.
Ein kurzes Beispiel mit einfachen Testdaten. Datei1:
1 jan
2 feb
3 mar
5 may
Datei2:
1 31
2 28
3 31
4 30
Ein Join aus beiden Dateien:
$ join Datei1 Datei2
1 jan 31
2 feb 28
3 mar 31
Standardmäßig wird das Feld 1 aus beiden Dateien verglichen. Möglich ist auch, unterschiedliche Felder zu nehmen. Dafür gibt die es Optionen -1 <feldnummer>
, um die Feldnummer für die erste Datei anzugeben. Für die zweite Datei gibt es die Option -2 <feldnummer>
. Ebenso ist es möglich, statt Space ein anderes Trennzeichen mit der Option -t <char>
anzugeben.
In der Ausgabe dürfte einem auffallen, dass Zeilen, die nicht verbunden werden können, nicht ausgegeben werden (INNER JOIN). Dies lässt sich mit den Optionen -a1
und -a2
ändern. Wenn -a1
angegeben ist, werden von der ersten Datei alle Zeilen ausgegeben, bei -a2
das gleiche für die zweite Datei. Es können auch beide Optionen benutzt werden.
Das war jetzt nur eine einfache 1:1-Beziehung. Mit join funktionieren aber auch 1:n-Beziehungen. Nehmen wir dafür ein paar neue Beispieldaten:
Datei1:
admin 1000 System Administrator
max 1001 Max Mustermann
john 1002 John Doe
Datei2:
max user 20 11. Dez 18:01 report.pdf
max user 12 11. Dez 17:56 calc.txt
admin admin 78 8. Dez 10:52 list.txt
john user 24 18. Dez 17:22 stuff1
john user 20 18. Dez 17:22 stuff2
max user 12 11. Dez 18:09 newfile1.txt
max user 10 11. Dez 18:09 todo.txt
admin admin 109 8. Dez 11:28 machines.txt
john user 58 25. Nov 07:26 names.txt
john user 42 25. Nov 07:27 states.txt
admin admin 152 7. Dez 17:42 table1
max user 25 8. Dez 10:12 testdata
max user 12 11. Dez 18:08 test.txt
Join:
$ join <(sort Datei1) <(sort Datei2)
admin 1000 System Administrator admin 109 8. Dez 11:28 machines.txt
admin 1000 System Administrator admin 152 7. Dez 17:42 table1
admin 1000 System Administrator admin 78 8. Dez 10:52 list.txt
john 1002 John Doe user 20 18. Dez 17:22 stuff2
john 1002 John Doe user 24 18. Dez 17:22 stuff1
john 1002 John Doe user 42 25. Nov 07:27 states.txt
john 1002 John Doe user 58 25. Nov 07:26 names.txt
max 1001 Max Mustermann user 10 11. Dez 18:09 todo.txt
max 1001 Max Mustermann user 12 11. Dez 17:56 calc.txt
max 1001 Max Mustermann user 12 11. Dez 18:08 test.txt
max 1001 Max Mustermann user 12 11. Dez 18:09 newfile1.txt
max 1001 Max Mustermann user 20 11. Dez 18:01 report.pdf
max 1001 Max Mustermann user 25 8. Dez 10:12 testdata
Funktioniert also auch hier wie in SQL. Da wir pro Zeile in Datei1 mehrere Zeilen in Datei2 haben, werden die Zeilen in Datei1 dupliziert.
Mit dem Programm comm können Dateien miteinander verglichen werden. Das Programm prüft, ob Zeilen in Datei1, in Datei2 oder in beiden vorhanden sind. Die Daten müssen hierfür bereits sortiert sein. Falls dies nicht der Fall ist, hilft wieder sort.
Hier ein einfaches Beispiel zu comm:
$ cat > file1
1
2
3
a
b
c
$ cat > file2
0
1
b
c
d
$ comm file1 file2
0
1
2
3
a
b
c
d
Die Ausgabe enthält 3 Spalten:
- Spalte 1: Zeilen, die nur in file1 sind
- Spalte 2: Zeilen, die nur in file2 sind
- Spalte 3: Zeilen, die in beiden Dateien vorhanden sind
Um die Ausgabe zu filtern, gibt es die Optionen -1 -2 und -3 um die jeweiligen Spalten bei der Ausgabe zu unterdrücken. Um z.B. nur die Spalte 3 auszugeben, muss -12 angegeben werden:
$ comm -12 file1 file2
1
b
c
Damit kann comm genutzt werden, um Schnittmengen oder Differenzmengen zu bilden.
In Teil 1 habe ich einige Unix-Tools für einfache Datenauswertungen vorgestellt. Dies möchte ich in diesem Artikel mit zwei Beispielen etwas vertiefen.
Dafür habe ich mir ein paar Testdaten von hier besorgt, und zwar die Datei airtravel.csv.
"Month", "1958", "1959", "1960"
"JAN", 340, 360, 417
"FEB", 318, 342, 391
"MAR", 362, 406, 419
"APR", 348, 396, 461
"MAY", 363, 420, 472
"JUN", 435, 472, 535
"JUL", 491, 548, 622
"AUG", 505, 559, 606
"SEP", 404, 463, 508
"OCT", 359, 407, 461
"NOV", 310, 362, 390
"DEC", 337, 405, 432
Diese Beispieldaten enthalten für drei Jahre die monatliche Anzahl an Flugreisenden.
Beispiel 1: Summe von Spalten bilden
Als erstes möchten wir die Gesamtsumme pro Jahr wissen, also die drei Spalten aufsummieren. Um die Daten leichter zu verarbeiten, entfernen wir die Tabellenüberschrift sowie die erste Spalte mit den Monatsbezeichnungen.
$ tail -12 airtravel.csv | cut -d ',' -f 2,3,4 > data1.csv
$ cat data1.csv
340, 360, 417
318, 342, 391
362, 406, 419
348, 396, 461
363, 420, 472
435, 472, 535
491, 548, 622
505, 559, 606
404, 463, 508
359, 407, 461
310, 362, 390
337, 405, 432
Wir summieren wir das jetzt? Hierfür benutzen wir das Programm bc, welcher ein Taschenrechner ist, der seine Eingabe von stdin lesen kann. Um eine vernünftige Eingabe für bc zu generieren, müssen wir alle Zeilen einer Spalte zu einer einzigen Zeile zusammenfügen, getrennt durch ein Plus für die Rechenoperation. Dies kann mit dem Tool paste erledigt werden, in dem die -s Option angegeben wird und mit -d ein Trennzeichen.
Zunächst einmal extrahieren wir mit cut eine Spalte und übergeben diese an paste. Dies liefert eine einzige Zeile mit unseren Zahlen, getrennt durch ein +, welche wir an bc übergeben können, um unsere Summe zu erhalten:
$ cut -d ',' -f 1 data1.csv | paste -s -d+ - | bc
4572
Beispiel 2: Maximum und Minimum finden
Als nächstes möchten wir rausfinden, in welchem Monat es im Jahr 1959 am meisten und am wenigsten Reisende gab. Hierfür schneiden wir wieder die Spaltenüberschriften weg. Danach sortieren wir die gewünschte Spalte und holen uns das Minimum und Maximum mit den Tools head und tail:
$ tail -12 airtravel.csv | cut -d ',' -f 1,3 | sort -t ',' -k 2 > col2_sorted.csv
$ cat col2_sorted.csv
"FEB", 342
"JAN", 360
"NOV", 362
"APR", 396
"DEC", 405
"MAR", 406
"OCT", 407
"MAY", 420
"SEP", 463
"JUN", 472
"JUL", 548
"AUG", 559
$ printf "min: %s\nmax: %s\n" "`head -1 col2_sorted.csv`" "`tail -1 col2_sorted.csv`"
min: "FEB", 342
max: "AUG", 559
Fazit: Einfache Aufgaben sind unproblematisch. Bei komplexeren Anforderungen könnte man aber schnell an die Grenzen stoßen. Das war aber noch nicht alles. Im nächsten Artikel warten weitere Werkzeuge auf uns.
Teil 1 einer kleinen Tutorial-Serie zum Thema, wie mit Unix-Tools einfache Datenanalysen durchgeführt werden können.
In Diesem Artikel soll es zunächst um einige grundlegende Tools gehen, die einem dabei helfen können, CSV-Dateien oder andere Textdateien zu verarbeiten.
grep
Vermutlich das wichtigste Tool überhaupt und wohl bekannt. Der Vollständigkeit halber möchte ich trotzdem darauf eingehen. Mit grep können Dateien nach einem Suchmuster durchsucht werden.
grep suchmuster datei
Ausgegeben werden alle Zeilen, die dem Suchmuster entsprechen. Das Suchmuster ist eine Regular Expression, worauf ich hier aber nicht im Detail eingehen möchte. Wem regex gar nichts sagt, für den gibt es im Internet tausende Tutorials zu dem Thema.
Möglich ist auch, die Suche umzukehren und alle Zeilen zu erhalten, die nicht dem Muster entsprechen. Hierfür gibt es die Option -v
grep -v suchmuster datei
Eine weitere praktische Option ist -c, womit die betroffenen Zeilen gezählt werden. Die Ausgabe enthält dann nur die Anzahl der Zeilen, die dem Muster entsprechen.
sort
Mit sort können Daten sortiert werden. Dies ist nicht nur für eine schöne Ausgabe wichtig, sondern diverse Tools, die wir uns noch anschauen werden, setzten sortierte Daten voraus.
sort liest Daten von stdin, oder optional aus einer Datei, und gibt die Daten in sortierter Reihenfolge auf stdout aus. Dabei gibt es mehrere Optionen dafür, wie sortiert werden soll. Ebenso ist es möglich, nach einer bestimmten Spalte in den Daten zu sortieren. Dabei wird Space als Standardtrennzeichen zwischen Spalten benutzt.
Ein Beispiel:
$ cat > testdata
13 9
41 3
14 6
12 7
99 5
$ sort testdata
12 7
13 9
14 6
41 3
99 5
sort -k 2 testdata
41 3
99 5
14 6
12 7
13 9
paste
paste fügt Dateien zeilenweise zusammen
paste [datei1] [datei2] ...
Standardmäßig werden die Zeilen durch einen Tabulator getrennt. Alternativ kann mit der Option -d ein anderes Trennzeichen angegeben werden.
$ cat > col1
project
dav
xnedit
ucx
$ cat > col2
version
1.2.4
1.1.1
2.0
$ paste -d ';' col1 col2
project;version
dav;1.2.4
xnedit;1.1.1
ucx;2.0
uniq
Mit uniq können Duplikate in der Eingabe gefiltert werden. Hierfür müssen die Daten bereits sortiert sein.
$ cat > data1
rhel
ubuntu
fedora
debian
ubuntu
rhel
ubuntu
arch
opensuse
fedora
rhel
debian
$ sort data1 | uniq
arch
debian
fedora
opensuse
rhel
ubuntu
uniq kann auch zählen, wie oft einzelne Zeilen vorkommen:
$ sort data1 | uniq -c
1 arch
2 debian
2 fedora
1 opensuse
3 rhel
3 ubuntu
Bedauerlicherweise ist es nicht möglich anzugeben, dass nur eine bestimmte Spalte betrachtet werden soll. Es können jedoch die ersten Felder übersprungen werden mit der Option -f nfields, wobei nfields die Anzahl der Felder ist, die übersprungen werden sollen.
cut
Mit Hilfe von cut können einzelne Bereiche aus einer Eingabe extrahiert werden. Dies können zum einen einzelne Byte-Ranges sein, aber auch einzelne Felder in tabellarischen Daten.
Felder können mit der Option -f feldliste angegeben werden. Die Feldliste ist entweder die Nummer eines Feldes, oder eine Kommagetrennte Liste an Feldnummern.
Wer ein anderes Trennzeichen als Tabulatoren benötigt, kann dies mit der -d Option angeben.
Ein Beispiel:
$ cat > machines
server1 admin 192.168.1.10
server2 root 192.168.1.20
client1 user1 192.168.1.100
client2 user2 192.168.1.101
$ cut -d ' ' -f 2 machines
admin
root
user1
user2
bash-5.0$
$ cut -d ' ' -f 1,3 machines
server1 192.168.1.10
server2 192.168.1.20
client1 192.168.1.100
client2 192.168.1.101
Kombiniertes Beispiel
Zum Abschluss wollen wir das Ganze testen, in dem wir Log-Dateien im Common Log Format analysieren. Angenommen wir haben eine Datei access.log mit folgendem Inhalt:
192.168.1.30 - - [08/Dec/2019:14:10:23 +0100] "GET /index.html HTTP/1.1" 200 5126
192.168.1.20 - - [08/Dec/2019:14:13:15 +0100] "GET /article1.html HTTP/1.1" 200 1876
192.168.1.65 - - [08/Dec/2019:14:14:55 +0100] "GET /index.html HTTP/1.1" 200 5126
192.168.1.30 - - [08/Dec/2019:14:16:01 +0100] "GET /article1.html HTTP/1.1" 200 1876
192.168.1.30 - - [08/Dec/2019:14:17:53 +0100] "GET /blog.css HTTP/1.1" 200 1543
192.168.1.65 - - [08/Dec/2019:14:19:12 +0100] "GET /downloads.html HTTP/1.1" 200 1245
192.168.1.71 - - [08/Dec/2019:14:23:59 +0100] "GET /article1.html HTTP/1.1" 200 1876
192.168.1.30 - - [08/Dec/2019:14:25:21 +0100] "GET /article2.html HTTP/1.1" 200 6362
192.168.1.20 - - [08/Dec/2019:14:26:17 +0100] "GET /index.html HTTP/1.1" 200 5126
192.168.1.30 - - [08/Dec/2019:14:28:05 +0100] "GET /index.html HTTP/1.1" 200 5126
192.168.1.65 - - [08/Dec/2019:14:31:10 +0100] "GET /package.zip HTTP/1.1" 200 65283
Als Beispiel möchten wir ermitteln, wie oft welche Seite aufgerufen wurde. Der Teil, den wir dafür genauer anschauen wollen ist das Feld 7. Die Daten aus diesem Feld erhalten wir mit folgendem Befehl:
$ cut -d ' ' -f 7 access.log
Mit uniq kann das Vorkommen jedes einzelnen Datensatzes gezählt werden. Die Daten müssen dafür jedoch sortiert sein, wofür sort zum Einsatz kommt. Mittels einer Pipe können wir das Ganze kombinieren:
$ cut -d ' ' -f 7 access.log | sort | uniq -c
3 /article1.html
1 /article2.html
1 /blog.css
1 /downloads.html
4 /index.html
1 /package.zip
Als zweites Beispiel möchten wir nachschauen, welche Seiten wie oft von der IP 192.168.1.30 aufgerufen wurden. Hierfür verwenden wir grep, um erst die Daten zu filtern.
$ grep '192\.168\.1\.30' access.log | cut -d ' ' -f 7 | sort | uniq -c
1 /article1.html
1 /article2.html
1 /blog.css
2 /index.html
So viel zu diesen Tools. In Teil 2 werden noch weitere Werkzeuge vorgestellt.
Mein bevorzugtes Tool, um z.B. aus PDFs Seiten zu extrahieren oder Dokumente zusammenzufügen, war mal pdftk, was allerdings nicht mehr weiterentwickelt wird und da es auch auf gcj basiert, der auch Geschichte ist, kriegt man es auch nicht mehr wirklich zum laufen. Als Alternative habe ich stapler entdeckt, was ähnlich leicht zu benutzen ist. Das Tool gibts auch im Fedora-Repo, dort heißt es allerdings pdf-stapler und wird auch über diesen Namen aufgerufen. Bei Ubuntu gibts das Programm hingegen nicht im Repository.
Hier mal ein paar Beispiele:
Seiten extrahieren
stapler sel doc.pdf 1 out.pdf
stapler sel doc.pdf 4-6 out.pdf
Reihenfolge von Seiten umkehren
stapler sel doc.pdf 3-1 out.pdf
Seiten drehen
stapler sel doc.pdf 1D out.pdf
stapler sel doc.pdf 1L out.pdf
stapler sel doc.pdf 1R out.pdf
D steht für Down, L für Left und R für Right.
Dokumente zusammenfügen
stapler sel file1.pdf file2.pdf out.pdf
Es können nach dem Dateinamen auch Selektierungsanweisungen (siehe oben) angegeben werden:
stapler sel file1.pdf 1-2 file2.pdf 6-9 out.pdf
Dokument splitten
stapler split doc.pdf