it-roy-ru.com

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

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

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

451
Vlad Vivdovitch

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

sed 'NUMq;d' file

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

Объяснение:

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

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

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

sed "${NUM}q;d" file
615
anubhava
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: вставить строку в определенную позицию

242
jm666

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

Настроить

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

Поскольку файл имеет так много строк:

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

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

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

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

Базовая линия

Сначала давайте посмотрим, как решение headtail:

$ 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 секунду медленнее, но все же не улучшило базовый уровень. При такой скорости, если исключить команду выхода, вероятно, потребуется около 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.

Заключение

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

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

Строка 50 000 000

  1. 00: 01: 12,705 (-00: 00: 02,616 = -3,47%) sed
  2. 00: 01: 13.146 (-00: 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
71
CaffeineConnoisseur

С 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
42
fedorqui

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

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

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

26
David W.
20
Steven Penny

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

Если вам нужно получить 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 (trim):

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
gniourf_gniourf

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

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

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

tail -n+N | head -1

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

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


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

head -7 input.txt | tail -1

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

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

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

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

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

Результаты могут отличаться, но производительность head | tail и 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 и 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
9
Philipp Claßen

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

Perl -wnl -e '$.== NUM && print && exit;' some.file
7
Timofey Stolbov

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

  • от начала файла до начальной строки. Давайте назовем это 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

6
user2350426

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

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

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

Поскольку длины строк произвольны, необходимо прочитать все байты файла перед 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 - нет.
  • Я не проверял это в отношении большого файла. 
  • Также см. этот ответ .
  • В качестве альтернативы - разбить файл на более мелкие файлы!
4
Sanjay Manohar

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

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

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

3
danger89

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

Выполните это или поместите в свой ~/.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
JJC

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

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

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

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

Затем добавьте это к своему ~/.bash_profile:

source ~/.functions

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

getline 441 myfile.txt

1
Mark Shust

Чтобы напечатать n-ю строку, используя sed с переменной в качестве номера строки:

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

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

1
aliasav

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

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

П

0
polarise