Skip to content
Tags

,

Supprimer des lignes identiques réparties dans un fichier

21 décembre 2011

Jules et Vincent doivent fournir un fichier texte en supprimant les lignes qui sont plusieurs fois dans le fichier, mais réparties de manière quelconque dans le fichier (par exemple un fichier journal ou une liste de nombres aléatoires). Alors qu’ils descendent de la voiture pour aller sur le lieu de leur mission, ils en discutent nonchalamment :
– Il nous faudrait des outils Unix pour ces affaires-là.
– Y’a combien de lignes ?
– 1.200
– En comptant les doublons ?
– Oui.
– Faudrait des outils Unix…

Après un moment de réflexion plus ou moins court (selon votre taux de caféine), comme eux, vous vous dites que cela ne devrait vous prendre que quelques secondes, le temps de dégainer un terminal et de taper une commande Unix adéquate…

Solutions basées sur le shell

uniq est un outil shell permettant de supprimer des lignes répétées. Voilà qui semble parfait pour ce genre d’affaire. Malheureusement, en lisant un peu plus la page de man, on apprend qu’uniq filtre « les lignes successives identiques ». La page de man propose un contournement en utilisant sort. Avec un fichier de log, cela peut être plus ou moins utilisable en triant d’abord par la chaîne de log puis par la date. En revanche, cela ne peut pas vraiment résoudre le problème de valeurs aléatoires. En revanche, il est possible de combiner plusieurs outils Unix en utilisant un compteur de ligne, qui sera supprimé ensuite.

Soit un fichier nommé pulp.txt et contenant les données suivantes :

1994-10-26
cheese
1994-10-26
1994-10-26
tarte
myrtille
1994-10-26
chalumeau
1994-10-26
1994-10-26
verset
massage

Commençons par ajouter un compteur de ligne :

$ nl pulp.txt
     1	1994-10-26
     2	cheese
     3	1994-10-26
     4	1994-10-26
     5	tarte
     6	myrtille
     7	1994-10-26
     8	chalumeau
     9	1994-10-26
    10	1994-10-26
    11	verset

Puis on trie en fonction des données préalablement fournies, tout en éliminant des chaînes en doubles en même temps :

$ nl pulp.txt | sort --key 2 --unique 
     1	1994-10-26
     8	chalumeau
     2	cheese
     6	myrtille
     5	tarte
    11	verset

On réordonne selon l’ordre précédent :

$ nl pulp.txt | sort --key 2 --unique | sort --key 1 --numeric-sort
     1	1994-10-26
     2	cheese
     5	tarte
     6	myrtille
     8	chalumeau
    11	verset

Pour finir, on supprime la numérotation :

$ nl pulp.txt | sort --key 2 --unique | sort --key 1 --numeric-sort | cut --fields 2
1994-10-26
cheese
tarte
myrtille
chalumeau
verset

Le problème est alors résolu, mais et la mise au point aura probablement pris plus de temps que prévu (selon votre taux d’alcoolémie) :

nl pulp.txt | sort –key 2 –unique | sort –key 1 –numeric-sort | cut –fields 2

Cependant cette succession de commande ne sera directement réutilisable dès que la structure des chaînes variera (même un tout petit peu).

Il est regrettable qu’il y ait pas une option permettant de filtrer l’ensemble des lignes par uniq. Si jamais vous êtes en train de faire une overdose de shell, demandez à votre voisin de vous faire une piqûre d’adrénaline. (Attention, ça ne fonctionne pas à chaque fois.) Le mainteneur d’uniq refuse l’ajout d’une telle fonctionalité à cause de l’augmentation de la complexité que cela provoquerait. Par contre, des solutions bien plus compactes de contournement ont été proposées sur la liste de diffusion de coreutils.

One-liners

On se repoudre le nez et on y retourne !

Les premières solutions ont été à base de Perl :

perl -lne ‘print $_ if ! defined $a{$_}; $a{$_}=$_;’

perl -MDigest::MD5=md5 -lne ‘$m=md5($_); print $_ if ! defined $a{$m}; $a{$m}=1’

