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

446

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

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

  • 10
    «Путь Unix» состоит в том, чтобы объединить инструменты, которые хорошо выполняют свою работу. Поэтому я думаю, что вы уже нашли очень подходящий метод. Другие методы включают awk и sed и я уверен, что кто-то может придумать Perl с одним вкладышем или около того;)
  • 3
    Двойная команда предполагает, что head | tail решение является неоптимальным. Были предложены другие более почти оптимальные решения.
Показать ещё 3 комментария
Теги:
awk
sed

19 ответов

627
Лучший ответ

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
  • 33
    Для тех, кому интересно, это решение кажется примерно в sed -n 'NUMp' раз быстрее, чем решения sed -n 'NUMp' и sed 'NUM!d' предложенные ниже.
  • 60
    Я думаю, что tail -n+NUM file | head -n1 скорее всего будет такой же быстрой или быстрой. По крайней мере, это было (значительно) быстрее в моей системе, когда я попробовал его с NUM 250000 для файла с полмиллиона строк. YMMV, но я не понимаю почему.
Показать ещё 21 комментарий
235
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: вставьте строку в определенную позицию

  • 0
    Почему «<» необходимо в этом случае? Разве я не достиг бы того же результата без него?
  • 5
    @RafaelBarbosa < в этом случае не требуется. Просто я предпочитаю использовать перенаправления, потому что я часто использовал перенаправления вроде sed -n '100p' < <(some_command) - так, универсальный синтаксис :). Это НЕ менее эффективно, потому что перенаправление выполняется с помощью shell при разветвлении себя, поэтому ... это всего лишь предпочтение ... (и да, это на один символ длиннее) :)
Показать ещё 6 комментариев
70

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

Настройка

У меня есть файл текстовых данных 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
  • 0
    Интересно, сколько времени займет просто закачивание всего файла в / dev / null. (Что, если это был только тест жесткого диска?)
43

С 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
  • 3
    Я надеялся прочитать здесь ответ на awk . Хорошая заметка на выходе, не подумал бы об этом. Возможно также включить эквивалент awk -vn=$num 'NR == n' ?
  • 1
    @ShellFish спасибо, только что обновил с этой эквивалентной версией!
Показать ещё 1 комментарий
26

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

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

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, вероятно, самый близкий и простой в использовании.

19

Этот вопрос помечен 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 встроенных!

19
10

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

sed -n '10{p;q;}' file   # print line 10
  • 2
    Что -n делать?
  • 5
    Опция -n отключает действие по умолчанию для печати каждой строки, как вы наверняка заметили, быстро взглянув на справочную страницу.
Показать ещё 1 комментарий
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
  • 1
    Отличается ли производительность между head | tail против tail | head ? Или это зависит от того, какая строка печатается (начало файла или конец файла)?
  • 1
    @wisbucky У меня нет точных цифр, но один недостаток, заключающийся в том, чтобы сначала использовать хвост, а затем «голову -1», заключается в том, что вам нужно заранее знать общую длину. Если вы этого не знаете, вам придется сначала посчитать это, что приведет к потере производительности. Еще одним недостатком является то, что он менее интуитивно понятен в использовании. Например, если у вас число от 1 до 10, и вы хотите получить 3-ю строку, вам придется использовать «tail -8 | head -1». Это более подвержено ошибкам, чем "head -3 | tail -1".
Показать ещё 2 комментария
7

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

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

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

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

  • 0
    Пожалуйста, уточните единицы S и E (т. E Байты, символы или строки).
4

В качестве следствия для 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

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

4

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

Так как длины строк произвольны, все байты файла перед 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 является "знающим персонажа", но хвост не является.
  • Я не тестировал это против большого файла.
  • Также см. этот ответ.
  • Альтернативно - разбить файл на более мелкие файлы.
3

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

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

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

  • 0
    Может также использоваться для отображения нескольких строк: cat FILE | cut -f2,5 -d$'\n' отобразит строки 2 и 5 ФАЙЛА. (Но это не сохранит порядок.)
2

Уже много хороших ответов. Я лично перехожу с 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

2

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

sed -n 'NUM{p;q}'

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

1

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

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

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

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

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

source ~/.functions

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

getline 441 myfile.txt

1

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

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

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

  • 1
    Двоеточие является синтаксической ошибкой и должно быть точкой с запятой.
0

Я поместил некоторые из приведенных выше ответов в короткий скрипт 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

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

п

Ещё вопросы

Сообщество Overcoder
Наверх
Меню