wake-up-neo.net

Listet Dateien auf, die "n" oder weniger Zeilen enthalten

Frage

In einem Ordner möchte ich den Namen aller .txt-Dateien ausdrucken, die n=27-Zeilen oder weniger Zeilen enthalten. ich könnte

wc -l *.txt | awk '{if ($1 <= 27){print}}'

Das Problem ist, dass viele Dateien in dem Ordner Millionen Zeilen enthalten (und die Zeilen ziemlich lang sind) und daher der Befehl wc -l *.txt sehr langsam ist. Im Prinzip könnte ein Prozess die Anzahl der Zeilen zählen, bis mindestens n Zeilen gefunden werden, und dann mit der nächsten Datei fortfahren.

Was ist eine schnellere Alternative?

Zu Ihrer Information, ich bin auf MAC OSX 10.11.6

Versuch

Hier ist ein Versuch mit awk

#!/bin/awk -f

function printPreviousFileIfNeeded(previousNbLines, previousFILENAME)
{
  if (previousNbLines <= n) 
  {
    print previousNbLines": "previousFILENAME
  }
}

BEGIN{
  previousNbLines=n+1
  previousFILENAME=NA
} 


{
  if (FNR==1)
  {
    printPreviousFileIfNeeded(previousNbLines, previousFILENAME)
    previousFILENAME=FILENAME
  }
  previousNbLines=FNR
  if (FNR > n)
  {
    nextfile
  }
}

END{
  printPreviousFileIfNeeded(previousNbLines, previousFILENAME)
}

das kann als aufgerufen werden

awk -v n=27 -f myAwk.awk *.txt

Der Code schlägt jedoch fehl, wenn vollkommen leere Dateien gedruckt werden. Ich bin nicht sicher, wie ich das beheben kann, und ich bin mir nicht sicher, ob mein awk-Skript der richtige Weg ist.

14
Remi.b

Mit GNU awk für nextfile und ENDFILE:

awk -v n=27 'FNR>n{f=1; nextfile} ENDFILE{if (!f) print FILENAME; f=0}' *.txt

Mit jedem awk:

awk -v n=27 '
    { fnrs[FILENAME] = FNR }
    END {
        for (i=1; i<ARGC; i++) {
            filename = ARGV[i]
            if ( fnrs[filename] < n ) {
                print filename
            }
        }
    }
' *.txt

Beide funktionieren, ob die Eingabedateien leer sind oder nicht. Die Vorbehalte für die Nicht-Gawk-Version sind die gleichen wie für Ihre anderen aktuellen Antworten von awk:

  1. Es hängt davon ab, dass derselbe Dateiname nicht mehrmals angezeigt wird (z. B. awk 'script' foo bar foo) und Sie möchten, dass er mehrmals angezeigt wird, und
  2. Es setzt voraus, dass in der arg-Liste keine Variablen gesetzt sind (z. B. awk 'script' foo FS=, bar).

Die Gawk-Version hat keine derartigen Einschränkungen.

UPDATE:

Um das Timing zwischen dem obigen GNU awk-Skript und dem GNU grep + sed-Skript von xhienne zu testen, _, da sie angab, dass ihre Lösung faster than a pure awk script wäre, erstellte ich 10.000 Eingangsdateien von 0 bis 1000 Zeilen mit diesem Skript:

$ awk -v numFiles=10000 -v maxLines=1000 'BEGIN{for (i=1;i<=numFiles;i++) {numLines=int(Rand()*(maxLines+1)); out="out_"i".txt"; printf "" > out; for (j=1;j<=numLines; j++) print ("foo" j) > out} }'

und dann die beiden Befehle ausgeführt und diese Timing-Ergebnisse für den dritten Lauf erhalten:

$ time grep -c -m28 -H ^ *.txt | sed '/:28$/ d; s/:[^:]*$//' > out.grepsed

real    0m1.326s
user    0m0.249s
sys     0m0.654s

$ time awk -v n=27 'FNR>n{f=1; nextfile} ENDFILE{if (!f) print FILENAME; f=0}' *.txt > out.awk

real    0m1.092s
user    0m0.343s
sys     0m0.748s

Beide Skripte erzeugten die gleichen Ausgabedateien. Das obige wurde in bash auf cygwin ausgeführt. Ich gehe davon aus, dass die Timing-Ergebnisse auf verschiedenen Systemen etwas variieren können, aber der Unterschied wird immer vernachlässigbar sein.


So drucken Sie 10 Zeilen mit bis zu 20 zufälligen Zeichen pro Zeile (siehe Kommentare):

$ maxChars=20
    LC_ALL=C tr -dc '[:print:]' </dev/urandom |
    fold -w "$maxChars" |
    awk -v maxChars="$maxChars" -v numLines=10 '
        { print substr($0,1,Rand()*(maxChars+1)) }
        NR==numLines { exit }
    '
