Bash инструмент для получения n-й строки из файла
Каково "каноническое" решение данной задачи? Я использую команду <code>head -n | tail -1</code>
, которая справляется с этой задачей, но мне интересно, есть ли инструмент в Bash, специально предназначенный для извлечения строки (или диапазона строк) из файла.
Под "каноническим" я имею в виду программу, основная функция которой заключается именно в этом.
5 ответ(ов)
Использование команд head
и tail
для обработки очень больших файлов может быть медленным. Вместо этого я бы предложил использовать sed
так:
sed 'NUMq;d' файл
Где NUM
— это номер строки, которую вы хотите вывести; например, так: sed '10q;d' файл
, чтобы напечатать 10-ю строку файла.
Пояснение:
NUMq
завершает выполнение сразу, как только достигается строка с номеромNUM
.d
удаляет строку вместо её вывода; это действие подавляется на последней строке, так какq
приводит к пропуску оставшейся части скрипта при завершении.
Если у вас есть NUM
в переменной, не забудьте использовать двойные кавычки вместо одинарных:
sed "${NUM}q;d" файл
У меня уникальная ситуация, когда я могу оценить предложенные на этой странице решения, поэтому я пишу этот ответ как сводку предложенных решений с включенными временными затратами для каждого из них.
Настройка
У меня есть файл данных ASCII размером 3.261 гигабайта с одной парой ключ-значение на строку. В файле содержится 3,339,550,320 строк, и мне не удалось открыть его ни в одном редакторе, который я пробовал, включая Vim. Мне нужно извлечь подмножество строк, чтобы исследовать некоторые значения, которые я обнаружил и которые начинаются примерно с ~500,000,000 строки.
Поскольку в файле так много строк:
- Мне нужно извлечь только подмножество строк, чтобы иметь возможность что-то полезное сделать с данными.
- Прокрутка всех строк до значений, которые меня интересуют, займет много времени.
- Если решение будет читать строки, которые мне не нужны, и продолжит чтение остальной части файла, это потратит много времени на чтение почти 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 секунду медленнее, но все же не улучшает базовый уровень. Если бы команда 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:13.146 (-00: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 $0}
.
Альтернативные версии
Если ваш файл очень большой, лучше завершить работу после чтения нужной строки. Таким образом, вы сэкономите время ЦП Смотрите сравнение времени в конце ответа.
awk 'NR == num_line {print; exit}' file
Если вы хотите передать номер строки из переменной bash, вы можете использовать:
awk 'NR == n' n=$num file
awk -v n=$num 'NR == n' file # эквивалентно
Посмотрите, сколько времени можно сэкономить, используя exit
, особенно если нужная строка находится в первой части файла:
# Создадим файл на 10M строк
for ((i=0; i<100000; i++)); do echo "bla bla"; done > 100Klines
for ((i=0; i<100; i++)); do cat 100Klines; done > 10Mlines
$ time awk 'NR == 1234567 {print}' 10Mlines
bla bla
real 0m1.303s
user 0m1.246s
sys 0m0.042s
$ time awk 'NR == 1234567 {print; exit}' 10Mlines
bla bla
real 0m0.198s
user 0m0.178s
sys 0m0.013s
Таким образом, разница составляет 0.198s против 1.303s, что примерно в 6 раз быстрее.
Согласно моим тестам, в плане производительности и читаемости я рекомендую использовать следующую конструкцию:
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
.
В моих тестах и версии с tail
и head
consistently outperform sed 'NUMq;d'
. Это соответствует другим бенчмаркам, которые были представлены. Сложно найти случай, где использование tail
и head
действительно плохо работало. Это также не удивительно, так как это операции, которые вы ожидаете увидеть глубоко оптимизированными в современной Unix-системе.
Чтобы получить представление о различиях в производительности, вот результаты для огромного файла (9.3G):
tail -n+N | head -1
: 3.7 секhead -N | tail -1
: 4.6 секsed Nq;d
: 18.8 сек
Результаты могут отличаться, но производительность комбинаций head | tail
и 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
Вы можете напечатать N-ую строчку файла, используя команду sed
без скобок и всего лишь два знака. Вот как это сделать:
sed -n Np <fileName>
^ ^
\ \___ 'p' для печати
\______ '-n' чтобы не печатать по умолчанию
Например, чтобы напечатать 100-ю строчку файла foo.txt
, вам нужно ввести следующую команду:
sed -n 100p foo.txt
Таким образом, вы экономите два знака и выводите желаемую строку эффективно!
Что означает "2>&1"?
Как установить переменную на вывод команды в Bash?
Разница между sh и Bash
Как сделать паузу в shell-скрипте на одну секунду перед продолжением?
Как выполнить рекурсивный поиск/замену строки с помощью awk или sed?