Инструмент Bash для получения N-й строки из файла

есть ли "канонический" способ делать это? Я использовал head -n | tail -1 Что делает трюк, но мне было интересно, есть ли инструмент Bash, который специально извлекает строку (или диапазон строк) из файла.

под "каноническим" я подразумеваю программу, основная функция которой делает это.

19 ответов


head и труба с tail будет медленным для огромного файла. Я бы предложил sed такой:

sed 'NUMq;d' file

здесь NUM - номер строки, которую вы хотите напечатать; так, например, sed '10q;d' file для печати 10-й строки file.

объяснение:

NUMq немедленно прекратит работу, когда номер строки NUM.

d удалит строку вместо печати; это запрещено в последней строке, потому что q вызывает пропуск остальной части сценария при выходе.

если у вас NUM в переменной, вы хотите использовать двойные кавычки вместо одинарных:

sed "${NUM}q;d" file

sed -n '2p' < file.txt

будет печатать 2-ю строку

sed -n '2011p' < file.txt

2011-й строке

sed -n '10,33p' < file.txt

строка 10 до строки 33

sed -n '1p;3p' < file.txt

1-я и 3-я строки

и так далее...

для добавления строк с помощью sed, вы можете проверить это:

sed: вставить строку в определенном положении


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

Настройка

у меня есть 3.261 гигабайт ASCII текстовый файл данных с одной парой ключ-значение в строке. Файл содержит 3,339,550,320 строк и не открывается в любом редакторе, который я пробовал, включая мой go-to Vim. Мне нужно подмножество этого файла в чтобы исследовать некоторые из значений, которые я обнаружил только начальную строку ~500,000,000.

потому что в файле так много строк:

  • мне нужно извлечь только подмножество строк, чтобы сделать что-то полезное с данными.
  • чтение каждой строки, ведущей к значениям, о которых я забочусь, займет много времени.
  • если решение читает мимо строк, о которых я забочусь, и продолжает читать остальную часть файла, он будет тратить время чтения почти 3 миллиардов нерелевантных строк и занимает 6x больше, чем необходимо.

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

я буду использовать time встроенный для проверки каждой команды.

базовый

сначала давайте посмотрим, как head tail устранение:

$ time head -50000000 myfile.ascii | tail -1
pgm_icnt = 0

real    1m15.321s

базовый по рядка 50 млн. 00:01:15.321, если бы я пошла прямо по строке 500 миллионов было бы ~12.5 протокол.

вырезать

я сомневаюсь в этом, но стоит попробовать:

$ time cut -f50000000 -d$'\n' myfile.ascii
pgm_icnt = 0

real    5m12.156s

это заняло 00: 05: 12.156 для запуска, что намного медленнее, чем базовая линия! Я не уверен, прочитал ли он весь файл или просто до строки 50 миллионов перед остановкой, но независимо от этого это не кажется жизнеспособным решением проблемы.

на awk

я только запустил решение с exit потому что я не собирался ждать запуска полного файла:

$ time awk 'NR == 50000000 {print; exit}' myfile.ascii
pgm_icnt = 0

real    1m16.583s

этот код был запущен в 00: 01: 16.583, что всего на ~1 секунду медленнее, но все еще не улучшилось по сравнению с базовой линией. При такой скорости, если бы команда exit была исключена, вероятно, потребовалось бы около ~76 минут, чтобы прочитать весь файл!

Perl

я также запустил существующее решение Perl:

$ time perl -wnl -e '$.== 50000000 && print && exit;' myfile.ascii
pgm_icnt = 0

real    1m13.146s

этот код был запущен в 00: 01: 13.146, что составляет ~2 секунды быстрее, чем базовый. Если бы я запустил его на полных 500,000,000, это, вероятно, заняло бы ~12 минут.

sed

лучшие ответы на доске, вот мой результат:

$ time sed "50000000q;d" myfile.ascii
pgm_icnt = 0

real    1m12.705s

этот код работал в 00: 01: 12.705, что на 3 секунды быстрее, чем базовая линия, и ~0.4 секунд быстрее, чем Perl. Если бы я запустил его на полных 500,000,000 строках, это, вероятно, заняло бы ~12 протокол.

