Existe-t-il un moyen de «uniq» par colonne?


195

J'ai un fichier .csv comme celui-ci:

stack2@example.com,2009-11-27 01:05:47.893000000,example.net,127.0.0.1
overflow@example.com,2009-11-27 00:58:29.793000000,example.net,255.255.255.0
overflow@example.com,2009-11-27 00:58:29.646465785,example.net,256.255.255.0
...

Je dois supprimer les e-mails en double (toute la ligne) du fichier (c'est-à-dire l'une des lignes contenant overflow@example.comdans l'exemple ci-dessus). Comment utiliser uniquniquement le champ 1 (séparé par des virgules)? Selon man, uniqn'a pas d'options pour les colonnes.

J'ai essayé quelque chose avec sort | uniqmais ça ne marche pas.

Réponses:


325
sort -u -t, -k1,1 file
  • -u pour unique
  • -t, donc la virgule est le délimiteur
  • -k1,1 pour le champ clé 1

Résultat du test:

overflow@domain2.com,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0 
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1 

3
cela ne fonctionne pas si la colonne contient la virgule elle-même (avec guillemet)
user775187

13
pourquoi avez-vous besoin du, 1 en -k1,1? pourquoi pas juste -k1?
hello_there_andy

18
@hello_there_andy: Ceci est expliqué dans le manuel ( man sort). Il représente la position de départ et d'arrêt.
Serrano

3
@CarlSmotricz: Je l'ai testé et il a confirmé ce que sortdit la page de manuel: " -u, --unique avec -c, vérifiez un ordre strict; sans -c, sortez uniquement le premier d'une exécution égale ." Il s'agit donc bien "de la première occurrence du doublon avant tri".
Geremia

2
cela change également l'ordre des lignes, n'est-ce pas?
rkachach

102
awk -F"," '!_[$1]++' file
  • -F définit le séparateur de champ.
  • $1 est le premier champ.
  • _[val]recherche valdans le hachage _(une variable régulière).
  • ++ incrémenter et retourner l'ancienne valeur.
  • ! renvoie non logique.
  • il y a une impression implicite à la fin.

4
Cette approche est deux fois plus rapide que le tri
bitek

9
Cela a également l'avantage supplémentaire de conserver les lignes dans l'ordre d'origine!
AffluentOwl

8
Si vous avez besoin du dernier uniq au lieu du premier, alors ce script awk vous aidera:awk -F',' '{ x[$1]=$0 } END { for (i in x) print x[i] }' file
Sukima

3
@eshwar ajoute simplement plus de champs à l'index du dictionnaire! Par exemple, !_[$1][$2]++peut être utilisé pour trier par les deux premiers champs. awkCependant, mon -fu n'est pas assez fort pour être unique sur une gamme de domaines. :(
Soham Chowdhury

1
Brillant! cette option est meilleure que la réponse car elle maintient l'ordre des lignes
rkachach

16

Pour considérer plusieurs colonnes.

Trier et donner une liste unique basée sur la colonne 1 et la colonne 3:

sort -u -t : -k 1,1 -k 3,3 test.txt
  • -t : deux points est séparateur
  • -k 1,1 -k 3,3 basé sur la colonne 1 et la colonne 3

8

ou si vous voulez utiliser uniq:

<mycvs.cvs tr -s ',' ' ' | awk '{print $3" "$2" "$1}' | uniq -c -f2

donne:

1 01:05:47.893000000 2009-11-27 tack2@domain.com
2 00:58:29.793000000 2009-11-27 overflow@domain2.com
1

5
Je voudrais signaler une simplification possible: vous pouvez vider le cat! Plutôt que de canaliser dans tr, laissez simplement tr lire le fichier en utilisant <. Le piping catest une complication inutile courante utilisée par les novices. Pour de grandes quantités de données, il doit y avoir un effet sur les performances.
Carl Smotricz

4
Bon à savoir. THX! (Bien sûr, cela a du sens, en pensant à "chat" et "paresse";))
Carsten C.

L'inversion des champs peut être simplifiée avec rev.
Hielke Walinga

5

Si vous souhaitez conserver le dernier des doublons que vous pourriez utiliser

 tac a.csv | sort -u -t, -r -k1,1 |tac

Quelle était mon exigence

ici

tac inversera le fichier ligne par ligne


1

Voici une façon très astucieuse.

Formatez d'abord le contenu de telle sorte que la colonne à comparer pour son caractère unique soit une largeur fixe. Une façon de procéder consiste à utiliser awk printf avec un spécificateur de largeur de champ / colonne ("% 15s").

Maintenant, les options -f et -w de uniq peuvent être utilisées pour ignorer les champs / colonnes précédents et pour spécifier la largeur de comparaison (largeur de colonne (s)).

Voici trois exemples.

Dans le premier exemple ...

1) Faites temporairement de la colonne d'intérêt une largeur fixe supérieure ou égale à la largeur maximale du champ.

