котяча лінія X до рядка Y на величезному файлі


132

Скажімо , у мене є величезний текстовий файл (> 2 Гб) , і я просто хочу catлінії Xдо Y(наприклад , 57890000 на 57890010).

З того, що я розумію, я можу це зробити, headпереходячи в tailабо viceversa, тобто

head -A /path/to/file | tail -B

або альтернативно

tail -C /path/to/file | head -D

де A, B, Cі Dможуть бути обчислені з числа рядків у файлі, Xі Y.

Але у цього підходу є дві проблеми:

  1. Ви повинні обчислити A, B, Cі D.
  2. Команди могли pipeодин одному набагато більше рядків, ніж мені цікаво читати (наприклад, якщо я читаю лише кілька рядків посеред величезного файлу)

Чи є спосіб, щоб оболонка просто працювала і виводила потрібні лінії? (надаючи лише Xта Y)?


1
FYI, фактичне порівняння швидкості тестування 6 методів додано до моєї відповіді.
Кевін

Відповіді:


119

Я пропоную sedрішення, але заради повноти,

awk 'NR >= 57890000 && NR <= 57890010' /path/to/file

Щоб вирізати після останнього рядка:

awk 'NR < 57890000 { next } { print } NR == 57890010 { exit }' /path/to/file

Тест на швидкість:

  • 100 000 000-рядковий файл, створений користувачем seq 100000000 > test.in
  • Рядки читання 50 000 000-50 000,010
  • Тести в конкретному порядку
  • realчас, як повідомляє bash's buildintime
 4.373  4.418  4.395    tail -n+50000000 test.in | head -n10
 5.210  5.179  6.181    sed -n '50000000,50000010p;57890010q' test.in
 5.525  5.475  5.488    head -n50000010 test.in | tail -n10
 8.497  8.352  8.438    sed -n '50000000,50000010p' test.in
22.826 23.154 23.195    tail -n50000001 test.in | head -n10
25.694 25.908 27.638    ed -s test.in <<<"50000000,50000010p"
31.348 28.140 30.574    awk 'NR<57890000{next}1;NR==57890010{exit}' test.in
51.359 50.919 51.127    awk 'NR >= 57890000 && NR <= 57890010' test.in

Це аж ніяк не точні орієнтири, але різниця є достатньо чіткою та повторюваною *, щоб добре зрозуміти відносну швидкість кожної з цих команд.

*: За винятком перших двох sed -n p;qта head|tail, які здаються по суті однаковими.


11
З цікавості: як ви протерли кеш диска між тестами?
Paweł Rumian

2
А як щодо того tail -n +50000000 test.in | head -n10, що на відміну від tail -n-50000000 test.in | head -n10цього дасть би правильний результат?
Жиль

4
Гаразд, я пішов і зробив деякі орієнтири. хвіст | голова швидше, ніж sed, різниця набагато більше, ніж я очікував.
Жиль

3
@Gilles ти маєш рацію, моя погана. tail+|headшвидше на 10-15%, ніж sed, я додав цей показник.
Кевін

1
Я розумію, що питання задає рядки, але якщо ви використовуєте -cдля пропуску символів, tail+|headце миттєво. Звичайно, ви не можете сказати "50000000" і, можливо, доведеться вручну шукати початок розділу, який ви шукаєте.
Danny Kirchmeier

51

Якщо ви хочете, щоб рядки від X до Y включали (починаючи нумерацію з 1), використовуйте

tail -n +$X /path/to/file | head -n $((Y-X+1))

tailпрочитає та відкине перші рядки X-1 (цього не обійти), потім прочитає та надрукує наступні рядки. headпрочитає та надрукує потрібну кількість рядків, а потім вийде. Коли headвиходить, tailотримує сигнал SIGPIPE і вмирає, тому з вхідного файлу він не буде прочитати більше, ніж розмір буфера (як правило, кілька кілобайт) рядків.

Як варіант, запропонований gorkypl , використовуйте sed:

sed -n -e "$X,$Y p" -e "$Y q" /path/to/file

Рішення sed є значно повільнішим (принаймні, для утиліт GNU та утиліт Busybox; sed може бути більш конкурентоспроможним, якщо витягнути велику частину файлу в ОС, де трубопровід повільний, а sed швидкий). Ось швидкі орієнтири під Linux; дані генеруються seq 100000000 >/tmp/a, середовище Linux / amd64, /tmpє tmpfs, і машина в іншому випадку не працює і не замінюється.

real  user  sys    command
 0.47  0.32  0.12  </tmp/a tail -n +50000001 | head -n 10 #GNU
 0.86  0.64  0.21  </tmp/a tail -n +50000001 | head -n 10 #BusyBox
 3.57  3.41  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #GNU
