Grep grande número de padrões de arquivo enorme

18

Eu tenho um arquivo que cresce cerca de 200.000 linhas por dia e é formado com blocos de três linhas, como tal:

1358726575123       # key
    Joseph Muller   # name
    carpenter       # job
9973834728345
    Andres Smith
    student
7836472098652
    Mariah Anthony
    dentist

Agora, tenho outro arquivo do qual extraio cerca de 10.000 padrões de chaves, como 1358726575123. Então eu corro um forloop com esses padrões e tenho que compará-los com o primeiro arquivo. Se o arquivo não contiver esse padrão, salve o padrão em um terceiro arquivo para processamento adicional:

for number in $(grep -o '[0-9]\{12\}' file2); do  # finds about 10.000 keys
     if ! grep -q ^$number$ file1; then           # file1 is a huge file
         printf "$number\n" >>file3               # we'll process file3 later
     fi
done

O código de exemplo recebe um arquivo enorme 10.000 vezes e eu corro esse loop cerca de uma vez por minuto, durante o dia inteiro .

Como o enorme arquivo continua crescendo, o que posso fazer para acelerar tudo isso e economizar um pouco de CPU? Gostaria de saber se classificar o arquivo de alguma forma por sua chave (se sim, como?) Ou usar um db em vez de texto sem formatação ajudaria ...

Teresa e Junior
fonte

Respostas:

11

Esta resposta é baseada na awkresposta postada por potong ..
É duas vezes mais rápido que o commmétodo (no meu sistema), para os mesmos 6 milhões de linhas no arquivo principal e 10 mil chaves ... (agora atualizado para usar o FNR, NR)

Embora awkseja mais rápido que o sistema atual e ofereça espaço para você e seu computador, lembre-se de que quando o processamento de dados for tão intenso quanto você descreveu, você obterá melhores resultados gerais ao mudar para um banco de dados dedicado; por exemplo. SQlite, MySQL ...


awk '{ if (/^[^0-9]/) { next }              # Skip lines which do not hold key values
       if (FNR==NR) { main[$0]=1 }          # Process keys from file "mainfile"
       else if (main[$0]==0) { keys[$0]=1 } # Process keys from file "keys"
     } END { for(key in keys) print key }' \
       "mainfile" "keys" >"keys.not-in-main"

# For 6 million lines in "mainfile" and 10 thousand keys in "keys"

# The awk  method
# time:
#   real    0m14.495s
#   user    0m14.457s
#   sys     0m0.044s

# The comm  method
# time:
#   real    0m27.976s
#   user    0m28.046s
#   sys     0m0.104s

