Parser un fichier CSV en Bash

Lorsque qu'il est question de traiter des fichiers CSV en bash, il est souvent fait allusion à l'usage de programmes externes, de pipes et d'utilisation abusive de certaines commandes buildin bash ... Cette article présente une solution simple et 100% interne au bash qui améliore les performances et la lisibilité du code.

Afin de prendre conscience de l'importance de l'implémentation d'un script, un premier exemple représentatif de ce qu'il ne faut pas faire est comparé avec la solution builtin bash.

L'exemple à ne pas suivre

for line in $(cat file.csv)
do
  # recuperation des champs
  field1=$(echo $line | awk -F";" '{ print $1 }')
  field2=$(echo $line | awk -F";" '{ print $2 }')
  field3=$(echo $line | awk -F";" '{ print $3 }')
  # traitement, ici un simple echo
  echo "$field1 - $field2 - $field3"
done

Dans cette exemple nous retrouvons une utilisation non justifiée et non modérée :

  • de substitution de commandes ($(...))
  • d'appel de programme externe (cat, awk)
  • de pipe (|)
  • de la commande builtin bash echo

La méthode builtin bash

while IFS=';' read field1 field2 field3; do
  # traitement, ici un simple echo
  echo "$field1 - $field2 - $field3"
done < file.csv

Ici le fichier est lu ligne par ligne (while read line; do ...; done < file.csv).

La fonction read permet de récupérer l'entrée standard dans une ou plusieurs variables en se basant sur l'IFS. Il suffit donc de modifier la valeur de l'IFS et d'indiquer les variables à utiliser pour récupérer les champs (IFS=';' read field1 field2 field3).

Pour information, l'IFS (Internal Field Separator) est une variable spéciale du bash utilisée par de nombreuses fonctions builtin (read dans notre cas). Il contient la liste des séparateurs à utiliser pour extraire les champs (par défaut l'espace, la tabulation et le retour chariot, soit IFS=' \t\n').

Comparaison des performances

Pour réaliser le test de performance, chaque solution parse le même fichier CSV de 479828 lignes (chaque ligne contenant trois champs séparés par un point-virgule). Le temps d'exécution est récupéré avec la commande time afin de comparer les performances.

Le résultat est sans appel :

  • première solution (ce qu'il ne faut pas faire) : 3 heures, 2 minutes et 41,93 secondes (31m34,08 user, 177m31,38s sys)
  • deuxième solution (builtin bash) : 20,25 secondes (18,36s user, 1,61s sys)

Améliorer la solution builtin bash

Il est encore possible d'améliorer les performances en assignant une seule fois la valeur de l'IFS. Ce n'était pas le cas de l'exemple précedant qui faisait une réassignation à chaque loop :

IFSori=$IFS
IFS=&#039;;&#039;

while read field1 field2 field3; do
  # traitement, ici un simple echo
  echo "$field1 - $field2 - $field3"
done < file.csv

# penser a restaurer l&#039;ancienne valeur de l&#039;IFS
IFS=$IFSori

Cette modification permet de parser le fichier CSV en seulement 9,73 secondes (soit 10,52 secondes de moins que le précédant test).

By @Mikael FLORA in
Tags : #bash,