ubuntuusers.de

AWK - zwei Dateien (Listen) sortiert zusammenführen

Status: Gelöst | Ubuntu-Version: Ubuntu 20.04 (Focal Fossa)
Antworten |

Nobody0815

Anmeldungsdatum:
29. April 2020

Beiträge: 12

Hallo,

hoffentlich kann mir jemand helfen! ☺

Ich habe zwei Dateien (1_liste, 2_liste). Inhalt der Dateien:

1_liste

AU001Krueger83747
AU003Meier03948
AU002Schulze09447
AU004Mueller0013848
AU006Schneider0404003
AU005Weber847847005
AU007Schmidt74904949

2_liste

AU002Emilia948948
AU005Liam93765
AU001Anna0038383
AU006Jonas363355
AU003Leonie37346
AU004Paul49494

Die beiden Listen sollen sortiert, relevante Informationen (Namen) herausgeschnitten und zusammengeführt werden. Es sollen nur Namen zusammengeführt werden, wo Vor- und Nachname existiert (AU001-AU005). Zu AU007 (Schmidt) gibt es keinen Vornamen (2_liste) und wird nicht berücksichtigt.

Ergebnis soll so aussehen:

Anna Krueger
Emilia Schulze
Leonie Meier
Paul Mueller
Liam Weber

Mein (nicht funktionierender) Ansatz:

cat 1_liste | awk '
 (FILENAME=="-"){substr($0,1,3)=AU1, substr($0,4,?)=vorname;}
(FILENAME!="-"){if(substr($0,4?)==AU1)print vorname, substr($0,4?)}
' 2_liste > Ergebnis.txt

Hat jemand eine Idee zu dem Problem?

Danke Nobody0815

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11263

Wohnort: München

Was ist denn in deinem Beispiel mit Jonas Schneider? Der fällt da irgendwie unter den Tisch...

