Инструмент Bash для получения N-й строки из файла
есть ли "канонический" способ делать это? Я использую head -n | tail -1 Что делает трюк, но мне было интересно, есть ли инструмент Bash, который специально извлекает строку (или диапазон строк) из файла.
под "каноническим" я подразумеваю программу, основная функция которой делает это.
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.txt2011-й строке
sed -n '10,33p' < file.txtстрока 10 до строки 33
sed -n '1p;3p' < file.txt1-я и 3-я строки
и так далее...
для добавления строк с помощью 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
- 00:01:12.705 (-00:00:02.616 = -3.47%)
sed- 00:01:(-00 13.146 :00:02.175 = -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
С
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!
по моим тестам, с точки зрения производительности и читаемости моя рекомендация:
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
самое быстрое решение для больших файлов всегда хвост|голову, при условии, что два расстояния:
- от начала файла до стартовой линии. Давайте назовем это
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
Comments