|
michahe
Anmeldungsdatum: 12. Dezember 2013
Beiträge: 888
|
Hallo, aus Maschinensteuerungen erhalte ich Datenlisten, A.txt und B.txt mit "Namen". Alle "Namen" in B.txt sind auch in A.txt vorhanden. Zu den "Namen" gibt es A.txt und B.txt ergänzende Daten, die mit einem oder mehreren BLANKS getrennt sind. Nun möchte ich eine neue Liste C.txt erstellen lassen, in der
zu jedem Namen definiert wird, ob er nur in B oder auch in A vorkommt, die ergänzenden Daten zu den Namen mit TAB (unten gekennzeichnet mit "| ") separiert geschrieben werden (d.h.: Blank erkennen und in TAB wandeln).
A.txt:
anton 17
beate 12
bettina 12
christian 3
B.txt:
anton 17 d4 uu
august 22 f5 88
beate 12 c55 xxx
bettina 12 q3 ww
christian 3 a1 tt
sabine 5 d6 ww
C.txt:
Quelle|Name |K1 |K2 |K3
A |antony |17 |d4 |uu
B |august |22 |f5 |88
A |beate |12 |c55|xxx
A |bettina |12 |q3 |ww
A |christian|3 |a1 |tt
B |sabine |5 |d6 |ww Wie würdet Ihr das angehen? Danke, Michael
|
|
seahawk1986
Anmeldungsdatum: 27. Oktober 2006
Beiträge: 11311
Wohnort: München
|
michahe schrieb:
Alle "Namen" in B.txt sind auch in A.txt vorhanden. [...]
Das passt nicht zusammen - meinst du, dass alle Namen, die in A vorkommen auch in B sind und du wissen willst, ob die nur in B vorhanden sind? Mit der Shell ginge das z.B. dann z.B. so:
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 | #!/usr/bin/bash
input_1="A.txt"
input_2="B.txt"
output="C.txt"
declare -A B_only
# get names only in the second input file
for e in $(cut -d ' ' -f 1 "$input_1" "$input_2" | sort | uniq -u)
do
B_only["$e"]=1
done
# get maximum number of fields from the second input file
n_fields=$(awk '{if (NF > max) {max = NF; line=$0}} END{print line}' "$input_2" | IFS=" " wc -w)
# print the header to the output file
echo -ne "Quelle\tName" > output
for n in seq(1,$(( n_fields - 1))):
echo -ne "\tK${n}" >> output
echo >> output
# process the second input file and write the output
while read -ra line
do
name="${line[0]}"
[ "${B_only["$name"]:=0}" -eq 1 ] && origin="B" || origin="A"
echo -ne "$origin$(printf "\t%s" "${line[@]}")\n"
done < "$input_2" >> output
|
Ich habe auch noch eine Lösung in Python geschrieben, die auch den Fall handeln kann, dass es Einträge in A gibt, die nicht in B sind:
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 python3
import csv
import pathlib
file_a = pathlib.Path("A.txt")
file_b = pathlib.Path("B.txt")
file_c = pathlib.Path("C.txt")
collection = dict()
max_n = 0
for filename in (file_a, file_b):
with open(filename) as f:
f_origin = filename.stem
for line in f:
idx, *data = line.split()
o, *_ = collection.get(idx, (None, None))
origin = o if o else f_origin
collection[idx] = (origin, *data)
max_n = max(len(data), max_n)
with open(file_c, 'w', newline='') as csvfile:
fieldnames = ('Quelle', 'Name', *[f"K{n}" for n in range(1, max_n + 1)])
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter='\t')
writer.writeheader()
for k, v in sorted(collection.items()):
origin, *data = v
write_data = {'Quelle': origin, 'Name': k}
for n, e in enumerate(data, start=1):
write_data[f"K{n}"] = e
writer.writerow(write_data)
|
|
|
michahe
(Themenstarter)
Anmeldungsdatum: 12. Dezember 2013
Beiträge: 888
|
seahawk1986 schrieb: michahe schrieb:
Alle "Namen" in B.txt sind auch in A.txt vorhanden.
[...]
Das passt nicht zusammen - meinst du, dass alle Namen, die in A vorkommen auch in B sind und du wissen willst, ob die nur in B vorhanden sind?
Hmm. A.txt ist die lange Liste, B.txt die kurze. Ich möchte für alle Einträge der langen Liste das Kennzeichen A (aus der langen Liste) oder B (nur in der kurzen Liste) ermitteln.
|
|
seahawk1986
Anmeldungsdatum: 27. Oktober 2006
Beiträge: 11311
Wohnort: München
|
michahe schrieb: Hmm. A.txt ist die lange Liste, B.txt die kurze. Ich möchte für alle Einträge der langen Liste das Kennzeichen A (aus der langen Liste) oder B (nur in der kurzen Liste) ermitteln.
Da passt dein Beispiel nicht so ganz dazu (woher der Wechsel von anton zu anthony kommt, ist mir da auch nicht klar) - kannst du da mal eine Problembeschreibung und Beispiele zeigen, die in sich stimmig sind?
|
|
shiro
Supporter
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1466
|
michahe schrieb: Nun möchte ich eine neue Liste C.txt erstellen lassen, in der
zu jedem Namen definiert wird, ob er nur in B oder auch in A vorkommt, die ergänzenden Daten zu den Namen mit TAB (unten gekennzeichnet mit "| ") separiert geschrieben werden (d.h.: Blank erkennen und in TAB wandeln).
Das ist ein eine klassische "join" Aufgabe. Nicht nur SQL beherrscht das. Da die Dateien a.txt und b.txt ja nicht zwangsläufig sortiert sind, müssen sie allerdings zuvor sortiert werden. Weil du in der ersten Spalte das A oder B haben willst, muss man das Merkmal den Tabellen auch erst noch mit sed zufügen.
Daher lautet die Lösung:
$ cat a.txt | sort | sed 's/^/A /' >$$;cat b.txt | sort | sed 's/^/B /' | join -j 2 -a 2 -o 1.1 2.1,2.2,2.3,2.4,2.5 $$ - | sed 's/^A B/A/;s/^ //;s/ /\t/g'
A anton 17 d4 uu
B august 22 f5 88
A beate 12 c55 xxx
A bettina 12 q3 ww
A christian 3 a1 tt
B sabine 5 d6 ww
$
Die angezeigten Spalten aus der ersten und 2. Datei kann in der Reihenfolge über das "-o" festgelegt werden. Der sed Befehl am Ende bereitet die erste Spalte auf und sorgt für den TAB als Trennzeichen.
|
|
rklm
Projektleitung
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13372
|
Das ist auch eine schöne Aufgabe für awk, aber bevor ich hier irgendwelche Vorschläge mache, kann ich nurseahawk1986 zustimmen, der schrieb:
Da passt dein Beispiel nicht so ganz dazu (woher der Wechsel von anton zu anthony kommt, ist mir da auch nicht klar) - kannst du da mal eine Problembeschreibung und Beispiele zeigen, die in sich stimmig sind?
Insbesondere müssen wir wissen:
Ist A.txt oder B.txt die lange Liste, die die Basis für die Ausgabe werden soll? Die Werte aus welcher Datei soll man hinter der ersten Spalte ("B" oder "A") ausgeben? Wenn die kurze Liste die Werte enthält, die Du ausgeben willst: Was willst Du für diejenigen Zeilen ausgeben, die nicht in der kurzen Liste sind?
Also, wie üblich: erst mal Anforderungen klären, bevor man etwas umsetzen kann.
|
|
michahe
(Themenstarter)
Anmeldungsdatum: 12. Dezember 2013
Beiträge: 888
|
Entschuldigung, es hätte heißen müssen: Alle "Namen" in A.txt (kurze Liste) sind auch in B.txt (lange Liste) vorhanden.
[...]
Nochmal Entschuldigung, bei Anton / Antony hatte ich versucht, allen "Namen" die gleiche Länge zu verpassen; als dies misslang, habe ich vergessen das "y" zu löschen.
Mit der Shell ginge das z.B. dann z.B. so:
Diese Variante möchte ich versuchen:
for n in seq(1,$(( n_fields - 1))):
kommt ein Syntax-Fehler und auch mit ShellCheck finde ich die Ursache nicht. Danke, Michael
|
|
rklm
Projektleitung
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13372
|
michahe schrieb:
for n in seq(1,$(( n_fields - 1))):
kommt ein Syntax-Fehler und auch mit ShellCheck finde ich die Ursache nicht.
Das muss wohl eher heißen | for n in $(seq 1 $(( n_fields - 1))); do
echo -ne "\tK${n}" >> output
done
echo >> output
|
|
|
seahawk1986
Anmeldungsdatum: 27. Oktober 2006
Beiträge: 11311
Wohnort: München
|
michahe schrieb: Das legt fest, dass die Variable B_only ein Assoziativer Array sein soll. for n in seq(1,$(( n_fields - 1))):
kommt ein Syntax-Fehler und auch mit ShellCheck finde ich die Ursache nicht.
Ja, da hat das gedankliche Umschalten zwischen Python's range() Funktion und seq nicht ganz funktioniert... - die Lösung von rklm sollte passen.
|
|
shiro
Supporter
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1466
|
seahawk1986 schrieb: for n in seq(1,$(( n_fields - 1))):
Ok, wenn mit einer variablem Anzahl von Spalten in b.txt gearbeitet werden soll, wäre der join Befehl als 1-Zeiler wie folgt abzuändern:
$ a=a.txt;b=b.txt;srt() { sort $1 | sed "s/^/$2 /"; }; join -j2 -a2 -o 1.1,$(eval echo 2.{1..$(head -1 $b | srt - B | wc -w)} | tr " " ",") <(srt $a A) <(srt $b B) | sed "s/^A B / A /" | column -t; unset srt
A anton 17 d4 uu
B august 22 f5 88
A beate 12 c55 xxx
A bettina 12 q3 ww
A christian 3 a1 tt
B sabine 5 d6 ww
Bei größeren Tabellen ist join wesentlich schneller als awk, ganz zu schweigen von einer bash-Loop.
|
|
rklm
Projektleitung
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13372
|
shiro schrieb:
Bei größeren Tabellen ist join wesentlich schneller als awk,
Kannst Du das etwas mehr ausführen? Meines Wissens benutzt awk Hashtabellen für assoziative Arrays (siehe Ende dieser Sektion). Wie kann join da "wesentlich" schneller sein?
ganz zu schweigen von einer bash-Loop.
Klar, das ist ja O(N) und nicht O(1) wie bei einer Hashtabelle.
|
|
shiro
Supporter
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1466
|
rklm schrieb: Kannst Du das etwas mehr ausführen?
Ich habe mal folgendes Beispiel rechnen lassen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | #!/bin/bash
[ -z "$1" ] || NAMMinLen=$1
[ -z "$NAMMinLen" ] && NAMMinLen=8
Anz=${2:-1}
echo "" | gawk \
-v NAMMinLen="$NAMMinLen" \
-v Anz="$Anz" \
'BEGIN {
seed=srand()
lchr=split("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",achr,"")
tail=split("17 d4 uu|22 f5 88|12 c55 xxx|12 q3 ww|3 a1 tt|5 d6 ww",atail,"|")
}{
for(j=1;j<=Anz;j++){
NAM=""
for(i=1;i<=NAMMinLen;i++){ NAM=NAM achr[int(rand()*lchr)+1] }
print NAM " " atail[j%tail+1]
}
}'
|
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 | #!/usr/bin/bash
input_1="aaa.txt"
input_2="bbb.txt"
output="ccc.txt"
declare -A B_only
# get names only in the second input file
for e in $(cut -d ' ' -f 1 "$input_1" "$input_2" | sort | uniq -u)
do
B_only["$e"]=1
done
# get maximum number of fields from the second input file
n_fields=$(awk '{if (NF > max) {max = NF; line=$0}} END{print line}' "$input_2" | IFS=" " wc -w)
# print the header to the output file
echo -ne "Quelle\tName" > output
for n in $(seq 1 $(( n_fields - 1))); do
echo -ne "\tK${n}" >> $output
done
echo >> $output
# process the second input file and write the output
while read -ra line
do
name="${line[0]}"
[ "${B_only["$name"]:=0}" -eq 1 ] && origin="B" || origin="A"
echo -ne "$origin$(printf "\t%s" "${line[@]}")\n"
done < "$input_2" >> $output
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | $ . namen.sh 8 100000 >bbb.txt
$ cat bbb.txt | cut -d" " -f1 | uniq -u | wc -l
100000
$ head -100 bbb.txt | cut -d" " -f1,2 | sort >aaa.txt
$ cat bbb.txt | sort >$$
$ mv $$ bbb.txt
$ time . ./join.sh
real 3m13,977s
user 1m56,171s
sys 1m26,214s
$
$ time (a=aaa.txt;b=bbb.txt;srt() { sort $1 | sed "s/^/$2 /"; }; join -j2 -a2 -o 1.1,$(eval echo 2.{1..$(head -1 $b | srt - B | wc -w)} | tr " " ",") <(srt $a A) <(srt $b B) | sed "s/^A B / A /;s/^ //;s/ /\t/g" >jjj.txt)
real 0m0,161s
user 0m0,101s
sys 0m0,034s
$
$ echo "scale=2;(3*60+13.997)/0.161" | bc
1204.95
$ # Mit join ist die Aufgabe ca 1200 Mal schneller erledigt
|
Ein awk Beispiel habe ich bei diesen Posts nicht gefunden. Ich kann natürlich ein awk schreiben, möchte dies aber den Protagonisten überlassen.
Beim Vergleich join Befehl zu bash-script erledigt der join Befehl die Aufgabe ca 1200 Mal schneller als das bash-script. PS: ich habe ein awk script geschrieben, dass praktisch die gleiche Laufzeit hat wie der join Befehl (user 0m0,147s). Das bedeutet, dass
| $ echo "scale=2;0.147/0.101" | bc
1.45
|
awk ca 45% länger braucht als der join Befehl. Na ja, eventuell schafft ihr es schneller. Bearbeitet von rklm: Farbe Bearbeitet von rklm: Und [mark] entfernt, was hier nicht mehr funktioniert
|
|
rklm
Projektleitung
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13372
|
Golfen wir ein wenig... | $ time (a=aaa.txt;b=bbb.txt;srt() { sort $1 | sed "s/^/$2 /"; }; join -j2 -a2 -o 1.1,$(eval echo 2.{1..$(head -1 $b | srt - B | wc -w)} | tr " " ",") <(srt $a A) <(srt $b B) | sed "s/^A B / A /;s/^ //;s/ /\t/g" >jjj.txt)
real 0m0,420s
user 0m0,225s
sys 0m0,008s
$ time ./a.awk aaa.txt bbb.txt >| awk.txt
real 0m0,352s
user 0m0,324s
sys 0m0,025s
|
-16%. a.awk sieht so aus | #!/usr/bin/awk -f
FNR==NR {++a[$1]}
FNR!=NR {printf "%s", a[$1]?"A":"B"; for (i=1;i<=NF;++i) printf "\t%s", $i; printf "\n"}
|
Sorry, das Syntaxhighlighting für awk kennt anscheinend den ternären Operator nicht.
|
|
shiro
Supporter
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1466
|
rklm schrieb: Golfen wir ein wenig...
Hallo rklm, die Einladung zum Golfen hat mich richtig erfreut! Ich habe dein awk script bei mir auch mal laufen lassen und folgendes Ergebnis erhalten:
$ time ./a.awk aaa.txt bbb.txt >|awk2.txt
real 0m0,298s
user 0m0,273s
sys 0m0,025s
Na ja, damit ist das auf meinem Rechner doch einen Tick langsamer. Aber ich würde dennoch sagen, dass die Ergebnisse von awk-Script und dem join Befehl praktisch gleich schnell sind.
Da ich normalerweise das Attribut eines awk-Fans habe, war es mir nicht so leicht, den join Befehl zu loben. Dennoch lernt man ja nie aus. Deinen "FNR!=NR" Trick um den Wechsel zwischen den beiden Dateien festzustellen (Chapeau!) hatte ich noch nie benutzt. Im Vergleich zu deinem eleganten 2-Zeiler ist mein awk ja richtig plump:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | #!/bin/bash
a=aaa.txt
b=bbb.txt
sort $a >tmp.txt
sort $b | gawk -v a="tmp.txt" 'BEGIN {
getline <a
name=$1
}{
if( $1 == name ) {
print "A "$0
getline <a
name=$1
} else {
print "B "$0
}
}' | tr " " "\t"
|
Mein obiges awk Script liefert auf meinem Rechner folgendes Ergebnis:
$ time . awk.sh >x.xxx
real 0m0,144s
user 0m0,132s
sys 0m0,041s
Kannst ja, mal schauen wie schnell das Script auf deinem Rechner ist (da gibt es offenbar Unterschiede). Ich habe einen Lenovo T420s von 2011. CPU, Memory und Disk verursachen die marginalen Unterschiede.
|
|
rklm
Projektleitung
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13372
|
shiro schrieb: rklm schrieb: Golfen wir ein wenig...
Hallo rklm, die Einladung zum Golfen hat mich richtig erfreut!
☺
Na ja, damit ist das auf meinem Rechner doch einen Tick langsamer. Aber ich würde dennoch sagen, dass die Ergebnisse von awk-Script und dem join Befehl praktisch gleich schnell sind.
Da kann ich mitgehen. Ich finde allerdings mein awk-Skript deutlich lesbarer (gut, man kann es noch auf mehrere Zeilen auffächern).
Da ich normalerweise das Attribut eines awk-Fans habe, war es mir nicht so leicht, den join Befehl zu loben. Dennoch lernt man ja nie aus. Deinen "FNR!=NR" Trick um den Wechsel zwischen den beiden Dateien festzustellen (Chapeau!) hatte ich noch nie benutzt.
Ja, den habe ich auch irgendwann mal aufgeschnappt - ich glaube, das war sogar hier im Forum. ☺
Im Vergleich zu deinem eleganten 2-Zeiler ist mein awk ja richtig plump:
Aber der Ansatz ist gut (s.u.)!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | #!/bin/bash
a=aaa.txt
b=bbb.txt
sort $a >tmp.txt
sort $b | gawk -v a="tmp.txt" 'BEGIN {
getline <a
name=$1
}{
if( $1 == name ) {
print "A "$0
getline <a
name=$1
} else {
print "B "$0
}
}' | tr " " "\t"
|
Ich bin mir nicht sicher, dass dieses Skript korrekt arbeitet. Der Grund ist, dass Du normalerweise mindestens eine Schleife brauchst, wenn Du so aus zwei sortierten Eingaben liest. Der Ansatz an sich ist gut, weil der besser arbeitet, falls beide Dateien richtig groß sind, weil man nicht alle Schlüssel einer Datei als Index in den Speicher laden muss. Ich muss da noch mal drüber schlafen, weil ich jetzt keine Zeit habe. Wollte im Wesentlichen das Messergebnis posten (s.u.).
Kannst ja, mal schauen wie schnell das Script auf deinem Rechner ist (da gibt es offenbar Unterschiede). Ich habe einen Lenovo T420s von 2011. CPU, Memory und Disk verursachen die marginalen Unterschiede.
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
31
32
33
34
35
36
37
38
39
40
41
42 | $ for i in {1..5}; do time ./a.awk aaa.txt bbb.txt >| awk.txt; done
real 0m0,365s
user 0m0,329s
sys 0m0,032s
real 0m0,366s
user 0m0,300s
sys 0m0,055s
real 0m0,363s
user 0m0,311s
sys 0m0,041s
real 0m0,363s
user 0m0,307s
sys 0m0,048s
real 0m0,365s
user 0m0,332s
sys 0m0,020s
$ for i in {1..5}; do time ./b.awk >|awk.txt; done
real 0m0,250s
user 0m0,253s
sys 0m0,026s
real 0m0,320s
user 0m0,327s
sys 0m0,016s
real 0m0,268s
user 0m0,244s
sys 0m0,035s
real 0m0,254s
user 0m0,255s
sys 0m0,021s
real 0m0,328s
user 0m0,328s
sys 0m0,021s
|
Deine Lösung im Schnitt 0,284s "real", meine 0,364s.
|