2) Utilisez l'option -f uniq pour ignorer les colonnes précédentes et utilisez l'option -w uniq pour limiter la largeur à tmp_fixed_width.

3) Supprimer les espaces de fin de la colonne pour "restaurer" sa largeur (en supposant qu'il n'y avait pas d'espaces de fin au préalable).

printf "%s" "$str" \
| awk '{ tmp_fixed_width=15; uniq_col=8; w=tmp_fixed_width-length($uniq_col); for (i=0;i<w;i++) { $uniq_col=$uniq_col" "}; printf "%s\n", $0 }' \
| uniq -f 7 -w 15 \
| awk '{ uniq_col=8; gsub(/ */, "", $uniq_col); printf "%s\n", $0 }'

Dans le deuxième exemple ...

Créer une nouvelle colonne uniq 1. Retirez-la ensuite après l'application du filtre uniq.

printf "%s" "$str" \
| awk '{ uniq_col_1=4; printf "%15s %s\n", uniq_col_1, $0 }' \
| uniq -f 0 -w 15 \
| awk '{ $1=""; gsub(/^ */, "", $0); printf "%s\n", $0 }'

Le troisième exemple est le même que le second, mais pour plusieurs colonnes.

printf "%s" "$str" \
| awk '{ uniq_col_1=4; uniq_col_2=8; printf "%5s %15s %s\n", uniq_col_1, uniq_col_2, $0 }' \
| uniq -f 0 -w 5 \
| uniq -f 1 -w 15 \
| awk '{ $1=$2=""; gsub(/^ */, "", $0); printf "%s\n", $0 }'

-3

eh bien, plus simple que d'isoler la colonne avec awk, si vous devez supprimer tout avec une certaine valeur pour un fichier donné, pourquoi ne pas simplement faire grep -v:

par exemple pour tout supprimer avec la valeur "col2" dans la deuxième ligne: col1, col2, col3, col4

grep -v ',col2,' file > file_minus_offending_lines

Si cela ne suffit pas, car certaines lignes peuvent être supprimées de manière incorrecte en affichant éventuellement la valeur correspondante dans une autre colonne, vous pouvez faire quelque chose comme ceci:

awk pour isoler la colonne incriminée: par exemple

awk -F, '{print $2 "|" $line}'

le -F définit le champ délimité à ",", $ 2 signifie la colonne 2, suivi d'un délimiteur personnalisé puis de la ligne entière. Vous pouvez ensuite filtrer en supprimant les lignes commençant par la valeur incriminée:

 awk -F, '{print $2 "|" $line}' | grep -v ^BAD_VALUE

puis retirez le contenu avant le délimiteur:

awk -F, '{print $2 "|" $line}' | grep -v ^BAD_VALUE | sed 's/.*|//g'

(notez que la commande sed est bâclée car elle n'inclut pas les valeurs d'échappement. De plus, le modèle sed devrait vraiment être quelque chose comme "[^ |] +" (c'est-à-dire tout ce qui n'est pas le délimiteur). Mais j'espère que c'est assez clair.


3
Il ne veut pas purger les lignes, il veut conserver une seule copie d'une ligne avec une chaîne spécifique. Uniq est le bon cas d'utilisation.
ingyhere

-3

En triant le fichier avec d' sortabord, vous pouvez ensuite postuler uniq.

Il semble bien trier le fichier:

$ cat test.csv
overflow@domain2.com,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
overflow@domain2.com,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0 
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

$ sort test.csv
overflow@domain2.com,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0 
overflow@domain2.com,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

$ sort test.csv | uniq
overflow@domain2.com,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0 
overflow@domain2.com,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

Vous pouvez également faire de la magie AWK:

$ awk -F, '{ lines[$1] = $0 } END { for (l in lines) print lines[l] }' test.csv
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
overflow@domain2.com,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0 

Ce n'est pas unique par colonne comme demandé dans la question. C'est tout simplement unique pour toute la gamme. De plus, vous n'avez pas à faire de tri pour faire un uniq. Les deux s'excluent mutuellement.
Javid Jamae

1
Oui, tu as raison. Le dernier exemple fait cependant ce que la question demandait, même si la réponse acceptée est beaucoup plus nette. En ce qui concerne sort, alors uniq, sortdoit être fait avant de faire uniqsinon cela ne fonctionne pas (mais vous pouvez ignorer la deuxième commande et simplement utiliser sort -u). De uniq(1): "Filtrer les lignes correspondantes adjacentes depuis INPUT (ou entrée standard), en écrivant vers OUTPUT (ou sortie standard)."
Mikael S

Ah, vous avez raison de trier avant uniq. Je n'ai jamais réalisé que uniq ne fonctionne que sur les lignes adjacentes. Je suppose que j'utilise toujours simplement sort -u.
Javid Jamae
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.