Traiter les fichiers CSV sous Gawk

Sommaire

Awk et Gawk sont des outils très puissants pour analyser des fichiers texte. Cependant, les fichiers CSV pose un problème particulier. Les champs sont séparés par des virgules ou des points virgules, selon les pays, mais on peut également retrouver ces séparateurs dans un champ entre guillemets doubles. Donc, impossible de découper les champs en se basant uniquement sur les séparateurs.

De plus, on peut retrouver des sauts de lignes dans un champ entre guillemets doubles, donc un champ peut occuper plusieurs lignes… Ce qui complique forcément les choses, Awk traitant les fichiers lignes par ligne.

L’extension gawk-csv

La solution la plus prometteuse pour traiter les fichiers CSV est sans doute l’extension gawk-csv de gawkextlib. Elle permet de traiter des fichiers CSV et fournit des fonctions permettant de lire et d’écrire des enregistrements CSV.

Avec cette extension, lire un fichier CSV se fait en 2 lignes :

@include "csv"
BEGIN { CSVMODE = 1 }
#... reste du programme ...

Traiter un fichier CSV en Gawk natif

Depuis la version 4.0, Gawk permet de traiter plus facilement les fichiers CSV grâce à la variable FPAT, qui permet de découper les champs d’entrée en fonction de leur contenu, au lieu de les découper en fonction de séparateurs.

FPAT permet de correctement découper les lignes, mais il ne gère pas le cas des champs contenant des passages à la ligne.

La mini-bibliothèque csv2awk ci-dessous a été conçue pour traiter les fichiers CSV contenant des fins de ligne. Pour cela, elle offre 2 fonctions, decoupe_csv() et produit_csv().

Elle présente les avantages suivants :

  • Fonctionne sans dépendre d’une extension.
  • Gère les passages à la ligne.
  • Gère les fichiers CSV produits par LibreOffice et Excel.
  • Permet de modifier un fichier CSV avec un programme Gawk ordinaire.

Elle a néanmoins quelques limitations :

  • Si un champ contient des passages à la ligne, ce champ doit être entre guillemets doubles (ce qui est seulement recommandé par le RFC 4180).
  • En l’état, elle ne traite que les fichiers en entrée de Gawk (i.e. pas les fichiers nommément ouverts, les commandes et coprocessus).
  • Elle nécessite Gawk 4.0 ou plus.
  • Après découpage, la ligne est séparée par des caractères NULL (caractère de code 0) et FS est redéfini à cette valeur. Cela permet d’utiliser des fonctions entraînant une réévaluation de la ligne (sub, gsub, $0 = ...) sans entraîner un redécoupage incorrect des champs.
  • En corolaire, si la ligne avant découpage contient un caractère NULL, en cas de réévaluation, les champs découpés ne correspondront plus aux champs lus dans le fichier CSV.

decoupe_csv()

La fonction decoupe_csv() permet de traiter les fichiers CSV en Gawk natif. Elle prend en paramètre le séparateur CSV utilisé (point-virgule par défaut).

Elle découpe les champs de la ligne courante ($0), si nécessaire lit des lignes supplémentaires avec getline et présente les champs comme une ligne nativement découpée par Gawk (NF, $0, $1, $2, et cætera).

Elle modifie le séparateur de champs (FS), en le positionnant à NULL (ASCII 0).

produit_csv()

La fonctionproduit_csv() renvoie la ligne courante traduite au format CSV. Elle prend en paramètre le séparateur CSV utilisé (point-virgule par défaut).

Elle peut être utilisée après decoupe_csv() pour afficher la ligne au format CSV. Ce qui va permettre de modifier un fichier CSV avec les fonctions Gawk classique sans avoir à tenir compte de son format.

Mais elle peut également être utilisée dans un script Gawk quelconque pour afficher la ligne en cours au format CSV.

La mini-bibliothèque awk2csv.awk

