grep com padrão de um arquivo (3.2Gb) correspondente em outro arquivo (4.8Gb)

7

Eu tenho dois arquivos de texto. Um é um arquivo de texto com nome, endereço de e-mail e outros campos. Algumas linhas de file1 :

John:[email protected]:johnson123:22hey
Erik:[email protected]:johnson133:22hey
Robert:[email protected]:johnson123:21hey
Johnnny:[email protected]:johnson123:22hey

O outro contém apenas endereços de email. Exemplos de file2 :

[email protected]
[email protected]
[email protected]
[email protected]

Eu quero que a saída seja cada linha completa de file1 que tenha um endereço de e-mail em file2 . Por exemplo, [email protected] está em file2 , então gostaria de ver a seguinte linha de file1 :

John:[email protected]:johnson123:22hey

Existe uma maneira fácil de pesquisar file1 e gerar as linhas que correspondem à "lista de endereços de e-mail" file2 ?

Eu tenho procurado por HOURS, mas minhas pesquisas no Google (e pesquisas do StackOverflow), juntamente com os esforços na linha de comando, não foram eficazes até o momento.

Comandos que tentei e acho que funcionariam:

fgrep -f file2.txt file1.txt > matched.txt
grep -F -f ....
grep -F -x -f file1 file2 > common 

etc, mas todos obtiveram grep memory exhausted - os arquivos que estou correspondendo são 4,8 GB ( file1 ) e 3,2 GB ( file2 , contendo apenas os endereços de e-mail). Eu suponho que a memória se esgota com esses comandos. Eu encontrei um método usando find para executar os comandos mais suaves, mas não consegui que funcionasse.

tldr ; precisa corresponder file2 com file1 e se houver uma linha de file2 que corresponda a uma linha em file1 , imprima-a. Os arquivos são grandes e eu preciso de uma maneira segura de não usar toda a memória.

Obrigado, procurei por um dia inteiro por isso e experimentei, não queria desistir (5 horas +).

    
por Axel Tobieson 01.08.2016 / 21:13

7 respostas

7

É bastante difícil operar arquivos grandes, mas você pode fazer isso em três etapas:

  1. Classifique arquivo1 pelo segundo campo

    sort -k2,2 -t: file1 >file1.sorted
    
  2. Classifique arquivo2

    sort file2 >file2.sorted
    
  3. Junte 2 arquivos pelo campo de e-mail

    join -t: -2 2 file2.sorted file1.sorted -o 2.1,0,2.3,2.4 >matched.txt
    
por 01.08.2016 / 22:47
5

Estou enviando uma segunda resposta para essa pergunta (esse é um problema interessante). Este é totalmente diferente da minha solução SQLite e das soluções sort + join bastante promissoras que começam a aparecer:

Usando sua abordagem inicial com grep -f , mas literalmente reduzindo um pouco o problema. Vamos dividir o "arquivo de consulta", file2 em partes gerenciáveis usando split .

O utilitário split é capaz de dividir um arquivo em vários arquivos menores com base em uma contagem de linhas.

Um arquivo de 3.2 Gb com um comprimento médio da linha de 20 caracteres está em algum lugar cerca de 172.000.000 linhas (a menos que eu tenha feito um erro aritmético). Dividir em 2000 arquivos de 85.000 linhas por arquivo é factível.

Então,

$ mkdir testing
$ cd testing
$ split -l 85000 -a 4 ../file2

A opção -a 4 informa split para usar quatro caracteres após um x inicial para criar os nomes de arquivos para os novos arquivos. Os arquivos serão chamados xaaaa , xaaab , etc.

Em seguida, execute o original grep -f nestes:

for f in x????; do
  grep -F -f "$f" ../file1
done

Isso pode tornar grep capaz de manter o conjunto agora muito menor de padrões de consulta na memória.

UPDATE : com 145.526.885 linhas, use split -l 72000 -a 4 para criar aproximadamente 2000 arquivos.

