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
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.
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:
awk 'script' foo bar foo
) und Sie möchten, dass er mehrmals angezeigt wird, undawk '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
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 Zeilensed '/:28$/ d; s/:[^:]*$//'
entfernt die Dateien mit mindestens 28 Zeilen und gibt den Dateinamen der anderen ausAlternative Version: sequentielle Verarbeitung statt paralleler Verarbeitung
res=$(grep -c -m28 -H ^ $files); sed '/:28$/ d; s/:[^:]*$//' <<< "$res"
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.
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.
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
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.
mit sed (GNU sed) 4.5:
sed -n -s '28q;$F' *.txt
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.
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
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 sed
v4.4-2 meines Systems wird der Befehl q
uit 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.
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.