11.91 11.68  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #BusyBox
 1.04  0.60  0.46  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #GNU
 7.12  6.58  0.55  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #BusyBox
 9.95  9.54  0.28  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #GNU
23.76 23.13  0.31  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #BusyBox

Якщо ви знаєте діапазон байтів, з яким ви хочете працювати, ви можете витягнути його швидше, перейшовши безпосередньо в початкове положення. Але для рядків потрібно читати з початку і рахувати нові рядки. Для вилучення блоків від x включно до y виключно, починаючи з 0, розмір блоку b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file

1
Ви впевнені, що між ними немає кешування? Різниці між хвостом | головою та sed виглядають для мене занадто великими.
Paweł Rumian

@gorkypl Я зробив кілька заходів і часи були порівнянні. Як я вже писав, це все відбувається в оперативній пам'яті (все є в кеші).
Жиль

1
@Gilles, tail will read and discard the first X-1 lineздається, уникнути, коли кількість рядків задано з кінця. У такому випадку хвіст, здається, читається назад від кінця відповідно до часу виконання. Будь ласка , прочитайте: http://unix.stackexchange.com/a/216614/79743.

1
@BinaryZebra Так, якщо вхід є звичайним файлом, деякі реалізації tail(включаючи хвіст GNU) мають евристику для читання з кінця. Це покращує tail | headрішення порівняно з іншими методами.
Жиль

22

head | tailПідхід є одним з кращих і найбільш «ідіоматичних» способів зробити це:

X=57890000
Y=57890010
< infile.txt head -n "$Y" | tail -n +"$X"

Як вказував Жилль у коментарях, швидший шлях

< infile.txt tail -n +"$X" | head -n "$((Y - X))"

Причина цього швидше - перші лінії X-1 не потребують проходження по трубі порівняно з head | tailпідходом.

Ваше фразове запитання трохи вводить в оману і, ймовірно, пояснює деякі ваші необгрунтовані сумніви щодо такого підходу.

  • Ви говорите , що ви повинні обчислити A, B, C, Dале , як ви можете бачити, кількість рядків файлу не потрібно , і розрахунок в більшості 1 необхідно, що оболонка може зробити для вас в будь-якому випадку.

  • Ви переживаєте, що трубопровід прочитає більше рядків, ніж потрібно. Насправді це неправда: tail | headє настільки ж ефективним, наскільки ви можете отримати з точки зору вводу / виводу файлів. По-перше, врахуйте необхідний мінімальний обсяг роботи: щоб знайти X -й рядок у файлі, єдиний загальний спосіб зробити це - читати кожен байт і зупинятись, коли ви рахуєте символи X нового рядка, оскільки немає ніякого способу ділитися файлом зміщення X -го рядка. Як тільки ви досягнете рядка * X *, вам доведеться прочитати всі рядки, щоб надрукувати їх, зупиняючись на Y -му рядку. Таким чином, жоден підхід не може уникнути читання менше, ніж Y рядків. Тепер head -n $Yчитає не більше Yрядки (округлені до найближчого буферного блоку, але буфери при правильному використанні покращують продуктивність, тому не потрібно турбуватися про цю накладну). Крім того, tailне буде читати більше head, тому таким чином ми показали, що head | tailчитає найменшу кількість можливих рядків (знову ж таки, плюс незначне буферизація, яку ми ігноруємо). Єдиною перевагою ефективності єдиного інструментального підходу, який не використовує труби, є менша кількість процесів (і, таким чином, менше накладних витрат).


1
Ніколи не бачив, щоб перенаправлення йшло спочатку по лінії. Прохолодно, це робить потік труби більш чітким.
clacke

14

Найбільш ортодоксальним способом (але не найшвидшим, як зазначав Жилл вище) було б використання sed.

У вашому випадку:

X=57890000
Y=57890010
sed -n -e "$X,$Y p" -e "$Y q" filename

-nВаріант передбачає , що тільки відповідні рядки друкуються на стандартний висновок.

Р в кінці оздоблювального номер рядка означає друкувати рядки в заданому діапазоні. Кв у другій частині сценарію економить час, пропускаючи частину файлу.


1
Я очікував sedі tail | headприблизно буду на одному рівні, але виявляється, що tail | headце значно швидше (див. Мою відповідь ).
Жиль

1
Я не знаю, що з того, що я прочитав, tail/ headвважаються більш "ортодоксальними", оскільки обрізка будь-якого кінця файлу - саме те, для чого вони створені. У цих матеріалах, sedздається, увійти в картину лише тоді, коли потрібні заміни - і швидко висунутись із зображення, коли щось набагато складніше починає відбуватися, оскільки його синтаксис для складних завдань набагато гірший, ніж AWK, який потім бере на себе .
підкреслюйте_d