# csv2awk.awk - Traitement des fichiers CSV avec Gawk 4.0+
#
# Écrit en 2020 par Jean-Philippe Guérard <jean-philippe.guerard@tigreraye.org>
#
# Autant que légalement possible, l'auteur a placé tous les droits
# d'auteur et droits voisins de ce logiciel dans le domaine public,
# sans restriction géographique. Ce logiciel est distribué sans aucune
# garantie.
#
# Vous pouvez consulter la licence sur <http://creativecommons.org/publicdomain/zero/1.0/>.
#
# Utilisation :
#
# @include "csv2awk.awk"
# { decoupe_csv( ";" ) }
# ... traitement ...
# { print produit_csv( ";" ) }
#
function decoupe_csv( SEPARATEUR_CSV,    MOTIF_CHAMP, NB, LIGNE_DECOUPEE, CHAMP_EST_COMPLET, I, J, LIGNE_CSV, OFS_PREC ){
  if ( SEPARATEUR_CSV == "" ) { SEPARATEUR_CSV = ";" }
  MOTIF_CHAMP = "([^" SEPARATEUR_CSV "]*)|(\"([^\"]|\"\")+(\"|$))"
  # Lecture de la ligne CSV dans LIGNE_CSV
  I = 1
  split( "", LIGNE_CSV )
  while( 1 ) {
    sub( /\r$/, "" )
    NB = patsplit( $0, LIGNE_DECOUPEE, MOTIF_CHAMP )
    for ( J = 1 ; J <= NB ; J++ ){
      CHAMP_EST_COMPLET = 0
      if ( I in LIGNE_CSV ) {
        LIGNE_CSV[ I ] = LIGNE_CSV[ I ] SEPARATEUR_CSV LIGNE_DECOUPEE[ J ]
      } else {
        LIGNE_CSV[ I ] = LIGNE_DECOUPEE[ J ]
      }
      if ( LIGNE_CSV[ I ] ~ /^"([^"]|"")+"$/ ){
        LIGNE_CSV[ I ] = substr( LIGNE_CSV[ I ], 2, length( LIGNE_CSV[ I ] ) - 2 )
        CHAMP_EST_COMPLET = 1
        I++
      } else if ( LIGNE_CSV[ I ] ~ "^[^" SEPARATEUR_CSV "\"]*$" ) {
        CHAMP_EST_COMPLET = 1
        I++
      }
    }
    if ( CHAMP_EST_COMPLET ) break
    if ( getline <= 0 ) break
    $0 = LIGNE_CSV[ I ] "\n" $0
    delete LIGNE_CSV[ I ]
  }
  # Reconstruction de la ligne d'entrée séparée par des caractères NULL
  NF = 0
  OFS_PREC = OFS
  OFS = FS ="\0"
  for ( I = 1 ; I <= length( LIGNE_CSV ) ; I++ ){
    $I = gensub( /""/, "\"", "g", LIGNE_CSV[ I ] )
  }
  OFS = OFS_PREC
}
function produit_csv( SEPARATEUR_CSV,    LIGNE_CSV, CHAMP_CSV, MOTIF_ECHAP ){
  if ( SEPARATEUR_CSV == "" ) { SEPARATEUR_CSV = ";" }
  MOTIF_ECHAP = "[\n\"" SEPARATEUR_CSV "]"
  for ( I = 1 ; I <= NF ; I++ ){
    if ( $I ~ MOTIF_ECHAP ){
      CHAMP_CSV = "\"" gensub( /"/, "\"\"", "g", $I ) "\""
    } else {
      CHAMP_CSV = $I
    }
    if ( LIGNE_CSV ){
      LIGNE_CSV = LIGNE_CSV SEPARATEUR_CSV CHAMP_CSV
    } else {
      LIGNE_CSV = CHAMP_CSV
    }
  }
  return LIGNE_CSV
}

Essais

Essai 1 : affichage des champs d’un fichier CSV

Faisons un petit essai avec un fichier CSV créé à partir de la table suivante :

-------------------------------------------
|   a   |   b   |   c   |  x y  | "x " y" |
-------------------------------------------

Le fichier CSV contient :

a;b;c;"x y";"""x "" y"""

On sauvegarde le script sous le nom csv2awk.awk, puis on ajoute un peu de code pour afficher le contenu du fichier CSV :

echo 'a;b;c;"x y";"""x "" y"""' \
| gawk '@include "csv2awk.awk"
        { decoupe_csv() ; for (I=1 ; I<=NF ; I++) { print I ": " $I } }'

Résultat :

1: a
2: b
3: c
4: x y
5: "x " y"

Essai 2 : modification d’un fichier CSV

Nouvel essai, cette fois-ci de modification d’un fichier CSV. Nous partons du même fichier CSV que dans l’exemple précédent :

a;b;c;"x y";"""x "" y"""

Nous allons remplacer tous les guillemets doubles par des soulignés :

echo 'a;b;c;"x y";"""x "" y"""' \
| gawk '@include "csv2awk.awk"
        { decoupe_csv() ; gsub( /"/, "_" ) ; print produit_csv() }'

Résultat :

a;b;c;x y;_x _ y_