Mantenha apenas as linhas que contêm o número exato de delimitadores

9

Eu tenho um arquivo csv enorme com 10 campos separados por vírgulas. Infelizmente, algumas linhas estão malformadas e não contêm exatamente 10 vírgulas (o que causa alguns problemas quando desejo ler o arquivo no R). Como filtrar apenas as linhas que contêm exatamente 10 vírgulas?

Miroslav Sabo
fonte
1
sua pergunta e a pergunta vinculada não são a mesma pergunta. você pergunta como lidar com linhas com não mais ou menos que um certo número de correspondências, enquanto essa pergunta requer apenas uma contagem mínima de correspondências. a realidade é que a pergunta é respondida com mais facilidade - ela não exige a varredura completa de uma linha ou (pelo menos, como sedé o caso aqui) apenas na medida em que mais uma correspondência do que a esperada, embora essa pergunta exija. Você não deveria ter fechado isso.
mikeserv
1
na verdade, olhando mais de perto, o solicitante não quer mais nem menos que correspondências. essa pergunta precisa de um novo título. mas a grepresposta não é uma resposta aceitável para uma dessas perguntas ...
mikeserv

Respostas:

21

Outro POSIX:

awk -F , 'NF == 11' <file

Se a linha tiver 10 vírgulas, haverá 11 campos nessa linha. Então, simplesmente fazemos awkuso ,como delimitador de campo. Se o número de campos for 11, a condição NF == 11é verdadeira e, em awkseguida , executa a ação padrão print $0.

cuonglm
fonte
5
Essa é realmente a primeira coisa que me veio à mente nesta questão. Eu pensei que era um exagero, mas olhando para o código ... com certeza é mais claro. Para o benefício de outros: -Fdefine o separador de campos e NFrefere-se ao número de campos em uma determinada linha. Como nenhum bloco de código {statement}é anexado à condição NF == 11, a ação padrão é imprimir a linha. (@cuonglm, sinta-se livre para incorporar esta explicação, se quiser.)
Wildcard
4
+1: Solução muito elegante e legível que também é muito geral. Eu posso, por exemplo, encontrar todas as linhas malformadas comawk -F , 'NF != 11' <file
Miroslav Sabo
@ Gardenhead: É fácil obtê-lo, como você vê o OP disse em seu comentário. Às vezes respondo pelo meu celular, por isso é difícil adicionar a explicação detalhada.
precisa saber é
1
@ MikeServ: Não, desculpe se eu fiz você confuso, é apenas o meu inglês ruim. Você não pode ter 11 campos com 1-9 vírgulas.
precisa saber é
1
@OlivierDulac: Protege contra o início do arquivo -ou o nome dele -.
cuonglm
8

Usando egrep(ou grep -Eno POSIX):

egrep "^([^,]*,){10}[^,]*$" file.csv

Isso filtra qualquer coisa que não contenha 10 vírgulas: corresponde a linhas completas ( ^no início e $no final), contendo exatamente dez repetições ( {10}) da sequência "qualquer número de caracteres, exceto ',', seguido por um único ','" ( ([^,]*,)), seguido novamente por qualquer número de caracteres, exceto ',' ( [^,]*).

Você também pode usar o -xparâmetro para soltar as âncoras:

grep -xE "([^,]*,){10}[^,]*" file.csv

Isso é menos eficiente que a solução da cuonglmawk ; o último é normalmente seis vezes mais rápido no meu sistema para linhas com cerca de 10 vírgulas. Linhas mais longas causarão grandes desacelerações.

Stephen Kitt
fonte
5

O grepcódigo mais simples que funcionará:

grep -xE '([^,]*,){10}[^,]*'

Explicação:

-xgarante que o padrão deve corresponder à linha inteira , em vez de apenas parte dela. Isso é importante para que você não combine as linhas com mais de 10 vírgulas.

-E significa "regex estendido", o que reduz o escape da barra invertida no seu regex.

Os parênteses são usados ​​para agrupar e, {10}posteriormente, significa que deve haver exatamente dez correspondências em uma linha do padrão entre parênteses.

[^,]é uma classe de caracteres - por exemplo, [c-f]corresponderia a qualquer caractere único a c, a d, an eou an fe [^A-Z]corresponderia a qualquer caractere único que NÃO seja uma letra maiúscula. Portanto, [^,]corresponde a qualquer caractere, exceto uma vírgula.

A *classe após o caractere significa "zero ou mais destes".

Portanto, a parte regex ([^,]*,)significa "Qualquer caractere, exceto uma vírgula, qualquer número de vezes (incluindo zero vezes), seguido por uma vírgula" e {10}especifica 10 deles. Em seguida, [^,]*para corresponder o restante dos caracteres que não são vírgulas até o final da linha.