Lembre-se de limpar o diretório testing toda vez que você tentar criar um novo conjunto de arquivos divididos.

Observe que os arquivos divididos desta resposta podem ser usados individualmente como entrada para qualquer outra resposta que você possa obter para essa pergunta.

    
por 01.08.2016 / 23:04
4

Costas answer é provavelmente o melhor dado o seu problema exato, porque você tem um campo que tem uma correspondência de 100%.

Mas se o seu problema realmente foi para milhões de regexps em bilhões de linhas, então o GNU Parallel tem uma descrição de como fazer isso: link

A solução mais simples para o grep de um arquivo grande para muitos regexps é:

grep -f regexps.txt bigfile

Ou se os regexps forem sequências fixas:

grep -F -f regexps.txt bigfile

Existem 3 fatores limitantes: CPU, RAM e E / S de disco.

A RAM é fácil de medir: se o processo grep ocupa a maior parte da sua memória livre (por exemplo, quando a parte superior é executada), a RAM é um fator limitante.

A CPU também é fácil de medir: se o grep levar 90% da CPU no topo, a CPU é um fator limitante e a paralelização acelerará isso.

É mais difícil ver se a E / S de disco é o fator limitante e, dependendo do sistema de disco, pode ser mais rápido ou mais lento paralelizar. A única maneira de saber com certeza é testar e medir.

Fator limitante: RAM

O grep normal -f regexs.txt bigfile funciona não importa o tamanho do bigfile, mas se o regexps.txt é tão grande que não cabe na memória, então você precisa dividir isso.

grep -F leva cerca de 100 bytes de RAM e o grep leva cerca de 500 bytes de RAM por 1 byte de regexp. Então, se regexps.txt é 1% da sua memória RAM, então pode ser muito grande.

Se você puder converter seus regexps em strings fixas, faça isso. Por exemplo. Se as linhas que você está procurando no bigfile, tudo se parece com:

ID1 foo bar baz Identifier1 quux
fubar ID2 foo bar baz Identifier2

então seu regexps.txt pode ser convertido de:

ID1.*Identifier1
ID2.*Identifier2

para:

ID1 foo bar baz Identifier1
ID2 foo bar baz Identifier2

Desta forma, você pode usar o grep -F, que consome cerca de 80% menos memória e é muito mais rápido.

Se ainda não couber na memória, você pode fazer isso:

parallel --pipepart -a regexps.txt --block 1M grep -F -f - -n bigfile |
sort -un | perl -pe 's/^\d+://'

O 1M deve ser sua memória livre dividida pelo número de núcleos e dividida por 200 para grep -F e por 1000 para grep normal. No GNU / Linux você pode fazer:

