Générer un fichier CSV UTF-8 avec Django

Exporter des données en CSV (comma-separated values) permet de transmettre des informations d’un système informatique/logiciel vers un autre. Ce type de fichier peut aussi servir à manipuler et analyser les données dans un tableur tel qu’Excel.

Dans mon cas, le but est d’exporter la liste des animaux du refuge pour lequel je travaille afin de les importer dans la plateforme officielle d’enregistrement des chiens (www.dogid.be) et chats (www.catid.be) de Belgique. Auparavant, l’encodage dans cette plateforme devait se faire manuellement, animal par animal. Il s’agissait d’une perte de temps et d’énergie.

Dorénavant, la manipulation demande quelques clics et prend moins de 5 minutes. Toutes les informations nécessaires étant déjà disponibles dans le logiciel de management du refuge, il est inutile de les recopier à nouveau sur l’autre plateforme.

from django.views.generic import View
from django.http import HttpResponse
from django.db.models.query import QuerySet
from administration.models import Encounter
from datetime import datetime
import csv

class DischargeExportView(View):

    def get(self, request):
        # préciser au navigateur que le contenu renvoyé sera de type CSV.
        response: HttpResponse = HttpResponse(content_type="text/csv;")
        # forcer le retour de données en  UTF-8.
        response.write(codecs.BOM_UTF8)
        # générer un nom de fichier.
        creation_date = datetime.now().strftime("%d%m%Y")
        filename = "bulk_{}_{}".format("sorties", creation_date)
        # préciser le nom de fichier et forcer le téléchargement de ce dernier.
        response['Content-Disposition'] = 'attachment; filename="{}.csv"'.format(filename)
        # récupérer dans la base de données la liste des données à exporter.
        qs: QuerySet = Encounter.objects.get_discharged()
        # préciser de quel type de CSV il s'agit. 
        writer = csv.writer(response, delimiter=";", dialect="excel", quoting=csv.QUOTE_ALL)
        # entête des colonnes du fichier CSV. 
        writer.writerow([
                "Espèce",
                "Résident",
                "Age en jours à la sortie",
                "Date sortie",
                "Raison sortie",
                "Adopt. nom",
                "Adopt. prénom",
                "Adopt. tel.",
                "Adopt. email",
            ])
        encounter: Encounter
        # écrire chaque ligne de donnée dans le fichier. 
        for encounter in qs:
            writer.writerow([
                encounter.subject.specie.display,
                encounter.subject.name,
                Period(encounter.subject.birth_date, encounter.period_end).days(),
                encounter.period_end,
                encounter.discharge_disposition,
                encounter.destination.first_name if encounter.destination else "",
                encounter.destination.last_name if encounter.destination else "",
                encounter.destination.phone_number if encounter.destination else "",
                encounter.destination.email_address if encounter.destination else "",
            ])
        return response

Erreur à cause de caractères qui ne sont pas ‘latin-1’

Lors du développement, j’ai rencontré l’erreur suivante concernant le jeu de caractères au moment de la génération du fichier CSV.

UnicodeEncodeError at /encounter/discharge/csv
'latin-1' codec can't encode character '\u030c' in position 55: ordinal not in range(256)
Request Method:	GET
Request URL:	http://xxx.xxx.xx/encounter/discharge/csv
Exception Type:	UnicodeEncodeError
Exception Value:	
'latin-1' codec can't encode character '\u030c' in position 55: ordinal not in range(256)

Ce message est affiché par Django car un caractère dans le fichier CSV ne fait pas partie du code page “latin-1”. Il s’agit dans cet exemple du caractère ^ renversé (probablement dû à une faute de frappe d’un utilisateur).

Pour corriger le problème, il faut dire à la vue Django de renvoyer la réponse vers le navigateur en UTF-8 grâce à la commande response.write(codecs.BOM_UTF8)

Voici le code dans son contexte :

class DischargeExportView(View):

    def get(self, request):
        response = HttpResponse(content_type="text/csv;")
        #  ligne magique pour que Django retourne le CSV en UTF-8
        response.write(codecs.BOM_UTF8)
        response['Content-Disposition'] = 'attachment; filename="csv-utf8.csv"'
        # écrire le contenu dans response...
        return response