Wie greife ich nach Gruppen von n Ziffern, aber nicht mehr als n?

33

Ich lerne Linux und habe eine Herausforderung, die ich anscheinend nicht alleine lösen kann. Hier ist es:

Grep eine Zeile aus einer Datei, die 4 Zahlen in einer Reihe, aber nicht mehr als 4 enthält.

Ich bin mir nicht sicher, wie ich das angehen soll. Ich kann nach bestimmten Zahlen suchen, aber nicht nach deren Anzahl in einer Zeichenfolge.

Buddha
quelle
2
Soll eine Zeile wie 1234a12345angezeigt werden oder nicht?
Eliah Kagan
@Buddha Sie müssen Ihre Frage zusammen mit einem Beispiel erklären.
Avinash Raj
Wenn vor den Zahlen ein Leerzeichen oder der Anfang des Zeilenankers und danach ein Leerzeichen oder das Ende des Zeilenankers steht, können Sie einfach Wortgrenzen verwenden. \b\d{4}\b
Avinash Raj
1
Diese Frage unterscheidet sich von einigen Fragen zu regulären Ausdrücken durch die explizite Verwendung von grep . Fragen zur Verwendung von Unix-Dienstprogrammen in Ubuntu, wie grep, sed und awk, wurden hier immer als in Ordnung angesehen. Manchmal fragen die Leute, wie sie mit dem falschen Werkzeug arbeiten sollen. dann ist ein Mangel an Kontext ein großes Problem, aber genau das passiert hier nicht. Dies ist themenbezogen, klar genug, um nützlich beantwortet zu werden, hilfreich für unsere Community, und es hat keinen Vorteil, weitere Antworten zu verhindern oder sie in Richtung Löschung oder Migration zu treiben. Ich stimme dafür, es wieder zu öffnen.
Eliah Kagan
1
Vielen Dank, ich hatte keine Ahnung, dass ich so viel Feedback bekommen würde. Dies ist die Antwort, nach der ich gesucht habe: grep -E '(^ ​​| [^ 0-9]) [0-9] {4} ($ | [^ 0-9])' file. Der Befehl muss in der Lage sein, einen String wie diesen zu ziehen (was er tut): abc1234abcd99999
Buddha

Antworten:

52

Es gibt zwei Möglichkeiten, diese Frage zu interpretieren. Ich werde beide Fälle ansprechen. Möglicherweise möchten Sie Zeilen anzeigen:

  1. die eine Folge von vier Ziffern enthalten, die selbst nicht Teil einer längeren Folge von Ziffern ist, oder
  2. das enthält eine vierstellige Folge aber keine Ziffernfolge mehr (auch nicht einzeln).

Zum Beispiel würde (1) angezeigt 1234a56789 , aber (2) nicht.


Wenn Sie alle Zeilen anzeigen möchten, die eine Folge von vier Ziffern enthalten, die selbst nicht zu einer längeren Folge von Ziffern gehört, haben Sie folgende Möglichkeiten:

grep -P '(?<!\d)\d{4}(?!\d)' file

Dies verwendet reguläre Perl-Ausdrücke , die Ubuntu grep( GNU grep ) über unterstützt -P. Es passt weder zu Text wie 12345, noch zu dem 1234oder dem 2345, der Teil davon ist. Aber es wird dem 1234in entsprechen1234a56789 .

In Perl reguläre Ausdrücke:

  • \dbedeutet eine beliebige Ziffer (es ist ein kurzer Weg, um [0-9]oder zu sagen[[:digit:]] ).
  • x{4}Spiele x4 mal. (Die { }Syntax ist nicht spezifisch für reguläre Perl-Ausdrücke, sondern auch für erweiterte reguläre Ausdrücke über grep -E.) So \d{4}ist es auch mit\d\d\d\d .
  • (?<!\d)ist eine negative Look-Behind-Behauptung mit der Breite Null. Es bedeutet "sofern nicht vorangestellt"\d ".
  • (?!\d)ist eine negative Vorausschau-Behauptung mit der Breite Null. Es bedeutet "sofern nicht gefolgt von \d".

