Как я могу прочитать первые n и последние n строк из файла?

Как я могу прочитать первые n строк и последние n строк файла?

Для n=2 я прочитал онлайн что (head -n2 && tail -n2) будет работать, но это не так.

$ cat x
1
2
3
4
5
$ cat x | (head -n2 && tail -n2)
1
2

Ожидаемый результат для n=2 будет следующим:

1
2
4
5

person Amir    schedule 19.02.2015    source источник
comment
unix.stackexchange.com/questions/139089/   -  person Amir    schedule 19.02.2015
comment
Кроме того, ссылка, которую вы прислали, бесполезна, потому что я действительно не знаю диапазона. Я ищу простое решение для этого   -  person Amir    schedule 19.02.2015
comment
Интересно, что cat x | (head -n2 && tail -n2) не работает, а (head -n2 && tail -n2) < x работает. Мне придется немного поразмыслить, почему это так.   -  person Wintermute    schedule 19.02.2015
comment
Каким будет ожидаемый результат, если входной файл будет состоять из 3 строк? Будет ли это 1 2 3 или 1 2 2 3 или что-то еще? Что, если бы это было всего 2 строки, вывод был бы 1 2 1 2 или 1 1 2 2 или 1 2 или что-то еще?   -  person Ed Morton    schedule 19.02.2015
comment
Я отредактировал вопрос с ожидаемым результатом.   -  person Amir    schedule 19.02.2015
comment
Хорошо, теперь покажите ожидаемый результат для n=3, n=5 и n=7 с учетом этого входного файла.   -  person Ed Morton    schedule 19.02.2015
comment
Ну, в моем файле больше строк, чем 2n. Я не совсем уверен, каков ответ на ваш вопрос, когда у вас меньше строк, чем 2n.   -  person Amir    schedule 19.02.2015
comment
Я не думаю, что трюк head && tail надежен. head из GNU coreutils ведет себя по-разному для каналов и обычных файлов (источник: источник), читая поблочно в одном случае, но не в другом. В зависимости от деталей реализации это кажется плохой идеей - нет гарантии, что head оставит все, что не печатает, для работы с tail.   -  person Wintermute    schedule 19.02.2015


Ответы (9)


Скорее всего, вы захотите что-то вроде:

... | awk -v OFS='\n' '{a[NR]=$0} END{print a[1], a[2], a[NR-1], a[NR]}'

или если вам нужно указать число и принять во внимание проницательное замечание @Wintermute о том, что вам не нужно буферизовать весь файл, что-то вроде этого - это то, что вам действительно нужно:

... | awk -v n=2 'NR<=n{print;next} {buf[((NR-1)%n)+1]=$0}
         END{for (i=1;i<=n;i++) print buf[((NR+i-1)%n)+1]}'

Я думаю, что математика в этом правильная - надеюсь, вы поняли идею использовать вращающийся буфер, индексированный NR, модифицированный размером буфера и настроенный на использование индексов в диапазоне 1-n вместо 0-(n-1) .

Чтобы помочь с пониманием оператора модуля, используемого в индексации выше, вот пример с промежуточными операторами печати, чтобы показать логику по мере ее выполнения:

$ cat file   
1
2
3
4
5
6
7
8

.

$ cat tst.awk                
BEGIN {
    print "Populating array by index ((NR-1)%n)+1:"
}
{
    buf[((NR-1)%n)+1] = $0

    printf "NR=%d, n=%d: ((NR-1 = %d) %%n = %d) +1 = %d -> buf[%d] = %s\n",
        NR, n, NR-1, (NR-1)%n, ((NR-1)%n)+1, ((NR-1)%n)+1, buf[((NR-1)%n)+1]

}
END { 
    print "\nAccessing array by index ((NR+i-1)%n)+1:"
    for (i=1;i<=n;i++) {
        printf "NR=%d, i=%d, n=%d: (((NR+i = %d) - 1 = %d) %%n = %d) +1 = %d -> buf[%d] = %s\n",
            NR, i, n, NR+i, NR+i-1, (NR+i-1)%n, ((NR+i-1)%n)+1, ((NR+i-1)%n)+1, buf[((NR+i-1)%n)+1]
    }
}
$ 
$ awk -v n=3 -f tst.awk file
Populating array by index ((NR-1)%n)+1:
NR=1, n=3: ((NR-1 = 0) %n = 0) +1 = 1 -> buf[1] = 1
NR=2, n=3: ((NR-1 = 1) %n = 1) +1 = 2 -> buf[2] = 2
NR=3, n=3: ((NR-1 = 2) %n = 2) +1 = 3 -> buf[3] = 3
NR=4, n=3: ((NR-1 = 3) %n = 0) +1 = 1 -> buf[1] = 4
NR=5, n=3: ((NR-1 = 4) %n = 1) +1 = 2 -> buf[2] = 5
NR=6, n=3: ((NR-1 = 5) %n = 2) +1 = 3 -> buf[3] = 6
NR=7, n=3: ((NR-1 = 6) %n = 0) +1 = 1 -> buf[1] = 7
NR=8, n=3: ((NR-1 = 7) %n = 1) +1 = 2 -> buf[2] = 8