mapfile

у меня есть bash 3.1 и поэтому я не могу проверить решение mapfile.

вывод

похоже, по большей части, трудно улучшить head tail решение. В лучшем случае sed решение обеспечивает увеличение эффективности ~3%.

(проценты, рассчитанные по формуле % = (runtime/baseline - 1) * 100)

строки 50,000,000

  1. 00:01:12.705 (-00:00:02.616 = -3.47%) sed
  2. 00:01:(-00 13.146 :00:02.175 = -2.89%) perl
  3. 00:01:15.321 (+00:00:00.000 = +0.00%) head|tail
  4. 00:01:16.583 (+00:00:01.262 = +1.68%) awk
  5. 00:05:12.156 (+00:03:56.835 = +314.43%) cut

строка 500,000,000

  1. 00:12:07.050 (-00:00:26.160) sed
  2. 00:12:11.460 (-00:00:21.750) perl
  3. 00:12:33.210 (+00:00:00.000) head|tail
  4. 00:12:45.830 (+00:00:12.620) awk
  5. 00:52:01.560 (+00:40:31.650) cut

ряд 3,338,559,320

  1. 01:20:54.599 (-00:03:05.327) sed
  2. 01:21:24.045 (-00:02:25.227) perl
  3. 01:23:49.273 (+00:00:00.000) head|tail
  4. 01:25:13.548 (+00:02:35.735) awk
  5. 05:47:23.026 (+04:24:26.246) cut

С awk Это довольно быстро:

awk 'NR == num_line' file

когда это верно, поведение по умолчанию awk выполняется: {print }.


альтернативные версии

если ваш файл окажется огромным, вам лучше exit после прочтения нужной линии. Таким образом, вы экономите время процессора.

awk 'NR == num_line {print; exit}' file

если вы хотите дать номер строки с переменной bash, вы можете использовать:

awk 'NR == n' n=$num file
awk -v n=$num 'NR == n' file   # equivalent

Вау, все возможности!

попробуйте это:

sed -n "${lineNum}p" $file

или один из них в зависимости от вашей версии Awk:

awk  -vlineNum=$lineNum 'NR == lineNum {print }' $file
awk -v lineNum=4 '{if (NR == lineNum) {print }}' $file
awk '{if (NR == lineNum) {print }}' lineNum=$lineNum $file

(возможно, вам придется попробовать nawk или gawk команда).

есть ли инструмент, который только печатает эту конкретную строку? Не один из стандартных инструментов. Однако,sed - Это, наверное, самый близкий и простой в использовании.



этот вопрос помечен как Bash, вот способ Bash (≥4): Используйте mapfile С -s (скип) и -n (count) вариант.

Если вам нужно получить 42-ю строку файла file:

mapfile -s 41 -n 1 ary < file

в этот момент у вас будет массив ary поля которых содержат строки file (включая конечную новую строку), где мы пропустили первые 41 строку (-s 41), и остановился после прочтения одной строки (-n 1). Так что это действительно 42-я линия. Чтобы распечатать его:

printf '%s' "${ary[0]}"

Если вам нужен диапазон строк, скажите диапазон 42-666 (включительно) и скажите, что вы не хотите делать математику самостоятельно, и распечатайте их на stdout:

mapfile -s $((42-1)) -n $((666-42+1)) ary < file
printf '%s' "${ary[@]}"

Если вам нужно обработать и эти строки, хранить конечную новую строку не очень удобно. В этом случае используйте (отделка):

mapfile -t -s $((42-1)) -n $((666-42+1)) ary < file
# do stuff
printf '%s\n' "${ary[@]}"

вы можете иметь функцию сделать это для вас:

print_file_range() {
    # - is the range of file  to be printed to stdout
    local ary
    mapfile -s $((-1)) -n $((-+1)) ary < ""
    printf '%s' "${ary[@]}"
}

никаких внешних команды, только Bash builtins!


вы также можете использовать sed print и quit:

sed -n '10{p;q;}' file   # print line 10

по моим тестам, с точки зрения производительности и читаемости моя рекомендация:

tail -n+N | head -1

N - это номер строки, которую вы хотите. Например, tail -n+7 input.txt | head -1 напечатает 7-ю строку файла.

