Инструмент 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.txt
2011-й строке
sed -n '10,33p' < file.txt
строка 10 до строки 33
sed -n '1p;3p' < file.txt
1-я и 3-я строки
и так далее...
для добавления строк с помощью sed, вы можете проверить это:
у меня есть уникальная ситуация, когда я могу сравнить решения, предложенные на этой странице, и поэтому я пишу этот ответ как консолидацию предлагаемых решений с включенным временем выполнения для каждого.
Настройка
у меня есть 3.261 гигабайт ASCII текстовый файл данных с одной парой ключ-значение в строке. Файл содержит 3,339,550,320 строк и не открывается в любом редакторе, который я пробовал, включая мой go-to Vim. Мне нужно подмножество этого файла в чтобы исследовать некоторые из значений, которые я обнаружил только начальную строку ~500,000,000.
потому что в файле так много строк:
- мне нужно извлечь только подмножество строк, чтобы сделать что-то полезное с данными.
- чтение каждой строки, ведущей к значениям, о которых я забочусь, займет много времени.
- если решение читает мимо строк, о которых я забочусь, и продолжает читать остальную часть файла, он будет тратить время чтения почти 3 миллиардов нерелевантных строк и занимает 6x больше, чем необходимо.
мой лучший сценарий-это решение, которое извлекает только одну строку из файла, не читая никаких других строк в файле, но я не могу думать о том, как я бы это сделал в Bash.
я буду использовать time
встроенный для проверки каждой команды.
базовый
сначала давайте посмотрим, как 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 минут.
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
- 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): Используйте 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[@]}"
Если вам нужно обработать и эти строки, хранить конечную новую строку не очень удобно. В этом случае используйте (отделка):
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
возможно, немного более читаемый. Например, это будет печатать 7th строка:
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.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"
howmany-это просто количество необходимых строк.
более подробно в https://unix.stackexchange.com/a/216614/79743
как продолжение очень полезного бенчмаркинга CaffeineConnoisseur... Мне было любопытно, как быстро метод "mapfile" сравнивался с другими (так как это не было протестировано), поэтому я попробовал сравнение быстрой и грязной скорости, поскольку у меня есть bash 4. Бросил тест метода "хвост / голова" (а не голова | хвост), упомянутый в одном из комментариев к верхнему ответу, пока я был на нем, поскольку люди поют его похвалы. У меня нет ничего почти такого же размера, как используемый testfile; лучшее, что я смог найти за короткое время,-это файл родословной 14M (длинные строки, разделенные пробелами, чуть меньше 12000 строк).
Short version: 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 (обычно новая строка). Вы также можете использовать 'cut':
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
, который на самом деле seek
s сразу к соотвествующему пункту внутри файл!
например, чтобы получить строку 1000:
tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
- это может не работать с 2-байтовыми / многобайтовыми символами, так как awk "знает о символах", но tail-нет.
- Я не тестировал это на большом файле.
- см. Также ответ.
- как вариант - разбить файл на более мелкие файлы!
один из возможных путей:
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