Accessing array by index ((NR+i-1)%n)+1:
NR=8, i=1, n=3: (((NR+i = 9) - 1 = 8) %n = 2) +1 = 3 -> buf[3] = 6
NR=8, i=2, n=3: (((NR+i = 10) - 1 = 9) %n = 0) +1 = 1 -> buf[1] = 7
NR=8, i=3, n=3: (((NR+i = 11) - 1 = 10) %n = 1) +1 = 2 -> buf[2] = 8
person Ed Morton    schedule 19.02.2015
comment
+1, так как это работает в трубе. Вы можете добавить более сложную версию, которая учитывает файлы (потоки), имеющие менее 4 строк (голова + хвост). - person hek2mgl; 19.02.2015
comment
@EdMorton Но все равно нужно будет буферизовать весь поток в памяти .. (Однако я не вижу способа без буферизации, если он должен работать в канале, кроме сохранения потока во временный файл) - person hek2mgl; 19.02.2015
comment
Ага, теперь не масштабируется под большой файл. Тем не менее, это работает для меня. - person Amir; 19.02.2015
comment
Интересно, почему кошка х | (head -n2 && tail -n2) не работает... потому что это было бы идеальным решением - person Amir; 19.02.2015
comment
Вы можете сохранить только текущее хранилище последних n строк, которые вы прочитали. tail не читает с самого начала обычные файлы, заметьте. - person Wintermute; 19.02.2015
comment
awk -v n=2 'NR <= n { print; next } { delete lines[NR - n]; lines[NR] = $0 } END { for(i = NR - n + 1; i <= NR; ++i) print lines[i] }' это то, что я собирался предложить. В качестве бонуса он не печатает повторяющиеся центральные линии в небольших файлах, хотя для OP это не было проблемой. РЕДАКТИРОВАТЬ: О, мне нравится идея по модулю. - person Wintermute; 19.02.2015
comment
Чтобы иметь \n между строками: awk -v ORS='\n' '{a[NR]=$0} END{print a[1]\na[2]\na[NR-1]\na[NR] }' - person Amir; 20.02.2015
comment
Я понимаю, но ошибка заключалась в том, что я устанавливал ORS='\n', когда должен был устанавливать OFS='\n'. Теперь, когда это исправлено, нет необходимости явно жестко кодировать "\n" между полями. - person Ed Morton; 20.02.2015
comment
Меня поразило, что логика модуля, используемая при заполнении/доступе к содержимому массива, может быть неочевидной, поэтому я обновил свой ответ примером, который печатает все соответствующие значения по мере их использования. Надеюсь, что это поможет сейчас и, возможно, в качестве будущей ссылки для других примеров. - person Ed Morton; 20.02.2015

Это может сработать для вас (GNU sed):

sed -n ':a;N;s/[^\n]*/&/2;Ta;2p;$p;D' file

Это сохраняет окно из 2 (замените 2 на n) строк, а затем печатает первые 2 строки, а в конце файла печатает окно, т.е. последние 2 строки.

person potong    schedule 20.02.2015

Вот однострочный код GNU sed, который печатает первые 10 и последние 10 строк:

gsed -ne'1,10{p;b};:a;$p;N;21,$D;ba'

Если вы хотите напечатать разделитель «--» между ними:

gsed -ne'1,9{p;b};10{x;s/$/--/;x;G;p;b};:a;$p;N;21,$D;ba'

Если вы работаете на Mac и у вас нет GNU sed, вы не сможете сжать так много:

sed -ne'1,9{' -e'p;b' -e'}' -e'10{' -e'x;s/$/--/;x;G;p;b' -e'}' -e':a' -e'$p;N;21,$D;ba'

Объяснение

gsed -ne' вызвать sed без автоматической печати шаблона пространства

-e'1,9{p;b}' напечатать первые 9 строк

-e'10{x;s/$/--/;x;G;p;b}' вывести строку 10 с добавленным разделителем '--'

-e':a;$p;N;21,$D;ba' напечатать последние 10 строк

person parleer    schedule 28.12.2017

awk -v n=4 'NR<=n; {b = b "\n" $0} NR>=n {sub(/[^\n]*\n/,"",b)} END {print b}'

