Effizientes Zusammenführen / Sortieren / eindeutige große Anzahl von Textdateien

8

Ich versuche eine naive:

$ cat * | sort -u > /tmp/bla.txt

was scheitert mit:

-bash: /bin/cat: Argument list too long

Um eine dumme Lösung wie (erstellt eine enorme temporäre Datei) zu vermeiden:

$ find . -type f -exec cat {} >> /tmp/unsorted.txt \;
$ cat /tmp/unsorted.txt | sort -u > /tmp/bla.txt

Ich dachte, ich könnte Dateien einzeln verarbeiten (dies sollte den Speicherverbrauch reduzieren und näher an einem Streaming-Mechanismus liegen):

$ cat proc.sh
#!/bin/sh
old=/tmp/old.txt
tmp=/tmp/tmp.txt
cat $old "$1" | sort -u > $tmp
mv $tmp $old

Gefolgt von:

$ touch /tmp/old.txt
$ find . -type f -exec /tmp/proc.sh {} \;

Gibt es einen einfacheren Ersatz im Unix-Stil für: cat * | sort -uWenn die Anzahl der Dateien erreicht ist MAX_ARG? Es fühlt sich gut an, ein kleines Shell-Skript für eine so häufige Aufgabe zu schreiben.

malat
quelle
2
Ist überhaupt eine Verkettung erforderlich? sortwird es automatisch für mehrere Dateieingang .. aber dann sort -u *würde scheitern mit Argument list too longals auch nehme ich
Sundeep

Antworten:

8

Mit GNU sortund einer printfeingebauten Shell (alle POSIX-ähnlichen heutzutage mit Ausnahme einiger Varianten von pdksh):

printf '%s\0' * | sort -u --files0-from=- > output

Ein Problem dabei ist, dass, da die beiden Komponenten dieser Pipeline gleichzeitig und unabhängig ausgeführt werden, *die rechte möglicherweise bereits die outputDatei erstellt hat, die Probleme verursachen könnte (möglicherweise nicht -uhier). outputDa es sich sowohl um eine Eingabe- als auch um eine Ausgabedatei handelt, möchten Sie möglicherweise, dass die Ausgabe in ein anderes Verzeichnis > ../outputverschoben wird ( z. B.), oder stellen Sie sicher, dass der Glob nicht mit der Ausgabedatei übereinstimmt.

Eine andere Möglichkeit, dies in diesem Fall zu beheben, besteht darin, es zu schreiben:

printf '%s\0' * | sort -u --files0-from=- -o output

Auf diese Weise wird es zum Schreiben sortgeöffnet outputund (in meinen Tests) nicht ausgeführt, bevor die vollständige Liste der Dateien empfangen wurde (so lange, nachdem der Glob erweitert wurde). Es wird auch ein Überfallen vermieden, outputwenn keine der Eingabedateien lesbar ist.

Eine andere Möglichkeit, es mit zshoder zu schreibenbash

sort -u --files0-from=<(printf '%s\0' *) -o output

Hierbei wird die Prozessersetzung verwendet (wobei <(...)durch einen Dateipfad ersetzt wird, der sich auf das Leseende bezieht, in das die Pipe printfschreibt). Diese Funktion stammt von ksh, kshbesteht jedoch darauf, <(...)ein separates Argument für den Befehl zu erweitern, damit Sie es nicht mit der --option=<(...)Syntax verwenden können. Es würde jedoch mit dieser Syntax funktionieren:

sort -u --files0-from <(printf '%s\0' *) -o output

Beachten Sie, dass Sie einen Unterschied zu Ansätzen catfeststellen, bei denen die Ausgabe der Dateien in Fällen eingespeist wird, in denen Dateien nicht mit einem Zeilenumbruchzeichen enden:

$ printf a > a
$ printf b > b
$ printf '%s\0' a b | sort -u --files0-from=-
a
b
$ printf '%s\0' a b | xargs -r0 cat | sort -u
ab

Beachten Sie außerdem, dass die sortSortierung mithilfe des Kollatierungsalgorithmus in locale ( strcollate()) erfolgt und sort -ueine von jedem Satz von Zeilen gemeldet wird, die nach diesem Algorithmus gleich sortiert sind, und keine eindeutigen Zeilen auf Byte-Ebene. Wenn Sie sich nur dafür interessieren, dass Zeilen auf Byte-Ebene eindeutig sind, und sich nicht so sehr für die Reihenfolge interessieren, in der sie sortiert sind, möchten Sie möglicherweise das Gebietsschema auf C festlegen, wobei die Sortierung auf Byte-Werten basiert ( memcmp(); das würde wahrscheinlich die Geschwindigkeit erhöhen Dinge deutlich):

