8

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

11

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

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

5 ответ(ов)

11

Использование команд head и tail для обработки очень больших файлов может быть медленным. Вместо этого я бы предложил использовать sed так:

sed 'NUMq;d' файл

Где NUM — это номер строки, которую вы хотите вывести; например, так: sed '10q;d' файл, чтобы напечатать 10-ю строку файла.

Пояснение:

  • NUMq завершает выполнение сразу, как только достигается строка с номером NUM.
  • d удаляет строку вместо её вывода; это действие подавляется на последней строке, так как q приводит к пропуску оставшейся части скрипта при завершении.

Если у вас есть NUM в переменной, не забудьте использовать двойные кавычки вместо одинарных:

sed "${NUM}q;d" файл
1

У меня уникальная ситуация, когда я могу оценить предложенные на этой странице решения, поэтому я пишу этот ответ как сводку предложенных решений с включенными временными затратами для каждого из них.

Настройка

У меня есть файл данных 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

  1. 00:01:12.705 (-00:00:02.616 = -3.47%) sed
  2. 00:01:13.146 (-00: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
0

С использованием 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 раз быстрее.

0

Согласно моим тестам, в плане производительности и читаемости я рекомендую использовать следующую конструкцию:

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
0

Вы можете напечатать N-ую строчку файла, используя команду sed без скобок и всего лишь два знака. Вот как это сделать:

sed -n Np <fileName>
      ^   ^
       \   \___ 'p' для печати
        \______ '-n' чтобы не печатать по умолчанию 

Например, чтобы напечатать 100-ю строчку файла foo.txt, вам нужно ввести следующую команду:

sed -n 100p foo.txt

Таким образом, вы экономите два знака и выводите желаемую строку эффективно!

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь