cat line X в строку Y на огромном файле

132

Скажем , у меня есть огромный текстовый файл (> 2 Гб) , и я просто хочу catлинии Xк Y(например , 57890000 на 57890010).

Из того, что я понимаю, я могу сделать это, подключившись headк сети tailили наоборот, т.е.

head -A /path/to/file | tail -B

или альтернативно

tail -C /path/to/file | head -D

где A, B, Cи Dмогут быть вычислены из числа строк в файле, Xи Y.

Но есть две проблемы с этим подходом:

  1. Вы должны вычислить A, B, Cи D.
  2. Команды могут передавать pipeдруг другу намного больше строк, чем мне интересно читать (например, если я читаю всего несколько строк в середине огромного файла)

Есть ли способ заставить оболочку просто работать и выводить нужные мне строки? (предоставляя только Xа Y)?

Амелио Васкес-Рейна
источник
1
К вашему сведению, фактическое сравнение скорости теста 6 методов добавлено к моему ответу.
Кевин

Ответы:

119

Я предлагаю sedрешение, но ради полноты,

awk 'NR >= 57890000 && NR <= 57890010' /path/to/file

Вырезать после последней строки:

awk 'NR < 57890000 { next } { print } NR == 57890010 { exit }' /path/to/file

Тест скорости:

  • Файл размером 100 000 000 строк, созданный seq 100000000 > test.in
  • Чтение строк 50 000 000 50 000 010
  • Тесты в произвольном порядке
  • realвремя, как сообщает bashвстроенныйtime
 4.373  4.418  4.395    tail -n+50000000 test.in | head -n10
 5.210  5.179  6.181    sed -n '50000000,50000010p;57890010q' test.in
 5.525  5.475  5.488    head -n50000010 test.in | tail -n10
 8.497  8.352  8.438    sed -n '50000000,50000010p' test.in
22.826 23.154 23.195    tail -n50000001 test.in | head -n10
25.694 25.908 27.638    ed -s test.in <<<"50000000,50000010p"
31.348 28.140 30.574    awk 'NR<57890000{next}1;NR==57890010{exit}' test.in
51.359 50.919 51.127    awk 'NR >= 57890000 && NR <= 57890010' test.in

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

*: За исключением между первыми двумя sed -n p;qи head|tail, которые кажутся по существу одинаковыми.

Kevin
источник
11
Из любопытства: как вы очищали дисковый кеш между тестами?
Павел Румиан
2
Как насчет того tail -n +50000000 test.in | head -n10, что в отличие от tail -n-50000000 test.in | head -n10даст правильный результат?
Жиль
4
Хорошо, я пошел и сделал несколько тестов. tail | head намного быстрее, чем sed, разница намного больше, чем я ожидал.
Жиль
3
@ Жиль, ты прав, мой плохой. tail+|headна 10-15% быстрее, чем sed, я добавил этот тест.
Кевин
1
Я понимаю, что вопрос требует строк, но если вы используете -cдля пропуска символов, tail+|headмгновенно. Конечно, вы не можете сказать «50000000», и вам, возможно, придется вручную искать начало раздела, который вы ищете.
Дэнни Кирхмайер
51

Если вы хотите, чтобы строки от X до Y включали (начиная с нумерации с 1), используйте

tail -n +$X /path/to/file | head -n $((Y-X+1))

tailпрочтет и откажется от первых строк X-1 (обойти это невозможно), затем прочитает и распечатает следующие строки. headпрочтет и распечатает запрошенное количество строк, затем выйдет. Когда headвыходит, tailполучает сигнал SIGPIPE и умирает, поэтому он не будет считывать из входного файла строки размером больше буфера (обычно несколько килобайт).

В качестве альтернативы, как предложил gorkypl , используйте sed:

sed -n -e "$X,$Y p" -e "$Y q" /path/to/file

Решение sed значительно медленнее (по крайней мере, для утилит GNU и Busybox; sed может быть более конкурентоспособным, если вы извлекаете большую часть файла в ОС, где скорость передачи данных медленная, а скорость седа). Вот быстрые тесты под Linux; данные были сгенерированы seq 100000000 >/tmp/a, среда Linux / amd64, /tmptmpfs, и машина в противном случае простаивает и не переставляет.

real  user  sys    command
 0.47  0.32  0.12  </tmp/a tail -n +50000001 | head -n 10 #GNU
 0.86  0.64  0.21  </tmp/a tail -n +50000001 | head -n 10 #BusyBox
 3.57  3.41  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #GNU
11.91 11.68  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #BusyBox
 1.04  0.60  0.46  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #GNU
 7.12  6.58  0.55  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #BusyBox
 9.95  9.54  0.28  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #GNU