printf '%s\0' * | LC_ALL=C sort -u --files0-from=- -o output
Stéphane Chazelas
quelle
Das Schreiben fühlt sich natürlicher an und bietet auch die Möglichkeit sort, den Speicherverbrauch zu optimieren. Ich finde es immer noch printf '%s\0' *etwas komplex zu tippen.
Malat
Sie könnten find . -type f -maxdepth 1 -print0anstelle von verwenden printf '%s\0' *, aber ich kann nicht behaupten, dass es einfacher zu tippen ist. Und letzteres ist natürlich leichter als Alias ​​zu definieren!
Toby Speight
@ TobySpeight echohat eine -n, ich hätte es vorgezogen, so etwas printf -0 %sscheint ein wenig weniger niedrig als'%s\0'
Malat
@Toby -maxdepthund -print0sind GNU-Erweiterungen (obwohl heutzutage weit verbreitet). Mit anderen finds (wenn Sie GNU-Sortierung haben, werden Sie wahrscheinlich auch GNU finden) können Sie dies tun LC_ALL=C find . ! -name . -prune -type f ! -name '.*' -exec printf '%s\0' {} +( LC_ALL=Cum versteckte Dateien auszuschließen, die ungültige Zeichen enthalten, selbst mit GNU find), aber das ist im Allgemeinen etwas übertrieben haben printfbuiltin.
Stéphane Chazelas
2
@malat, Sie könnten immer eine print0Funktion definieren als print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@";}und dannprint0 * | sort...
Stéphane Chazelas
11

Eine einfache Lösung funktioniert zumindest in Bash, da sie printfintegriert ist und die Befehlszeilenargumentbeschränkungen nicht für sie gelten:

printf "%s\0" * | xargs -0 cat | sort -u > /tmp/bla.txt

( echo * | xargswürde auch funktionieren, außer für die Behandlung von Dateinamen mit Leerzeichen usw.)

ilkkachu
quelle
Dies scheint eine bessere Antwort zu sein als die akzeptierte, da nicht catfür jede Datei ein separater Prozess erstellt werden muss.
LarsH
4
@LarsH, bündelt find -exec {} +mehrere Dateien pro Ausführung. Damit find -exec \;wäre eine Katze pro Datei.
Ilkkachu
Ah, gut zu wissen. (
Polsterung
9
find . -maxdepth 1 -type f ! -name ".*" -exec cat {} + | sort -u -o /path/to/sorted.txt

Dadurch werden alle nicht ausgeblendeten regulären Dateien im aktuellen Verzeichnis verkettet und ihre kombinierten Inhalte (während doppelte Zeilen entfernt werden) in der Datei sortiert /path/to/sorted.txt.

Kusalananda
quelle
Ich habe versucht, jeweils nur zwei Dateien zu verwenden, um nicht viel Speicher zu verbrauchen (meine Anzahl an Dateien ist ziemlich groß). Glauben Sie, dass |Vorgänge ordnungsgemäß verkettet werden, um die Speichernutzung zu begrenzen?
Malat
2
@malat sortführt eine Sortierung außerhalb des Kerns durch, wenn die Speicheranforderungen dies erfordern. Die linke Seite der Pipeline verbraucht im Vergleich sehr wenig Speicher.
Kusalananda
1

Effizienz ist ein relativer Begriff, daher müssen Sie wirklich angeben, welchen Faktor Sie minimieren möchten. CPU, Speicher, Festplatte, Zeit usw. Aus Gründen der Argumentation gehe ich davon aus, dass Sie die Speichernutzung minimieren wollten und bereit sind, mehr CPU-Zyklen aufzuwenden, um dies zu erreichen. Lösungen wie die von Stéphane Chazelas funktionieren gut

sort -u --files0-from <(printf '%s\0' *) > ../output

Sie gehen jedoch davon aus, dass die einzelnen Textdateien zunächst einen hohen Grad an Einzigartigkeit aufweisen. Wenn nicht, dh wenn nachher

sort -u < sample.txt > sample.srt

sample.srt ist mehr als 10% kleiner als sample.txt. Dann sparen Sie erheblichen Speicher, indem Sie die Duplikate in den Dateien entfernen, bevor Sie sie zusammenführen. Sie sparen außerdem noch mehr Speicher, indem Sie die Befehle nicht verketten. Dies bedeutet, dass die Ergebnisse verschiedener Prozesse nicht gleichzeitig gespeichert werden müssen.

find /somedir -maxdepth 1 type f -exec sort -u -o {} {} \;
sort -u --files0-from <(printf '%s\0' *) > ../output
Paul Smith
quelle
1
Die Speichernutzung ist selten ein Problem, sortda sortauf die Verwendung temporärer Dateien zurückgegriffen wird, wenn die Speichernutzung einen Schwellenwert überschreitet (normalerweise relativ gering). base64 /dev/urandom | sort -ufüllt Ihre Festplatte, verbraucht aber nicht viel Speicher.
Stéphane Chazelas
Nun, zumindest ist dies bei den meisten sortImplementierungen der Fall , einschließlich der ursprünglichen in Unix v3 von 1972, aber anscheinend nicht bei busybox sort. Vermutlich, weil dieser auf kleinen Systemen ohne permanenten Speicher ausgeführt werden soll.
Stéphane Chazelas
Beachten Sie, dass yes | sort -u(alle duplizierten Daten) nicht mehr als ein paar Bytes Speicher benötigen, geschweige denn Festplatte. Aber sortzumindest bei GNU und Solaris werden viele 2 Byte große Dateien geschrieben /tmp( y\npro paar Megabyte Eingabe), sodass die Festplatte schließlich voll wird.
Stéphane Chazelas
0

Wie @ilkkachu, aber die Katze (1) ist unnötig:

printf "%s\0" * | xargs -0 sort -u

Wenn die Daten so lang sind, möchten Sie möglicherweise die Option sort (1) --parallel = N verwenden

Wenn N die Anzahl der CPUs ist, über die Ihr Computer verfügt

Udi
quelle