(?<!\d) und (?!\d) stimmen Sie nicht mit Text außerhalb der vierstelligen Reihenfolge überein. Stattdessen verhindern sie (wenn sie zusammen verwendet werden), dass eine Folge von vier Ziffern übereinstimmt, wenn sie Teil einer längeren Folge von Ziffern ist.

Nur den Look-Behind oder nur den Look-Ahead zu verwenden, ist unzureichend, da die ganz rechts oder ganz links liegende vierstellige Teilfolge immer noch übereinstimmen würde.

Ein Vorteil der Verwendung von Look-Behind- und Look-Ahead-Behauptungen besteht darin, dass Ihr Muster nur den vierstelligen Folgen selbst und nicht dem umgebenden Text entspricht. Dies ist hilfreich bei der Verwendung der Farbmarkierung (mit der --colorOption).

ek@Io:~$ grep -P '(?<!\d)\d{4}(?!\d)' <<< 12345abc789d0123e4
12345abc789d0123e4

Standardmäßig hat jeder Benutzer in Ubuntu alias grep='grep --color=auto'eine ~.bashrcDatei . Sie erhalten also automatisch eine farbige Hervorhebung, wenn Sie einen einfachen Befehl ausführen, der mit grep(dies ist, wenn Aliase erweitert werden) beginnt, und die Standardausgabe ist ein Terminal (das prüft, ob). Übereinstimmungen sind in der Regel rot hervorgehoben (in der Nähe von Zinnoberrot ), aber ich habe es in Fettschrift kursiv dargestellt. Hier ist ein Screenshot:--color=auto
Screenshot, der den Befehl grep mit 12345abc789d0123e4 als Ausgabe zeigt, wobei 0123 rot hervorgehoben ist.

Und Sie können sogar grepnur passenden Text und nicht die ganze Zeile drucken lassen -o:

ek@Io:~$ grep -oP '(?<!\d)\d{4}(?!\d)' <<< 12345abc789d0123e4
0123

Alternativer Weg, ohne hinterherzuschauen und vorausschauende Behauptungen

Wenn Sie jedoch:

  1. benötigt einen Befehl, der auch auf Systeme ausgeführt werden , wo grepnicht unterstützt -Poder auf andere Weise nicht mag , dass ein Perl regulären Ausdrücke verwenden, und
  2. Sie müssen die vier Ziffern nicht speziell abgleichen. Dies ist normalerweise der Fall, wenn Sie nur Zeilen mit Übereinstimmungen und anzeigen möchten
  3. sind in Ordnung mit einer Lösung, die ein bisschen weniger elegant ist

... dann können Sie dies stattdessen mit einem erweiterten regulären Ausdruck erreichen :

grep -E '(^|[^0-9])[0-9]{4}($|[^0-9])' file

Dies entspricht vier Ziffern und dem sie umgebenden nichtstelligen Zeichen - oder dem Anfang oder Ende der Zeile. Speziell:

  • [0-9]Entspricht einer beliebigen Ziffer (wie [[:digit:]]oder \din regulären Perl-Ausdrücken) und {4}bedeutet "viermal". Entspricht also [0-9]{4}einer vierstelligen Folge.
  • [^0-9]Sucht nach Zeichen, die nicht im Bereich von 0bis liegen 9. Es ist äquivalent zu [^[:digit:]](oder \Din regulären Perl-Ausdrücken).
  • ^Wenn es nicht in [ ]Klammern steht, stimmt es mit dem Anfang einer Zeile überein. Entspricht $dem Ende einer Zeile.
  • |bedeutet oder und Klammern sind für die Gruppierung (wie in der Algebra). Entspricht also (^|[^0-9])dem Zeilenanfang oder einem nichtstelligen Zeichen, während das Zeilenende oder ein nichtstelliges Zeichen ($|[^0-9])übereinstimmt.

Übereinstimmungen treten also nur in Zeilen auf, die eine vierstellige Sequenz ( [0-9]{4}) enthalten, die gleichzeitig ist:

  • am Anfang der Zeile oder mit vorangestelltem Zeichen ( (^|[^0-9])) und
  • am Ende der Zeile oder gefolgt von einer Nicht-Ziffer ( ($|[^0-9])).

