![Wie funktioniert awk '!a[$0]++'?](https://rvso.com/image/57077/Wie%20funktioniert%20awk%20'!a%5B%240%5D%2B%2B'%3F.png)
Dieser Einzeiler entfernt doppelte Zeilen aus der Texteingabe ohne Vorsortierung.
Zum Beispiel:
$ cat >f
q
w
e
w
r
$ awk '!a[$0]++' <f
q
w
e
r
$
Der Originalcode, den ich im Internet gefunden habe, lautete:
awk '!_[$0]++'
Das war für mich umso verwirrender, da ich dachte, _
es hätte in awk eine spezielle Bedeutung, wie in Perl, sich aber herausstellte, dass es nur der Name eines Arrays war.
Jetzt verstehe ich die Logik hinter dem Einzeiler: Jede Eingabezeile wird als Schlüssel in einem Hash-Array verwendet, sodass der Hash nach Abschluss eindeutige Zeilen in der Reihenfolge ihres Eintreffens enthält.
Was ich gerne erfahren würde, ist, wie genau diese Notation von awk interpretiert wird. Also was das Ausrufezeichen ( !
) bedeutet und die anderen Elemente dieses Codeausschnitts.
Wie funktioniert es?
Antwort1
Hier ist eine "intuitive" Antwort, für eine ausführlichere Erklärung des awk-Mechanismus siehe entweder @Cuonglm's
In diesem Fall kann !a[$0]++
das Postinkrement ++
für einen Moment beiseite gelegt werden, es ändert den Wert des Ausdrucks nicht. Betrachten Sie also nur !a[$0]
. Hier:
a[$0]
verwendet die aktuelle Zeile $0
als Schlüssel für das Array a
und übernimmt den dort gespeicherten Wert. Wenn dieser bestimmte Schlüssel noch nie zuvor referenziert wurde, a[$0]
wird die leere Zeichenfolge ausgewertet.
!a[$0]
Dies !
negiert den Wert von vorher. Wenn er leer oder null (falsch) war, haben wir jetzt ein wahres Ergebnis. Wenn er ungleich null (wahr) war, haben wir ein falsches Ergebnis. Wenn der gesamte Ausdruck als wahr ausgewertet wurde, also a[$0]
nicht von Anfang an gesetzt war, wird die gesamte Zeile als Standardaktion gedruckt.
Außerdem addiert der Post-Increment-Operator, unabhängig vom alten Wert, eins zu a[$0]
, sodass der nächste Zugriff auf denselben Wert im Array positiv sein wird und die gesamte Bedingung fehlschlägt.
Antwort2
Hier die Abwicklung:
a[$0]
: Sehen Sie sich den Wert des Schlüssels$0
im assoziativen Array ana
. Wenn er nicht vorhanden ist, erstellen Sie ihn automatisch mit einer leeren Zeichenfolge.a[$0]++
: Erhöhen Sie den Wert vona[$0]
, geben Sie den alten Wert als Wert des Ausdrucks zurück. Der++
Operator gibt einen numerischen Wert zurück, wenn alsoa[$0]
anfangs leer war,0
wird zurückgegeben unda[$0]
auf erhöht1
.!a[$0]++
: negiert den Wert des Ausdrucks. Wenn (ein falscher Wert)a[$0]++
zurückgegeben wird , wird der gesamte Ausdruck als „true“ ausgewertet und die Standardaktion ausgeführt . Andernfalls wird keine weitere Aktion ausgeführt, wenn der gesamte Ausdruck als „false“ ausgewertet wird.0
awk
print $0
Verweise:
Mit gawk
können wir verwendendgawk (oder awk --debug
mit neuerer Version)um ein gawk
Skript zu debuggen. Erstellen Sie zunächst ein gawk
Skript mit dem Namen test.awk
:
BEGIN {
a = 0;
!a++;
}
Dann renne:
dgawk -f test.awk
oder:
gawk --debug -f test.awk
In der Debugger-Konsole:
$ dgawk -f test.awk
dgawk> trace on
dgawk> watch a
Watchpoint 1: a
dgawk> run
Starting program:
[ 1:0x7fe59154cfe0] Op_rule : [in_rule = BEGIN] [source_file = test.awk]
[ 2:0x7fe59154bf80] Op_push_i : 0 [PERM|NUMCUR|NUMBER]
[ 2:0x7fe59154bf20] Op_store_var : a [do_reference = FALSE]
[ 3:0x7fe59154bf60] Op_push_lhs : a [do_reference = TRUE]
Stopping in BEGIN ...
Watchpoint 1: a
Old value: untyped variable
New value: 0
main() at `test.awk':3
3 !a++;
dgawk> step
[ 3:0x7fe59154bfc0] Op_postincrement :
[ 3:0x7fe59154bf40] Op_not :
Watchpoint 1: a
Old value: 0
New value: 1
main() at `test.awk':3
3 !a++;
dgawk>
Wie Sie sehen, Op_postincrement
wurde es zuvor ausgeführt Op_not
.
Für eine klarere Darstellung können Sie statt si
oder auch oder verwenden:stepi
s
step
dgawk> si
[ 3:0x7ff061ac1fc0] Op_postincrement :
3 !a++;
dgawk> si
[ 3:0x7ff061ac1f40] Op_not :
Watchpoint 1: a
Old value: 0
New value: 1
main() at `test.awk':3
3 !a++;
Antwort3
ah, der allgegenwärtige, aber auch ominöse awk-Duplikatsentferner
awk '!a[$0]++'
Dieses süße Baby ist das Kind der Liebe aus der Kraft und Prägnanz von Awk. Der Höhepunkt der Einzeiler von Awk. Kurz, aber kraftvoll und geheimnisvoll zugleich. Entfernt Duplikate, während die Ordnung erhalten bleibt. Eine Leistung, die von nicht erreicht wird uniq
oder sort -u
bei der nur benachbarte Duplikate entfernt werden oder die Ordnung durchbrochen werden muss, um Duplikate zu entfernen.
hier ist mein Versuch zu erklären, wie dieser AWK-Einzeiler funktioniert. Ich habe mir Mühe gegeben, die Dinge so zu erklären, dass auch jemand, der kein AWK kennt, folgen kann. Ich hoffe, das ist mir gelungen.
zunächst einige Hintergrundinformationen: awk ist eine Programmiersprache. Dieser Befehl awk '!a[$0]++'
ruft den awk-Interpreter/Compiler für den awk-Code auf !a[$0]++
. Ähnlich wie python -c 'print("foo")'
oder node -e 'console.log("foo")'
. awk-Code besteht häufig aus Einzeilern, da awk speziell für die Textfilterung entwickelt wurde.
jetzt etwas Pseudocode. Dieser Einzeiler macht im Wesentlichen Folgendes:
for every line of input
if i have not seen this line before then
print line
take note that i have now seen this line
ich hoffe, Sie erkennen, wie hierdurch Duplikate entfernt werden, während die Ordnung erhalten bleibt.
aber wie passen eine Schleife, ein „if“, ein „print“ und ein Mechanismus zum Speichern und Abrufen von Zeichenfolgen in 8 Zeichen Awk-Code? Die Antwort ist implizit.
die Schleife, das „if“ und das „print“ sind implizit.
zur Erklärung wollen wir uns noch einmal etwas Pseudocode ansehen:
for every line of input
if line matches condition then
execute code block
dies ist ein typischer Filter, den Sie wahrscheinlich schon oft in der einen oder anderen Form in Ihren Code geschrieben haben, egal in welcher Sprache. Die Sprache AWK ist so konzipiert, dass das Schreiben dieser Art von Filtern sehr schnell geht.
awk führt die Schleife für uns aus, sodass wir nur den Code innerhalb der Schleife schreiben müssen. Die Syntax von awk lässt außerdem den Boilerplate-Code eines if weg und wir müssen nur die Bedingung und den Codeblock schreiben:
condition { code block }
in awk wird dies als „Regel“ bezeichnet.
wir können entweder die Bedingung oder den Codeblock weglassen (offensichtlich können wir nicht beides weglassen) und awk füllt den fehlenden Teil mit einigen impliziten Elementen auf.
wenn wir die Bedingung weglassen
{ code block }
dann ist es implizit wahr
true { code block }
Das bedeutet, dass der Codeblock für jede Zeile ausgeführt wird
wenn wir den Codeblock weglassen
condition
dann wird die aktuelle Zeile implizit gedruckt
condition { print current line }
Schauen wir uns noch einmal unseren ursprünglichen Awk-Code an
!a[$0]++
Es steht nicht in geschweiften Klammern und ist daher der bedingte Teil einer Regel.
Schreiben wir die implizite Schleife auf und drucken
for every line of input
if !a[$0]++ then
print line
Vergleichen Sie mit unserem ursprünglichen Pseudocode
for every line of input # implicit by awk
if i have not seen this line before then # at least we know the conditional part
print line # implicit by awk
take note that i have now seen this line # ???
wir verstehen die Schleife, das „if“ und das „print“. Aber wie funktioniert es, sodass es nur bei doppelten Zeilen als „false“ ausgewertet wird? Und wie werden bereits gesehene Zeilen zur Kenntnis genommen?
Lasst uns dieses Biest auseinandernehmen:
!a[$0]++
Wenn Sie ein wenig C oder Java kennen, sollten Ihnen einige der Symbole bereits bekannt sein. Die Semantik ist identisch oder zumindest ähnlich.
Das Ausrufezeichen ( !
) ist ein Negator. Es wertet den Ausdruck als Booleschen Wert aus und das Ergebnis wird negiert. Wenn der Ausdruck als „true“ ausgewertet wird, ist das Endergebnis „false“ und umgekehrt.
a[..]
ist ein Array. Ein assoziatives Array. Andere Sprachen nennen es Map oder Dictionary. In awk sind alle Arrays assoziative Arrays. Das a
hat keine besondere Bedeutung. Es ist nur ein Name für das Array. Es könnte genauso gut x
oder sein eliminatetheduplicate
.
$0
ist die aktuelle Zeile der Eingabe. Dies ist eine awk-spezifische Variable.
Das Pluszeichen plus ( ++
) ist ein Post-Inkrement-Operator. Dieser Operator ist etwas knifflig, da er zwei Dinge tut: Der Wert in der Variablen wird inkrementiert. Aber er „gibt“ auch den ursprünglichen, nicht inkrementierten Wert zur weiteren Verarbeitung zurück.
! a[ $0 ] ++
negator array current line post increment
wie arbeiten sie zusammen?
ungefähr in dieser Reihenfolge:
$0
ist die aktuelle Zeilea[$0]
ist der Wert im Array für die aktuelle Zeile- das Post-Increment (
++
) ruft den Wert von aba[$0]
, erhöht ihn und speichert ihn wieder ina[$0]
; dann „gibt“ es den ursprünglichen Wert an den nächsten Operator in der Zeile „zurück“: den Negator. - der Negator (
!
) erhält einen Wert von , der++
der ursprüngliche Wert von wara[$0]
; er wird als Boolescher Wert ausgewertet, dann negiert und dann an das implizite if übergeben. - Das if entscheidet dann, ob die Zeile gedruckt wird oder nicht.
das heißt also, ob die Zeile gedruckt wird oder nicht, oder im Kontext dieses awk-Programms: ob die Zeile ein Duplikat ist oder nicht, wird letztendlich durch den Wert in entschieden a[$0]
.
als Erweiterung: Der Mechanismus, der feststellt, ob diese Zeile bereits gesehen wurde, muss dann ausgeführt werden, wenn ++
der erhöhte Wert wieder in gespeichert wird a[$0]
.
Schauen wir uns unseren Pseudocode noch einmal an
for every line of input
if i have not seen this line before then # decided based on value in a[$0]
print line
take note that i have now seen this line # happens by increment from ++
Einige von Ihnen sehen vielleicht schon, wie sich das abspielt, aber wir sind so weit gekommen, lassen Sie uns die letzten Schritte unternehmen und die++
Wir beginnen mit dem in den impliziten
for each line as $0
if !a[$0]++ then
print $0
Lassen Sie uns Variablen einführen, um etwas Spielraum zum Arbeiten zu haben
for each line as $0
tmp = a[$0]++
if !tmp then
print $0
jetzt nehmen wir es ++
auseinander.
Denken Sie daran, dass dieser Operator zwei Dinge tut: Er erhöht den Wert in der Variablen und gibt den Originalwert zur weiteren Verarbeitung zurück. So ++
werden daraus zwei Zeilen:
for each line as $0
tmp = a[$0] # get original value
a[$0] = tmp + 1 # increment value in variable
if !tmp then
print $0
oder mit anderen Worten
for each line as $0
tmp = a[$0] # query if have seen this line
a[$0] = tmp + 1 # take note that has seen this line
if !tmp then
print $0
vergleichen Sie mit unserem ersten Pseudocode
for every line of input:
if i have not seen this line before:
print line
take note that i have now seen this line
also da haben wir es. Wir haben die Schleife, das „Wenn“, das „Drucken“, die Abfrage und die Notizen. Nur in einer anderen Reihenfolge als der Pseudocode.
komprimiert auf 8 Zeichen
!a[$0]++
möglich aufgrund der impliziten Schleife, des impliziten „if“ und des impliziten „print“ von awks und weil ++
sowohl die Abfrage als auch die Notizenerfassung durchgeführt werden.
Es bleibt eine Frage. Was ist der Wert a[$0]
für die erste Zeile? Oder für jede Zeile, die noch nicht zuvor gesehen wurde? Die Antwort ist wiederum implizit.
in awk wird jede Variable, die zum ersten Mal verwendet wird, implizit deklariert und mit einem leeren String initialisiert. Außer Arrays. Arrays werden mit einem leeren Array deklariert und initialisiert.
dies ++
führt implizite Konvertierungen in Zahlen durch. Die leere Zeichenfolge wird in Null umgewandelt. Andere Zeichenfolgen werden durch einen Best-Effort-Algorithmus in eine Zahl umgewandelt. Wenn die Zeichenfolge nicht als Zahl erkannt wird, wird sie erneut in Null umgewandelt.
dies !
führt eine implizite Konvertierung in einen Booleschen Wert durch. Die Zahl Null und die leere Zeichenfolge werden in „false“ konvertiert. Alles andere wird in „true“ konvertiert.
das bedeutet, wenn eine Zeile zum ersten Mal angezeigt wird, a[$0]
wird dann auf die leere Zeichenfolge gesetzt. Die leere Zeichenfolge wird durch in Null umgewandelt ++
(ebenfalls auf 1 erhöht und wieder in gespeichert a[$0]
). Die Null wird durch in „false“ umgewandelt !
. Das Ergebnis von !
ist „true“, sodass die Zeile gedruckt wird.
der Wert in a[$0]
ist jetzt die Zahl 1.
Wenn eine Zeile zum zweiten Mal angezeigt wird, a[$0]
ist dies die Zahl 1, die in „true“ umgewandelt wird, und das Ergebnis !
ist „false“, sodass es nicht gedruckt wird.
Jedes weitere Vorkommen derselben Zeile erhöht die Zahl. Da alle Zahlen außer Null wahr sind, !
ist das Ergebnis immer falsch, sodass die Zeile nie erneut gedruckt wird.
Auf diese Weise werden die Duplikate entfernt.
Kurz zusammengefasst: es zählt, wie oft eine Zeile gesehen wurde. Wenn Null, dann drucken. Wenn eine andere Zahl, dann nicht drucken. Es kann aufgrund vieler impliziter Angaben kurz sein.
Bonus: einige Varianten des Einzeilers und eine superkurze Erklärung seiner Wirkung.
Ersetzen Sie $0
(gesamte Zeile) durch $2
(zweite Spalte). Dadurch werden Duplikate entfernt, allerdings nur basierend auf der zweiten Spalte.
$ cat input
x y z
p q r
a y b
$ awk '!a[$2]++' input
x y z
p q r
Ersetzen Sie !
(Negator) durch ==1
(gleich eins) und es wird die erste Zeile gedruckt, die ein Duplikat ist
$ cat input
a
b
c
c
b
b
$ awk 'a[$0]++==1' input
c
b
Ersetzen durch >0
(größer als Null) und Hinzufügen {print NR":"$0}
druckt alle doppelten Zeilen mit der Zeilennummer. NR
ist eine spezielle Awk-Variable, die die Zeilennummer enthält (Datensatznummer im Awk-Jargon).
$ awk 'a[$0]++>0 {print NR":"$0}' input
4:c
5:b
6:b
ich hoffe, diese Beispiele tragen zum besseren Verständnis der oben erläuterten Konzepte bei.
Antwort4
Ich möchte nur hinzufügen, dass sowohl expr++
als auch ++expr
nur eine Abkürzung für sind expr=expr+1
. Aber
$ awk '!a[$0]++' f # or
$ awk '!(a[$0]++)' f
druckt alle eindeutigen Werte, da vor der Addition expr++
ausgewertet wird , währendexpr
$ awk '!(++a[$0])' f
gibt einfach nichts aus, da als ++expr
ausgewertet wird expr+1
, was in diesem Fall immer einen Wert ungleich Null zurückgibt, und die Negation gibt immer einen Wert von Null zurück.