free=$(awk '/^((Swap)?Cached|MemFree|Buffers):/ { sum += $2 }
          END { print sum }' /proc/meminfo)
percpu=$((free / 200 / $(parallel --number-of-cores)))k

parallel --pipepart -a regexps.txt --block $percpu --compress grep -F -f - -n bigfile |
sort -un | perl -pe 's/^\d+://'

Se você pode viver com linhas duplicadas e ordem errada, é mais rápido fazer isso:

parallel --pipepart -a regexps.txt --block $percpu --compress grep -F -f - bigfile

Fator limitante: CPU

Se a CPU é o fator limitante, a paralelização deve ser feita no regexps:

cat regexp.txt | parallel --pipe -L1000 --round-robin --compress grep -f - -n bigfile |
sort -un | perl -pe 's/^\d+://'

O comando iniciará um grep por CPU e lerá bigfile uma vez por CPU, mas como isso é feito em paralelo, todas as leituras, exceto a primeira, serão armazenadas em cache na RAM. Dependendo do tamanho de regexp.txt, pode ser mais rápido usar --block 10m ao invés de -L1000.

Alguns sistemas de armazenamento têm melhor desempenho ao ler vários blocos em paralelo. Isso vale para alguns sistemas RAID e para alguns sistemas de arquivos de rede. Para paralelizar a leitura de bigfile:

parallel --pipepart --block 100M -a bigfile -k --compress grep -f regexp.txt

Isso dividirá o bigfile em blocos de 100MB e executará o grep em cada um desses blocos. Para paralelizar a leitura de bigfile e regexp.txt, combine os dois usando --fifo:

parallel --pipepart --block 100M -a bigfile --fifo cat regexp.txt \
\| parallel --pipe -L1000 --round-robin grep -f - {}

Se uma linha corresponder a várias expressões regulares, a linha poderá ser duplicada.

Problema maior

Se o problema for grande demais para ser resolvido, provavelmente você está pronto para o Lucene.

    
por 02.08.2016 / 13:53
2

Aviso importante: Eu testei isso nos dados fornecidos na pergunta. O carregamento de vários gigabytes de dados em um banco de dados SQLite pode levar muito tempo. A consulta usando dois campos de texto pode ser ineficiente. O desempenho do disco pode influenciar, etc. etc.

O script sh a seguir criará o banco de dados SQLlite database.db (esse arquivo será excluído se já existir), crie as tabelas qadr e data e carregue os dados nas duas tabelas ( file1 em data e file2 em qadr ). Em seguida, ele criará um índice em data.adr .

#!/bin/sh

address_file="file2"
data_file="file1"

database="database.db"

rm -f "$database"

sqlite3 "$database" <<END_SQL
CREATE TABLE qadr ( adr TEXT );
CREATE TABLE data ( name TEXT, adr TEXT, tag1 TEXT, tag2 TEXT );
.separator :
.import "$data_file" data
.import "$address_file" qadr
VACUUM;
CREATE UNIQUE INDEX adri ON data(adr);
VACUUM;
END_SQL

A criação do índice pressupõe que os endereços em file1 sejam exclusivos (isto é, que o segundo campo : -delimited seja exclusivo). Se não estiverem, remova UNIQUE da instrução CREATE INDEX (idealmente, eles são exclusivos e, idealmente, as linhas em file2 também são exclusivas).

Eu nunca trabalhei com SQLite e essas quantidades de dados, mas sei que importações de vários gigabytes para o MongoDB e MySQL podem ser dolorosamente lentas, e que a criação de índices também pode ser demorada. Então, o que eu estou basicamente dizendo é que eu estou apenas jogando isso para alguém com muitos dados para testar.

Então, é uma questão de uma consulta simples:

$ sqlite3 database.db 'SELECT data.* FROM data JOIN qadr ON (data.adr = qadr.adr)'
John|[email protected]|johnson123|22hey

ou possivelmente apenas

$ sqlite3 database.db 'SELECT * FROM data NATURAL JOIN qadr'
John|[email protected]|johnson123|22hey

Alguém com mais conhecimento em SQLite certamente dará um comentário construtivo sobre isso.

    
por 01.08.2016 / 22:26
2

Se você precisar evitar uma solução de banco de dados (não sei por que, parece a melhor idéia para mim), você poderia fazer isso classificando os dois arquivos nos endereços de e-mail e usando o comando join , que aproxima um DB faria.

Veja o que eu fiz:

sort -t: +1 file1 -o file1
sort file2 -o file2
join -t: -o 1.1,1.2,1.3,1.4 -1 2 file1 file2

Isso parece fazer a coisa certa com seus dados de amostra. Ele classifica os arquivos no lugar . Se você não quiser isso, altere a opção -o nos nomes de arquivos sort s para temp e use-os na união. Além disso, se você realmente tiver outros que não 4 campos no primeiro arquivo, você deve considerar isso na opção -o para join .

Para mais detalhes, consulte as páginas do manual.

    
por 01.08.2016 / 22:46
1

Algo como isso funcionaria, mas não tenho certeza se é uma boa ideia, dependendo do seu caso de uso (não testado):

while read f2line
do
  f1=$(grep $line file1)

  [[ ! -z $f1 ]] && echo $f1line 
done < file2

Outra solução possível se você quiser mais de um método de uma linha (testado rapidamente abaixo):

grep . file2 | xargs -i^ grep ^ file1

Que resultou:

root@7Z233W1 (/tmp)# cat f1
John:[email protected]:johnson123:22hey
Erik:[email protected]:johnson133:22hey
Robert:[email protected]:johnson123:21hey
Johnnny:[email protected]:johnson123:22hey

root@7Z233W1 (/tmp)# cat f2
[email protected]
[email protected]
[email protected]
[email protected]

root@7Z233W1 (/tmp)# grep . f2 | xargs -i^ grep ^ f1
John:[email protected]:johnson123:22hey
    
por 01.08.2016 / 21:58
0

Aqui está uma versão do script do Kusalananda que usa perl para transformar file1 de : separado em TAB separado antes de alimentá-lo em sqlite3 .

O script perl incorporado verifica se há 5 campos em vez de 4. Se houver, ele anexa o campo 3 ao campo 2 (restaurando o : que foi removido pelo autosplit) e, em seguida, exclui o campo 3 .

#!/bin/sh

address_file="file2"
data_file="file1"

database="database.db"

rm -f "$database"

sqlite3 "$database" <<END_SQL
CREATE TABLE qadr ( adr TEXT );
CREATE TABLE data ( name TEXT, adr TEXT, tag1 TEXT, tag2 TEXT );
.mode line
.import "$address_file" qadr
END_SQL

perl -F: -lane 'if (@F == 5) {
    $F[1] .= ":" . $F[2];  # perl arrays are zero-based
    delete $F[2];
  };
  print join("\t",@F);' $data_file | 
    sqlite3 "$database" -separator $'\t' '.import /dev/stdin data'