0J)-8MzO2V\XA/o'qJH
@r5|g<WOP780
^[email protected]\
vP{l^pgKUFH9
-6r&]/-6dl}pp W
&.UnTYLoi['2CEtB
Y~wrM3>4{
^F1mc9
?~NHh}a-EEV=O1!y
of

Um alles innerhalb von awk zu tun (was viel langsamer sein wird):

$ cat tst.awk
BEGIN {
    for (i=32; i<127; i++) {
        chars[++charsSize] = sprintf("%c",i)
    }
    minChars = 1
    maxChars = 20
    srand()
    for (lineNr=1; lineNr<=10; lineNr++) {
        numChars = int(minChars + Rand() * (maxChars - minChars + 1))
        str = ""
        for (charNr=1; charNr<=numChars; charNr++) {
            charsIdx = int(1 + Rand() * charsSize)
            str = str chars[charsIdx]
        }
        print str
    }
}

$ awk -f tst.awk
Heer H{QQ?qHDv|
Psuq
Ey`-:O2v7[]|N^EJ0
j#@/y>CJ3:=3*b-joG:
?
^|O.[tYlmDo
TjLw
`2Rs=
!('IC
hui
8
Ed Morton

Wenn Sie GNU grep verwenden (leider liefert MacOSX> = 10.8 BSD-grep, dessen -m- und -c-Optionen global agieren , nicht pro Datei), finden Sie diese Alternative möglicherweise interessant (und schneller als ein reines awk-Skript) ):

grep -c -m28 -H ^ *.txt | sed '/:28$/ d; s/:[^:]*$//'

Erläuterung:

  • grep -c -m28 -H ^ *.txt gibt den Namen jeder Datei mit der Anzahl der Zeilen in jeder Datei aus, liest jedoch niemals mehr als 28 Zeilen
  • sed '/:28$/ d; s/:[^:]*$//' entfernt die Dateien mit mindestens 28 Zeilen und gibt den Dateinamen der anderen aus

Alternative Version: sequentielle Verarbeitung statt paralleler Verarbeitung

res=$(grep -c -m28 -H ^ $files); sed '/:28$/ d; s/:[^:]*$//' <<< "$res"

Benchmarking

Ed Morton hat meine Behauptung in Frage gestellt, dass diese Antwort möglicherweise schneller als awk ist. Er fügte einige Benchmarks zu seiner Antwort hinzu, und obwohl er keine Schlussfolgerung zieht, halte ich die von ihm geposteten Ergebnisse für irreführend, da er für meine Antwort eine größere Wanduhrzeit ohne Rücksicht auf Benutzer- und Sys-Zeiten zeigt. Deshalb hier meine Ergebnisse.

Zuerst die Testplattform:

  • Ein Intel-i5-Laptop mit vier Kernen und Linux, wahrscheinlich nahe am OP-System (Apple iMac).

  • Ein brandneues Verzeichnis mit 100.000 Textdateien mit durchschnittlich ~ 400 Zeilen, insgesamt 640 MB, das sich vollständig in meinen Systempuffern befindet. Die Dateien wurden mit diesem Befehl erstellt:

    for ((f = 0; f < 100000; f++)); do echo "File $f..."; for ((l = 0; l < RANDOM & 1023; l++)); do echo "File $f; line $l"; done > file_$f.txt; done
    

Ergebnisse:

Fazit:

Auf einem normalen Unix-Multi-Core-Laptop, der dem Computer von OP ähnelt, ist diese Antwort zum Zeitpunkt des Schreibens die schnellste, die genaue Ergebnisse liefert. Auf meinem Rechner ist es doppelt so schnell wie das schnellste awk-Skript.

Anmerkungen:

  • Warum ist die Plattform wichtig? Weil meine Antwort darauf abzielt, die Verarbeitung zwischen grep und sed zu parallelisieren. Wenn Sie nur einen CPU-Kern (VM?) Oder andere Einschränkungen Ihres Betriebssystems hinsichtlich der CPU-Zuweisung haben, sollten Sie für unparteiische Ergebnisse die alternative (sequentielle) Version als Benchmark verwenden.

  • Natürlich können Sie nicht allein auf die Wandzeit schließen, da dies von der Anzahl der gleichzeitigen Prozesse, die nach der CPU fragen, und der Anzahl der Prozessorkerne abhängt. Deshalb habe ich die user + sys Timings hinzugefügt

  • Diese Zeiten liegen im Durchschnitt über 20 Läufe, außer wenn der Befehl mehr als 1 Minute dauerte (nur ein Lauf).

  • Bei allen Antworten, die weniger als 10 Sekunden dauern, ist die Zeit, die die Shell für die Verarbeitung von *.txt aufgewendet hat, nicht unerheblich. Daher habe ich die Dateiliste vorverarbeitet, in eine Variable eingefügt und den Inhalt der Variablen an den Befehl angehängt, den ich als Benchmarking verwendet hatte .

  • Alle Antworten lieferten die gleichen Ergebnisse, mit Ausnahme der 1. Antwort des Tripelempfängers, die argv[0] ("awk") in das Ergebnis einschließt (in meinen Tests festgelegt); 2. Antwort von kvantour, die nur leere Dateien auflistete (mit -v n=27 behoben); und 3. die find + sed-Antwort, bei der leere Dateien fehlen (nicht behoben).

  • Ich konnte die Antwort von ctac_ nicht testen , da ich keine GNU sed 4.5 zur Hand habe. Es ist wahrscheinlich das schnellste von allen, aber es fehlen auch leere Dateien.

  • Die Python-Antwort schließt ihre Dateien nicht. Ich musste zuerst ulimit -n hard machen.

4
xhienne

Wie ist das?

awk 'BEGIN { for(i=1;i<ARGC; ++i) arg[ARGV[i]] }
  FNR==28 { delete arg[FILENAME]; nextfile }
  END { for (file in arg) print file }' *.txt

Wir kopieren die Liste der Dateinamenargumente in ein assoziatives Array und entfernen dann alle Dateien, die eine 28. Zeile enthalten. Leere Dateien stimmen offensichtlich nicht mit dieser Bedingung überein, so dass am Ende alle Dateien übrig bleiben, die weniger Zeilen enthalten, einschließlich der leeren.

nextfile war eine verbreitete Erweiterung in vielen Awk-Varianten und wurde dann 2012 von POSIX kodifiziert. Wenn Sie dies benötigen, um an wirklich alten Dinosaurier-Betriebssystemen (oder guten Himmeln, wahrscheinlich Windows) zu arbeiten, viel Glück und/oder versuchen Sie es mit GNU Awk. 

3
tripleee

Während awk der interessanteste Weg zu sein scheint, ist hier noch eine weitere zu den bereits bestehenden Lösungen von Triple , Anubhava und Ed Morton . Wo Lösungen von Triple und Anubhava die nextfile-Anweisung verwenden und die POSIX-Proof-Lösung von Ed Morton vollständige Dateien liest, biete ich eine Lösung an, die nicht die gesamten Dateien liest.

awk -v n=27 'BEGIN{ for(i=1;i<ARGC;++i) {
                       j=0; fname=ARGV[i];
                       while( ((getline < fname) > 0 ) && j<=n) { j++ }
                       if(j<=n) print fname; close(fname)
                  }
                  exit
             }' *.txt
