Список файлов, содержащих '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:

  1. Он основан на том же имени файла, который не отображается несколько раз (например, awk 'script' foo bar foo), и вы хотите, чтобы он отображался несколько раз, и
  2. Он полагается на отсутствие переменных, заданных в списке 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
    

Результаты:

Заключение:

На момент написания этой статьи на обычном многопортовом ноутбуке 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

В то время как , кажется, самый интересный способ продолжения, вот еще один из уже существующих решений 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.