Excluindo linhas de um arquivo que está em outro arquivo


126

Eu tenho um arquivo f1:

line1
line2
line3
line4
..
..

Eu quero excluir todas as linhas que estão em outro arquivo f2:

line2
line8
..
..

Eu tentei algo com cate sed, que não era nem perto do que eu pretendia. Como posso fazer isso?



Se você deseja remover linhas de um arquivo que "contém até" cadeias de outro arquivo (por exemplo, correspondências parciais), consulte unix.stackexchange.com/questions/145079/…
rogerdpack

Respostas:


154

grep -v -x -f f2 f1 deve fazer o truque.

Explicação:

  • -v para selecionar linhas não correspondentes
  • -x para corresponder apenas a linhas inteiras
  • -f f2 para obter padrões de f2

Em vez disso, pode-se usar grep -Fou fgrepcombinar seqüências de caracteres fixas em f2vez de padrões (no caso de você desejar remover as linhas da maneira "o que você vê se obtém", em vez de tratar as linhas f2como padrões regex).


22
Isso tem complexidade O (n²) e começará a demorar horas para ser concluído, uma vez que os arquivos contenham mais do que algumas linhas K.
Arnaud Le Blanc

11
Descobrir quais algoritmos sugeridos pelo SO têm complexidade O (n ^ 2) têm complexidade O (n), mas ainda podem levar horas para competir.
HDave

2
Eu apenas tentei isso em 2 arquivos de ~ 2k linhas cada, e foi morto pelo sistema operacional (concedido, essa é uma VM não tão poderosa, mas ainda assim).
Trebor rude

1
Eu amo a elegância disso; Prefiro a velocidade da resposta de Jona Christopher Sahnwal.
Alex Hall

1
@ arnaud576875: Você tem certeza? Depende da implementação de grep. Se ele se processar f2adequadamente antes de iniciar a pesquisa, a pesquisa levará apenas O (n) tempo.
HelloGoodbye

56

Tente comm em vez disso (assumindo que f1 e f2 "já estão classificados")

comm -2 -3 f1 f2

5
Eu não tenho certeza commé a solução tem a questão não indica que as linhas f1são classificadas que é um pré-requisito para o usocomm
gabuzo

1
Isso funcionou para mim, pois meus arquivos foram classificados e tinham mais de 250.000 linhas em um deles, apenas 28.000 no outro. Obrigado!
Inverno

1
Quando isso funciona (os arquivos de entrada são classificados), isso é extremamente rápido!
Mike Jarvis

Como na solução de arnaud576875, para mim, usando o cygwin, isso eliminou linhas duplicadas no segundo arquivo que podem querer ser mantidas.
Alex Hall

8
Você pode usar substituição processo para classificar os arquivos primeiro, é claro:comm -2 -3 <(sort f1) <(sort f2)
davemyron

14

Para excluir arquivos que não são muito grandes, você pode usar as matrizes associativas do AWK.

awk 'NR == FNR { list[tolower($0)]=1; next } { if (! list[tolower($0)]) print }' exclude-these.txt from-this.txt 

A saída será na mesma ordem que o arquivo "from-this.txt". A tolower()função faz distinção entre maiúsculas e minúsculas, se você precisar.

A complexidade algorítmica provavelmente será O (n) (tamanho excluir-estes.txt) + O (n) (tamanho-deste.txt)


Por que você diz arquivos que não são muito grandes? O medo aqui é (presumo) awk executando o sistema a partir da memória do sistema para criar o hash, ou há alguma outra limitação?
Rogerdpack #

para os seguidores, há ainda outra opção mais agressiva para "sanear" as linhas (uma vez que a comparação tem que ser exato para usar a matriz associativa), ex unix.stackexchange.com/a/145132/8337
rogerdpack

@rogerdpack: um arquivo de exclusão grande exigirá uma grande variedade de hash (e um longo tempo de processamento). Um "from-this.txt" grande exigirá apenas um longo tempo de processamento.
Pausado até novo aviso.

1
Isso falha (ou seja, não produz nenhuma saída) se exclude-these.txtestiver vazio. A resposta de @ jona-christopher-sahnwaldt abaixo funciona nesse caso. Você também pode especificar arquivos múltiplos por exemploawk '{if (f==1) { r[$0] } else if (! ($0 in r)) { print $0 } } ' f=1 done.out failed.out f=2 all-files.out
Graham Russell

11

Semelhante à resposta de Dennis Williamson (principalmente alterações sintáticas, por exemplo, definir explicitamente o número do arquivo em vez do NR == FNRtruque):

awk '{if (f==1) { r[$0] } else if (! ($0 in r)) { print $0 } } ' f=1 exclude-these.txt f=2 from-this.txt

O acesso r[$0]cria a entrada para essa linha, sem a necessidade de definir um valor.

Supondo que o awk use uma tabela de hash com pesquisa constante e (em média) tempo de atualização constante, a complexidade do tempo será O (n + m), onde n e m são os comprimentos dos arquivos. No meu caso, n era ~ 25 milhões e m ~ 14000. A solução awk era muito mais rápida que a classificação, e eu também preferia manter a ordem original.


