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



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



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

678   19  

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

вывод

похоже, по большей части, трудно улучшить 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:(-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): use 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[@]}"

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

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 и выйти:

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 возможно, немного более читаемым. Например, это будет печатать 7-й линия:

head -7 input.txt | tail -1

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

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

в моих тестах обе версии tails/heads превзошли 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"

сколько раз количество необходимых линий.

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

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

короткая версия: 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 (обычно новая строка). Вы также можете использовать "вырезать":

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

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

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-номер строки, которую мы хотим напечатать.

Comments

    Ничего не найдено.