Wenn Sie dagegen alle Zeilen anzeigen möchten, die eine vierstellige Folge enthalten, aber keine Folge mit mehr als vier Ziffern enthalten (auch keine , die von einer anderen Folge mit nur vier Ziffern getrennt ist), dann ist dies konzeptionell Ihre Aufgabe Ziel ist es, Linien zu finden, die zu einem Muster passen, aber nicht zu einem anderen.

Daher würde ich, selbst wenn Sie wissen, wie man es mit einem einzelnen Muster macht, vorschlagen, so etwas wie Matts zweiten Vorschlag zu verwenden.grep für die beiden Muster separat gilt.

Dabei profitieren Sie nicht stark von den erweiterten Funktionen der regulären Perl-Ausdrücke. Daher ziehen Sie es möglicherweise vor, diese nicht zu verwenden. In Übereinstimmung mit dem obigen Stil ist hier eine Verkürzung der Matt-Lösung mit \d(und geschweiften Klammern) anstelle von [0-9]:

grep -P '\d{4}' file | grep -Pv '\d{5}'

Da matt's way verwendet wird [0-9], ist es portabler - es funktioniert auf Systemen, auf denen reguläre Perl-Ausdrücke nicht unterstützt werden. Wenn Sie (oder ) anstelle von verwenden , aber weiterhin verwenden , erhalten Sie die Portabilität von Matt's Way etwas präziser:grep[0-9][[:digit:]]\d{ }

grep -E '[0-9]{4}' file | grep -Ev '[0-9]{5}'

Alternative Methode mit einem einzigen Muster

Wenn Sie wirklich einen grepBefehl bevorzugen , der

  1. verwendet einen einzelnen regulären Ausdruck (nicht zwei grepdurch ein Pipe getrennte s) wie oben)
  2. um Zeilen anzuzeigen, die mindestens eine Folge von vier Ziffern enthalten,
  3. aber keine Sequenzen von fünf (oder mehr) Ziffern,
  4. und es macht Ihnen nichts aus, die ganze Zeile abzugleichen, nicht nur die Ziffern (das stört Sie wahrscheinlich nicht)

... dann können Sie verwenden:

grep -Px '(\d{0,4}\D)*\d{4}(\D\d{0,4})*' file

Das -xFlag bewirkt, grepdass nur Zeilen angezeigt werden, bei denen die gesamte Zeile übereinstimmt (und keine Zeile, die eine Übereinstimmung enthält).

Ich habe ein Perl regulären Ausdrücke verwendet , weil ich die Kürze denke \dund im \DWesentlichen Klarheit in diesem Fall erhöhen. Wenn Sie jedoch etwas Tragbares für Systeme benötigen, grepdie nicht unterstützt werden -P, können Sie diese durch [0-9]und [^0-9](oder durch [[:digit:]]und [^[:digit]]) ersetzen :

grep -Ex '([0-9]{0,4}[^0-9])*[0-9]{4}([^0-9][0-9]{0,4})*' file

