Récemment, j’ai dû assembler le contenu de plusieurs fichiers CSV en effectuant une jointure sur une clé unique pour chaque ligne.

Contexte

Les fichiers se composent de 2 colonnes. La première est une clé et la seconde une valeur quelconque. Les clés ne sont pas toutes présentes dans chaque fichier. Les lignes de ces fichiers peuvent être triées par ordre alphabétique sur la clé.

Pour information, le séparateur de champ utilisé est la virgule. De plus, les fichier ne contiennent pas de point virgule (ce qui se révélera pratique plus tard).

Voici les fichier utilisés (et simplifiés) pour les tests :

$ cat fichier1.csv
001,Black Lotus
002,Ancestral Recall
003,Counterspell
005,Island
006,Swamp
007,Mana Drain
008,Boomerang

$ cat fichier2.csv
003,Countresort
004,Plaine
005,Île
006,Marais

$ cat fichier3.csv
003,Gegenzauber
004,Ebene

$ cat fichier4.csv
004,Pianura
007,Risucchia Potere

$ cat fichier5.csv
008,Búmerang

$ cat resultat.csv
001,Black Lotus,-,-,-,-
002,Ancestral Recall,-,-,-,-
003,Counterspell,Contresort,Gegenzauber,-,-
004,-,Plaine,Ebene,Pianura,-
005,Island,Île,-,-,-
006,Swamp,Marais,-,-,-
007,Mana Drain,-,-,Risucchia Potere,-
008,Boomerang,-,-,-,Búmerang

Solution

Jointure sur 2 fichiers

Pour cela, il existe un programme qui se nomme tout simplement join :

join -e- -t, -a1 -a2 -j 1 -o 0,1.2,2.2 fichier1.csv fichier2.csv > resultat.csv

Cette commande permet de remplir les cases vides avec un tiret. Le format de sortie utilise le champ "0" afin de gérer le cas où le premier fichier d’entrée ne contienne pas toutes les clés.

Jointure sur 5 fichiers

A partir de la commande précédente, il est assez facile de réaliser une jointure sur 5 fichiers :

join -e- -t, -a1 -a2 -j 1 -o 0,1.2,2.2 fichier1.csv fichier2.csv > tmp1.csv
join -e- -t, -a1 -a2 -j 1 -o 0,1.2,1.3,2.2 tmp1.csv fichier3.csv > tmp2.csv
join -e- -t, -a1 -a2 -j 1 -o 0,1.2,1.3,1.4,2.2 tmp2.csv fichier4.csv > tmp3.csv
join -e- -t, -a1 -a2 -j 1 -o 0,1.2,1.3,1.4,1.5,2.2 tmp3.csv fichier5.csv > resultat.csv

Même si cette méthode est fonctionnelle, on sent que ce n’est pas très élégant. De plus, si l’on souhaite proposer un script acceptant un nombre variable de fichiers, il faut revoir la méthode.

Jointure sur N fichiers

Afin de pouvoir faire une jointure sur un nombre variable de fichiers, il faut écrire un script bash acceptant un nombre variable d’arguments.
De plus, le script va transformer les fichiers CSV temporaires afin de n’avoir toujours que 2 colonnes. Cette astuce va permettre de n’avoir qu’un seul et unique format de sortie pour join. Le résultat final sera transformé pour rétablir les N+1 colonnes.

Voici à quoi ressemble le script multijoin.sh :

#!/bin/bash

# load first file for join
LEFT_FILE=$1
shift

# iterate over all the remaining files to join
while (( "$#" )); do
  RIGHT_FILE=$1
  JOINED_FILE="out${#}.csv"
  TMP_FILES+=("${JOINED_FILE}")
  join -e- -t, -a1 -a2 -j 1 -o 0,1.2,2.2 "${LEFT_FILE}" "${RIGHT_FILE}" | sed 's/,/;/g;s/;/,/' > "${JOINED_FILE}"
  LEFT_FILE=$JOINED_FILE
  shift
done

# render the result
sed 's/;/,/g' "$JOINED_FILE"

# remove all the temporary files
for TMP_FILE in "${TMP_FILES[@]}"; do
  rm -f "${TMP_FILE}"
done

Il y a 2 astuces dans ce script :

  1. Il boucle sur les arguments en prenant toujours le premier et en le supprimant avec la commande shift.
  2. Dans les fichiers temporaires, tous les séparateurs "," sont remplacés par un ";" puis le premier ";" est repositionné avec une ",". A la fin, tous les ";" sont remplacés par des ",". Cela permet de n’avoir toujours que 2 colonnes dans les fichiers temporaires (outX.csv).

Le script s’appelle de cette façon :

$ multijoin fichier1.csv fichier2.csv fichier3.csv fichier4.csv fichier5.csv > resultat.csv

Conclusion

Le script a quelques limitations :

  • le script a toutes les contraintes de la commande join
  • la clé doit être dans la première colonne
  • les fichiers d’entrée ne peuvent contenir que 2 colonnes (même s’il est possible d’outre passer cette limitation)
  • le séparateur de champs doit être la virgule et les fichiers ne doivent pas contenir de point virgule (cette limitation peut également être sautée avec un peu de code)

Le cas n’a pas été rencontré avec les fichiers de données manipulés mais il y a également un bug : si les X premiers fichiers ne contiennent pas une clé, alors il manquera les X-1 premières cellules pour la ligne de la clé en question… 🙁

Néanmoins, cet exemple montre qu’avec des commandes de base et un peu d’astuce, il est possible de mettre en place des traitements qui seraient parfois implémentés avec des outils bien plus lourds ! 😉