linha de gato X a linha Y em um arquivo enorme


132

Digamos que eu tenha um arquivo de texto enorme (> 2 GB) e eu só quero catas linhas Xpara Y(por exemplo 57.890.000-57.890.010).

Pelo que entendi, posso fazer isso headentrando em tailcontato ou vice-versa, ou seja,

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

ou alternativamente

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

onde A, B, Ce Dpode ser calculado a partir do número de linhas no arquivo, Xe Y.

Mas há dois problemas com essa abordagem:

  1. Você tem que calcular A, B, Ce D.
  2. Os comandos podem pipeum para o outro muito mais linhas do que estou interessado em ler (por exemplo, se estou lendo apenas algumas linhas no meio de um arquivo enorme)

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


1
Para sua informação, teste de velocidade real comparado a 6 métodos adicionados à minha resposta.
Kevin

Respostas:


119

Sugiro a sedsolução, mas por uma questão de integridade,

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

Para cortar após a última linha:

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

Teste rápido:

  • Arquivo de 100.000.000 de linhas gerado por seq 100000000 > test.in
  • Linhas de leitura 50.000.000-50.000.010
  • Testes em nenhuma ordem específica
  • realtempo, como relatado por bashé builtintime
 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

Esses não são, de modo algum, 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;qe head|tail, que parecem ser essencialmente os mesmos.


11
Por curiosidade: como você liberou o cache do disco entre os testes?
Paweł Rumian 8/09/12

2
E o tail -n +50000000 test.in | head -n10que, diferentemente tail -n-50000000 test.in | head -n10, daria o resultado correto?
Gilles

4
Ok, eu fui e fiz alguns benchmarks. tail | head é muito mais rápido que sed, a diferença é muito mais do que eu esperava.
Gilles

3
@Gilles você está certo, meu mal. tail+|headé mais rápido em 10-15% do que o sed, eu adicionei esse benchmark.
Kevin

1
Sei que a pergunta pede linhas, mas se você usar -cpara pular caracteres, tail+|headé instantânea. Obviamente, você não pode dizer "50000000" e talvez precise procurar manualmente o início da seção que está procurando.
Danny Kirchmeier

51

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

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

taillerá e descartará as primeiras linhas X-1 (não há como contornar isso), depois lerá e imprimirá as seguintes linhas. headirá ler e imprimir o número solicitado de linhas e sair. Quando headsai, tailrecebe um sinal SIGPIPE e morre, para que não tenha lido mais do que o tamanho de um buffer (normalmente alguns kilobytes) de linhas do arquivo de entrada.

Como alternativa, como sugerido pelo gorkypl , use sed:

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

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

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ê conhece o intervalo de bytes com o qual deseja trabalhar, pode extraí-lo mais rapidamente, pulando diretamente para a posição inicial. Mas para linhas, você precisa ler desde o início e contar novas linhas. Para extrair blocos de x inclusive para exclusivo y começando em 0, com um tamanho de bloco de b:

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

1
Tem certeza de que não há cache entre eles? As diferenças entre cauda | cabeça e sed parecem grandes demais para mim.
Paweł Rumian 08/09/12

@gorkypl Fiz várias medidas e os tempos eram comparáveis. Como escrevi, tudo isso está acontecendo na RAM (tudo está no cache).
Gilles

1
@Gilles tail will read and discard the first X-1 lineparece ser evitado quando o número de linhas é dado a partir do final. Nesse caso, a cauda parece ler para trás a partir do final, de acordo com os tempos de execução. Por favor, leia-se: http://unix.stackexchange.com/a/216614/79743.

1
@BinaryZebra Sim, se a entrada for um arquivo regular, algumas implementações de tail(incluindo GNU tail) têm heurísticas para ler a partir do final. Isso melhora a tail | headsolução em comparação com outros métodos.
Gilles

22

A head | tailabordagem é 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 tubo em comparação com a head | tailabordagem.

Sua pergunta formulada é um pouco enganadora e provavelmente explica algumas de suas dúvidas infundadas em relação a essa abordagem.

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

  • Você teme que a tubulação leia mais linhas do que o necessário. De fato, isso não é verdade: tail | headé tão eficiente quanto você pode obter 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 todos os bytes e parar quando você contar X símbolos de nova linha, pois não há como adivinhar o arquivo deslocamento da X 'ésima linha. Quando chegar ao * X * th linha, você tem que ler todas as linhas, a fim de imprimi-los, parando no Y linha th'. Portanto, nenhuma abordagem pode se safar da leitura de menos de linhas Y. Agora, head -n $Ynão lê mais que Ylinhas (arredondadas para a unidade de buffer mais próxima, mas os buffers, se usados ​​corretamente, melhoram o desempenho, portanto, não é necessário se preocupar com essa sobrecarga). Além disso, tailnão lerá mais do que head, portanto, mostramos que head | taillê o menor número possível de linhas (novamente, mais algum buffer insignificante que estamos ignorando). A única vantagem de eficiência de uma abordagem de ferramenta única que não usa tubos é menos processos (e, portanto, menos sobrecarga).


1
Nunca vi o redirecionamento ir primeiro na linha antes. Frio, torna o fluxo do tubo mais claro.
clacke

14

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

No seu caso:

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

A -nopção implica que apenas as linhas relevantes sejam impressas em stdout.

O p no final de acabamento número de linha meios para imprimir linhas em determinado intervalo. O q na segunda parte do script economiza algum tempo, pulando o restante do arquivo.


1
Eu esperava sede tail | headestava em pé de igualdade, mas acontece que tail | headé significativamente mais rápido (veja minha resposta ).
Gilles

1
Não sei, pelo que li, tail/ headsão considerados mais "ortodoxos", já que aparar as extremidades de um arquivo é exatamente o que eles foram feitos. Nesses materiais, sedapenas parece entrar em cena quando são necessárias substituições - e ser rapidamente retirado da cena quando algo muito mais complexo começa a acontecer, já que sua sintaxe para tarefas complexas é muito pior que o AWK, que então assume o controle. .
Underscore_d

7

Se soubermos o intervalo a ser selecionado, da primeira linha: lStartaté a última linha: lEndpoderemos calcular:

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

Se soubermos a quantidade total de linhas: lAlltambém poderemos calcular a distância até o final do arquivo:

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

Então conheceremos os dois:

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

Escolhendo o menor de qualquer um deles: tailnumbercomo este:

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

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 $linestartselecionado.

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 tempos mudam drasticamente se as linhas selecionadas estão perto do início ou do fim. Um comando que parece funcionar bem em um lado do arquivo pode ser extremamente lento no outro lado do arquivo.


Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
terdon

@BinaryZebra - muito melhor.
mikeserv

0

Faço isso com bastante frequência e escrevi esse script. Não preciso encontrar os números das linhas, 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

2
Você está respondendo a uma pergunta que não foi feita. Sua resposta é 10% tail|head, que foi discutida extensivamente na pergunta e nas outras respostas, e 90% determinando os números de linha onde as strings / padrões especificados aparecem, o que não fazia parte da pergunta . PS, você deve sempre citar seus parâmetros e variáveis ​​do shell; por exemplo, "$ 3" e "$ 4".
G-Man
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.