cat linia X do linii Y na ogromnym pliku

132

Powiedzmy, że mam ogromny plik tekstowy (> 2 GB) i chcę tylko catwiersze Xdo Y(np. 57890000 do 57890010).

Z tego, co rozumiem, mogę to zrobić przez pipingowanie headdo taillub odwrotnie, tj

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

lub alternatywnie

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

gdzie A, B, Ci Dmogą być obliczane na podstawie liczby linii w pliku, Xa Y.

Ale z tym podejściem wiążą się dwa problemy:

  1. Trzeba obliczyć A, B, Ci D.
  2. Polecenia mogą przesyłać pipesobie o wiele więcej wierszy niż jestem zainteresowany czytaniem (np. Jeśli czytam tylko kilka wierszy w środku dużego pliku)

Czy istnieje sposób, aby powłoka po prostu działała i wyświetlała wiersze, które chcę? (zapewniając tylko Xi Y)?

Amelio Vazquez-Reina
źródło
1
Do Twojej odpowiedzi dodano porównanie testu prędkości rzeczywistej z 6 metodami.
Kevin

Odpowiedzi:

119

Proponuję sedrozwiązanie, ale ze względu na kompletność,

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

Aby wyciąć po ostatniej linii:

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

Test prędkości:

  • Plik 100 000 000 wierszy wygenerowany przez seq 100000000 > test.in
  • Czytanie wierszy 50 000 000-50 000,010
  • Testy w określonej kolejności
  • realczas podany przez bashwbudowanetime
 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

Nie są to w żadnym razie precyzyjne testy porównawcze, ale różnica jest wyraźna i wystarczająco powtarzalna *, aby dać dobre wyobrażenie o względnej prędkości każdego z tych poleceń.

*: Z wyjątkiem pierwszych dwóch sed -n p;qi head|tail, które wydają się zasadniczo takie same.

Kevin
źródło
11
Z ciekawości: jak opróżniłeś pamięć podręczną dysku między testami?
Paweł Rumian
2
Co tail -n +50000000 test.in | head -n10, w przeciwieństwie do tail -n-50000000 test.in | head -n10tego , co dałoby prawidłowy wynik?
Gilles
4
Ok, poszedłem i zrobiłem kilka testów porównawczych. głowa | ogona jest znacznie szybsza niż sed, różnica jest znacznie większa niż się spodziewałem.
Gilles,
3
@Gilles masz rację, mój zły. tail+|headjest szybszy o 10-15% niż sed, dodałem ten punkt odniesienia.
Kevin
1
Zdaję sobie sprawę, że pytanie wymaga wierszy, ale jeśli użyjesz go -cdo pominięcia znaków, tail+|headjest natychmiastowy. Oczywiście nie można powiedzieć „50000000” i może być konieczne ręczne wyszukanie początku szukanej sekcji.
Danny Kirchmeier
51

Jeśli chcesz wiersze od X do Y włącznie (zaczynając od numeracji od 1), użyj

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

tailodczyta i odrzuci pierwsze linie X-1 (nie da się tego obejść), a następnie przeczyta i wydrukuje kolejne linie. headodczyta i wydrukuje żądaną liczbę wierszy, a następnie wyjdzie. Kiedy headwychodzi, tailodbiera sygnał SIGPIPE i umiera, więc nie będzie czytał więcej niż rozmiar bufora (zwykle kilka kilobajtów) linii z pliku wejściowego.

Alternatywnie, jak sugeruje gorkypl , użyj sed:

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

Rozwiązanie sed jest jednak znacznie wolniejsze (przynajmniej w przypadku narzędzi GNU i Busybox; sed może być bardziej konkurencyjny, jeśli wyodrębnisz dużą część pliku w systemie operacyjnym, w którym przesyłanie jest wolne, a sed szybki). Oto krótkie testy porównawcze pod Linuksem; dane zostały wygenerowane przez seq 100000000 >/tmp/a, środowisko to Linux / amd64, /tmpjest tmpfs, a maszyna jest bezczynna i nie zamienia się.

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

Jeśli znasz zakres bajtów, z którym chcesz pracować, możesz go szybciej wyodrębnić, przechodząc bezpośrednio do pozycji początkowej. Ale w przypadku linii musisz czytać od początku i liczyć nowe wiersze. Aby wyodrębnić bloki od x włącznie do y wyłącznie od 0, przy rozmiarze bloku b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file
Gilles
źródło
1
Czy na pewno nie ma buforowania między nimi? Różnice między ogonem | głową a sedem wydają mi się zbyt duże.
Paweł Rumian
@gorkypl Zrobiłem kilka pomiarów i czasy były porównywalne. Jak napisałem, wszystko dzieje się w pamięci RAM (wszystko jest w pamięci podręcznej).
Gilles
1
@Gilles tail will read and discard the first X-1 linewydaje się być unikany, gdy liczba linii jest podawana od końca. W takim przypadku ogon wydaje się czytać wstecz od końca zgodnie z czasem wykonania. Proszę przeczytać: http://unix.stackexchange.com/a/216614/79743.
1
@BinaryZebra Tak, jeśli dane wejściowe są zwykłym plikiem, niektóre implementacje tail(w tym GNU tail) mają heurystykę do odczytania od końca. To poprawia tail | headrozwiązanie w porównaniu do innych metod.
Gilles,
22