23.76 23.13  0.31  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #BusyBox

Если вам известен диапазон байтов, с которым вы хотите работать, вы можете извлечь его быстрее, перейдя непосредственно к начальной позиции. Но для строк, вы должны читать с начала и считать новые строки. Чтобы извлечь блоки от x включительно до y эксклюзивно, начиная с 0, с размером блока b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file
жилль
источник
1
Вы уверены, что между ними нет кэширования? Различия между tail | head и sed кажутся мне слишком большими.
Павел Румиан
@gorkypl Я сделал несколько мер, и времена были сопоставимы. Как я уже писал, все это происходит в оперативной памяти (все находится в кеше).
Жиль
1
@ Жиля, tail will read and discard the first X-1 lineкажется, избегают, когда число строк дается с конца, В этом случае, хвост, кажется, читает назад с конца в соответствии с временем выполнения. Пожалуйста , прочитайте: http://unix.stackexchange.com/a/216614/79743.
1
@BinaryZebra Да, если входной файл является обычным файлом, некоторые реализации tail(включая хвост GNU) имеют эвристику для чтения с конца. Это улучшает tail | headрешение по сравнению с другими методами.
Жиль
22

head | tailПодход является одним из лучших и наиболее «идиоматических» способов сделать это:

X=57890000
Y=57890010
< infile.txt head -n "$Y" | tail -n +"$X"

Как отметил Жиль в комментариях, более быстрый путь

< infile.txt tail -n +"$X" | head -n "$((Y - X))"

Причина, по которой это происходит быстрее, заключается в том, что первым линиям X - 1 не нужно проходить через канал по сравнению с head | tailподходом.

Ваш вопрос, как сформулированный, немного вводит в заблуждение и, вероятно, объясняет некоторые из ваших необоснованных опасений по поводу этого подхода.

  • Вы говорите , что вы должны вычислить A, B, C, Dно , как вы можете видеть, количество строк файла не требуется , и расчет в большинстве 1 необходимо, что оболочка может сделать для вас в любом случае.

  • Вы беспокоитесь, что трубопровод будет читать больше строк, чем необходимо. На самом деле это не так: tail | headон настолько эффективен, насколько это возможно с точки зрения файлового ввода-вывода. Во-первых, рассмотрим минимальный объем необходимой работы: чтобы найти X -ю строку в файле, единственный общий способ сделать это - прочитать каждый байт и остановиться, когда вы подсчитываете символы новой строки X, так как нет способа предугадать файл смещение X -й линии. Как только вы достигнете * X * -ой строки, вы должны прочитать все строки, чтобы напечатать их, останавливаясь на Y -й строке. Таким образом, ни один подход не может сойти с рук, читая меньше Y строк. Теперь head -n $Yчитает не более Yстроки (округлены до ближайшего элемента буфера, но буферы при правильном использовании улучшают производительность, поэтому не нужно беспокоиться об этих издержках). Кроме того, tailне будет читать больше, чем head, поэтому, таким образом, мы показали, что head | tailчитает наименьшее количество возможных строк (опять же, плюс незначительная буферизация, которую мы игнорируем). Единственное преимущество в эффективности подхода с одним инструментом, в котором не используются трубы, - это меньше процессов (и, следовательно, меньше накладных расходов).

jw013
источник
1
Никогда не видел, чтобы перенаправление шло первым на линии раньше. Круто, это делает поток трубы более четким.
Clacke
14

Наиболее ортодоксальным способом (но не самым быстрым, как отметил Жиль выше) было бы использовать sed.

В твоем случае:

X=57890000
Y=57890010
sed -n -e "$X,$Y p" -e "$Y q" filename

-nВариант подразумевает , что только соответствующие строки печатаются на стандартный вывод.

Буква p в конце номера конечной строки означает печать строк в заданном диапазоне. Кв во второй части сценария экономит время, пропуская оставшуюся часть файла.

Павел Румиан
источник
1
Я ожидал sedи tail | headбудет примерно на одном уровне, но оказывается, что tail | headэто значительно быстрее (см. Мой ответ ).
Жиль
1
Я не знаю, из того, что я прочитал, tail/ headсчитаются более "ортодоксальными", поскольку обрезка любого конца файла - это именно то, для чего они созданы. В этих материалах sedтолько кажется, что вводится в картину, когда требуются замены - и быстро выталкивается из картинки, когда начинает происходить что-то намного более сложное, поскольку его синтаксис для сложных задач намного хуже, чем AWK, который затем вступает во владение ,
underscore_d
7

