Список файлов, которые содержат `n` или меньше строк - PullRequest
0 голосов
/ 03 октября 2018

Вопрос

В папке я хотел бы напечатать имя каждого .txt файла, содержащего n=27 строк или меньше строк.Я мог бы сделать

wc -l *.txt | awk '{if ($1 <= 27){print}}'

Проблема в том, что многие файлы в папке имеют миллионы строк (и строки довольно длинные), и, следовательно, команда wc -l *.txt очень медленная.В принципе, процесс может подсчитать количество строк до тех пор, пока не найдет не менее n строк, а затем перейти к следующему файлу.

Что является более быстрой альтернативой?

К вашему сведению, я на 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 поможет.

Ответы [ 10 ]

0 голосов
/ 04 октября 2018

Хотя представляется наиболее интересным способом продолжения, вот еще один вариант уже существующих решений triplee , anubhava и Эд Мортон .Где решения triplee и anubhava используют оператор nextfile, а решение 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
0 голосов
/ 03 октября 2018

Вы можете использовать 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 строк.

0 голосов
/ 04 октября 2018

Программные средства и GNU sed (более ранние версии до v4.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 uit в сочетании с переключателем --separate не просто выходит из текущего файла, она полностью выходит из sed.Это означает, что для каждого файла требуется отдельный экземпляр sed.

0 голосов
/ 03 октября 2018

С GNU awk для следующего файла и 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

Они оба будут работать независимо от того, являются ли входные файлы пустыми или нет.Предостережения для не-gawk-версии те же, что и для других ваших текущих ответов на awk:

  1. Он основан на том же имени файла, которое не появляется несколько раз (например, awk 'script' foo bar foo), и вы хотите, чтобы оно отображалосьнесколько раз, и
  2. Он основан на том, что в списке аргументов не заданы переменные (например, awk 'script' foo FS=, bar)

В версии gawk таких ограничений нет.

ОБНОВЛЕНИЕ:

Чтобы проверить время между вышеупомянутым сценарием GNU awk и сценарием 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
^O@bM\
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
0 голосов
/ 03 октября 2018

с sed (GNU sed) 4.5:

sed -n -s '28q;$F' *.txt
0 голосов
/ 03 октября 2018

Если вы используете 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/:[^:]*$//' удаляет файлы, содержащие не менее 28 строк, и печатает имя файла остальных

Альтернативная версия: последовательная обработка вместо параллельнойодин

res=$(grep -c -m28 -H ^ $files); sed '/:28$/ d; s/:[^:]*$//' <<< "$res"

Сравнительный анализ

Эд Мортон оспорил мое утверждение о том, что этот ответ может быть быстрее, чем awk.Он добавил некоторые критерии в свой ответ и, хотя он не дает никаких заключений, я считаю, что опубликованные им результаты вводят в заблуждение, показывая большее время для ответа на мой ответ без учета времени пользователя и системы.Поэтому вот мои результаты.

Сначала тестовая платформа:

  • Четырехъядерный ноутбук Intel i5 под управлением Linux, вероятно, довольно близко к системе OP (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, аналогичном машине OP, этот ответr самый быстрый, который дает точные результаты.На моей машине он в два раза быстрее самого быстрого сценария awk.

Примечания:

  • Почему платформа имеет значение?Потому что мой ответ основан на распараллеливании обработки между grep и sed.Конечно, для непредвзятых результатов, если у вас есть только одно ядро ​​ЦП (ВМ?) Или другие ограничения вашей ОС в отношении выделения ЦП, вы должны сравнить альтернативную (последовательную) версию.

  • Очевидно, что вы не можете сделать вывод только за время ожидания, так как оно зависит от количества одновременных процессов, запрашивающих ЦП, против количества ядер на машине.Поэтому я добавил пользователя + sys timings

  • Эти временные интервалы составляют в среднем более 20 прогонов, кроме случаев, когда команда заняла более 1 минуты (только один прогон)

  • Для всех ответов, которые занимают менее 10 с, время, затрачиваемое оболочкой на обработку *.txt, не является незначительным, поэтому я предварительно обработал список файлов, поместил его в переменную и добавил содержимоепеременная для команды, которую я тестировал.

  • Все ответы дали одинаковые результаты, кроме 1. Ответ tripleee, который включает argv[0] ("awk") в свой результат (зафиксированный в моих тестах);2. ответ Квантура, в котором перечислены только пустые файлы (исправлено с -v n=27);и 3. ответ find + sed, в котором пропущены пустые файлы (не исправлено).

  • Я не смог проверить ответ ctac_ , поскольку у меня нет GNU sed 4.5 врука.Это, пожалуй, самый быстрый из всех, но также пропускает пустые файлы.

  • Ответ Python не закрывает свои файлы.Сначала я должен был сделать ulimit -n hard.

0 голосов
/ 03 октября 2018

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

0 голосов
/ 03 октября 2018

Если вам нужно вызывать 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 успешным, и печатает имя файла.

0 голосов
/ 03 октября 2018

Как это?

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 году. Если вам это нужно для работы на действительно старых ОС динозавров (или, боже мой, вероятно, на Windows), удачи и / или попробуйте GNU Awk.

0 голосов
/ 03 октября 2018

Вы можете попробовать этот 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 обрабатывает файлы построчно, поэтому он не будет пытатьсяпрочитайте полный файл, чтобы получить количество строк.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...