Первые n строк покрыты NR<=n;. Для последних n строк мы просто отслеживаем буфер, содержащий последние n строк, постоянно добавляя одну в конец и удаляя одну впереди (после первых n).

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

ETA: Поскольку приведенная выше оценка времени вызвала некоторое обсуждение в (теперь удаленных) комментариях, я добавлю анекдоты, опробованные на практике.

С огромным файлом (100 миллионов строк, 3,9 ГиБ, n = 5) это заняло 454 секунды, по сравнению с решением @EdMorton с линейным буфером, которое выполнялось всего за 30 секунд. С более скромными входными данными (просто миллионы строк) соотношение аналогичное: 4,7 секунды против 0,53 секунды.

Почти все это дополнительное время в этом решении, по-видимому, потрачено на функцию sub(); крошечная часть также происходит из-за того, что конкатенация строк выполняется медленнее, чем просто замена члена массива.

person hemflit    schedule 20.02.2015

Если вы используете оболочку, которая поддерживает подстановку процессов, еще один способ добиться этого — записать в несколько процессов, один для head, а другой для tail. Предположим для этого примера, что ваш ввод поступает из канала, который подает вам контент неизвестной длины. Вы хотите использовать только первые 5 строк и последние 10 строк и передать их другому каналу:

cat | { tee >(head -5) >(tail -10) 1>/dev/null} | cat

Использование {} собирает выходные данные внутри группы (будут две разные программы, записывающие в stdout внутри оболочек процессов). 1>/dev/null состоит в том, чтобы избавиться от дополнительной копии, которую tee попытается записать в собственный стандартный вывод.

Это демонстрирует концепцию и все движущиеся части, но на практике ее можно немного упростить, используя поток STDOUT tee вместо его отбрасывания. Обратите внимание, что здесь по-прежнему необходима группировка команд для передачи вывода через следующий канал!

cat | { tee >(head -5) | tail -15 } | cat

Очевидно, замените cat в конвейере тем, что вы на самом деле делаете. Если ваш ввод может обрабатывать один и тот же контент для записи в несколько файлов, вы можете полностью отказаться от использования tee, а также от использования STDOUT. Допустим, у вас есть команда, которая принимает несколько флагов имени выходного файла -o:

{ mycommand -o >(head -5) -o >(tail -10)} | cat
person Caleb    schedule 05.08.2019

Используйте параллель GNU. Чтобы напечатать первые три строки и последние три строки:

parallel {} -n 3 file ::: head tail
person kko    schedule 19.04.2018

На основе dcaswell, следующий скрипт sed печатает первую и последнюю 10 строк файла:

# Make a test file first
testit=$(mktemp -u)
seq 1 100 > $testit
# This sed script:
sed -n ':a;1,10h;N;${x;p;i\
-----
;x;p};11,$D;ba' $testit
rm $testit

Выдает это:

1
2
3
4
5
6
7
8
9
10
-----
90
91
92
93
94
95
96
97
98
99
100
person steveo'america    schedule 29.05.2018
comment
И хотя это работает для файлов короче 20 строк, кажется, что последняя строка проглатывается для файлов короче 10 строк. фу. - person steveo'america; 29.05.2018

Вот еще один скрипт AWK. Предполагая, что может быть перекрытие головы и хвоста.

Файл script.awk

BEGIN {range = 3} # Define the head and tail range
NR <= range {print} # Output the head; for the first lines in range
{ arr[NR % range] = $0} # Store the current line in a rotating array
END { # Last line reached
    for (row = NR - range + 1; row <= NR; row++) { # Reread the last range lines from array
        print arr[row % range];
    }
}

Запуск скрипта

seq 1 7 | awk -f script.awk

Выход

1
2
3
5
6
7

Для перекрытия головы и хвоста:

seq 1 5 |awk -f script.awk


1
2
3
3
4
5
person Dudi Boy    schedule 05.08.2019

person    schedule
comment
УУОК. head -n2 x && tail -n2 x - person rici; 19.02.2015
comment
@rici: это было легко исправить :D. - person gniourf_gniourf; 19.02.2015
comment
Это не приведет к правильному выводу, если файл имеет длину 3 строки или меньше. - person Walter Nissen; 17.01.2020
comment
Объяснение было бы в порядке. - person Peter Mortensen; 21.08.2020
comment
Это не гарантирует работу, даже если ваш файл длиннее 4 строк, если один буфер head настолько длинный, что в файле осталось недостаточно строк для работы tail. - person Charles Duffy; 19.02.2021
comment
Попробуйте printf '%s\n' one two three four | { head -n1 && tail -n1; } -- вы увидите, что tail не имеет вывода, потому что head заполнил свой буфер данными, которые tail должны были бы иметь для правильной работы. - person Charles Duffy; 19.02.2021