sqlite3 "$database" <<END_SQL
VACUUM;
CREATE UNIQUE INDEX adri ON data(adr);
VACUUM;
END_SQL

IMO, o sqlite não é adequado para um banco de dados tão grande. Eu recomendaria usar mysql ou postgresql . Para esse tipo de tarefa, a velocidade bruta de mysql provavelmente a torna uma escolha melhor - é mais rápido para coisas simples como esta, mas o postgresql é muito mais rápido para tarefas mais complexas - na minha experiência, pg é "inteligente rápido" pode alcançar grandes melhorias de velocidade em tarefas complexas trabalhando de forma inteligente em vez de trabalhar duro), o mysql é "mudo rápido" (isto é, trabalha duro, sem muita capacidade de trabalhar com inteligência).

O script acima pode ser facilmente adaptado para trabalhar com os clientes de linha de comando psql ou mysql em vez de sqlite3 , mas modificaria os comandos CREATE TABLE para usar CHARACTER(size) de tamanho fixo em vez de TEXT , em que size é um palpite razoável para o tamanho máximo de cada campo - por exemplo, talvez 255 caracteres para o campo adr e 10-50 caracteres para os outros.

uma possível otimização é escolher cuidadosamente os tamanhos dos campos para que cada registro seja um divisor par do tamanho do bloco da sua unidade (levando em conta a sobrecarga por registro do mysql / postgresql). 512 bytes devem ser bons para todos os tamanhos de bloco comuns. faça os campos do tamanho que precisar e adicione um campo CHARACTER(size) extra, não utilizado, para compensar a diferença. O objetivo de fazer isso é que os registros nunca ultrapassem um limite de bloco, portanto, o mecanismo de db só precisa ler em um bloco de disco para obter todos os dados de um determinado registro (na verdade, ele lerá vários registros em um bloco com tamanhos de bloco mais atuais, mas isso só ajuda o desempenho, não pode prejudicá-lo).

O

link é provavelmente o melhor site para pesquisar ou solicitar informações sobre como otimizar os tamanhos dos registros.

    
por 04.08.2016 / 04:45