Инструмент Bash для получения n-й строки из файла - PullRequest
499 голосов
/ 16 мая 2011

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

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

Ответы [ 18 ]

670 голосов
/ 16 мая 2011

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

sed 'NUMq;d' file

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

Пояснение:

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

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

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

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

79 голосов
/ 30 августа 2016

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

Настройка

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

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

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

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

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

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

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

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

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

real    1m15.321s

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

cut

Я сомневаюсь в этом,но оно того стоит:

$ 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.

Заключение

Похоже, что по большей части трудно улучшить решение 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: 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
44 голосов
/ 22 января 2014

С 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
26 голосов
/ 17 мая 2011

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

Попробуйте:

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

20 голосов
/ 17 мая 2014

Этот вопрос помечен как 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 (отделка):

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!

20 голосов
/ 18 октября 2012
12 голосов
/ 31 июля 2017

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

tail -n+N | head -1

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

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,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.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

Вот вывод прогона на моем компьютере (ThinkPadX1 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
11 голосов
/ 17 мая 2011

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

sed -n '10{p;q;}' file   # print line 10
7 голосов
/ 16 мая 2011

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

perl -wnl -e '$.== NUM && print && exit;' some.file
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...