Эффективно объединять / сортировать / уникально большое количество текстовых файлов

8

Я пытаюсь наивно

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

который терпит неудачу с:

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

Таким образом, чтобы избежать глупого решения типа (создает огромный временный файл):

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

Хотя я мог обрабатывать файлы один за другим (это должно уменьшить потребление памяти и быть ближе к потоковому механизму):

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

Затем следует:

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

Есть ли более простая замена в стиле Unix для: cat * | sort -uкогда количество файлов достигнет MAX_ARG? Нелегко писать небольшой скрипт для такой распространенной задачи.

малат
источник
2
нужна ли конкатенация вообще? sortделает это автоматически для ввода нескольких файлов ... но тогда sort -u *, Argument list too longя полагаю, тоже потерпел бы неудачу
Sundeep

Ответы:

8

С GNU sortи printfвстроенной оболочкой (в настоящее время все POSIX-подобные, кроме некоторых вариантов pdksh):

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

Теперь проблема в том, что, поскольку два компонента этого конвейера запускаются одновременно и независимо, к тому времени, когда левый расширяет *глобус, правый может уже создать outputфайл, который может вызвать проблемы (возможно, не -uздесь) Как outputи входной, и выходной файл, вы можете захотеть, чтобы выходные данные перешли в другой каталог ( > ../outputнапример), или убедитесь, что глобус не соответствует выходному файлу.

Другой способ решения этой проблемы в этом случае - написать:

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

Таким образом, он sortоткрывается outputдля записи и (в моих тестах) не будет делать этого, пока не получит полный список файлов (так долго после того, как глобус будет расширен). Это также позволит избежать дублирования, outputесли ни один из входных файлов не будет читаемым.

Другой способ написать это с помощью zshилиbash

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

При этом используется подстановка процесса (где <(...)заменяется путь к файлу, который относится к концу чтения канала, в который printfпроизводится запись). Эта функция взята ksh, но kshтребует расширения <(...)отдельного аргумента для команды, чтобы вы не могли использовать его с --option=<(...)синтаксисом. Это будет работать с этим синтаксисом, хотя:

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

Обратите внимание, что вы увидите отличие от подходов, которые catпередают вывод файлов, в тех случаях, когда есть файлы, которые не заканчиваются символом новой строки:

$ 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

Также обратите внимание, что sortсортирует, используя алгоритм сортировки в locale ( strcollate()), и sort -uсообщает одну из каждого набора строк, которые сортируются по этому алгоритму, а не уникальные строки на уровне байтов. Если вы заботитесь только об уникальности строк на уровне байтов и не заботитесь о порядке их сортировки, возможно, вы захотите зафиксировать локаль в C, где сортировка основана на значениях байтов ( memcmp(); это, вероятно, ускорит дела обстоят значительно):

printf '%s\0' * | LC_ALL=C sort -u --files0-from=- -o output
Стефан Шазелас
источник
Чувствует себя более естественно, чтобы написать, это также дает возможность sortоптимизировать потребление памяти. Я все еще нахожу printf '%s\0' *немного сложным для ввода, хотя.
малат
Вы можете использовать find . -type f -maxdepth 1 -print0вместо printf '%s\0' *, но я не могу утверждать, что это легче набрать. И последний, конечно, легче определить как псевдоним!
Тоби Спейт
@TobySpeight echoимеет -n, я бы предпочел что-то вроде printf -0 %sэтого, кажется, немного менее низкий уровень, чем'%s\0'
малат
@Toby, -maxdepthи -print0являются расширениями GNU (хотя и широко поддерживаются в наши дни). С другими finds (хотя если у вас есть сортировка GNU, вы, вероятно, также найдете GNU), вы можете это сделать LC_ALL=C find . ! -name . -prune -type f ! -name '.*' -exec printf '%s\0' {} +( LC_ALL=Cпо-прежнему исключая скрытые файлы, содержащие недопустимые символы, даже с GNU find), но это немного излишне, когда вы обычно есть printfвстроенный.
Стефан
2
@malat, вы всегда можете определить print0функцию как print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@";}и тогдаprint0 * | sort...
Стефан
11

Простое исправление работает, по крайней мере, в Bash, поскольку printfвстроено, и ограничения аргументов командной строки к нему не применяются:

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

( echo * | xargsтакже будет работать, за исключением обработки имен файлов с пробелами и т. д.)

ilkkachu
источник
Это кажется лучшим ответом, чем принятый, так как не требует создания отдельного catпроцесса для каждого файла.
LarsH
4
@LarsH, find -exec {} +объединяет несколько файлов за одно выполнение. С find -exec \;ним будет один кот на файл.
ilkkachu
Ах, приятно знать. (Padding)
LarsH
9
find . -maxdepth 1 -type f ! -name ".*" -exec cat {} + | sort -u -o /path/to/sorted.txt

Это объединит все не скрытые обычные файлы в текущем каталоге и отсортирует их объединенное содержимое (при удалении дублированных строк) в файл /path/to/sorted.txt.

Кусалананда
источник
Я пытался использовать только два файла одновременно, чтобы избежать потребления большого количества памяти (мое количество файлов довольно большое). Считаете ли вы |, правильно ли будут цепочки операций для ограничения использования памяти?
малат
2
@malat sortбудет выполнять сортировку вне ядра, если этого требуют требования памяти. По сравнению с ним левая сторона конвейера потребляет очень мало памяти.
Кусалананда
1

Эффективность - это относительный термин, поэтому вам действительно нужно указать, какой фактор вы хотите минимизировать; Процессор, память, диск, время и т. д. В качестве аргумента я собираюсь предположить, что вы хотели минимизировать использование памяти и готовы потратить больше циклов процессора для достижения этой цели. Решения, подобные предложенным Стефаном Шазеласом, хорошо работают

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

но они предполагают, что отдельные текстовые файлы имеют высокую степень уникальности для начала. Если нет, то есть, если после

sort -u < sample.txt > sample.srt

sample.srt более чем на 10% меньше, чем sample.txt, тогда вы сэкономите значительную память, удалив дубликаты в файлах перед объединением. Вы также сэкономите еще больше памяти, не цепляя команды, а это значит, что результаты разных процессов не обязательно должны быть в памяти одновременно.

find /somedir -maxdepth 1 type f -exec sort -u -o {} {} \;
sort -u --files0-from <(printf '%s\0' *) > ../output
Пол Смит
источник
1
Использование памяти редко вызывает беспокойство, так sortкак sortприбегает к использованию временных файлов, когда использование памяти превышает пороговое значение (обычно относительно небольшое). base64 /dev/urandom | sort -uзаполнит ваш диск, но не займет много памяти.
Стефан
Ну, по крайней мере, так обстоит дело с большинством sortреализаций, включая оригинальную в Unix v3 в 1972 году, но, видимо, нет busybox sort. Предположительно потому, что он предназначен для работы в небольших системах, которые не имеют постоянного хранилища.
Стефан
Обратите внимание, что yes | sort -u(все дублированные данные) не должны использовать больше, чем несколько байтов памяти, не говоря уже о диске. Но, sortпо крайней мере, с GNU и Solaris мы видим, что он записывает много 2-байтовых больших файлов /tmp( y\nна каждые несколько мегабайт ввода), так что в конечном итоге он заполнит диск.
Стефан
0

Вроде @ilkkachu, но cat (1) не нужен:

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

Кроме того, если данные слишком длинные, возможно, вы захотите использовать опцию sort (1) --parallel = N

Когда N - это количество процессоров, которые есть у вашего компьютера

Уди
источник