7

Якщо ми знаємо діапазон для вибору, від першого рядка lStartдо останнього рядка: lEndми можемо обчислити:

lCount="$((lEnd-lStart+1))"

Якщо ми знаємо загальну кількість рядків: lAllми також могли обчислити відстань до кінця файлу:

toEnd="$((lAll-lStart+1))"

Тоді ми будемо знати обидва:

"how far from the start"            ($lStart) and
"how far from the end of the file"  ($toEnd).

Вибір найменшого з будь-якого з них tailnumber:

tailnumber="$toEnd"; (( toEnd > lStart )) && tailnumber="+$linestart"

Дозволяє нам використовувати послідовно найшвидшу команду виконання:

tail -n"${tailnumber}" ${thefile} | head -n${lCount}

Зверніть увагу на додатковий знак плюс ("+"), коли $linestartвибрано.

Єдине застереження полягає в тому, що нам потрібна загальна кількість рядків, і це може зайняти деякий додатковий час, щоб знайти.
Як зазвичай це стосується:

linesall="$(wc -l < "$thefile" )"

Деякі виміряні рази:

lStart |500| lEnd |500| lCount |11|
real   user   sys    frac
0.002  0.000  0.000  0.00  | command == tail -n"+500" test.in | head -n1
0.002  0.000  0.000  0.00  | command == tail -n+500 test.in | head -n1
3.230  2.520  0.700  99.68 | command == tail -n99999501 test.in | head -n1
0.001  0.000  0.000  0.00  | command == head -n500 test.in | tail -n1
0.001  0.000  0.000  0.00  | command == sed -n -e "500,500p;500q" test.in
0.002  0.000  0.000  0.00  | command == awk 'NR<'500'{next}1;NR=='500'{exit}' test.in


lStart |50000000| lEnd |50000010| lCount |11|
real   user   sys    frac
0.977  0.644  0.328  99.50 | command == tail -n"+50000000" test.in | head -n11
1.069  0.756  0.308  99.58 | command == tail -n+50000000 test.in | head -n11
1.823  1.512  0.308  99.85 | command == tail -n50000001 test.in | head -n11
1.950  2.396  1.284  188.77| command == head -n50000010 test.in | tail -n11
5.477  5.116  0.348  99.76 | command == sed -n -e "50000000,50000010p;50000010q" test.in
10.124  9.669  0.448  99.92| command == awk 'NR<'50000000'{next}1;NR=='50000010'{exit}' test.in


lStart |99999000| lEnd |99999010| lCount |11|
real   user   sys    frac
0.001  0.000  0.000  0.00  | command == tail -n"1001" test.in | head -n11
1.960  1.292  0.660  99.61 | command == tail -n+99999000 test.in | head -n11
0.001  0.000  0.000  0.00  | command == tail -n1001 test.in | head -n11
4.043  4.704  2.704  183.25| command == head -n99999010 test.in | tail -n11
10.346  9.641  0.692  99.88| command == sed -n -e "99999000,99999010p;99999010q" test.in
21.653  20.873  0.744  99.83 | command == awk 'NR<'99999000'{next}1;NR=='99999010'{exit}' test.in

Зауважте, що часи різко змінюються, якщо вибрані лінії знаходяться поблизу початку або наприкінці. Команда, яка, як видається, добре працює з одного боку файлу, може бути вкрай повільною з іншого боку файлу.


Коментарі не для розширеного обговорення; ця розмова переміщена до чату .
terdon

@BinaryZebra - шлях краще.
mikeserv

0

Я роблю це досить часто і так написав цей сценарій. Мені не потрібно знаходити номери рядків, сценарій робить це все.

#!/bin/bash

# $1: start time
# $2: end time
# $3: log file to read
# $4: output file

# i.e. log_slice.sh 18:33 19:40 /var/log/my.log /var/log/myslice.log

if [[ $# != 4 ]] ; then 
echo 'usage: log_slice.sh <start time> <end time> <log file> <output file>'
echo
exit;
fi

if [ ! -f $3 ] ; then
echo "'$3' doesn't seem to exit."
echo 'exiting.'
exit;
fi

sline=$(grep -n " ${1}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of start time
eline=$(grep -n " ${2}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of end time

linediff="$((eline-sline))"

tail -n+${sline} $3|head -n$linediff > $4

2
Ви відповідаєте на запитання, яке не задавали. Ваша відповідь - 10% tail|head, про що досить широко обговорювалося в запитанні та інших відповідях, і 90% визначають номери рядків там, де відображаються задані рядки / шаблони, які не входили до питання . PS ви завжди повинні цитувати ваші параметри оболонки та змінні; наприклад, "$ 3" і "$ 4".
G-Man
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.