Есть ли "канонический" способ сделать это? Я использую head -n | tail -1
, который делает трюк, но мне было интересно, есть ли инструмент Bash, который специально извлекает строку (или диапазон строк) из файла.
Под "каноническим" подразумевается программа, основная функция которой выполняется.
head
, а pipe с tail
будет медленным для огромного файла. Я бы предложил sed
следующим образом:
sed 'NUMq;d' file
Где NUM
- номер строки, которую вы хотите распечатать; так, например, sed '10q;d' file
для печати 10-й строки file
.
Пояснение:
NUMq
немедленно прекратится, когда номер строки NUM
.
d
удалит строку вместо ее печати; это заблокировано на последней строке, потому что q
заставляет остальную часть script пропускаться при выходе.
Если у вас есть NUM
в переменной, вам нужно использовать двойные кавычки вместо одиночного:
sed "${NUM}q;d" file
sed -n 'NUMp'
раз быстрее, чем решения sed -n 'NUMp'
и sed 'NUM!d'
предложенные ниже.
tail -n+NUM file | head -n1
скорее всего будет такой же быстрой или быстрой. По крайней мере, это было (значительно) быстрее в моей системе, когда я попробовал его с NUM 250000 для файла с полмиллиона строк. YMMV, но я не понимаю почему.
sed -n '2p' < file.txt
напечатает вторую строку
sed -n '2011p' < file.txt
2011-я линия
sed -n '10,33p' < file.txt
строка 10 до строки 33
sed -n '1p;3p' < file.txt
1-я и 3-я строка
и т.д.
Для добавления строк с помощью sed вы можете проверить это:
<
в этом случае не требуется. Просто я предпочитаю использовать перенаправления, потому что я часто использовал перенаправления вроде sed -n '100p' < <(some_command)
- так, универсальный синтаксис :). Это НЕ менее эффективно, потому что перенаправление выполняется с помощью shell при разветвлении себя, поэтому ... это всего лишь предпочтение ... (и да, это на один символ длиннее) :)
У меня есть уникальная ситуация, когда я могу проверить предлагаемые решения на этой странице, и поэтому я пишу этот ответ в качестве консолидации предлагаемых решений с включенным временем выполнения для каждого.
Настройка
У меня есть файл текстовых данных ASCII 3.261 гигабайт с одной парой ключ-значение для каждой строки. Файл содержит 3,339,550,320 строк в целом и бросает вызов открытию в любом редакторе, который я пробовал, в том числе и в моем Vim. Мне нужно подмножить этот файл, чтобы исследовать некоторые из значений, которые я обнаружил, только начинающиеся вокруг строки ~ 500 000 000.
Поскольку в файле столько строк:
Мой лучший сценарий - это решение, которое извлекает только одну строку из файла без чтения каких-либо других строк в файле, но я не могу представить, как это сделать в Bash.
В целях моего здравомыслия я не собираюсь читать полные 500 000 000 строк, которые мне нужны для моей собственной проблемы. Вместо этого я попытаюсь извлечь строку 50 000 000 из 3,339,550,320 (что означает, что чтение полного файла займет в 60 раз больше необходимого).
Я буду использовать встроенный time
для тестирования каждой команды.
Baseline
Сначала рассмотрим, как решение 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 минут.
СЕПГ
Главный ответ на доске, вот мой результат:
$ time sed "50000000q;d" myfile.ascii
pgm_icnt = 0
real 1m12.705s
Этот код работал в 00: 01:12.705, что на 3 секунды быстрее базовой линии и ~ 0,4 секунды быстрее, чем Perl. Если бы я запустил его на полных 500 000 000 строк, это, вероятно, заняло бы ~ 12 минут.
файле проекта
У меня есть bash 3.1 и поэтому не могу проверить решение mapfile.
Заключение
Похоже, что по большей части трудно улучшить решение head
tail
. В лучшем случае решение sed
обеспечивает повышение эффективности на 3%.
(проценты, рассчитанные по формуле % = (runtime/baseline - 1) * 100
)
Строка 50 000 000
sed
perl
head|tail
awk
cut
Ряд 500 000 000
sed
perl
head|tail
awk
cut
Строка 3,338,559,320
sed
perl
head|tail
awk
cut
С 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
awk -vn=$num 'NR == n'
?
Ничего себе, все возможности!
Попробуйте следующее:
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
, вероятно, самый близкий и простой в использовании.
Этот вопрос помечен Bash, здесь Bash (≥4): используйте mapfile
с опцией -s
(skip) и -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 (включительно) и скажите, что вы не хотите самостоятельно выполнять математику и печатать их на стандартном выводе:
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 встроенных!
# print line number 52
sed '52!d' file
Вы также можете использовать sed print и quit:
sed -n '10{p;q;}' file # print line 10
-n
делать?
-n
отключает действие по умолчанию для печати каждой строки, как вы наверняка заметили, быстро взглянув на справочную страницу.
Согласно моим тестам, с точки зрения производительности и удобочитаемости моя рекомендация:
tail -N+N | head -1
N
- номер строки, которую вы хотите. Например, tail -N+7 input.txt | head -1
tail -N+7 input.txt | head -1
напечатает 7-ю строку файла.
tail -N+N
будет печатать все, начиная с строки N
, а head -1
остановит ее после одной строки.
Альтернативный head -N | tail -1
head -N | tail -1
, возможно, немного читаем. Например, это напечатает 7-ю строку:
head -7 input.txt | tail -1
Когда дело доходит до производительности, нет большой разницы для меньших размеров, но он будет превосходить tail | head
tail | head
(сверху), когда файлы становятся огромными.
Самое интересное узнать о sed 'NUMq;d'
, но я бы сказал, что это будет понято меньшим количеством людей из коробки, чем решение голова/хвост, а также медленнее, чем хвост/голова.
В моих тестах обе версии хвостов/головок превосходили sed 'NUMq;d'
последовательно. Это соответствует другим показателям, которые были опубликованы. Трудно найти случай, когда хвосты/головы были действительно плохими. Это также неудивительно, так как это операции, которые, как вы ожидаете, будут сильно оптимизированы в современной системе Unix.
Чтобы получить представление о различиях в производительности, это число, которое я получаю за огромный файл (9.3G):
tail -N+N | head -1
tail -N+N | head -1
: 3,7 сhead -N | tail -1
head -N | tail -1
: 4,6 сsed Nq;d
: 18,8 с Результаты могут отличаться, но производительность head | tail
head | tail
и tail | head
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 и 16 ГБ памяти). Я предполагаю, что в конечном итоге все будет происходить из кеша, а не с диска:
*** 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
head | tail
против tail | head
? Или это зависит от того, какая строка печатается (начало файла или конец файла)?
Вы также можете использовать 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"
howmany - это просто количество требуемых строк.
Дополнительная информация в https://unix.stackexchange.com/a/216614/79743
S
и E
(т. E
Байты, символы или строки).
В качестве следствия для CaffeineConnoisseur очень полезный бенчмаркинг ответа... Мне было любопытно, насколько быстро метод "mapfile" сравнивался с другими (так как это не было проверено), поэтому я попытался быстро и грязно сравнить скорость, как У меня есть bash 4. Бросил тест на метод "хвост" (вместо головы), упомянутый в одном из комментариев на верхний ответ, когда я был на нем, так как люди поют свои похвалы. У меня почти нет размера используемого тестового файла; лучшее, что я смог найти в кратчайшие сроки, это 14M родословный файл (длинные строки, разделенные пробелами, всего 12000 строк).
Короткая версия: mapfile появляется быстрее, чем метод cut, но медленнее, чем все остальное, поэтому я бы назвал его dud. хвост | 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
Надеюсь это поможет!
Все приведенные выше ответы напрямую отвечают на вопрос. Но здесь менее прямое решение, но потенциально более важная идея, чтобы спровоцировать мысль.
Так как длины строк произвольны, все байты файла перед n-й строкой нужно читать. Если у вас огромный файл или вам нужно многократно повторять эту задачу, и этот процесс занимает много времени, вам следует серьезно подумать о том, следует ли вам сначала хранить ваши данные по-другому.
Реальное решение состоит в том, чтобы иметь индекс, например. в начале файла, с указанием позиций, где начинаются строки. Вы можете использовать формат базы данных или просто добавить таблицу в начале файла. Альтернативно создайте отдельный файл индекса, который будет сопровождать ваш большой текстовый файл.
например. вы можете создать список позиций символов для строк новой строки:
awk 'BEGIN{c=0;print(c)}{c+=length()+1;print(c+1)}' file.txt > file.idx
затем прочитайте с помощью tail
, который фактически seek
непосредственно в соответствующую точку в файле!
например. для получения строки 1000:
tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
Если вы получили несколько строк, разделив их на \n (обычно новая строка). Вы также можете использовать "cut":
echo "$data" | cut -f2 -d$'\n'
Вы получите вторую строку из файла. -f3
дает вам 3-ю строку.
cat FILE | cut -f2,5 -d$'\n'
отобразит строки 2 и 5 ФАЙЛА. (Но это не сохранит порядок.)
Уже много хороших ответов. Я лично перехожу с awk. Для удобства, если вы используете bash, просто добавьте ниже в свой файл ~/.bash_profile
. И в следующий раз, когда вы входите в систему (или если вы отправите свой.bash_profile после этого обновления), у вас будет новая отличная "n-я" функция, доступная для передачи ваших файлов через.
Выполните это или поместите его в свой файл ~/.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
Один из возможных способов:
sed -n 'NUM{p;q}'
Обратите внимание, что без команды q
, если файл большой, sed продолжает работать, что замедляет вычисление.
Используя то, что говорили другие, я хотел, чтобы это была быстрая и денди-функция в моей оболочке bash.
Создать файл: ~/.functions
Добавьте к нему содержимое:
getline() { line=$1 sed $line'q;d' $2 }
Затем добавьте это в свой файл ~/.bash_profile
:
source ~/.functions
Теперь, когда вы открываете новое окно bash, вы можете просто вызвать функцию так:
getline 441 myfile.txt
Для печати n-й строки с помощью sed с переменной в виде номера строки:
a=4
sed -e $a'q:d' file
Здесь флаг '-e' предназначен для добавления script для выполнения команды.
Я поместил некоторые из приведенных выше ответов в короткий скрипт 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
Наслаждайтесь ответственно!
п
awk
иsed
и я уверен, что кто-то может придумать Perl с одним вкладышем или около того;)head | tail
решение является неоптимальным. Были предложены другие более почти оптимальные решения.