mardi 5 décembre 2017

Filtrer les guillements parasites d'un fichier CSV destiné à être importé dans une base de donnée

Les imports de bases de données à l'aide de fichiers CSV sont toujours critiques, car de nombreux facteurs peuvent déraper. Pour rappel, un fichier CSV est:
  • 1 fichier texte
  • dont chaque ligne correspond à un enregistrement d'une table
  • et dont les lignes sont découpées en colonne par un délimiteur
Pour importer correctement un fichier CSV, il faut se mettre d'accord sur de nombreux éléments: le caractère délimiteur de colonne (souvent le ;), le ou les délimiteurs de ligne (CR-LF sous DOS, LF sous unix, ou CR pour d'autre), et sur le "charset" (la table de correspondance ASCCI, UTF8, unicode, etc...).

Malheureusement, il faut également se préoccuper du caractère délimiteur des chaines de caractères que l'on trouve dans la plupart des colonnes d'un CSV, comme par exemple des libellés. Traditionnellement, on utilise les guillemets (") pour délimiter une colonne de type texte (chaine de caractère).  Ainsi, une ligne de fichier CSV peut se présenter comme cela:
"CODEnnn";"xxxxxxxxx xxxx xxxx"
Le problème est que très souvent, il y a des caractères guillemets dans ces colonnes textes. Voici un exemple d'un CSV de 2 colonnes, contenant des guillemets parasites:
"CODEnnn";"xxxxxxxxx "xxxx" x""xxx""
Dans ce cas, l'utilitaire d'import ne pourra pas fonctionner correctement. Le but de cet article est de filtrer ces caractères guillemets parasites, présents dans des colonnes d'un CSV.

Pour filtrer ces guillemets parasites, utilisons l'utilitaire SED, outils unix rapide et pratique pour ce genre d'actions. SED applique des actions ligne par ligne sur des fichier texte. L'action (la commande) SED qui nous intéresse est la substitution de caractère. On va demander à SED, de remplacer tous les guillemets parasites par une simple quotte ('). On demande à SED de réaliser cette substitution uniquement sur les guillemets précédés par un caractère différent de ";", et suivi par un caractère lui aussi différent de ";". Le ";" étant le délimiteur de colonne du fichier CSV. En résumé, tous les guillemets seront remplacés, sauf ceux en début de ligne, sauf ceux en fin de ligne, et sauf ceux qui se présente comme cela (";"), cad entourés par au moins 1 point-virgule.

La syntaxe SED que j'utilise est la suivante:
sed -f instructions.sed fichier_CSV
Je préfère utiliser la syntaxe permettant d'écrire les instructions (les commandes) SED dans un fichier, car il est plus facile à éditer, et permet d'insérer des commentaires salutaires pour une relecture quelques jours ou quelques mois plus tard.

La syntaxe de la commande de substitution que j'utilise est la suivante:
s/Exp_Reg_chaine_à_remplacer/nouvelle_chaine/g
Le dernier caractère "g" signifie que cette action de substitution se fera pour toutes les sous-chaines de la ligne respectant l'expression régulière.

L'expression régulière dans notre cas est relativement simple:
[^;]"[^;]
Cela correspond à une sous-chaine de 3 caractères, dont le premier et le dernier ne sont pas un ;, et dont le deuxième est obligatoirement un guillemet.
Cela donne une commande sed:
s/\([^;]\)"\([^;]\)/\1'\2/g

\1 et \2 correspondent respectivement au premier caractère et au 3ème caractère de la sous-chaine correspondante à l'expression régulière. Il suffit de mettre cette commande dans un fichier texte, et d’appeler la commande SED
sed -f instruction.sed fichier.csv > fichier_filtre.csv

