it-swarm-eu.dev

kočka linie X na řádku Y na obrovském souboru

Řekněme, že mám obrovský textový soubor (> 2 GB) a chci pouze cat řádky XY (např. 57890000 až 57890010).

Z toho, co jsem pochopil, mohu to udělat tím, že pošlu head do tail nebo viceversa, tj.

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

nebo alternativně

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

kde A, B, C a D lze vypočítat z počtu řádků v souboru, X a Y.

S tímto přístupem však existují dva problémy:

  1. Musíte spočítat A, B, C a D.
  2. Příkazy by si mohly navzájem pipemnohem více řádků, než mám zájem o čtení (např. Pokud přečtu jen několik řádků uprostřed obrovského souboru)

Existuje způsob, jak Shell prostě pracovat a vydávat řádky, které chci? (zatímco poskytujete pouze X a Y)?

142

Navrhuji řešení sed, ale kvůli úplnosti

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

Vystřihnout za poslední řádek:

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

Test rychlosti (zde na MacOS, YMMV na dalších systémech):

  • 100 000 000 řádků vytvořených programem seq 100000000 > test.in
  • Čtecí řádky 50 000 000–50 000 010
  • Testy v žádném zvláštním pořadí
  • real čas podle vestavěného bashtime
 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

V žádném případě to nejsou přesná měřítka, ale rozdíl je dostatečně jasný a opakovatelný *, aby poskytl dobrý pocit relativní rychlosti každého z těchto příkazů.

*: Kromě prvních dvou, sed -n p;q a head|tail, které se zdají být v podstatě stejné.

128
Kevin

Pokud chcete, aby řádky X až Y včetně (začátek číslování na 1), použijte

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

tail přečte a zahodí první řádky X-1 (neexistuje žádný způsob), pak přečte a vytiskne následující řádky. head přečte a vytiskne požadovaný počet řádků a poté skončí. Když head skončí, tail obdrží signál SIGPIPE a zemře, takže nebude číst více než vyrovnávací paměť velikost má hodnotu (obvykle několik kilobajtů) řádků ze vstupního souboru.

Alternativně, jak gorkypl navrhl, použijte sed:

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

Řešení sed je však výrazně pomalejší (alespoň pro GNU utilitky a utilitky Busybox; sed může být konkurenceschopnější, pokud rozbalíte velkou část souboru na OS, kde je potrubí pomalé a sed je Zde jsou rychlé testy v systému Linux, data byla vygenerována programem seq 100000000 >/tmp/a, prostředím je Linux/AMD64, /tmp je tmpfs a stroj je jinak nečinný a nevyměňuje se.

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

Pokud znáte rozsah bajtů, se kterým chcete pracovat, můžete jej rychleji extrahovat přeskočením přímo na počáteční pozici. Ale pro řádky musíte číst od začátku a počítat nové řádky. Pro extrakci bloků z x inclusive na y exkluzivní počínaje 0, s velikostí bloku b:

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

The head | tail přístup je jedním z nejlepších a nejvíce "idiomatických" způsobů, jak toho dosáhnout:

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

Jak poznamenal Gilles v komentářích, rychlejší cesta je

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

Důvodem je to rychlejší je, že první X - 1 řádky nemusí procházet potrubím ve srovnání s head | tail přístup.

Vaše dotazovaná formulace je trochu zavádějící a pravděpodobně vysvětluje některá z vašich nepodložených pochybností k tomuto přístupu.

  • Říkáte, že musíte vypočítat A, B, C, D, ale jak vidíte, počet řádků souboru není potřeba a na je nutný nejvýše 1 výpočet, který pro vás Shell stejně může udělat.

  • Bojíte se, že potrubí přečte více řádků, než je nutné. Ve skutečnosti to není pravda: tail | head je asi tak efektivní, jak můžete získat, pokud jde o soubor I/O. Nejprve zvažte minimální potřebnou práci: najít X 'řádek v souboru, jediný obecný způsob, jak to udělat, je přečíst každý bajt a zastavte se, když počítáte X symboly nových řádků, protože neexistuje žádný způsob, jak zabránit posunutí souboru X 'th line. Jakmile dosáhnete * X * th řádku, musíte si přečíst všechny řádky, abyste je mohli vytisknout, zastavení na Y 'th řádku. Žádný přístup se tedy nemůže dostat pryč při čtení méně než Y řádků. Nyní, head -n $Y nečte více než Y řádků (zaokrouhleno na nejbližší vyrovnávací jednotku, ale vyrovnávací paměti, pokud jsou použity správně, zlepšují výkon, takže se nemusíte starat o tuto režii) . Navíc tail nebude číst více než head, takže jsme ukázali, že head | tail přečte nejmenší možný počet řádků (opět plus nějaké zanedbatelné ukládání do vyrovnávací paměti, které ignorujeme). Jediná výhoda efektivnosti jediného přístupu k nástroji, který nepoužívá potrubí, je méně procesů (a tedy méně režijních nákladů).

24
jw013

Nej ortodoxnějším způsobem (ale ne nejrychlejším, jak poznamenal Gilles výše) by bylo použití sed.

Ve vašem případě:

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

The -n volba znamená, že se do výtisku vytisknou pouze příslušné řádky.

p na konci čísla cílové čáry znamená tisk linek v daném rozsahu. q ve druhé části skriptu ušetří nějaký čas přeskočením zbytku souboru.

16
Paweł Rumian

Pokud známe rozsah, který chceme vybrat, od prvního řádku: lStart do posledního řádku: lEnd bychom mohli spočítat:

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

Známe-li celkový počet řádků: lAll, můžeme také vypočítat vzdálenost do konce souboru:

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

Pak budeme vědět oba:

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

Výběr nejmenší z těchto: tailnumber takto:

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

Umožňuje nám používat důsledně nejrychlejší prováděcí příkaz:

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

Vezměte prosím na vědomí další znaménko plus („+“), když $linestart je vybráno.

Jedinou výzvou je, že potřebujeme celkový počet řádků, a to může nějakou dobu trvat, než najdeme.
Jak je obvyklé u:

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

Někdy se měří:

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

Upozorňujeme, že časy se drasticky mění, pokud jsou vybrané řádky blízko začátku nebo konce. Příkaz, který vypadá dobře na jedné straně souboru, může být na druhé straně souboru velmi pomalý.

7
user79743

Dělám to dost často, a tak napsal tento skript. Nepotřebuji najít čísla řádků, skript to všechno provede.

#!/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
1
Doolan