$_ représente la ligne en cours de traitement ; elle est affichée si ce n’est pas une clef d’un tableau associatif puis on ajoute la chaîne comme clef. La seconde version reprend ce même principe en utilisant une signature md5. Perl est vraiment adapté à l’écriture de code d’une ligne : c’est compact et illisible. Ce qui est tout de même un grand principe de Perl : « il y a plus d’une façon de le faire », mais on choisira toujours la plus incompréhensible.😉

Puis une solution en awk a été proposée :

awk ‘!a[$0]++’

La ligne traitée est $0 ; la valeur d’un tableau a ayant pour clé la ligne en cours de traitement est incrémentée. Si c’est la deuxième fois qu’elle est rencontrée, la valeur sera non nulle. Le ! inverse le résultat qui sera donc évalué à faux (la chaîne est déjà présente).

Ces solutions résolvent le problème, de manière plus souple que la suite de commandes précédentes. Par contre, elles ne résolvent pas le problème de complexité. Comme cela est réalisé par un interpréteur plutôt que du code C compilé (pour uniq), il est quasi-certain que la consommation mémoire est plus importante et que la vitesse d’exécution est plus lente… Je ne pense pas que ce soit un problème mais la réponse est pire que le contre-argument de ne pas le réaliser via uniq.
Pour autant, cela ne m’a pas apparu nécessaire de tenter de convaincre à nouveau le mainteneur. C’est comme masser les pieds de la femme du parrain de la pègre locale. Vous le feriez, vous ? Moi non plus.

Mise en place d’un alias

Pour garder le filtre à disposition, il est possible d’en faire un alias. Pour cela, il suffit de copier la ligne suivante in ~/.bash_aliases (si ce fichier est activé dans ~/.bashrc) :

alias uniqall=’awk ‘ »‘ »‘! a[$0]++' »‘ »

Alors qu’ils sont sur la route du retour, Vincent a les mains sur le clavier et un cahot de son fauteuil à roulette fait partir d’un coup une série de caractères supplémentaires !
Vincent : Oh putain le con j’ai ajouté plein d’apostrophes et de guillemets !
Jules : Mais pourquoi t’as fait ça putain ?
Vincent : Mais parce qu’il le fallait, c’est pas un accident !

En effet, Vincent était obligé : la commande awk ou perl fonctionne très bien seule mais elle ne peut pas être insérée directement comme alias. En effet, il faut transformer la commande en chaîne de caractère pour l’insérer dans le fichier .bash_aliases, donc mettre des apostrophes autour de la commande. Or, il y a déjà des apostrophes à l’intérieur de la commande (interpréteur 'code'). On aurait donc des apostrophes incluses entre d’autres apostrophes et le shell interprèterait la seconde apostrophe comme la fin de la première chaîne. Quelques solutions semblent évidentes :

  • Protéger les apostrophes intérieures avec un antislash (\') ;
  • Remplacer les apostrophes intérieures par des guillemets ;
  • Remplacer les apostrophes extérieures par des guillemets.

Ces différentes stratégies vont d’un fonctionnement incorrect (il ne retourne que la première ligne du flux) à une erreur lors de l’ajout de l’alias.

Explication du ‘ »‘ »‘

La ruse est de découper la commande en plusieurs sous-chaînes qui seront concaténées automatiquement par le shell.

Il est tout à fait possible de mettre des espaces à l’intérieur des chaînes mais pas entre elles. Sinon le shell les interprétera comme des mots différents et ne les concatènera pas.

Générique de fin

Merci aux participants de liste de diffusion bug-coreutils qui ont été très réactifs : Pádraig Brady, Jim Meyering et Bob Proulx (le rapport de bug, début de la discussion).

Merci à Arthur et Colin qui m’ont suggéré d’en faire un article. (Ils ne pensaient pas que j’allais vous faire subir un article de plus de 50 tweets de longueur…)

Les discussions de Vincent et de Jules sont des références à Pulp Fiction. (liste non exhaustive)

Le shell utilisé est bash, version 4.2.20. L’interpréteur Awk est mawk, version 1.3.3.

Aucun compilateur n’a été maltraité durant l’écriture de cet article, ni lors de la mise au point de l’alias final.

From → Autre

Laisser un commentaire

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :