Инструмент Bash для вывода n-й строки из файла

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

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

Ответ 1

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

sed 'NUMq;d' file

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

Пояснение:

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

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

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

sed "${NUM}q;d" file

Ответ 2

sed -n '2p' < file.txt

напечатает вторую строку

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

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

Настройка

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

Поскольку в файле столько строк:

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

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

В целях моего здравомыслия я не собираюсь читать полные 500 000 000 строк, которые мне нужны для моей собственной проблемы. Вместо этого я попытаюсь извлечь строку 50 000 000 из 3,339,550,320 (что означает, что чтение полного файла займет в 60 раз больше необходимого).

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

Baseline

Сначала рассмотрим, как решение 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 минут.

СЕПГ

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

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

real    1m12.705s

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

файле проекта

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

Заключение

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

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

Строка 50 000 000

  • 00: 01:12,705 (-00: 00: 02,616 = -3,47%) sed
  • 00: 01:13,146 (-00: 00: 02,177 = -2,89%) perl
  • 00: 01:15.321 (+00: 00: 00.000 = + 0.00%) head|tail
  • 00: 01:16,583 (+00: 00: 01,262 = + 1,68%) awk
  • 00: 05: 12,156 (+00: 03: 56,835 = + 314,43%) cut

Ряд 500 000 000

  • 00: 12: 07.050 (-00: 00: 26.160) sed
  • 00: 12: 11.460 (-00: 00: 21.750) perl
  • 00: 12: 33.210 (+00: 00: 00.000) head|tail
  • 00: 12: 45,830 (+00: 00: 12,620) awk
  • 00: 52: 01.560 (+00: 40: 31.650) cut

Строка 3,338,559,320

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

Ответ 4

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

awk 'NR == num_line' file

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


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

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

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

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

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

Ответ 5

Ничего себе, все возможности!

Попробуйте следующее:

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

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

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

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

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

Ответ 7

Этот вопрос помечен Bash, здесь Bash (≥4): используйте mapfile с опцией -s (skip) и -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 (включительно) и скажите, что вы не хотите самостоятельно выполнять математику и печатать их на стандартном выводе:

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

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

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

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

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

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

Ответ 8

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

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

Ответ 9

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

tail -N+N | head -1

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

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


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

head -7 input.txt | tail -1

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

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

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

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

  • tail -N+N | head -1 tail -N+N | head -1: 3,7 с
  • head -N | tail -1 head -N | tail -1: 4,6 с
  • sed Nq;d: 18,8 с

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

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

#!/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 Carbon с SSD и 16 ГБ памяти). Я предполагаю, что в конечном итоге все будет происходить из кеша, а не с диска:

*** 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

Ответ 10

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

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

Ответ 11

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

  • от начала файла до стартовой строки. Позволяет называть его 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

Ответ 12

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

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

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

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

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

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

например. для получения строки 1000:

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

Ответ 13

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

Короткая версия: mapfile появляется быстрее, чем метод cut, но медленнее, чем все остальное, поэтому я бы назвал его dud. хвост | head, 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

Надеюсь это поможет!

Ответ 14

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

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

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

Ответ 15

Один из возможных способов:

sed -n 'NUM{p;q}'

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

Ответ 16

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

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

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

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

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

Ответ 17

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

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

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

Ответ 18

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

Создать файл: ~/.functions

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

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

Затем добавьте это в свой файл ~/.bash_profile:

source ~/.functions

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

getline 441 myfile.txt

Ответ 19

Я поместил некоторые из приведенных выше ответов в короткий скрипт bash, который вы можете поместить в файл с именем get.sh и связать его с /usr/local/bin/get (или любым другим именем, которое вы предпочитаете).

#!/bin/bash
if [ "${1}" == "" ]; then
    echo "error: blank line number";
    exit 1
fi
re='^[0-9]+$'
if ! [[ $1 =~ $re ]] ; then
    echo "error: line number arg not a number";
    exit 1
fi
if [ "${2}" == "" ]; then
    echo "error: blank file name";
    exit 1
fi
sed "${1}q;d" $2;
exit 0

Убедитесь, что он исполняется с

$ chmod +x get

Свяжите это, чтобы сделать это доступным на PATH с

$ ln -s get.sh /usr/local/bin/get

Наслаждайтесь ответственно!

п