Diese regulären Ausdrücke funktionieren folgendermaßen:

  • In der Mitte \d{4}oder [0-9]{4}entspricht einer Folge von vier Ziffern. Wir haben vielleicht mehr als eine davon, aber wir müssen mindestens eine haben.

  • Auf der linken Seite (\d{0,4}\D)*oder ([0-9]{0,4}[^0-9])*entspricht null oder mehr ( *) Instanzen von nicht mehr als vier Ziffern, gefolgt von einer Nicht-Ziffer. Nullstellen (dh nichts) ist eine Möglichkeit für "nicht mehr als vier Stellen". Dies entspricht (a) der leeren Zeichenfolge oder (b) einer Zeichenfolge, die nicht mit einer Ziffer endet und keine Sequenzen mit mehr als vier Ziffern enthält.

    Da der Text unmittelbar links von der Mitte \d{4}(oder [0-9]{4}) entweder leer sein oder mit einer Nicht-Ziffer enden muss, wird verhindert, dass die Zentrale \d{4}vier Ziffern mit einer weiteren (fünften) Ziffer links davon abgleichen kann.

  • Auf der rechten Seite (\D\d{0,4})*oder ([^0-9][0-9]{0,4})*entspricht null oder mehr ( *) Instanzen einer Nicht-Ziffer, gefolgt von nicht mehr als vier Ziffern (die wie zuvor vier, drei, zwei, eins oder gar keine sein können). Dies entspricht (a) der leeren Zeichenfolge oder (b) einer Zeichenfolge, die nicht mit einer Ziffer beginnt und keine Sequenzen mit mehr als vier Ziffern enthält.

    Da der Text unmittelbar rechts von der Mitte \d{4}(oder [0-9]{4}) entweder leer sein muss oder mit einer Nicht-Ziffer beginnen muss, wird verhindert, dass die Zentrale \d{4}vier Ziffern mit einer weiteren (fünften) Ziffer rechts davon abgleichen kann.

Dies stellt sicher, dass irgendwo eine vierstellige Folge vorhanden ist und dass nirgendwo eine Folge von fünf oder mehr Ziffern vorhanden ist.

Es ist nicht schlecht oder falsch, es so zu machen. Der vielleicht wichtigste Grund, diese Alternative in Betracht zu ziehen, besteht darin, den Nutzen der Verwendung (oder ähnlicher) zu verdeutlichen , wie oben und in Matts Antwort vorgeschlagen .grep -P '\d{4}' file | grep -Pv '\d{5}'

Auf diese Weise ist es klar, dass Ihr Ziel darin besteht, Zeilen auszuwählen, die eine Sache, aber keine andere enthalten. Außerdem ist die Syntax einfacher (so dass sie von vielen Lesern / Betreuern möglicherweise schneller verstanden wird).

Eliah Kagan
quelle
9

Dies zeigt Ihnen 4 Zahlen hintereinander, aber nicht mehr

grep '[0-9][0-9][0-9][0-9][^0-9]' file

Beachten Sie, dass ^ nicht bedeutet

Es gibt ein Problem damit, obwohl ich nicht sicher bin, wie ich es beheben soll ... Wenn die Nummer das Ende der Zeile ist, wird sie nicht angezeigt.

Diese hässlichere Version würde jedoch für diesen Fall funktionieren

grep '[0-9][0-9][0-9][0-9]' file | grep -v [0-9][0-9][0-9][0-9][0-9]
matt
quelle
hoppla, brauchte nicht egrep zu sein - ich habe es bearbeitet
matt
2
Der erste ist falsch - er findet a12345b, weil er passt 2345b.
Volker Siegel
0

Wenn grepreguläre Perl-Ausdrücke ( -P) nicht unterstützt werden , verwenden Sie den folgenden Shell-Befehl:

grep -w "$(printf '[0-9]%.0s' {1..4})" file

wo printf '[0-9]%.0s' {1..4}wird 4 mal produzieren [0-9]. Diese Methode ist nützlich, wenn Sie lange Ziffern haben und das Muster nicht wiederholen möchten (ersetzen 4Sie es einfach durch die Anzahl der zu suchenden Ziffern).

Mit -wwird nach den ganzen Wörtern gesucht. Wenn Sie jedoch an alphanumerischen Zeichenfolgen interessiert sind, wie z. B. 1234a, fügen Sie diese [^0-9]am Ende des Musters hinzu, z

grep "$(printf '[0-9]%.0s' {1..4})[^0-9]" file

Verwenden $()ist im Grunde eine Befehlsersetzung . Überprüfen Sie diesen Beitrag, um zu sehen, wie sich printfdas Muster wiederholt.

Kenorb
quelle
0

Sie können den folgenden Befehl versuchen, indem Sie ihn filedurch den tatsächlichen Dateinamen in Ihrem System ersetzen:

grep -E '(^|[^0-9])[0-9]{4}($|[^0-9])' file

Sie können auch überprüfen diesem Lernprogramm auch nach weiteren Verwendungen des Befehls grep suchen.

Mike Tyson
quelle