Malheureusement, cette commande est inefficace sur certains caractères guillemet parasite.
Comme par exemple cette ligne CSV:
"CODEnnn";"xxxxxx"""""xxx "xxxx" x""xxx""

L'explication est simple: le parseur d'expression régulière ne s'applique pas 2 fois sur la même zone texte. Quand une expression régulière est trouvé par le parseur, la zone trouvée est supprimée pour les recherches suivante du parseur (pour la même instruction sed). Pour une chaine de 4 guillemets consécutifs (xx""""xx), le parseur détectera le premier guillemet, mais pas les deux suivants. Il détectera tout de même le 4 ième.

Pour résoudre le problème, il faut appliquer la commande de substitution 2 fois pour obliger le parseur à refaire la recherche. Il faut même relancer une 3ème fois pour supprimer le cas de guillemets parasites qui serait encore une fois espacés d'un seul caractère.
s/\([^;]\)"\([^;]\)/\1'\2/g  # 1er fois
s/\([^;]\)"\([^;]\)/\1'\2/g  # une 2ème fois
s/\([^;]\)"\([^;]\)/\1'\2/g  # 3ème et dernière fois

Et pour finir, il faut gérer également le problème des CR-LF dans SED. Si votre fichier CSV vient de DOS, SED va considérer le CR (code ASCII 13) comme un caractère comme un autre. Ce qui déclenchera la substitution de dernier guillemet. Il faut donc supprimer préalablement le CR:

s/\x0D$//g

Ne pas oublier si nécessaire de réaliser la commande inverse.
Pour être complet, penser à filtrer les ; en fin de ligne. Le CSV peut parfois avoir ce genre de colonne vide. 2 choix s'offre à vous: les supprimer
 s/;$//
ou les remplacer
 s/;$/; / # on ajoute un espace

Tout ça pour ça. Voici le fichier d’instruction SED pour filtrer nos guillemets parasites:
s/\x0D$//g    # conversion fichier DOS => unix (\r = 13 en ascii)
s/;$// # suppression des ; en fin de ligne
s/\([^;]\)"\([^;]\)/\1'\2/g # conversion des " en ' si le précédent n'est pas ;, ni le suivant
s/\([^;]\)"\([^;]\)/\1'\2/g # à refaire pour les "" contigus
s/\([^;]\)"\([^;]\)/\1'\2/g # à faire en tout 3 fois
s/$/\r/    #conversion fichier texte unix => DOS

La dernière ligne peut poser une soucis selon le SED que vous utilisez. Si votre sed est un exécutif unix, pas de soucis. En revanche, si c'est une version DOS, cela peut poser problème, car ce dernier ajoutera en fin de ligne des CR en plus du LF. Si c'est le cas, voici la commande pour supprimer les CR à postériori:
sed -i "s/\r//" fichier_filtré.csv


L'histoire serait belle si seulement, il n'y avait le cas des délimiteurs parasites.
Exemple de CSV avec 2 colonnes seulement:
"CODEnnnnn";"libellé ";" titre"
Dans ce cas, les 2 guillemets parasites ne seront pas filtrés par la commande sed. Concrètement, les sous-chaines de 2 caractères associant 1 guillemet et 1 point-virgule rendrons inefficace la commande sed conçu ci-dessus. Pour ce dernier cas, je n'ai pas de solution simple. D'ailleurs, comment humainement faire la différence entre un délimiteur et un simple caractère dans ce cas précis: la ligne précédente peut autant être interprétée comme une ligne de 2 colonnes ou une ligne de 3 colonnes. De quoi rendre fou un outil d'import de CSV. Ce cas particulier mérite un article à part entière.

Contentons-nous aujourd'hui de jouir de notre ignorance concernant ce cas perfide, et rappelons nous que dans tout voyage, le chemin est plus important que la destination ;)


PS :
Pour info, voici la commande SED permettant de vérifier la présence de caractère parasite:
/[^;]"[^;]/!d
Cette commande signifie "supprimez toutes les lignes qui ne contienne pas au moins une sous-chaines de 3 caractère décrite par l'expression régulière en question.

liens;
Exemples sed d'une grande clarté 
Introduction rapide à sed
Explication approfondie du fonctionnement de sed
Bases pour comprendre les Expressions Régulières
Boite à outils sed (liste d'exemples très variés)