Список файлов, содержащих 'n' или меньше строк
Вопрос
В папке я хотел бы напечатать имя каждого .txt
файла, содержащего n=27
строк или меньше строк. я мог бы сделать
wc -l *.txt | awk '{if ($1 <= 27){print}}'
Проблема в том, что многие файлы в папке составляют миллионы строк (и строки довольно длинны), и поэтому команда wc -l *.txt
очень медленная. В принципе процесс может подсчитывать количество строк до нахождения не менее n
строк, а затем перейти к следующему файлу.
Что такое более быстрая альтернатива?
FYI, я на MAC OSX 10.11.6
попытка
Вот попытка с 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)
}
который можно назвать
awk -v n=27 -f myAwk.awk *.txt
Тем не менее, код не удается распечатать совершенно пустые файлы. Я не уверен, как это исправить, и я не уверен, что мой awk-скрипт - это путь.
Ответы
Ответ 1
С GNU awk для nextfile и ENDFILE:
awk -v n=27 'FNR>n{f=1; nextfile} ENDFILE{if (!f) print FILENAME; f=0}' *.txt
С любым 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
Они будут работать независимо от того, пустыми ли входные файлы или нет. Оговорки для версии non-gawk такие же, как и для ваших других текущих ответов awk:
- Он основан на том же имени файла, который не отображается несколько раз (например,
awk 'script' foo bar foo
), и вы хотите, чтобы он отображался несколько раз, и - Он полагается на отсутствие переменных, заданных в списке arg (например,
awk 'script' foo FS=, bar
)
Версия gawk не имеет таких ограничений.
ОБНОВИТЬ:
Чтобы проверить время между вышеупомянутым сценарием awk GNU и скриптом GNU grep + sed, опубликованным xhienne, поскольку она заявила, что ее решение будет faster than a pure awk script
я создал 10 000 входных файлов, все от 0 до 1000 строк в длину, используя этот скрипт:
$ 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} }'
и затем выполнил 2 команды на них и получил эти результаты с 3-мя результатами:
$ 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
Оба сценария создавали одинаковые выходные файлы. Вышеизложенное было выполнено в bash на cygwin. Я ожидаю, что в разных системах результаты синхронизации могут немного отличаться, но разница всегда будет незначительной.
Чтобы напечатать 10 строк до 20 случайных символов в строке (см. Комментарии):
$ 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
Сделать это все в awk (что будет намного медленнее):
$ 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
Ответ 2
Если вы используете GNU grep
(к сожалению, MacOSX> = 10.8 предоставляет BSD grep, чьи -m
и -c
действуют глобально, а не на файл), вам может показаться интересной эта альтернатива (и более быстрая, чем сценарий чистого awk
):
grep -c -m28 -H ^ *.txt | sed '/:28$/ d; s/:[^:]*$//'
Объяснение:
-
grep -c -m28 -H ^ *.txt
выводит имя каждого файла с количеством строк в каждом файле, но не считывая более 28 строк -
sed '/:28$/d; s/:[^:]*$//'
sed '/:28$/d; s/:[^:]*$//'
удаляет файлы, содержащие не менее 28 строк, и печатает имя файла остальных
Альтернативная версия: последовательная обработка вместо параллельной
res=$(grep -c -m28 -H ^ $files); sed '/:28$/ d; s/:[^:]*$//' <<< "$res"
Бенчмаркинг
Эд Мортон оспорил мое утверждение, что этот ответ может быть быстрее, чем awk
. Он добавил некоторые критерии к своему ответу и, хотя он не дает никакого заключения, я считаю, что опубликованные им результаты вводят в заблуждение, показывая большее время блокировки -c для моего ответа без учета времени пользователя и системы. Поэтому вот мои результаты.
Сначала тестовая платформа:
-
Четырех -c или Intel i5 ноутбук под управлением Linux, вероятно, довольно близко к операционной системе (Apple iMac).
-
Новый каталог из 100 000 текстовых файлов, в среднем ~ 400 строк, что в сумме составляет 640 МБ, которые полностью хранятся в моих системных буферах. Файлы были созданы с помощью этой команды:
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
Результаты:
- grep + sed (этот ответ): 561 мс прошло, 586 мс user + sys
- grep + sed (этот ответ, последовательная версия): 678 мс прошло, 688 мс user + sys
- awk (Эд Мортон): 1050 мс прошло, 1036 мс пользователь + sys
- awk (tripleee): 1137 мс прошло, 1123 мс пользователь + sys
- awk (анубхава): 1150 мс прошло, 1137 мс пользователь + sys
- awk (квантур): 1280 мс прошло, 1266 мс пользователь + sys
- питон (Джои Харрингтон): 1543 мс прошло, 1537 мс пользователь + sys
- find + xargs + sed (agc): 91 с, 10 с пользователя + sys
- для + awk (Джефф Шаллер): 247 с, 83 с пользователя + сис
- find + bash + grep (hek2mgl): 356 с прошло, 116 с пользователь + sys
Заключение:
На момент написания этой статьи на обычном многопортовом ноутбуке Unix -c, похожем на OP, этот ответ является самым быстрым и дает точные результаты. На моей машине это в два раза быстрее, чем самый быстрый скрипт на awk.
Заметки:
-
Почему платформа имеет значение? Потому что мой ответ основан на распараллеливании обработки между grep
и sed
. Конечно, для непредвзятых результатов, если у вас есть только одно ядро ЦП (ВМ?) Или другие ограничения вашей ОС в отношении распределения ЦП, вы должны сравнить альтернативную (последовательную) версию.
-
Очевидно, что вы не можете сделать вывод только за время ожидания, так как оно зависит от количества одновременных процессов, запрашивающих ЦП, против количества ядер на машине. Поэтому я добавил пользователя + sys тайминги
-
Эти интервалы в среднем составляют более 20 запусков, кроме случаев, когда команда заняла более 1 минуты (только один прогон)
-
Для всех ответов, которые занимают менее 10 с, время, затрачиваемое оболочкой на обработку *.txt
, не является ничтожным, поэтому я предварительно обработал список файлов, поместил его в переменную и добавил содержимое переменной в команду я был бенчмаркинг.
-
Все ответы дали одинаковые результаты, кроме 1. tripleee, который включает argv[0]
("awk") в свой результат (исправлено в моих тестах); 2. kvantour ответ, в котором перечислены только пустые файлы (исправлено с -v n=27
); и 3. ответ find + sed, в котором пропущены пустые файлы (не исправлено).
-
Я не смог проверить ответ ctac_, так как у меня нет GNU sed 4.5 под рукой. Это, вероятно, самый быстрый из всех, но также пропускает пустые файлы.
-
Ответ Python не закрывает свои файлы. ulimit -n hard
мне пришлось ulimit -n hard
сделать ulimit -n hard
.
Ответ 3
Вы можете попробовать этот awk
который переходит к следующему файлу, как только количество строк будет превышать 27
:
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
обрабатывает файлы по строкам, поэтому он не будет пытаться прочитать полный файл, чтобы получить количество строк.
Ответ 4
Как это?
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
Мы копируем список аргументов имени файла в ассоциативный массив, а затем удаляем из него все файлы, у которых есть 28-я строка. Пустые файлы, очевидно, не будут соответствовать этому условию, поэтому в конце мы оставляем все файлы с меньшим количеством строк, включая пустые.
nextfile
был распространенным расширением во многих вариантах Awk, а затем был кодифицирован POSIX в 2012 году. Если вам это нужно, чтобы работать с действительно старыми операционными системами динозавров (или, nextfile
, возможно, с Windows), удачи и/или попробовать GNU Awk.
Ответ 5
В то время как awk, кажется, самый интересный способ продолжения, вот еще один из уже существующих решений triplee, anubhava и Ed Morton. Где решения triplee и anubhava используют оператор nextfile
а решение Ed Morton POSIX - это чтение полных файлов, я предоставляю решение, которое не читает полные файлы.
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
Ответ 6
с sed (GNU sed) 4.5:
sed -n -s '28q;$F' *.txt
Ответ 7
Вы можете использовать find
с помощью небольшого встроенного скрипта bash:
find -type f -exec bash -c '[ $(grep -cm 28 ^ "${1}") != "28" ] && echo "${1}"' -- {} \;
Команда [ $(grep -cm 28 ^ "${1}") != "28" ] && echo "${1}"
использует grep для поиска начала строки (^
) максимум 28 раз. Если эта команда вернется! = "28", файл должен содержать менее 28 строк.
Ответ 8
Если вам нужно позвонить awk отдельно, попросите его остановиться в строке 28:
for f in ./*.txt
do
if awk 'NR > 27 { fail=1; exit; } END { exit fail; }' "$f"
then
printf '%s\n' "$f"
fi
done
Значение по умолчанию awk-переменных равно нулю, поэтому, если мы никогда не ударяем строку 28, код выхода равен нулю, что делает успешный тест if
и поэтому печатает имя файла.
Ответ 9
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
Ответ 10
Программные средства и GNU sed
(более старые версии до версии 4.5) mashup:
find *.txt -print0 | xargs -0 -L 1 sed -n '28q;$F'
Это пропускает 0-байтные файлы, чтобы включить их также:
find *.txt \( -exec sed -n '28{q 1}' '{}' \; -or -size 0 \) -print
(По некоторым причинам запуск sed
через -exec
примерно на 12% медленнее, чем xargs
.)
Код sed
украденный из ответа ctac.
Примечание: На моей системе старше sed
v4.4-2, то q
команда ПИФ в сочетании с --separate
переключатель не просто выйти из текущего файла, он завершает работу sed
целиком. Это означает, что для каждого файла требуется отдельный экземпляр sed
.