Como isso difere da resposta de Dennis Williamson? A única diferença é que ele não faz uma atribuição no hash, tão ligeiramente mais rápido que isso? Complexidade algorítmica é a mesma que a dele?
Rogerdpack #

A diferença é principalmente sintática. Acho a variável fmais clara que NR == FNR, mas isso é uma questão de gosto. A atribuição no hash deve ser tão rápida que não há diferença de velocidade mensurável entre as duas versões. Eu acho que estava errado sobre a complexidade - se a pesquisa é constante, a atualização também deve ser constante (em média). Não sei por que pensei que a atualização seria logarítmica. Vou editar minha resposta.
Jcsahnwaldt Reinstate Monica

Eu tentei várias dessas respostas, e essa foi rápida no AMAZEBALLS. Eu tinha arquivos com centenas de milhares de linhas. Trabalhou como um encanto!
Mr. T

1
Esta é a minha solução preferida. Ele funciona com vários arquivos e também arquivos de exclusão vazios, por exemplo awk '{if (f==1) { r[$0] } else if (! ($0 in r)) { print $0 } } ' f=1 empty.file done.out failed.out f=2 all-files.out. Considerando que a outra awksolução falha com o arquivo de exclusão vazio e pode levar apenas um.
Graham Russell

5

se você tem Ruby (1.9+)

#!/usr/bin/env ruby 
b=File.read("file2").split
open("file1").each do |x|
  x.chomp!
  puts x if !b.include?(x)
end

O qual possui complexidade O (N ^ 2). Se você quer se preocupar com desempenho, aqui está outra versão

b=File.read("file2").split
a=File.read("file1").split
(a-b).each {|x| puts x}

que usa um hash para efetuar a subtração, assim como a complexidade O (n) (tamanho de a) + O (n) (tamanho de b)

aqui está uma pequena referência, cortesia do user576875, mas com 100 mil linhas, acima:

$ for i in $(seq 1 100000); do echo "$i"; done|sort --random-sort > file1
$ for i in $(seq 1 2 100000); do echo "$i"; done|sort --random-sort > file2
$ time ruby test.rb > ruby.test

real    0m0.639s
user    0m0.554s
sys     0m0.021s

$time sort file1 file2|uniq -u  > sort.test

real    0m2.311s
user    0m1.959s
sys     0m0.040s

$ diff <(sort -n ruby.test) <(sort -n sort.test)
$

diff foi usado para mostrar que não há diferenças entre os 2 arquivos gerados.


1
Isso tem complexidade O (n²) e começará a demorar horas para ser concluído, uma vez que os arquivos contenham mais do que algumas linhas K.
Arnaud Le Blanc

Eu realmente não me importo neste momento, porque ele não mencionou nenhum arquivo grande.
kurumi

3
Não há necessidade de ser tão defensivo, não é como se @ user576875 diminuísse sua resposta ou algo assim. :-)
John Parker

muito bom segunda versão, vitórias rubi :)
Arnaud Le Blanc

4

Algumas comparações de tempo entre várias outras respostas:

$ for n in {1..10000}; do echo $RANDOM; done > f1
$ for n in {1..10000}; do echo $RANDOM; done > f2
$ time comm -23 <(sort f1) <(sort f2) > /dev/null

real    0m0.019s
user    0m0.023s
sys     0m0.012s
$ time ruby -e 'puts File.readlines("f1") - File.readlines("f2")' > /dev/null

real    0m0.026s
user    0m0.018s
sys     0m0.007s
$ time grep -xvf f2 f1 > /dev/null

real    0m43.197s
user    0m43.155s
sys     0m0.040s

sort f1 f2 | uniq -u nem sequer é uma diferença simétrica, porque remove linhas que aparecem várias vezes em qualquer arquivo.

comm também pode ser usado com stdin e aqui strings:

echo $'a\nb' | comm -23 <(sort) <(sort <<< $'c\nb') # a

2

Parece ser um trabalho adequado para o shell SQLite:

create table file1(line text);
create index if1 on file1(line ASC);
create table file2(line text);
create index if2 on file2(line ASC);
-- comment: if you have | in your files then specify  .separator ××any_improbable_string×× 
.import 'file1.txt' file1
.import 'file2.txt' file2
.output result.txt
select * from file2 where line not in (select line from file1);
.q

1

Você tentou isso com sed?

sed 's#^#sed -i '"'"'s%#g' f2 > f2.sh

sed -i 's#$#%%g'"'"' f1#g' f2.sh

sed -i '1i#!/bin/bash' f2.sh

sh f2.sh

0

Não é uma resposta de 'programação', mas aqui está uma solução rápida e suja: basta ir para http://www.listdiff.com/compare-2-lists-difference-tool .

Obviamente não funcionará para arquivos enormes, mas fez o truque para mim. Algumas notas:

  • Não sou afiliado ao site de forma alguma (se você ainda não acredita em mim, basta procurar uma ferramenta on-line diferente; usei o termo de pesquisa "definir lista de diferenças on-line")
  • O site vinculado parece fazer chamadas de rede em todas as comparações de lista, portanto, não alimente dados confidenciais
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.