cat linha X para linha Y em um arquivo enorme

114

Digamos que eu tenha um arquivo de texto grande (> 2 GB) e só quero cat das linhas X a Y (por exemplo, 57890000 a 57890010).

Pelo que entendi, posso fazer isso canalizando head para tail ou vice-versa, ou seja,

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

ou alternativamente

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

em que A , B , C e D podem ser calculados a partir do número de linhas no arquivo, X e Y .

Mas há dois problemas com essa abordagem:

  1. Você precisa computar A , B , C e D .
  2. Os comandos poderiam pipe entre si muitas mais linhas do que eu estou interessado em ler (por exemplo, se eu estiver lendo apenas algumas linhas no meio de um arquivo enorme)

Existe uma maneira de fazer com que o shell apenas trabalhe e produza as linhas que eu quero? (fornecendo apenas X e Y )?

    
por Amelio Vazquez-Reina 07.09.2012 / 00:38

6 respostas

106

Sugiro a solução sed , mas por questão de integridade,

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

Para recortar depois da última linha:

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

Teste de velocidade:

  • arquivo de 100.000.000 linhas gerado por seq 100000000 > test.in
  • Linhas de leitura 50.000.000 - 50.000.010
  • Testes sem ordem específica
  • real tempo, conforme relatado por bash embutido time
 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

Estes não são de forma alguma benchmarks precisos, mas a diferença é clara e repetível o suficiente * para dar uma boa noção da velocidade relativa de cada um desses comandos.

*: Exceto entre os dois primeiros, sed -n p;q e head|tail , que parecem ser essencialmente os mesmos.

    
por 07.09.2012 / 03:28
45

Se você quiser linhas X a Y inclusive (começando a numeração em 1), use

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

tail lerá e descartará as primeiras linhas X-1 (não há como evitar isso), depois leia e imprima as linhas a seguir. head lerá e imprimirá o número solicitado de linhas e, em seguida, sairá. Quando head sai, tail recebe um sinal SIGPIPE e morre, por isso não terá lido mais do que um tamanho do buffer (normalmente alguns kilobytes) de linhas do arquivo de entrada.

Como alternativa, como gorkypl sugerido, use sed:

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

A solução sed é significativamente mais lenta (pelo menos para utilitários GNU e utilitários Busybox; o sed pode ser mais competitivo se você extrair uma grande parte do arquivo em um sistema operacional onde a tubulação é lenta e o sed é rápido). Aqui estão os benchmarks rápidos no Linux; os dados foram gerados por seq 100000000 >/tmp/a , o ambiente é Linux / amd64, /tmp é tmpfs e a máquina está ociosa e não está trocando.

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

Se você souber o intervalo de bytes com o qual deseja trabalhar, poderá extraí-lo mais rapidamente pulando diretamente para a posição inicial. Mas para as linhas, você tem que ler desde o início e contar as novas linhas. Para extrair blocos de x inclusive para y exclusivo começando em 0, com um tamanho de bloco de b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file
    
por 07.09.2012 / 03:39
22

A abordagem head | tail é uma das melhores e mais "idiomáticas" maneiras de fazer isso:

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

Como apontado por Gilles nos comentários, uma maneira mais rápida é

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

A razão pela qual isso é mais rápido é que as primeiras linhas X-1 não precisam passar pelo canal em comparação com a abordagem head | tail .

A sua pergunta, tal como está formulada, é um pouco enganadora e provavelmente explica algumas das suas dúvidas infundadas relativamente a esta abordagem.

  • Você diz que precisa calcular A , B , C , D , mas como você pode ver, a contagem de linhas do arquivo não é necessária e, no máximo, 1 cálculo é necessário, que o shell pode fazer por você de qualquer maneira.

  • Você se preocupa que a tubulação leia mais linhas do que o necessário. Na verdade, isso não é verdade: tail | head é o mais eficiente possível em termos de E / S de arquivo. Primeiro, considere a quantidade mínima de trabalho necessária: para encontrar a linha X em um arquivo, a única maneira geral de fazer isso é ler cada byte e parar quando contar X newline símbolos como não há como adivinhar o deslocamento do arquivo da linha X 'th. Uma vez que você alcança a linha * X * th, você tem que ler todas as linhas para imprimi-las, parando na linha Y . Portanto, nenhuma abordagem pode ser usada com leitura de linhas menores que Y . Agora, head -n $Y não lê mais do que as linhas Y (arredondadas para a unidade de buffer mais próxima, mas os buffers, se usados corretamente, melhoram o desempenho, portanto não há necessidade de se preocupar com essa sobrecarga). Além disso, tail não lerá mais do que head , portanto, mostramos que head | tail lê o menor número possível de linhas (mais uma vez, além de algum buffer desprezível que estamos ignorando). A única vantagem de eficiência de uma única abordagem de ferramenta que não usa pipes é menos processos (e, portanto, menos sobrecarga).

por 07.09.2012 / 01:44
12

A maneira mais ortodoxa (mas não a mais rápida, como observado por Gilles acima) seria usar% código%.

No seu caso:

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

A opção sed implica que apenas as linhas relevantes sejam impressas para stdout.

O p no final do número da linha de chegada significa imprimir linhas em um determinado intervalo. O q na segunda parte do script economiza tempo ignorando o restante do arquivo.

    
por 07.09.2012 / 00:59
7

Se soubermos o intervalo a selecionar, da primeira linha: lStart até a última linha: lEnd , poderíamos calcular:

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

Se soubermos a quantidade total de linhas: lAll , também poderíamos calcular a distância até o final do arquivo:

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

Depois, saberemos os dois:

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

Escolhendo o menor de qualquer um desses: tailnumber como este:

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

Nos permite usar o comando de execução consistentemente mais rápido:

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

Observe o sinal de mais ("+") adicional quando $linestart for selecionado.

A única ressalva é que precisamos da contagem total de linhas, e isso pode levar algum tempo adicional para ser encontrado.
Como é habitual com:

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

Algumas vezes medidas são:

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

Observe que os horários mudam drasticamente se as linhas selecionadas estiverem próximas do início ou próximas ao final. Um comando que parece funcionar bem em um lado do arquivo, pode ser extremamente lento no outro lado do arquivo.

    
por 17.07.2015 / 06:42
0

Eu faço isso com bastante frequência e então escrevi este script. Eu não preciso encontrar os números de linha, o script faz tudo.

#!/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
    
por 09.10.2014 / 00:31