tail -n+N будет печатать все, начиная с строки N и head -1 остановит его после одной строки.


альтернатива head -N | tail -1 возможно, немного более читаемый. Например, это будет печатать 7th строка:

head -7 input.txt | tail -1

когда дело доходит до производительности, нет большой разницы для меньших размеров, но она будет превзойдена tail | head (сверху), когда файлы становятся огромными.

топ-проголосовали sed 'NUMq;d' интересно знать, но я бы сказал, что это будет понято меньшим количеством людей из коробки, чем решение головы/хвоста, и это также медленнее, чем хвост/голова.

в моих тестах обе версии хвостов/голов превзошли sed 'NUMq;d' последовательно. Это соответствует другим контрольным показателям, которые были опубликованы. Трудно найти случай, когда хвосты/головы были действительно плохими. Это также неудивительно, так как это операции, которые вы ожидаете сильно оптимизировать в современной системе Unix.

чтобы получить представление о различиях в производительности, это число, которое я получаю огромный файл (9,3 г):

  • tail -n+N | head -1: 3.7 сек
  • head -N | tail -1: 4.6 сек
  • sed Nq;d: 18.8 сек

результаты могут отличаться, но производительность head | tail и tail | head, в общем, сопоставимо для меньших входов и sed всегда медленнее значительным фактором (около 5x или около того).

чтобы воспроизвести мой бенчмарк, вы можете попробовать следующее, Но имейте в виду, что он создаст файл 9.3 G в текущем рабочем каталоге:

#!/bin/bash
readonly file=tmp-input.txt
readonly size=1000000000
readonly pos=500000000
readonly retries=3

seq 1 $size > $file
echo "*** head -N | tail -1 ***"
for i in $(seq 1 $retries) ; do
    time head "-$pos" $file | tail -1
done
echo "-------------------------"
echo
echo "*** tail -n+N | head -1 ***"
echo

seq 1 $size > $file
ls -alhg $file
for i in $(seq 1 $retries) ; do
    time tail -n+$pos $file | head -1
done
echo "-------------------------"
echo
echo "*** sed Nq;d ***"
echo

seq 1 $size > $file
ls -alhg $file
for i in $(seq 1 $retries) ; do
    time sed $pos'q;d' $file
done
/bin/rm $file

вот результат запуска на моей машине (ThinkPad X1 Углерод с SSD и 16G памяти). Я предполагаю, что в конечном итоге все выйдет из кэша, а не с диска:

*** head -N | tail -1 ***
500000000

real    0m9,800s
user    0m7,328s
sys     0m4,081s
500000000

real    0m4,231s
user    0m5,415s
sys     0m2,789s
500000000

real    0m4,636s
user    0m5,935s
sys     0m2,684s
-------------------------

*** tail -n+N | head -1 ***

-rw-r--r-- 1 phil 9,3G Jan 19 19:49 tmp-input.txt
500000000

real    0m6,452s
user    0m3,367s
sys     0m1,498s
500000000

real    0m3,890s
user    0m2,921s
sys     0m0,952s
500000000

real    0m3,763s
user    0m3,004s
sys     0m0,760s
-------------------------

*** sed Nq;d ***

-rw-r--r-- 1 phil 9,3G Jan 19 19:50 tmp-input.txt
500000000

real    0m23,675s
user    0m21,557s
sys     0m1,523s
500000000

real    0m20,328s
user    0m18,971s
sys     0m1,308s
500000000

real    0m19,835s
user    0m18,830s
sys     0m1,004s

вы также можете использовать Perl для этого:

perl -wnl -e '$.== NUM && print && exit;' some.file

самым быстрым решением для больших файлов всегда является хвост / голова, при условии, что два расстояния:

  • от начала файла до стартовой линии. Давайте назовем это S
  • расстояние от последней строки до конца файла. Будь то E

известны. Тогда мы могли бы использовать это:

mycount="$E"; (( E > S )) && mycount="+$S"
howmany="$(( endline - startline + 1 ))"
tail -n "$mycount"| head -n "$howmany"

howmany-это просто количество необходимых строк.

более подробно в https://unix.stackexchange.com/a/216614/79743


