2017-10-13 5 views
4

Depuis quelques années, j'ai souvent besoin de combiner des lignes de texte (triées) avec un premier champ correspondant, et je n'ai jamais trouvé de manière élégante (par exemple un ligne de ligne de commande) fais le. Ce que je veux est similaire à ce qui est possible avec la commande unix join, mais join attend 2 fichiers, avec chaque clé apparaissant au maximum une fois. Je veux commencer avec un seul fichier, dans lequel une clé peut apparaître plusieurs tuiles.Combiner des lignes avec le premier champ correspondant

J'ai à la fois un script ruby ​​et perl qui le font, mais il n'y a aucun moyen de raccourcir mon algorithme dans un one-liner. Après des années d'utilisation d'unix, je suis toujours en train d'apprendre de nouveaux trucs avec comm, paste, uniq, etc, et je soupçonne qu'il y a une façon intelligente de le faire.

Il existe quelques questions connexes, comme join all lines that have the same first column to the same line; Command line to match lines with matching first field (sed, awk, etc.); et Combine lines with matching keys - mais ces solutions ne donnent jamais vraiment une solution propre et fiable.

est ici entrée échantillon:

apple:A fruit 
apple:Type of: pie 
banana:tropical fruit 
cherry:small burgundy fruit 
cherry:1 for me to eat 
cherry:bright red 

Voici un exemple de sortie:

apple:A fruit;Type of: pie 
banana:tropical fruit 
cherry:small burgundy fruit;1 for me to eat;bright red 

Voici ma syntaxe idéale:

merge --inputDelimiter=":" --outputDelimiter=";" --matchfield=1 infile.txt 

Le "champcorrespondant" est vraiment en option. Cela pourrait toujours être le premier champ. Les apparences suivantes du délimiteur doivent être traitées comme du texte brut.

Cela ne me dérange pas un perl, ruby, awk one-liner, si vous pouvez penser à un algorithme court et élégant. Cela devrait être capable de gérer des millions de lignes d'entrée. Des idées?

Répondre

2

Découvrez awk langue:

awk -F':' '{ v=substr($0, index($0,":")+1); a[$1]=($1 in a? a[$1]";" : "")v } 
      END{ for(i in a) print i,a[i] }' OFS=':' infile.txt 

La sortie:

apple:A fruit;Type of: pie 
banana:tropical fruit 
cherry:small burgundy fruit;1 for me to eat;bright red 
+0

Merci @RomanPerekhrest, cela fonctionne. Mieux que d'autres solutions awk que j'ai essayées dans le passé et qui se briseraient sur des lignes complexes. Cela dit, j'aimerais toujours une commande plus courte avec une syntaxe plus simple, mais je suis heureux d'avoir un one-liner. – MichaelD

1
for F in `cut -f1 -d ':' infile.txt | sort | uniq`; do echo "$F:$(grep $F infile.txt | cut -f2- -d ':' | paste -s -d ';' -)"; done 

Je ne sais pas, il se qualifie comme 'élégant', mais cela fonctionne, mais je suis sûr que pas rapidement pour des millions de lignes - que le nombre d'appels grep augmente, il ralentirait de manière significative. Quel% des champs correspondants pensez-vous être unique?

+0

Merci pour la chaîne unix. Je m'attends à environ 1-5 répétitions d'un champ clé/correspondant, donc dans un million de lignes, il pourrait y avoir 300k clés. – MichaelD

+0

Ah, 300k appels grep serait déraisonnable. Merci pour les commentaires – jgrundstad

1

Je pense que celui-ci faire le travail

awk -F':' '$1!=a{if(b);print b;b=""}a=$1{$1="";if(!b)b=a;b=b$0}END{print b}' infile 
+2

Pouvez-vous l'expliquer? – ghoti

3

En utilisant awk une doublure

awk -F: -v ORS="" 'a!=$1{a=$1; $0=RS $0} a==$1{ sub($1":",";") } 1' file 

Sortie:

apple:A fruit;Type of: pie 
banana:tropical fruit 
cherry:small burgundy fruit;1 for me to eat;bright red 

mise ORS=""; Par défaut, il s'agit de \n.
La raison pour laquelle nous avons défini ORS="" (Output Record Separator) est parce que nous ne voulons pas que awk inclue des retours à la ligne à la fin de chaque enregistrement. Nous voulons le gérer à notre manière, à travers notre propre logique.Nous incluons en fait des nouvelles lignes au début de chaque enregistrement qui a le premier champ différent du précédent.

a!=$1: Lorsque la variable a (initialement null) ne correspond pas au premier champ $1 qui est par exemple. apple dans la première ligne, puis définir a=$1 et $0=RS $0 par exemple $0 ou simplement whole record devient "\n"$0 (en ajoutant fondamentalement newline au début de l'enregistrement). a!=$1 satisfera toujours lorsqu'il existe un premier champ différent ($1) que le $1 de la ligne précédente et est donc un critère pour séparer nos enregistrements en fonction du premier champ.

a==$1: Si cela correspond, cela signifie probablement que vous itérez sur un enregistrement appartenant à l'ensemble d'enregistrements précédent. Dans ce cas, remplacer la première occurrence de $1: (Notez le :) pour par exemple. apple: avec ;. $1":" peut également être écrit comme $1FSFS is :

Si vous avez des millions de ligne dans votre fichier, cette approche serait plus rapide car elle ne comporte pas de pré-traitement et aussi nous ne sommes pas en utilisant toute autre structure de données dites tableau pour stocker vos clés ou vos enregistrements.

+0

Merci pour la bonne explication. – MichaelD

+0

@MichaelD: Bienvenue Michael. – batMan