Peter.O
fonte
Isso é rápido, mas não entendo muito do awk: como devem ser os nomes dos arquivos? Eu tentei file1 -> mainfilee file2 -> keyscom gawk e mawk, e ele gera chaves erradas.
Teresa e Junior
O arquivo1 possui chaves, nomes e trabalhos.
Teresa e Junior
'mainfile' é o arquivo grande (com chaves, nomes e trabalhos). Acabei de chamá-lo de "arquivo principal" porque eu continuava confundindo qual arquivo era qual (arquivo1 x arquivo2) .. 'chaves' contém apenas as 10 mil ou mais chaves. Para sua situação, NÃO redirecione nada. .. apenas use file1 Arquivo EOF2 Eles são os nomes dos seus arquivos. "EOF" é um arquivo de uma linha criado pelo script para indicar o final do primeiro arquivo (arquivo de dados principal) e o início do segundo arquivo ( . chaves) awkpermitem que você leia em uma série de arquivos .. neste caso, essa série tem 3 arquivos nele a saída vai para.stdout
Peter.O
Este script irá imprimir todas as chaves que estão presentes em mainfile, E que também irá imprimir todas as chaves do keysarquivo que são NÃO no mainfile... Isso é provavelmente o que está acontecendo ... (eu vou olhar um pouco mais para ele ...
Pedro.O
Obrigado, @ Peter.O! Como os arquivos são confidenciais, estou tentando criar arquivos de amostra $RANDOMpara o upload.
Teresa e Junior
16

O problema, é claro, é que você executa grep no grande arquivo 10.000 vezes. Você deve ler os dois arquivos apenas uma vez. Se você quiser ficar fora das linguagens de script, pode fazê-lo desta maneira:

  1. Extraia todos os números do arquivo 1 e os ordene
  2. Extraia todos os números do arquivo 2 e os ordene
  3. Execute commnas listas classificadas para obter o que há apenas na segunda lista

Algo assim:

$ grep -o '^[0-9]\{12\}$' file1 | sort -u -o file1.sorted
$ grep -o  '[0-9]\{12\}'  file2 | sort -u -o file2.sorted
$ comm -13 file1.sorted file2.sorted > file3

Veja man comm.

Se você pudesse truncar o arquivo grande todos os dias (como um arquivo de log), poderia manter um cache de números classificados e não precisaria analisá-lo todo o tempo.

angus
fonte
11
Arrumado! 2 segundos (em unidades não particularmente rápidas) com 200.000 entradas aleatórias de linhas no arquivo principal (por exemplo, 600.000 linhas) e 143.000 chaves aleatórias (foi assim que meus dados de teste terminaram) ... testados e funcionaram (mas você sabia que: ) ... Eu me pergunto sobre o {12}.. OP usou 12, mas as chaves exemplo são 13 longo ...
Peter.O
2
Apenas uma pequena nota, você pode fazê-lo sem lidar com arquivos temporários usando <(grep...sort)onde estão os nomes dos arquivos.
Kevin
Obrigado, mas grepping e classificar os arquivos leva muito mais tempo que o meu loop anterior (+ 2 min.).
Teresa e Junior
@Teresa e Junior. Qual é o tamanho do seu arquivo principal? ... Você mencionou que cresce 200.000 linhas por dia, mas não o tamanho ... Para reduzir a quantidade de dados que você precisa processar, você pode ler apenas as 200.000 linhas dos dias atuais, observando o número da última linha processada (ontem) e usandotail -n +$linenum para gerar apenas os dados mais recentes. Dessa forma, você processará apenas aproximadamente 200.000 linhas por dia. Acabei de testá-lo com 6 milhões de linhas no arquivo principal e 10 mil chaves ... tempo : 0m0.016s reais, usuário 0m0.008s, sys 0m0.008s
Pedro.O
Estou realmente bastante intrigado / curioso sobre como você pode receber seu arquivo principal 10.000 vezes e encontrá-lo mais rápido que esse método, que apenas o recebe uma vez (e uma vez para o muito menor1 ) ... Mesmo que sua classificação demore mais que a minha teste, eu simplesmente não posso colocar minha cabeça em torno da idéia de que a leitura de um grande arquivo que muitas vezes não superam uma única espécie (timewise)
Peter.O
8

Sim, definitivamente use um banco de dados. Eles são feitos exatamente para tarefas como esta.

Mika Fischer
fonte
Obrigado! Não tenho muita experiência com bancos de dados. Qual banco de dados você recomenda? Eu tenho o MySQL e o comando sqlite3 instalado.
Teresa e Junior
11
Ambos são bons para isso, o sqlite é mais simples porque é basicamente apenas um arquivo e uma API SQL para acessá-lo. Com o MySQL, você precisa configurar um servidor MySQL para usá-lo. Embora isso não seja muito difícil, o sqlite pode ser o melhor para começar.
Mika Fischer
3

Isso pode funcionar para você:

 awk '/^[0-9]/{a[$0]++}END{for(x in a)if(a[x]==1)print x}' file{1,2} >file3

EDITAR:

O script alterado para permitir duplicatas e chaves desconhecidas nos dois arquivos ainda produz chaves do primeiro arquivo que não está presente no segundo:

 awk '/^[0-9]/{if(FNR==NR){a[$0]=1;next};if($0 in a){a[$0]=2}}END{for(x in a)if(a[x]==1)print x}' file{1,2} >file3
potong
fonte
Isso perderá novas chaves que ocorrem mais de uma vez no arquivo principal (e, nesse caso, que ocorreram mais de uma vez no arquivo de chaves) Parece exigir que a contagem da matriz incrementada no arquivo principal não permita exceder 1, ou alguma solução alternativa equivalente (+1 porque é muito perto da marca)
Peter.O
11
Eu tentei com gawk e mawk, e ele gera chaves erradas ...
Teresa e Junior
@ Peter.OI assumiu que o arquivo principal tinha chaves exclusivas e que o arquivo 2 era um subconjunto do arquivo principal.
Potong
@potong O segundo funciona bem e muito rápido! Obrigado!
Teresa e Junior
@Teresa e Junior Você tem certeza de que ainda está funcionando corretamente? .. Usando os dados de teste que você forneceu , que devem produzir 5000 chaves, quando eu o executo, produz 136703 chaves, exatamente como eu cheguei até que finalmente entendi quais eram seus requisitos ... @potong Claro! FNR == NR (eu nunca usei isso antes :)
Peter.O
2

Com tantos dados, você realmente deve mudar para um banco de dados. Enquanto isso, uma coisa que você deve fazer para chegar a um desempenho decente é não procurarfile1 separadamente cada chave. Execute um único greppara extrair todas as chaves não excluídas de uma só vez. Como isso greptambém retorna linhas que não contêm uma chave, filtre-as.

grep -o '[0-9]\{12\}' file2 |
grep -Fxv -f - file1 |
grep -vx '[0-9]\{12\}' >file3

( -Fxsignifica pesquisar linhas inteiras, literalmente.-f - significa ler uma lista de padrões a partir da entrada padrão.)

Gilles 'SO- parar de ser mau'
fonte
A menos que eu esteja enganado, isso não soluciona o problema de armazenar chaves que não estão no arquivo grande, mas armazenará as chaves que estão nele.
Kevin
@ Kevin exatamente, e isso me forçou a usar o loop.
Teresa e Junior
@TeresaeJunior: adicionando -v( -Fxv) pode cuidar disso.
Pausado até novo aviso.
@DennisWilliamson Isso iria pegar todas as linhas no arquivo grande que não correspondem a nenhum no arquivo de chave, incluindo nomes, empregos, etc.
Kevin
@ Kevin Obrigado, eu interpretou mal a pergunta. Eu adicionei um filtro para linhas sem chave, embora minha preferência agora seja usarcomm .
Gilles 'SO- stop be evil'
2

Permita-me reforçar o que outros disseram: "Leve-o a um banco de dados!"

tem binários MySQL disponíveis gratuitamente para a maioria das plataformas.

Por que não o SQLite? É baseado em memória, carregando um arquivo simples quando você o inicia e depois fechá-lo quando terminar. Isso significa que, se o computador travar ou o processo SQLite desaparecer, todos os dados também desaparecerão.

Seu problema parece apenas algumas linhas de SQL e será executado em milissegundos!

Depois de instalar o MySQL (que eu recomendo em relação a outras opções), pagaria US $ 40 pelo SQL Cookbook da O'Reilly, de Anthony Molinaro, que tem muitos padrões de problemas, começando com SELECT * FROM tableconsultas simples e passando por agregações e várias junções.

Jan Steinman
fonte
Sim, vou começar a migrar meus dados para o SQL em alguns dias, obrigado! Os scripts do awk têm me ajudado muito até eu terminar tudo!
Teresa e Junior
1

Não tenho certeza se esta é a saída exata que você está procurando, mas provavelmente a maneira mais fácil é:

grep -o '[0-9]\{12\}' file2 | sed 's/.*/^&$/' > /tmp/numpatterns.grep
grep -vf /tmp/numpatterns.grep file1 > file3
rm -f /tmp/numpatterns.grep

Você também pode usar:

sed -ne '/.*\([0-9]\{12\}.*/^\1$/p' file2 > /tmp/numpatterns.grep
grep -vf /tmp/numpatterns.grep file1 > file3
rm -f /tmp/numpatterns.grep

Cada um deles cria um arquivo de padrão temporário que é usado para coletar os números do arquivo grande ( file1).

Arcege
fonte
Acredito que isso também encontre números que estão no arquivo grande, não aqueles que não estão.
Kevin
Correto, eu não vi o '!' no OP. Só precisa usar em grep -vfvez de grep -f.
Arcege
2
No @arcege, o grep -vf não exibirá chaves não correspondentes, exibirá tudo, incluindo nomes e trabalhos.
Teresa e Junior
1

Concordo plenamente que você tenha um banco de dados (o MySQL é bastante fácil de usar). Até você começar a funcionar, eu gosto da commsolução da Angus , mas muitas pessoas estão tentando grepe errando que pensei em mostrar a (ou pelo menos uma) maneira correta de fazer isso grep.

grep -o '[0-9]\{12\}' keyfile | grep -v -f <(grep -o '^[0-9]\{12\}' bigfile) 

O primeiro greprecebe as chaves. O terceiro grep(no <(...)) pega todas as chaves usadas no arquivo grande e o <(...)passa como um arquivo como argumento para -fo segundo grep. Isso faz com que o segundo o grepuse como uma lista de linhas para corresponder. Ele então usa isso para corresponder à sua entrada (a lista de chaves) do canal (primeiro grep) e imprime todas as chaves extraídas do arquivo de chaves e não (-v ) o arquivo grande.

Claro que você pode fazer isso com arquivos temporários que você precisa acompanhar e lembre-se de excluir:

grep -o '[0-9]\{12\}'  keyfile >allkeys
grep -o '^[0-9]\{12\}' bigfile >usedkeys
grep -v -f usedkeys allkeys

Isso imprime todas as linhas allkeysque não aparecem usedkeys.

Kevin
fonte
Infelizmente é lento , e recebo um erro de memória após 40 segundos:grep: Memory exhausted
Peter.O
@ Peter.O Mas está correto. Enfim, é por isso que sugiro um banco de dados ou comm, nessa ordem.
Kevin
Sim, isso funciona, mas é muito mais lento que o loop.
Teresa e Junior
1

O arquivo de chave não muda? Então você deve evitar procurar as entradas antigas repetidamente.

Com tail -fvocê pode obter a saída de um arquivo crescente.

tail -f growingfile | grep -f keyfile 

grep -f lê os padrões de um arquivo, uma linha como padrão.

Usuário desconhecido
fonte
Isso seria bom, mas o arquivo-chave é sempre diferente.
Teresa e Junior
1

Não postaria minha resposta porque achava que essa quantidade de dados não deveria ser processada com um script de shell, e a resposta certa para usar um banco de dados já foi dada. Mas desde agora existem 7 outras abordagens ...

Lê o primeiro arquivo na memória, depois recebe o segundo arquivo em busca de números e verifica se os valores estão armazenados na memória. Deve ser mais rápido que vários greps, se você tiver memória suficiente para carregar o arquivo inteiro.

declare -a record
while read key
do
    read name
    read job
    record[$key]="$name:$job"
done < file1

for number in $(grep -o '[0-9]\{12\}' file2)
do
    [[ -n ${mylist[$number]} ]] || echo $number >> file3
done
forcefsck
fonte
Tenho memória suficiente, mas achei essa ainda mais lenta. Obrigado embora!
Teresa e Junior
1

Concordo com o @ jan-steinman que você deve usar um banco de dados para esse tipo de tarefa. Existem várias maneiras de hackear uma solução com um script de shell, como as outras respostas mostram, mas fazê-lo dessa maneira levará a muita miséria se você usar e manter o código por qualquer período de tempo maior que apenas um projeto descartável de um dia.

Supondo que você esteja em uma caixa Linux, é provável que você tenha o Python instalado por padrão, o que inclui a biblioteca sqlite3 a partir do Python v2.5. Você pode verificar sua versão do Python com:

% python -V
Python 2.7.2+

Eu recomendo o uso da biblioteca sqlite3 porque é uma solução baseada em arquivo simples que existe para todas as plataformas (inclusive dentro do seu navegador da web!) E não requer a instalação de um servidor. Essencialmente com configuração zero e manutenção zero.

Abaixo está um script python simples que analisa o formato do arquivo que você deu como exemplo e, em seguida, faz uma consulta simples "selecionar tudo" e gera tudo o que é armazenado no banco de dados.

#!/usr/bin/env python

import sqlite3
import sys

dbname = '/tmp/simple.db'
filename = '/tmp/input.txt'
with sqlite3.connect(dbname) as conn:
    conn.execute('''create table if not exists people (key integer primary key, name text, job text)''')
    with open(filename) as f:
        for key in f:
            key = key.strip()
            name = f.next().strip()
            job = f.next().strip()
            try:
                conn.execute('''insert into people values (?,?,?)''', (key, name, job))
            except sqlite3.IntegrityError:
                sys.stderr.write('record already exists: %s, %s, %s\n' % (key, name, job))
    cur = conn.cursor()

    # get all people
    cur.execute('''select * from people''')
    for row in cur:
        print row

    # get just two specific people
    person_list = [1358726575123, 9973834728345]
    cur.execute('''select * from people where key in (?,?)''', person_list)
    for row in cur:
        print row

    # a more general way to get however many people are in the list
    person_list = [1358726575123, 9973834728345]
    template = ','.join(['?'] * len(person_list))
    cur.execute('''select * from people where key in (%s)''' % (template), person_list)
    for row in cur:
        print row

Sim, isso significa que você precisará aprender um pouco de SQL , mas valerá a pena a longo prazo. Além disso, em vez de analisar seus arquivos de log, talvez você possa gravar dados diretamente no seu banco de dados sqlite.

aculich
fonte
Obrigado pelo script python! Eu acho que /usr/bin/sqlite3funciona da mesma maneira para scripts de shell ( packages.debian.org/squeeze/sqlite3 ), embora eu nunca o tenha usado.
Teresa e Junior
Sim, você pode usar /usr/bin/sqlite3com scripts de shell, no entanto, recomendo evitar scripts de shell, exceto programas simples de descarte e, em vez disso, use uma linguagem como python que tenha melhor tratamento de erros e seja mais fácil de manter e crescer.
Aculich