Curinga
fonte
5
sed -ne's/,//11;t' -e's/,/&/10p' <in >out

Isso primeiro ramifica qualquer linha com 11 ou mais vírgulas e depois imprime o que resta apenas aqueles que correspondem a 10 vírgulas.

Aparentemente, eu já respondi isso antes ... Aqui está um plágio de uma pergunta que procura exatamente 4 ocorrências de algum padrão:

Você pode direcionar [num]a ocorrência de um padrão com um s///comando sed ubstitution apenas adicionando o [num]ao comando. Quando você deseja tuma substituição bem-sucedida e não especifica um :rótulo de destino , o test se ramifica fora do script. Isso significa que tudo o que você precisa fazer é testar s///5uma vírgula ou mais e depois imprimir o que resta.

Ou, pelo menos, que lida com as linhas que excedem o máximo de 4. Aparentemente, você também tem um requisito mínimo. Felizmente, isso é tão simples:

sed -ne 's|,||5;t' -e 's||,|4p'

... apenas substitua a 4ª ocorrência de ,uma linha por si mesma e prenda sua pdica nas s///bandeiras da ubstituição. Como qualquer linha correspondente a ,5 ou mais vezes já foi removida, as linhas que contêm 4 ,correspondências contêm apenas 4.

mikeserv
fonte
1
@ cuonglm - é o que eu realmente tinha, a princípio, mas as pessoas sempre me dizem que eu deveria escrever um código mais legível. desde que eu posso ler as coisas que os outros contestam como ilegíveis, não tenho certeza do que guardar e do que largar ...? então eu coloquei a segunda vírgula.
precisa saber é o seguinte
@cuonglm - você pode zombar de mim - isso não vai prejudicar meus sentimentos. Eu posso pegar uma piada. se você estava zombando de mim, era um pouco engraçado. tudo bem - eu não tinha certeza e queria saber. na minha opinião, as pessoas devem ser capazes de rir de si mesmas. de qualquer maneira, eu ainda não entendi!
mikeserv
Haha, certo, é um pensamento muito positivo. Enfim, é muito engraçado conversar com você e, às vezes, você estressa meu cérebro.
cuonglm
É interessante que nesta resposta , se eu substituir s/hello/world/2por s//world/2, o GNU sed funcione bem. Com dois sedda herança, /usr/5bin/posix/sedlevante segfault, /usr/5bin/sedentra em loop infinitivo.
cuonglm
@mikeserv, em referência à nossa discussão anterior sobre sedeawk (nos comentários) - eu gosto desta resposta e a votei positivamente, mas observe que a tradução da awkresposta aceita é: "Imprima linhas com 11 campos" e a tradução desta sedresposta é: " Tente remover a 11ª vírgula; pule para a próxima linha se você falhar. Tente substituir a 10ª vírgula por si mesma; imprima a linha se tiver êxito. " A awkresposta fornece as instruções para o computador da maneira que você as expressaria em inglês. ( awké bom para dados baseados em campo).
Curinga
4

Jogando um pouco curto python:

#!/usr/bin/env python2
with open('file.csv') as f:
    print '\n'.join(line for line in f if line.count(',') == 10)

Isso lerá cada linha e verificará se o número de vírgulas na linha é igual a 10 line.count(',') == 10; caso contrário, imprima a linha.

heemail
fonte
2

E aqui está uma maneira Perl:

perl -F, -ane 'print if $#F==10'

As -ncausas perlpara ler seu arquivo de entrada linha por linha e executar o script fornecido -eem cada linha. As -acurvas na divisão automática: cada linha de entrada será dividida no valor dado por -F(aqui, uma vírgula) e guardado como a matriz @F.

O $#F(ou, de maneira mais geral $#array), é o índice mais alto da matriz @F. Como as matrizes começam em 0, uma linha com 11 campos terá um @Fde 10. O script, portanto, imprime a linha se tiver exatamente 11 campos.

terdon
fonte
Você também pode fazer print if @F==11como uma matriz em um contexto escalar retorna o número de elementos.
Sobrique
1

Se os campos puderem conter vírgulas ou novas linhas, seu código precisará entender csv. Exemplo (com três colunas):

$ cat filter.csv
a,b,c
d,"e,f",g
1,2,3,4
one,two,"three
...continued"

$ cat filter.csv | python3 -c 'import sys, csv
> csv.writer(sys.stdout).writerows(
> row for row in csv.reader(sys.stdin) if len(row) == 3)
> '
a,b,c
d,"e,f",g
one,two,"three
...continued"

Suponho que a maioria das soluções até agora descartaria a segunda e a quarta linha.

Peter Otten
fonte