Conserver uniquement les lignes contenant le nombre exact de délimiteurs

9

J'ai un énorme fichier csv avec 10 champs séparés par des virgules. Malheureusement, certaines lignes sont malformées et ne contiennent pas exactement 10 virgules (ce qui cause des problèmes lorsque je veux lire le fichier dans R). Comment filtrer uniquement les lignes qui contiennent exactement 10 virgules?

Miroslav Sabo
la source
1
votre question et la question liée ne sont pas la même question. vous demandez comment gérer les lignes ne comportant pas plus ou moins d'un certain nombre de correspondances, alors que cette question ne nécessite qu'un nombre minimal de correspondances. la réalité est que la question est plus facile à répondre - cela ne nécessite pas de balayer une ligne en entier, ou (au moins, comme c'est le sedcas ici) seulement jusqu'à une correspondance de plus que ce qui est recherché, bien que cette question le fasse. Vous ne devriez pas avoir fermé cela.
mikeserv
1
en fait, en y regardant de plus près, le demandeur ne veut ni plus ni moins que des correspondances. cette question a besoin d'un nouveau titre. mais la grepréponse n'est pas une réponse acceptable pour l'une ou l'autre question ...
mikeserv

Réponses:

21

Un autre POSIX:

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

Si la ligne a 10 virgules, alors il y aura 11 champs dans cette ligne. Nous faisons donc tout simplement awkutiliser ,comme séparateur de champ. Si le nombre de champs est de 11, la condition NF == 11est vraie, awkpuis effectue l'action par défaut print $0.

cuonglm
la source
5
C'est en fait la première chose qui m'est venue à l'esprit sur cette question. Je pensais que c'était exagéré, mais en regardant le code ... c'est certainement plus clair. Pour le bénéfice des autres: -Fdéfinit le séparateur de champs et NFfait référence au nombre de champs dans une ligne donnée. Puisqu'aucun bloc de code {statement}n'est ajouté à la condition NF == 11, l'action par défaut consiste à imprimer la ligne. (@cuonglm, n'hésitez pas à incorporer cette explication si vous le souhaitez.)
Wildcard
4
+1: Solution très élégante et lisible mais également très générale. Je peux par exemple trouver toutes les lignes mal formées avecawk -F , 'NF != 11' <file
Miroslav Sabo
@gardenhead: Il est facile de l'obtenir, comme vous le voyez dans le commentaire de l'OP. Je réponds parfois depuis mon mobile, il est donc difficile d'ajouter l'explication des détails.
cuonglm
1
@mikeserv: Non, désolé si je vous ai rendu confus, c'est juste mon mauvais anglais. Vous ne pouvez pas avoir 11 champs avec 1-9 virgules.
cuonglm
1
@OlivierDulac: Il vous protège contre le démarrage du fichier par -ou nommé -.
cuonglm
8

En utilisant egrep(ou grep -Een POSIX):

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

Cela filtre tout ce qui ne contient pas 10 virgules: il correspond aux lignes complètes ( ^au début et $à la fin), contenant exactement dix répétitions ( {10}) de la séquence "n'importe quel nombre de caractères sauf", ", suivi d'un seul", "" ( ([^,]*,)), suivi à nouveau par n'importe quel nombre de caractères à l'exception de ',' ( [^,]*).

Vous pouvez également utiliser le -xparamètre pour supprimer les ancres:

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

Ceci est moins efficace que cuonglm de awksolution si; ce dernier est généralement six fois plus rapide sur mon système pour les lignes avec environ 10 virgules. Des lignes plus longues entraîneront d'énormes ralentissements.

Stephen Kitt
la source
5

Le grepcode le plus simple qui fonctionnera:

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

Explication:

-xgarantit que le motif doit correspondre à la ligne entière , plutôt qu'à une partie seulement. Ceci est important pour ne pas faire correspondre les lignes avec plus de 10 virgules.

-E signifie "regex étendu", ce qui permet de réduire l'échappement anti-slash dans votre regex.

Les parenthèses sont utilisées pour le regroupement, ce qui {10}signifie par la suite qu'il doit y avoir exactement dix correspondances dans une rangée du motif dans les parenthèses.

[^,]est une classe de caractères — par exemple, [c-f]correspondrait à n'importe quel caractère unique qui est a c, a d, an eou an f, et [^A-Z]correspondrait à tout caractère unique qui n'est PAS une lettre majuscule. Correspond donc à [^,]n'importe quel caractère, à l'exception d'une virgule.

L' *après la classe de caractères signifie «zéro ou plusieurs d'entre eux».

Ainsi, la partie regex ([^,]*,)signifie "N'importe quel caractère sauf une virgule un certain nombre de fois (y compris zéro fois), suivi d'une virgule" et le {10}spécifie 10 d'entre eux. Ensuite, [^,]*pour faire correspondre le reste des caractères non virgule à la fin de la ligne.

Caractère générique
la source
5
sed -ne's/,//11;t' -e's/,/&/10p' <in >out

Cela ramifie d'abord toute ligne avec 11 virgules ou plus, puis imprime ce qui reste uniquement ceux qui correspondent à 10 virgules.

Apparemment, j'ai répondu à cela avant ... Voici un plagiat à partir d'une question recherchant exactement 4 occurrences d'un modèle:

Vous pouvez cibler l' [num]occurrence d'un motif avec une s///commande sed ubstitution en ajoutant simplement le [num]à la commande. Lorsque vous trecherchez une substitution réussie et que vous ne spécifiez pas d' :étiquette cible , l' test se dérive du script. Cela signifie que tout ce que vous avez à faire est de tester des s///5virgules ou plus, puis d'imprimer ce qui reste.

Ou, au moins, qui gère les lignes qui dépassent votre maximum de 4. Apparemment, vous avez également une exigence minimale. Heureusement, c'est tout aussi simple:

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

... il suffit de remplacer la 4ème occurrence de ,sur une ligne avec elle-même et de coller votre print sur les s///drapeaux d'ubstitution. Étant donné que toutes les lignes correspondant à ,5 fois ou plus ont déjà été élaguées, les lignes contenant 4 ,correspondances n'en contiennent que 4.

mikeserv
la source
1
@cuonglm - c'est ce que j'avais en fait au début, mais les gens me disent toujours que je devrais écrire du code plus lisible. puisque je peux lire les choses que les autres contestent comme illisibles, je ne sais pas quoi garder et quoi laisser tomber ...? j'ai donc mis la deuxième virgule.
mikeserv
@cuonglm - vous pouvez vous moquer de moi - ça ne fera pas mal à mes sentiments. je peux prendre une blague. si vous vous moquiez de moi, c'était un peu drôle. c'est ok - je n'étais tout simplement pas sûr et je voulais savoir. à mon avis, les gens devraient pouvoir se moquer d'eux-mêmes. de toute façon, je ne comprends toujours pas!
mikeserv
Haha, c'est une pensée très positive. Quoi qu'il en soit, c'est très drôle de discuter avec vous et parfois, vous stressez mon cerveau.
cuonglm
Il est intéressant de noter que dans cette réponse , si je remplace s/hello/world/2par s//world/2, GNU sed fonctionne très bien. Avec deux sedde l'héritage, /usr/5bin/posix/sedsoulevez segfault, /usr/5bin/sedpasse en boucle infinitive.
cuonglm
@mikeserv, en référence à notre discussion précédente sur sedetawk (dans les commentaires) —J'aime cette réponse et j'ai voté pour elle, mais notez que la traduction de la awkréponse acceptée est: "Imprimer les lignes avec 11 champs" et la traduction de cette sedréponse est: " Essayez de supprimer la 11e virgule; passez à la ligne suivante si vous échouez. Essayez de remplacer la 10e virgule par elle-même; imprimez la ligne si vous réussissez. " La awkréponse donne les instructions à l'ordinateur exactement comme vous les exprimeriez en anglais. ( awkconvient aux données basées sur le terrain.)
Wildcard
4

Jeter un court python:

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

Cela lira chaque ligne et vérifiera si le nombre de virgules dans la ligne est égal à 10 line.count(',') == 10, si c'est le cas, imprimez la ligne.

heemayl
la source
2

Et voici une façon Perl:

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

La -ncause perllit son fichier d'entrée ligne par ligne et exécute le script donné par -esur chaque ligne. L' -aactivation du fractionnement automatique est activée: chaque ligne d'entrée sera divisée selon la valeur donnée par -F(ici, une virgule) et enregistrée en tant que tableau @F.

Le $#F(ou, plus généralement $#array), est l'indice le plus élevé du tableau @F. Puisque les tableaux commencent à 0, une ligne avec 11 champs aura un @Fde 10. Le script imprime donc la ligne s'il contient exactement 11 champs.

terdon
la source
Vous pouvez également faire print if @F==11comme un tableau dans un contexte scalaire renvoie le nombre d'éléments.
Sobrique
1

Si les champs peuvent contenir des virgules ou des sauts de ligne, votre code doit comprendre csv. Exemple (avec trois colonnes):

$ 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"

Je suppose que la plupart des solutions jusqu'à présent élimineraient les deuxième et quatrième rangées.

Peter Otten
la source