Mit ohne awk würde ich das so machen (benötigt Python >= 3.8 wegen dem Walrus-Operator (:=):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
import re
import sys

entry_re = re.compile(r"(?P<index>\D+\d+)(?P<name>\D+)(?P<other>\d+)")

if len(sys.argv) != 3:
  print(f"usage: {sys.argv[0]} FILE1 FILE2", file=sys.stderr)
  exit(1)

with open(sys.argv[1]) as f1, open(sys.argv[2]) as f2:
    last_names = {}
    for line in f1:
        if (match := entry_re.match(line)):
            last_names[match.group('index')] = match.group('name')

    first_names = {}
    for line in f2:
        if (match := entry_re.match(line.strip())):
            first_names[match.group('index')] = match.group('name')

for key, name in sorted(last_names.items()):
    if (first_name := first_names.get(key)):
        print(first_name, name)
$ ./group_names.py 1_liste 2_liste
Anna Krueger
Emilia Schulze
Leonie Meier
Paul Mueller
Liam Weber
Jonas Schneider 

Nobody0815

(Themenstarter)

Anmeldungsdatum:
29. April 2020

Beiträge: 12

Hallo seahawk1986,

danke für die Überlegung mit Python.

'Jonas Schneider' habe ich in meiner Ergebisliste schlicht vergessen. Sorry 😕

Leider steht mir, auf dem Zielsystem, nur Python in der Version 2.7.17 zur Verfügung. Daran kann ich auch leider nichts ändern.

Kannst Du mir trotzdem die nachfolgende Zeile aus Deinem Skript erklären (Python ist noch Neuland für mich)?

1
try_re = re.compile(r"(?P<index>\D+\d+)(?P<name>\D+)(?P<other>\d+)")

Danke und VG Nobody0815

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11263

Wohnort: München

Nobody0815 schrieb:

Leider steht mir, auf dem Zielsystem, nur Python in der Version 2.7.17 zur Verfügung. Daran kann ich auch leider nichts ändern.

Dann ist es etwas unsinnig Ubuntu 20.04 als Systemversion anzugeben, bei dem mit großen Aufwand Python2 aus den Kernpaketen eliminiert und nach universe verschoben wurde 😇

Man kann das natürlich auch mit Python2 machen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import codecs
import re
import sys

entry_re = re.compile(r"(?P<index>\D+\d+)(?P<name>\D+)(?P<other>\d+)")

if len(sys.argv) != 3:
  print "usage: {} FILE1 FILE2".format(sys.argv[0]) >> sys.stderr
  exit(1)

with codecs.open(sys.argv[1], encoding='utf-8') as f1:
    last_names = {}
    for line in f1:
        match = entry_re.match(line)
        if match:
            last_names[match.group('index')] = match.group('name')

with codecs.open(sys.argv[2], encoding='utf-8') as f2:
    first_names = {}
    for line in f2:
        match = entry_re.match(line.strip())
        if match:
            first_names[match.group('index')] = match.group('name')

for key, name in sorted(last_names.items()):
    first_name = first_names.get(key)
    if first_name:
        print first_name, name

Kannst Du mir trotzdem die nachfolgende Zeile aus Deinem Skript erklären (Python ist noch Neuland für mich)?

1
try_re = re.compile(r"(?P<index>\D+\d+)(?P<name>\D+)(?P<other>\d+)")

Das kompiliert ein Objekt für einen regulären Ausdruck aus mit drei benannten Gruppen (in https://docs.python.org/2.7/library/re.html nach (?P<name>...) suchen). Das r vor dem String sorgt dafür, dass Backslashes nicht als Escape-Sequenzen interpretiert werden. Ich matche also für die Gruppe index auf mindestens ein Zeichen, das keine Zahl ist \D+, gefolgt von mindestens einer Ziffer \d+.

Die Zweite Gruppe name besteht aus mindestens einem Zeichen, das keine Ziffer ist und die dritte Gruppe other wiederum aus mindestens einer Ziffer.

Nobody0815

(Themenstarter)

Anmeldungsdatum:
29. April 2020

Beiträge: 12

Hallo,

@seahawk1986: Danke für die Mühe, aber meine Pythonkenntnisse sind leider nicht so besonders. Ich schaue mir Dein Skript bei Gelegenheit genauer an, brauche aber zeitnah eine Lösung.

Ich habe mein AWK-Script überarbeitet (siehe unten), dass Ergebnis geht in die richtige Richtung, ist aber noch nicht wirklich gut. Für den Augenblick reicht es mir, wenn die "AU-Nummern" am Anfang einer jeden Zeile genutzt werden und damit die beiden Listen zusammengeführt werden. Die Zahlenkolonnen nach den Namen können erstmal ignoriert werden.

Mein aktueller Code:

cat 2_liste | awk '
    (FILENAME=="-"){AUvorname=substr($0,1,5); vorname=substr($0,6);}
    (FILENAME!="-"){AUname=substr($0,1,5); name=substr($0,6);}
        {if(AUvorname == AUname) print vorname, name;}
' - 1_liste >> ERGEBNIS

Ergebnis der Ausführung:

Paul49494 Mueller0013848

Positiv: Vorname und Name ist korrekt zusammengeführt.

Negativ: Wo sind die restlichen Namen? Vermutlich überschreibe ich mir die Vornamen und Namen. Der Vorname 'Paul' steht ganz unten in '2_liste'. Der Grund für das Ergebnis ist, dass AWK die Listen nacheinander abarbeitet. Zuerst wird die Liste '2_liste', welche mit 'cat |' übergeben wurde, Zeile für Zeile abgearbeitet. Die Variable 'AUvorname' wird dabei immer wieder überschrieben. Der letzte Eintrag der Liste 'AU004' (vgl. Paul) bleibt zum Schluss in der Variable 'AUvorname' stehen. Jetzt wird die nächste Liste '1_liste' Zeile für Zeile abgearbeitet. An der Stelle, wo die IF-Bedingung greift (Variable 'AUvorname' = 'AUname') wird ein Eintrag in 'ERGEBNIS' erzeugt und das ist jetzt genau einmal der Fall. 😕

Das Problem verursacht die sequenzielle Abarbeitung der beiden Listen.

1. Lösungsansatz: Die Variablen 'AUvorname' und 'AUname' müssen Zeile für Zeile verglichen werden. Dazu müsste AWK ständig zwischen den beiden Listen wechseln. Geht das überhaupt?

2. Lösungsansatz: Die Variable 'AUvorname' muss in ein Array (oder etwas ähnliches), damit alle AUvorname-Einträge gespeichert werden und nicht nur der Letzte.

Wie erzeugt man mit awk ein Array und spricht diese hinterher wieder an? 🙄

Erklärung zu FILENAME: In FILENAME ist der Name der Datei gespeichert, die gearde abgearbeitet wird:

(FILENAME=="-") –> 2_liste –> Solange FILENAME = 2_liste mache irgendwas.

(FILENAME!="-") –> 2_liste –> Wenn FILENAME nicht mehr = 2_liste mache irgendwas anderes.

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11263

Wohnort: München

Hier mal ein awk-Skript, das die Dateien mit den Vor- und Nachnamen als Argumente nimmt und das selbe Ergebnis wie das Python-Skript liefert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/awk -f
(ARGIND==1) {
        s=substr($0,6)
        sub(/[0-9]+/,"", s)
        first_names[substr($0,1,5)]=s
}
(ARGIND==2) {
        s=substr($0,6)
        sub(/[0-9]+/,"", s)
        last_names[substr($0,1,5)]=s
}

END {
        asorti(last_names, sorted_last_names_indices)
        for (i in sorted_last_names_indices) {
                id = sorted_last_names_indices[i]
                if (id in first_names) {
                        print last_names[id] " " first_names[id]
                }
        }
}
$ ./test.awk 1_liste 2_liste
Anna Krueger
Emilia Schulze
Leonie Meier
Paul Mueller
Liam Weber
Jonas Schneider 

Nobody0815

(Themenstarter)

Anmeldungsdatum:
29. April 2020

Beiträge: 12

Hallo seahawk1986,

funktioniert super! 💡

Jetzt muss ich noch im www nachlesen, wie ARGIND und asorti genau funktionieren. 😎

Danke für die Lösung des Problems. 👍

VG Nobody0815

seahawk1986 schrieb:

Hier mal ein awk-Skript, das die Dateien mit den Vor- und Nachnamen als Argumente nimmt und das selbe Ergebnis wie das Python-Skript liefert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/awk -f
(ARGIND==1) {
        s=substr($0,6)
        sub(/[0-9]+/,"", s)
        first_names[substr($0,1,5)]=s
}
(ARGIND==2) {
        s=substr($0,6)
        sub(/[0-9]+/,"", s)
        last_names[substr($0,1,5)]=s
}

END {
        asorti(last_names, sorted_last_names_indices)
        for (i in sorted_last_names_indices) {
                id = sorted_last_names_indices[i]
                if (id in first_names) {
                        print last_names[id] " " first_names[id]
                }
        }
}
$ ./test.awk 1_liste 2_liste
Anna Krueger
Emilia Schulze
Leonie Meier
Paul Mueller
Liam Weber
Jonas Schneider 

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13212

seahawk1986 schrieb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/awk -f
(ARGIND==1) {
        s=substr($0,6)
        sub(/[0-9]+/,"", s)
        first_names[substr($0,1,5)]=s
}
(ARGIND==2) {
        s=substr($0,6)
        sub(/[0-9]+/,"", s)
        last_names[substr($0,1,5)]=s
}

END {
        asorti(last_names, sorted_last_names_indices)
        for (i in sorted_last_names_indices) {
                id = sorted_last_names_indices[i]
                if (id in first_names) {
                        print last_names[id] " " first_names[id]
                }
        }
}

Warum baust Du da runde Klammern um die Kriterien? Die sind jedenfalls überflüssig.

snafu1

Avatar von snafu1

Anmeldungsdatum:
5. September 2007

Beiträge: 2133

Wohnort: Gelsenkirchen

Mit geschicktem Pattern-Matching ist das auch als (etwas längerer) Einzeiler machbar.

Zunächst einmal die Trennung zwischen ID und dem Namen. Dabei gehe ich von 5 beliebigen Zeichen am Anfang aus, welche die ID repräsentieren. Dann folgt eine beliebige Anzahl von Groß- und Kleinbuchstaben. Am Ende kommt "etwas anders" (.*), nämlich diese Zahlen. Interessant sind aber nur die ersten beiden Dinge, weshalb ich hierfür Klammern nutze, um sie später als Gruppe ansprechen zu können. match() speichert das Ergebnis als Array ins letzte Argument (hier: m). Das erste Argument ist der zu durchsuchende Text (hier: komplette Zeile) und das zweite ist mein Pattern. Im Folgenden einfach mal stumpf die Ausgabe der Inhalte von der ersten und zweiten Gruppe:

$ awk 'match($0, /(.{5})([A-Za-z]+).*/, m){ print m[1], m[2] }' vornamen.txt nachnamen.txt
AU002 Emilia
AU005 Liam
AU001 Anna
AU006 Jonas
AU003 Leonie
AU004 Paul
AU001 Krueger
AU003 Meier
AU002 Schulze
AU004 Mueller
AU006 Schneider
AU005 Weber
AU007 Schmidt 

Und nun als Erweiterung das Merken der Namen über beide Dateien hinweg, sowie das abschließende Zusammenführen. ARGIND ist die Nummer der aktuellen Datei. Die Eins steht halt fürs erste Argument (hier: vornamen.txt) und die Zwei entsprechend für die Nachnamen. Ich lege nun also die ID und den Namen ins names-Array ab und trenne das nach dem jeweiligen ARGIND. Und am Ende, wenn alle Zeilen durch sind, werden halt meine Ergebnisse durchlaufen und zusammengeführt. Hier das Ganze in Code:

$ awk 'match($0, /(.{5})([A-Za-z]+).*/, m){ names[ARGIND][m[1]] = m[2] } END{ for (id in names[1]) print names[1][id], names[2][id] }' vornamen.txt nachnamen.txt
Leonie Meier
Paul Mueller
Liam Weber
Jonas Schneider
Anna Krueger
Emilia Schulze 

Ich hoffe, die Erklärung war einigermaßen anfängerfreundlich. 😉

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11263

Wohnort: München

rklm schrieb:

Warum baust Du da runde Klammern um die Kriterien? Die sind jedenfalls überflüssig.

Genauso wie der Whitespace und sprechende Variablennamen... - ich finde das visuell übersichtlicher, aber ich mache normalerweise kaum etwas mit awk, weil mir das ohne Typ-Prüfung und implizitem verschlucken von Fehlern nur bei "perfekten" Daten sinnvoll erscheint.

snafu1 schrieb:

Mit geschicktem Pattern-Matching ist das auch als (etwas längerer) Einzeiler machbar.

Interessant, dann kann awk also auch Gruppen in regulären Ausdrücken nutzen. Der Ansatz sortiert aber noch nicht die Ergebnisse nach der ID.

Zumindest mit gawk (Version GNU Awk 5.1.0, API: 3.0 (GNU MPFR 4.0.2, GNU MP 6.2.0)) schaut er auch nicht, ob es tatsächlich einen Vornamen zum Nachnamen gibt, da taucht Schmidt alleine in der Ausgabe auf (ich sehe da auch nichts im Code, das die Prüfung machen würde, ob es einen Eintrag im Assoziativen Array gibt):

$ awk 'match($0, /(.{5})([A-Za-z]+).*/, m){ names[ARGIND][m[1]] = m[2] } END{ for (id in names[1]) print names[1][id], names[2][id] }' 1_liste 2_liste
Meier Leonie
Mueller Paul
Weber Liam
Schneider Jonas
Schmidt
Krueger Anna
Schulze Emilia 

Nobody0815

(Themenstarter)

Anmeldungsdatum:
29. April 2020

Beiträge: 12

Hallo snafu1,

danke für die super Erklärung. 👍

Hat der Anfänger auf dieser Seite des Bildschirms grundsätzlich verstanden. ☺

VG Nobody0815

snafu1

Avatar von snafu1

Anmeldungsdatum:
5. September 2007

Beiträge: 2133

Wohnort: Gelsenkirchen

Hier mal ein Ansatz mit sed. Die Sortierung führe ich vorab durch, da jede Zeile durch ihre vorangestellte ID ja schon sortierbar ist. Mit dem paste-Kommando fügt er die beiden Dateien zeilenweise zu jeweils einer Zeile zusammen (getrennt durch einen Tabulator). Die Umleitungen sind an der Stelle nötig, damit ich die getrennten Datenströme von der Sortierung als Argumente ("Pseudo-Dateien") übergeben kann. Auch zur Übergabe an sed habe ich eine Umleitung benutzt, weil ich dann die Dateinamen am Ende stehen hab - könnte man aber auch mit Pipes machen und entsprechend nach vorne stellen. Bei sed nutze ich nun das schon bekannte Pattern, aber quasi doppelt (getrennt durch den Tabulator). Die IDs sind mir hierbei egal, weil ich schon die gewünschte Sortierung habe. Meine Gruppen 1 und 2 sind an der Stelle also Vor- und Nachname. Sieht dann am Ende so bei mir aus:

$ sed -rn 's/.{5}([A-Za-z]+).*\t.{5}([A-Za-z]+).*/\1 \2/p' <(paste <(sort vornamen.txt) <(sort nachnamen.txt))
Anna Krueger
Emilia Schulze
Leonie Meier
Paul Mueller
Liam Weber
Jonas Schneider 

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11263

Wohnort: München

Das funktioniert nur zufällig, weil Schmidt nach dem Sortieren der Dateien am Ende steht. Wenn so eine Lücke in den Daten bei einem Eintrag davor auftreten kann, wird es zu einer Leserasterverschiebung mit falschen Paaren für die nachfolgenden Einträge kommen.

snafu1

Avatar von snafu1

Anmeldungsdatum:
5. September 2007

Beiträge: 2133

Wohnort: Gelsenkirchen

Dann nochmal mit Unterstützung von awk:

$ awk 'match($0, /(.{5})([A-Za-z]+).*/, m) {if (m[1]==id) {print name, m[2]} id=m[1]; name=m[2]}' <(paste -d'\n' <(sort vornamen.txt) <(sort nachnamen.txt))
Anna Krueger
Emilia Schulze
Leonie Meier
Paul Mueller
Liam Weber
Jonas Schneider 

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11263

Wohnort: München

Das klappt bei mehreren Lücken in den Indices nicht - z.B.:

vornamen.txt

1
2
3
4
5
6
AU001Simpson4358309
AU004Simpson4358309
AU006Simpson4358309
AU017Burns23489
AU010Smithers439508
AU014Quimby3048998

nachnamen.txt

1
2
3
4
5
6
7
8
AU001Homer4358394
AU003Bar3248932
AU005Marge345893748
AU004Lisa384723894
AU006Maggie430598
AU009Grandpa434390
AU017Charles4958
AU014Joe439584
awk 'match($0, /(.{5})([A-Za-z]+).*/, m) {if (m[1]==id) {print name, m[2]} id=m[1]; name=m[2]}' <(paste -d'\n' <(sort vornamen.txt) <(sort nachnamen.txt))
Homer Simpson
Simpson Lisa 

In dem Fall kommt er mit Vor- und Nachnamen durcheinander und gibt nicht alle Matches aus, die man erwarten würde...

Homer Simpson
Lisa Simpson
Maggie Simpson
Joe Quimby
Charles Burns
Antworten |