Selecionar linhas do arquivo de texto que tenham os IDs listados em outro arquivo


13

Eu uso muita classificação grep awk no meu shell unix para trabalhar com arquivos de texto de coluna separados por tabulação de tamanho médio (em torno de 10M a 100M linhas). A esse respeito, o shell unix é minha planilha.

Mas eu tenho um grande problema, que é selecionar registros com uma lista de IDs.

Tendo table.csvarquivo com formato id\tfoo\tbar...e ids.csvarquivo com lista de IDs, selecione apenas os registros table.csvcom ID presente em ids.csv.

tipo /programming/13732295/extract-all-lines-from-text-file-based-on-a-given-list-of-ids mas com shell, não perl.

grep -Fobviamente produz falsos positivos se os IDs tiverem largura variável. joiné um utilitário que eu nunca consegui descobrir. Antes de tudo, requer classificação alfabética (meus arquivos geralmente são numerados), mas mesmo assim não consigo fazê-lo funcionar sem reclamar de ordem incorreta e pular alguns registros. Então eu não gosto disso. grep -f no arquivo com ^id\t-s é muito lento quando o número de IDs é grande. awké complicado.

Existem boas soluções para isso? Alguma ferramenta específica para arquivos separados por tabulação? Funcionalidades extras também serão bem-vindas.

UPD: corrigido sort->join


Se grep -ffor muito lento, manter essa estratégia parecerá mais problemático do que vale a pena - as variações provavelmente serão afetadas pelos mesmos problemas de desempenho O (N * M). Talvez o seu tempo seria melhor gasto aprendendo a usar um normalizada SQL DB ...
Goldilocks

1
Por que não usar o script Perl da pergunta que você vinculou? Como alternativa, deve ser possível escrever um script semelhante no awk.
CJM

O Bash 4 possui matrizes associativas, que é o que você precisa para contornar os loops aninhados no exemplo perl.
23814 goldilocks

1
sortpode fazer todos os tipos de classificação, numéricos, alfabéticos e outros. Veja man sort.
terdon

Eu tenho uma consulta aqui, como é que vamos fazer o mesmo se o arquivo de origem de onde se deseja extrair os dados é um arquivo não-delimitado

Respostas:


19

Eu acho que você quis dizer grep -fnão, grep -Fmas você realmente precisa de uma combinação de ambos e -w:

grep -Fwf ids.csv table.csv

A razão pela qual você estava obtendo falsos positivos é (acho que você não explicou) porque se um ID pode estar contido em outro, ambos serão impressos. -wremove esse problema e -Fgarante que seus padrões sejam tratados como seqüências de caracteres, não como expressões regulares. De man grep:

   -F, --fixed-strings
          Interpret PATTERN as a  list  of  fixed  strings,  separated  by
          newlines,  any  of  which is to be matched.  (-F is specified by
          POSIX.)
   -w, --word-regexp
          Select  only  those  lines  containing  matches  that form whole
          words.  The test is that the matching substring must  either  be
          at  the  beginning  of  the  line,  or  preceded  by  a non-word
          constituent character.  Similarly, it must be either at the  end
          of  the  line  or  followed by a non-word constituent character.
          Word-constituent  characters  are  letters,  digits,   and   the
          underscore.

   -f FILE, --file=FILE
          Obtain  patterns  from  FILE,  one  per  line.   The  empty file
          contains zero patterns, and therefore matches nothing.   (-f  is
          specified by POSIX.)

Se seus falsos positivos forem porque um ID pode estar presente em um campo sem ID, faça um loop no seu arquivo:

while read pat; do grep -w "^$pat" table.csv; done < ids.csv

ou, mais rápido:

xargs -I {} grep "^{}" table.csv < ids.csv

Pessoalmente, eu faria isso perl:

perl -lane 'BEGIN{open(A,"ids.csv"); while(<A>){chomp; $k{$_}++}} 
            print $_ if defined($k{$F[0]}); ' table.csv

1
+1 Mas: e se houver possíveis falsos positivos que correspondam ao ID exatamente por palavras, mas não na coluna? Se você não pode usar ^com -F, não pode segmentar especificamente a primeira coluna.
23814 goldilocks

@goldilocks, se corresponderem exatamente, não serão falsos positivos. Entendi o que você quer dizer, mas nesse caso, o OP deve mostrar seus arquivos de entrada.
terdon

O ^id\tbit do OP implica que idpode ocorrer em outra coluna. Caso contrário, isso não importa.
23814 goldilocks

@goldilocks fair point, resposta editada.
terdon

A maneira como costumávamos fazer isso era criar arquivos temporários (usando awk ou sed) que adicionavam um caractere único (digamos, control-A) delimitando o campo que queríamos procurar e, em seguida, use grep -F -f temppatternfile temptargetfile | Tr-d '\ 001'
Mark Plotnick 23/01

7

O joinutilitário é o que você deseja. Requer que os arquivos de entrada sejam classificados lexicamente.

Supondo que seu shell seja bash ou ksh:

join -t $'\t' <(sort ids.csv) <(sort table.csv)

Sem precisar classificar, a solução awk usual é

awk -F '\t' 'NR==FNR {id[$1]; next} $1 in id' ids.csv table.csv

Como eu tentei, mas finalmente não consegui transmitir, a junção é um clamor. Não funciona tão bem para mim.
alamar

1
joinnão é um engano: suas palavras foram que você não conseguiu entender. Abra sua mente e aprenda. Que resultado você obteve e como isso difere do que você espera?
Glenn Jackman

+1, este é um trabalho para join.
Don_crissti

A awksolução aqui é muito rápida e eficiente para meus propósitos (estou extraindo subconjuntos de algumas centenas de arquivos com ~ 100 milhões de linhas)
Luke

2

As respostas para essa pergunta SO me ajudaram a contornar os problemas com a junção. Essencialmente, quando você classifica o arquivo em preparação para enviá-lo para a associação, precisa se certificar de que está classificando com base na coluna na qual está ingressando. Portanto, se esse é o primeiro, você precisa dizer qual é o caractere separador no arquivo e que deseja classificá-lo no primeiro campo (e somente no primeiro campo). Caso contrário, se o primeiro campo tiver larguras variáveis ​​(por exemplo), seus separadores e possivelmente outros campos poderão começar a afetar a ordem de classificação.

Portanto, use a opção -t de classificação para especificar seu caractere de separação, e use a opção -k para especificar o campo (lembrando que você precisa de um campo de início e fim - mesmo que seja o mesmo) - ou ele classificará esse caractere até o final da linha).

Portanto, para um arquivo separado por tabulação, como nesta pergunta, o seguinte deve funcionar (com agradecimentos à resposta de glenn para a estrutura):

join -t$'\t' <(sort -d ids.csv) <(sort -d -t$'\t' -k1,1 table.csv) > output.csv

(Para referência, o sinalizador -d significa classificação do dicionário. Você também pode usar o sinalizador -b para ignorar os espaços em branco à esquerda, consulte man sorte man join).

Como um exemplo mais geral, suponha que você esteja juntando dois arquivos separados por vírgula - input1.csv na terceira coluna e input2.csvna quarta. Você poderia usar

join -t, -1 3 -2 4 <(sort -d -t, -k3,3 input2.csv) <(sort -d -t, -k4,4 input2.csv) > output.csv

Aqui, as opções -1e -2especificam em quais campos juntar no primeiro e no segundo arquivos de entrada, respectivamente.


0

Você também pode usar o ruby ​​para fazer algo semelhante:

ruby -pe 'File.open("id.csv").each { |i| puts i if i =~ /\$\_/ }' table.csv
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.