head | tailPodejście jest jednym z najlepszych i najbardziej „idiomatyczne” sposobów, aby to zrobić:

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

Jak zauważył Gilles w komentarzach, szybszy sposób

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

Jest tak dlatego, że pierwsze linie X - 1 nie muszą przechodzić przez rurę w porównaniu do head | tailpodejścia.

Twoje pytanie w formie wyrażenia jest nieco mylące i prawdopodobnie wyjaśnia niektóre z twoich bezpodstawnych wątpliwości co do tego podejścia.

  • Mówisz, że trzeba obliczyć A, B, C, Dale jak widać, liczba linii pliku nie jest potrzebne, a co najwyżej 1 obliczenie jest konieczne, którego powłoka może zrobić dla ciebie tak czy inaczej.

  • Martwisz się, że rurociągi będą czytały więcej wierszy niż to konieczne. W rzeczywistości nie jest to prawdą: tail | headjest tak wydajne, jak można uzyskać pod względem operacji we / wy pliku. Najpierw rozważ minimalną ilość pracy: aby znaleźć X wiersz w pliku, jedynym ogólnym sposobem na to jest odczyt każdego bajtu i zatrzymanie się po policzeniu X symboli nowej linii, ponieważ nie ma sposobu na podzielenie pliku przesunięcie X -tej linii. Po osiągnięciu * X * th linii, trzeba czytać wszystkie linie w celu ich wydrukowania, zatrzymując się na Y tej linii. Zatem żadne podejście nie może uciec czytaniu mniej niż linii Y. Teraz head -n $Yczyta nie więcej niż Ywiersze (zaokrąglone do najbliższej jednostki bufora, ale bufory, jeśli są używane prawidłowo, poprawiają wydajność, więc nie trzeba się martwić o to narzut). Ponadto tailnie będzie więcej niż czytać head, dlatego pokazaliśmy, że head | tailczyta najmniejszą możliwą liczbę wierszy (ponownie plus nieznaczne buforowanie, które ignorujemy). Jedyną korzyścią wynikającą z podejścia opartego na jednym narzędziu, które nie wykorzystuje rur, jest mniejsza liczba procesów (a tym samym mniejsze koszty ogólne).

jw013
źródło
1
Nigdy wcześniej nie widziałem przekierowania na linii. Fajnie, sprawia, że ​​przepływ rur jest wyraźniejszy.
clacke 27.04.16
14

Najbardziej ortodoksyjnym sposobem (ale nie najszybszym, jak zauważył Gilles powyżej) byłoby użycie sed.

W Twoim przypadku:

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

Ta -nopcja oznacza, że ​​tylko odpowiednie linie są drukowane na standardowym wyjściu.

Symbol p na końcu numeru linii mety oznacza drukowanie linii w danym zakresie. Q w drugiej części skryptu oszczędza trochę czasu omijając resztę pliku.

Paweł Rumian
źródło
1
Spodziewałem się sedi tail | headbędę na równi, ale okazuje się, że tail | headjest znacznie szybszy (zobacz moją odpowiedź ).
Gilles
1
Nie wiem, z tego, co przeczytałem, tail/ headsą uważane za bardziej „ortodoksyjne”, ponieważ przycinanie dowolnego końca pliku jest dokładnie tym, do czego zostały stworzone. W tych materiałach sedwydaje się, że wchodzi do obrazu tylko wtedy, gdy wymagane są zmiany - i jest szybko wypychany z obrazu, gdy zaczyna się dziać coś znacznie bardziej złożonego, ponieważ jego składnia dla złożonych zadań jest o wiele gorsza niż AWK, który następnie przejmuje kontrolę .
underscore_d
7

Jeśli znamy zakres do wyboru, od pierwszej linii: lStartdo ostatniej linii: lEndmożemy obliczyć:

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

Jeśli znamy całkowitą liczbę wierszy: lAllmożemy również obliczyć odległość do końca pliku:

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

Wtedy poznamy oba:

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

Wybór najmniejszego z tych: tailnumberjak to:

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

Pozwala nam używać konsekwentnie najszybszego polecenia:

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

Zwróć uwagę na dodatkowy znak plus („+”), gdy $linestartjest zaznaczony.

Jedynym zastrzeżeniem jest to, że potrzebujemy całkowitej liczby linii, a znalezienie może zająć trochę czasu.
Jak zwykle w przypadku:

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

Niektóre mierzone czasy to:

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

Zauważ, że czasy zmieniają się drastycznie, jeśli wybrane linie znajdują się blisko początku lub końca. Polecenie, które wydaje się działać dobrze po jednej stronie pliku, może być bardzo wolne po drugiej stronie pliku.


źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
terdon
@BinaryZebra - znacznie lepiej.
mikeserv
0

Robię to dość często i dlatego napisałem ten skrypt. Nie muszę znajdować numerów linii, skrypt robi wszystko.

#!/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
Doolan
źródło
2
Odpowiadasz na pytanie, które nie zostało zadane. Twoja odpowiedź wynosi 10% tail|head, co zostało obszernie omówione w pytaniu i innych odpowiedziach, a 90% określa numery linii, w których pojawiają się określone ciągi / wzorce, co nie było częścią pytania . PS zawsze powinieneś podawać parametry i zmienne powłoki; np. „3 USD” i „4 USD”.
G-Man,