как продолжение очень полезного бенчмаркинга CaffeineConnoisseur... Мне было любопытно, как быстро метод "mapfile" сравнивался с другими (так как это не было протестировано), поэтому я попробовал сравнение быстрой и грязной скорости, поскольку у меня есть bash 4. Бросил тест метода "хвост / голова" (а не голова | хвост), упомянутый в одном из комментариев к верхнему ответу, пока я был на нем, поскольку люди поют его похвалы. У меня нет ничего почти такого же размера, как используемый testfile; лучшее, что я смог найти за короткое время,-это файл родословной 14M (длинные строки, разделенные пробелами, чуть меньше 12000 строк).

Short version: mapfile появляется быстрее, чем метод cut, но медленнее, чем все остальное, поэтому я бы назвал его неудачным. хвост / голова, OTOH, похоже, может быть самым быстрым, хотя с файлом такого размера разница не так существенна по сравнению с sed.

$ time head -11000 [filename] | tail -1
[output redacted]

real    0m0.117s

$ time cut -f11000 -d$'\n' [filename]
[output redacted]

real    0m1.081s

$ time awk 'NR == 11000 {print; exit}' [filename]
[output redacted]

real    0m0.058s

$ time perl -wnl -e '$.== 11000 && print && exit;' [filename]
[output redacted]

real    0m0.085s

$ time sed "11000q;d" [filename]
[output redacted]

real    0m0.031s

$ time (mapfile -s 11000 -n 1 ary < [filename]; echo ${ary[0]})
[output redacted]

real    0m0.309s

$ time tail -n+11000 [filename] | head -n1
[output redacted]

real    0m0.028s

надеюсь, что это помогает!


Если вы получили несколько строк, разделенных \n (обычно новая строка). Вы также можете использовать 'cut':

echo "$data" | cut -f2 -d$'\n'

вы получите 2-ю строку из файла. -f3 дает вам 3-й линии.


все вышеперечисленные ответы непосредственно отвечают на вопрос. Но вот менее прямое решение, но потенциально более важная идея, чтобы спровоцировать мысль.

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

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

например, вы можете создать список позиций символов для новых строк:

awk 'BEGIN{c=0;print(c)}{c+=length()+1;print(c+1)}' file.txt > file.idx

тогда читайте с tail, который на самом деле seeks сразу к соотвествующему пункту внутри файл!

например, чтобы получить строку 1000:

tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
  • это может не работать с 2-байтовыми / многобайтовыми символами, так как awk "знает о символах", но tail-нет.
  • Я не тестировал это на большом файле.
  • см. Также ответ.
  • как вариант - разбить файл на более мелкие файлы!

один из возможных путей:

sed -n 'NUM{p;q}'

обратите внимание, что без q команда, если файл большой, sed продолжает работать, что замедляет вычисление.


много хороших ответов уже. Я лично иду с awk. Для удобства, если вы используете bash, просто добавьте ниже к вашему ~/.bash_profile. И, при следующем входе в систему (или если вы источник вашего .bash_profile после этого обновления), у вас будет новая отличная функция "nth", доступная для передачи ваших файлов.

выполните это или поместите его в свой~/.bash_profile (при использовании bash) и повторно открыть bash (или выполнить source ~/.bach_profile)

# print just the nth piped in line nth () { awk -vlnum= 'NR==lnum {print; exit}'; }

затем, чтобы использовать его, просто труба через него. Напр.:

$ yes line | cat -n | nth 5 5 line


для печати N-й строки с помощью sed с переменной в качестве номера строки:

a=4
sed -e $a'q:d' file

здесь флаг'- e ' предназначен для добавления скрипта в выполняемую команду.


используя то, что упоминали другие, я хотел, чтобы это была функция quick & dandy в моей оболочке bash.

создайте файл: ~/.functions

добавьте к нему содержимое:

getline() { line= sed $line'q;d' }

затем добавьте это в ваш ~/.bash_profile:

source ~/.functions

теперь, когда вы открываете новое окно bash, вы можете просто вызвать функцию так:

getline 441 myfile.txt


echo <filename> | head <n>

где N-номер строки, которую мы хотим напечатать.