Если мы знаем диапазон для выбора, от первой строки: lStartдо последней строки: lEndмы могли бы вычислить:

lCount="$((lEnd-lStart+1))"

Если мы знаем общее количество строк: lAllмы также можем рассчитать расстояние до конца файла:

toEnd="$((lAll-lStart+1))"

Тогда мы будем знать оба:

"how far from the start"            ($lStart) and
"how far from the end of the file"  ($toEnd).

Выбор самого маленького из них: tailnumberкак это:

tailnumber="$toEnd"; (( toEnd > lStart )) && tailnumber="+$linestart"

Позволяет нам использовать наиболее быстро выполняемую команду:

tail -n"${tailnumber}" ${thefile} | head -n${lCount}

Обратите внимание на дополнительный знак плюс («+»), когда $linestartвыбран.

Единственное предостережение в том, что нам нужно общее количество строк, и это может занять некоторое дополнительное время, чтобы найти.
Как обычно с:

linesall="$(wc -l < "$thefile" )"

Несколько раз измеряются:

lStart |500| lEnd |500| lCount |11|
real   user   sys    frac
0.002  0.000  0.000  0.00  | command == tail -n"+500" test.in | head -n1
0.002  0.000  0.000  0.00  | command == tail -n+500 test.in | head -n1
3.230  2.520  0.700  99.68 | command == tail -n99999501 test.in | head -n1
0.001  0.000  0.000  0.00  | command == head -n500 test.in | tail -n1
0.001  0.000  0.000  0.00  | command == sed -n -e "500,500p;500q" test.in
0.002  0.000  0.000  0.00  | command == awk 'NR<'500'{next}1;NR=='500'{exit}' test.in


lStart |50000000| lEnd |50000010| lCount |11|
real   user   sys    frac
0.977  0.644  0.328  99.50 | command == tail -n"+50000000" test.in | head -n11
1.069  0.756  0.308  99.58 | command == tail -n+50000000 test.in | head -n11
1.823  1.512  0.308  99.85 | command == tail -n50000001 test.in | head -n11
1.950  2.396  1.284  188.77| command == head -n50000010 test.in | tail -n11
5.477  5.116  0.348  99.76 | command == sed -n -e "50000000,50000010p;50000010q" test.in
10.124  9.669  0.448  99.92| command == awk 'NR<'50000000'{next}1;NR=='50000010'{exit}' test.in


lStart |99999000| lEnd |99999010| lCount |11|
real   user   sys    frac
0.001  0.000  0.000  0.00  | command == tail -n"1001" test.in | head -n11
1.960  1.292  0.660  99.61 | command == tail -n+99999000 test.in | head -n11
0.001  0.000  0.000  0.00  | command == tail -n1001 test.in | head -n11
4.043  4.704  2.704  183.25| command == head -n99999010 test.in | tail -n11
10.346  9.641  0.692  99.88| command == sed -n -e "99999000,99999010p;99999010q" test.in
21.653  20.873  0.744  99.83 | command == awk 'NR<'99999000'{next}1;NR=='99999010'{exit}' test.in

Обратите внимание, что времена резко меняются, если выбранные линии находятся в начале или в конце. Команда, которая работает хорошо на одной стороне файла, может быть очень медленной на другой стороне файла.


источник
Комментарии не для расширенного обсуждения; этот разговор был перемещен в чат .
Terdon
@BinaryZebra - намного лучше.
mikeserv
0

Я делаю это достаточно часто и поэтому написал этот сценарий. Мне не нужно искать номера строк, скрипт делает все это.

#!/bin/bash

# $1: start time
# $2: end time
# $3: log file to read
# $4: output file

# i.e. log_slice.sh 18:33 19:40 /var/log/my.log /var/log/myslice.log

if [[ $# != 4 ]] ; then 
echo 'usage: log_slice.sh <start time> <end time> <log file> <output file>'
echo
exit;
fi

if [ ! -f $3 ] ; then
echo "'$3' doesn't seem to exit."
echo 'exiting.'
exit;
fi

sline=$(grep -n " ${1}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of start time
eline=$(grep -n " ${2}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of end time

linediff="$((eline-sline))"

tail -n+${sline} $3|head -n$linediff > $4
Doolan
источник
2
Вы отвечаете на вопрос, который не был задан. Ваш ответ - 10% tail|head, который подробно обсуждался в этом вопросе и других ответах, и 90% определяют номера строк, где появляются указанные строки / шаблоны, что не было частью вопроса . PS вы всегда должны указывать параметры и переменные вашей оболочки; например, «3 доллара» и «4 доллара».
G-Man