3
kvantour

Sie können dies awk versuchen, das zur nächsten Datei wechselt, sobald die Zeilenanzahl 27 übersteigt.

awk -v n=27 'BEGIN{for (i=1; i<ARGC; i++) f[ARGV[i]]}
FNR > n{delete f[FILENAME]; nextfile}
END{for (i in f) print i}' *.txt

awk verarbeitet Dateien zeilenweise, sodass nicht versucht wird, eine vollständige Datei zu lesen, um die Zeilenzahl zu ermitteln.

3
anubhava

mit sed (GNU sed) 4.5:

sed -n -s '28q;$F' *.txt
1
ctac_

Sie können find mit Hilfe eines kleinen Bash-Inline-Skripts verwenden:

find -type f -exec bash -c '[ $(grep -cm 28 ^ "${1}") != "28" ] && echo "${1}"' -- {} \;

Der Befehl [ $(grep -cm 28 ^ "${1}") != "28" ] && echo "${1}" sucht mit grep maximal 28 Mal nach dem Beginn einer Zeile (^). Wenn dieser Befehl! = "28" zurückgibt, muss die Datei mehr als 28 Zeilen haben.

1
hek2mgl

python -c "import sys; print '\n'.join([of.name for of in [open(fn) for fn in sys.argv[1:]] if len(filter(None, [of.readline() for _ in range(28)])) <= 27])" *.txt

0
Joey Harrington

Softwaretools und GNUsed (ältere Versionen vor v4.5) Mashup:

find *.txt -print0 | xargs -0 -L 1 sed -n '28q;$F'

Damit fehlen 0-Byte-Dateien, um diese auch einzuschließen:

find *.txt \( -exec sed -n '28{q 1}' '{}' \; -or -size 0 \) -print

(Aus irgendeinem Grund ist die Ausführung von sed über -exec um 12% langsamer als xargs.)


sed Code aus der Antwort von ctac gestohlen .

Hinweis: Bei älteren Systemen sedv4.4-2 meines Systems wird der Befehl quit in Kombination mit dem Schalter --separate nicht nur die aktuelle Datei beendet, sondern sed vollständig. Dies bedeutet, dass für jede Datei eine separate Instanz von sed erforderlich ist.

0
agc

Wenn Sie awk einzeln anrufen müssen, bitten Sie um Haltestelle 28,

for f in ./*.txt
do
  if awk 'NR > 27 { fail=1; exit; } END { exit fail; }' "$f"
  then
    printf '%s\n' "$f"
  fi
done

Der Standardwert von awk-Variablen ist Null. Wenn wir also niemals Zeile 28 treffen, ist der Exit-Code Null, wodurch der Test if erfolgreich ausgeführt wird und der Dateiname ausgegeben wird.

0
Jeff Schaller