Activité : Lire et interpréter un fichier CSV

Situation

Un client aimerait récupérer des données qui viennent d’une vieille application MS-DOS et vous demande de créer un programme pour convertir ces données dans un format XML qui pourra être lu par l’application actuelle. Le client souhaite également pouvoir utiliser le programme sous Linux et aimerait donc que le fichier XML soit toujours codé avec UTF-8. Pour effectuer ce travail, le client vous a fourni un fichier qui contient une petite partie des données à traiter.

Consigne

Pour ce travail, on vous demande de réaliser les tâches décrites ci-après.

Objectifs

À la fin de ce travail, vous devez :

  1. Connaître le principe de base du format CSV.
  2. Connaître la syntaxe de base d'un document XML
  3. Être capable de déterminer l'encodage d'un fichier de texte.

Résultat attendu

Un compte rendu de l’activité et les réponses aux questions posées dans une section de votre rapport du module.

Ressources

Documents :

Logiciel :

Mise en route

Nous devons lire les données contenues dans un fichier source (p. ex. contact.txt) au format CSV et les enregistrer dans un fichier XML.

Pour éviter d’avoir à écrire une interface utilisateur, pour lire le fichier d’entrée, nous allons utiliser la redirection de l’entrée standard comme nous l’avons fait pour le programme HexDump. Pour écrire le fichier de sortie, nous allons faire de même et utiliser la redirection de la sortie standard.

Pour rediriger la sortie standard, la syntaxe est la même que pour rediriger l’entrée standard, mais en utilisant le signe « plus grand que » > au lieu du signe « plus petit que » <. Par exemple :

monprogramme < input.txt > output.xml

Pour commencer, nous allons écrire un programme qui lit l’entrée standard ligne par ligne et écrire ces lignes avec un numéro devant dans la sortie standard.

Créez un nouveau projet nommé ConvertPersonCsvToXml et copiez le code de la figure 1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package ch.epaifribourg.ict.m100;

import java.io.PrintStream;
import java.util.Scanner;

public class ConvertPersonCsvToXml {

    // Identifiant de la norme d’encodage du fichier d’entrée.
    static private final String IN_ENCODING = "???";

    public static void main(String[] args) {

        // Initialise une variable de type Scanner pour lire le flux d’entrée.
        Scanner input = new Scanner(System.in, IN_ENCODING);

        // Initialise une variable de type PrintStream pour écrire dans le flux de sortie.
        PrintStream output = System.out;

        // Initialise un compteur de ligne.
        int lineNumber = 1;

        while (input.hasNextLine()) {
            String csvLine = input.nextLine();

            if (!csvLine.isEmpty()) {
                output.printf("%03d %s\n", lineNumber, csvLine);
                lineNumber += 1;
            }
        }
    }
}
Fig. 1 – Première étape de ConvertPersonCsvToXml

Pour pouvoir essayer ce programme, on doit remplacer les points d’interrogation de la ligne 9 par l’identifiant de la norme d’encodage du fichier d’entrée.

Pour cela, télécharger le fichier de données le fichier de données contact.txt dans votre répertoire Documents, déterminez l’encodage utilisé par n’importe quel moyen et trouvez l’identifiant correspondant dans la documentation de Java.

Pour essayer votre programme, ouvrez une fenêtre de terminal et lancez le programme à l’aide de la ligne de commande suivante :

1
2
3
D :
CD %USERPROFILE%\Documents\NetBeansProjects\ConvertPersonCsvToXml
java -jar dist\ConvertPersonCsvToXml.jar < ..\..\contact.txt > ..\..\out.txt

Vous devriez maintenant avoir dans votre répertoire Documents, un fichier out.txt qui contient les lignes du fichier contact.txt avec un numéro.

Déterminez l’encodage du fichier out.txt. Sous Windows, il s’agit probablement Windows-1252. Sous Linux, il s’agit probablement d’UTF-8. C’est encodage dépend de décisions prises par les concepteurs de Java. Comme le client souhaite que le fichier de sortie soit encodé en UFT-8, nous devons trouver un moyen de spécifier la norme d’encodage.

L’objet System.out est initialisé par Java au lancement du programme. Comme la norme d’encodage utilisée les méthodes print, printf et printlnest spécifiées au moment de l’initialisation de l’objet, on ne peut pas le modifier par la suite. Heureusement pour nous, System.out dispose d’une méthode write qui permet d’écrire un tableau de bytes directement dans le flux de sortie.

Nous devons donc créer un tableau de bytes qui contient les caractères du fichier de sortie codés en UTF-8. Pour cela, nous allons créer un nouvel objet de type PrintStream que nous allons utiliser pour coder des caractères en UTF-8 dans un objet de type ByteArrayOutputStream. Modifier votre code pour qu’il soit semblable à celui de la figure 2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package ch.epaifribourg.ict.m100;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;

public class ConvertPersonCsvToXml {

    // Identifiant de la norme d’encodage du fichier d’entrée.
    static private final String IN_ENCODING = "???";

    public static void main(String[] args) throws UnsupportedEncodingException, IOException {

        // Initialise une variable de type Scanner pour l’entrée.
        Scanner input = new Scanner(System.in, IN_ENCODING);

        // Crée un flux de sortie vers un tableau dynamique d’octets.
        ByteArrayOutputStream bs = new ByteArrayOutputStream();

        // Initialise une variable de type PrintStream pour le flux de sortie.
        PrintStream output = new PrintStream(bs, true, "UTF-8");

        // Initialise un compteur de ligne.
        int lineNumber = 1;

        while (input.hasNextLine()) {
            String csvLine = input.nextLine().trim();

            if (!csvLine.isEmpty()) {
                output.printf("%03d %s\r\n", lineNumber, csvLine);
                lineNumber += 1;
            }
        }

        // Écris le contenu du tableau d’octets dans la sortie standard
        System.out.write(bs.toByteArray());
    }
}
Fig. 2 – Coder la sortie en UTF-8

La prochaine étape consiste à obtenir les valeurs des champs d’une ligne. Chaque ligne contient les valeurs des attributs d’une personne. Pour effectuer cette opération, on peut utiliser la méthode split du type String nous allons opter pour un objet de type Scanner comme le montre le code de la figure 3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package ch.epaifribourg.ict.m100;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;

public class ConvertPersonCsvToXml {

    // Identifiant de la norme d’encodage du fichier d’entrée.
    static private final String IN_ENCODING = "???";

    public static void main(String[] args) throws UnsupportedEncodingException, IOException {

        // Initialise une variable de type Scanner pour lire le flux d’entrée.
        Scanner input = new Scanner(System.in, IN_ENCODING);

        // Crée un flux de sortie vers un tableau dynamique d’octets.
        ByteArrayOutputStream bs = new ByteArrayOutputStream();

        // Initialise une variable de type PrintStream pour écrire dans le flux de sortie.
        PrintStream output = new PrintStream(bs, true, "UTF-8");

        // Initialise un compteur de ligne.
        int lineNumber = 1;

        while (input.hasNextLine()) {
            String csvLine = input.nextLine();

            if (!csvLine.isEmpty()) {

                // Initialise un scanner pour interpréter la ligne.
                Scanner lineScan = new Scanner(csvLine);
                lineScan.useDelimiter(";");

                // Utilise le numéro de ligne comme identifiant
                int id = lineNumber;

                // Lis chaque champ de la ligne
                String lastname = lineScan.next().trim();
                String firstname = lineScan.next().trim();
                String street = lineScan.next().trim();
                String postalCode = lineScan.next().trim();
                String city = lineScan.next().trim();
                String email = lineScan.next().trim();

                // Ferme le scanner (libère les ressources qu'il utilise).
                lineScan.close();

                // Formate les données dans le flux de sortie.
                output.printf("No : %03d\r\n", id);
                output.printf("Nom, prénom : %s %s\r\n", lastname, firstname);
                output.printf("Adresse : %s, %s %s\r\n", street, postalCode, city);
                output.printf("Courriel : %s\r\n", email);
                output.print("\r\n");

                lineNumber += 1;
            }
        }

        // Écris le contenu du tableau d’octets dans la sortie standard
        System.out.write(bs.toByteArray());
    }
}
Fig. 3 – Interpréter les lignes et formater les données

Il serait bien de pouvoir faire une fonction qui interprète une ligne du CSV et qui renvoie les valeurs des attributs de la personne. Le problème est qu’une fonction ne peut renvoyer qu’une seule valeur. On connaît déjà un moyen de traiter plusieurs valeurs comme une seule : les tableaux.

On pourrait utiliser un tableau de chaîne de sept éléments (les six champs plus l’identifiant) comme le montre la figure 4. Toutefois, cette solution pose plusieurs problèmes. D’abord, là où l’on avait des variables avec des noms parlant, on a maintenant des index numériques. Bien que ce premier problème pourrait être résolu avec des constantes symboliques, il reste un second problème plus important. En effet, un tableau ne peut contenir que des éléments du type. Or, dans notre cas, l’identifiant de la personne (le numéro de ligne) est de type int. Il est donc nécessaire de le convertir en chaîne avant de le stocker. Dans ce cas, on pourrait cela n’a pas une grande importance et on pourrait se contenter de cette solution, mais il en existe une bien meilleure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    // Crée un tableau pour stocker les valeurs
    // des attributs d'une personne.
    String[] person = new String[7];

    // Utilise le numéro de ligne comme identifiant
    person[0] = String.valueOf(lineNumber);

    // Lis chaque champ de la ligne
    person[1] = lineScan.next().trim();
    person[2] = lineScan.next().trim();
    person[3] = lineScan.next().trim();
    person[4] = lineScan.next().trim();
    person[5] = lineScan.next().trim();
    person[6] = lineScan.next().trim();

    // Formate les données dans le flux de sortie.
    output.printf("No : %03d\r\n", person[0]);
    output.printf("Nom, prénom : %s %s\r\n", person[1], person[2]);
    output.printf("Adresse : %s, %s %s\r\n", person[3], person[4], person[5]);
    output.printf("Courriel : %s\r\n", person[6]);
    output.print("\r\n");
Fig. 4 – Interpréter les lignes et formater les données

Comme vous avez déjà pu le constater, en plus des type primitif (byte, short, int, long, float, double, boolean et char) il y a un très grand nombre de type en Java. Tous ces types sont des combinaisons plus ou moins complexes de ces types de bases et il est possible de créer de nouveaux types pour nos besoins spécifiques.

Dans notre cas, nous avons besoin d’un type dont les valeurs sont une combinaison d’un entier et de six chaîne de caractère. Le code figure 5 montre une nouvelle version de notre programme avec un type Person.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package ch.epaifribourg.ict.m100;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;

public class ConvertPersonCsvToXml {

    // Identifiant de la norme d’encodage du fichier d’entrée.
    static private final String IN_ENCODING = "CP850";

    // Déclaration du type Person avec ses sept champs.
    private static class Person {
        int id;
        String firstname = "";
        String lastname = "";
        String street = "";
        String postalCode = "";
        String city = "";
        String email = "";
    }

    public static void main(String[] args) throws UnsupportedEncodingException, IOException {

        // Initialise une variable de type Scanner pour l'entrée.
        Scanner input = new Scanner(System.in, IN_ENCODING);

        // Initialise une variable de type PrintStream pour la sortie.
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        PrintStream output = new PrintStream(bs, true, "UTF-8");

        // Initialise un compteur de ligne.
        int lineNumber = 1;

        while (input.hasNextLine()) {
            String csvLine = input.nextLine();

            if (!csvLine.isEmpty()) {

                // Initialise un scanner pour interpréter la ligne.
                Scanner lineScan = new Scanner(csvLine);
                lineScan.useDelimiter(";");

                // Initialise une variable de type Person. L'opérateur
                // new permet de construire une nouvelle valeur.
                Person person = new Person();

                // Lis chaque champ de la ligne
                person.id = lineNumber;
                person.lastname = lineScan.next().trim();
                person.firstname = lineScan.next().trim();
                person.street = lineScan.next().trim();
                person.postalCode = lineScan.next().trim();
                person.city = lineScan.next().trim();
                person.email = lineScan.next().trim();

                // Ferme le scanner (libère les ressources qu'il utilise).
                lineScan.close();

                // Formate les données dans le flux de sortie.
                output.printf("No : %03d\r\n", person.id);
                output.printf("Nom, prénom : %s %s\r\n",
                        person.lastname, person.firstname);
                output.printf("Adresse : %s, %s %s\r\n",
                        person.street, person.postalCode, person.city);
                output.printf("Courriel : %s\r\n", person.email);
                output.print("\r\n");

                lineNumber += 1;
            }

        }

        // Écris le contenu du tableau d'octets dans la sortie standard
        System.out.write(bs.toByteArray());
    }
}
Fig. 5 – Déclaration d'un nouveau type de données

On peut maintenant créer une fonction pour interpréter (to parse) les données d’une ligne et une procédure pour formater les données dans un flux de sortie. La figure 6 montre le code de cette nouvelle version du programme avec en plus deux procédures printHeader et printFooter pour écrire des données avant et après la liste des personnes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package ch.epaifribourg.ict.m100;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;

public class ConvertPersonCsvToXml {

    // Identifiant de la norme d’encodage du fichier d’entrée.
    static private final String IN_ENCODING = "CP850";

    /**
     * Représente les données d'une personne.
     */
    private static class Person {
        int id;
        String firstname = "";
        String lastname = "";
        String street = "";
        String postalCode = "";
        String city = "";
        String email = "";
    }

    /**
     * Lis les données du fichier CSV depuis l'entrée standard et format les données
     * lu dans la sortie standard.
     *
     * @param args
     * @throws UnsupportedEncodingException
     * @throws IOException
     */
    public static void main(String[] args) throws UnsupportedEncodingException, IOException {

        // Initialise une variable de type Scanner pour l'entrée.
        Scanner input = new Scanner(System.in, IN_ENCODING);

        // Initialise une variable de type PrintStream pour la sortie.
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        PrintStream output = new PrintStream(bs, true, "UTF-8");

        // Initialise un compteur de ligne.
        int lineNumber = 1;


        printHeader(output);

        while (input.hasNextLine()) {
            String csvLine = input.nextLine();

            if (!csvLine.isEmpty()) {

                Person person = parsePersonData(csvLine, lineNumber);
                formatPersonData(person, output);

                lineNumber += 1;
            }
        }

        printFooter(output);

        // Écris le contenu du tableau d'octets dans la sortie standard
        System.out.write(bs.toByteArray());
    }

    /**
     * Interprète une ligne du fichier CSV pour extraire les valeurs des
     * attribut d'une personne.
     *
     * @param line la ligne de texte
     * @param lineNumber le numéro de la ligne
     * @return un objet de type Person
     */
    private static Person parsePersonData(String line, int lineNumber) {
        Scanner lineScan = new Scanner(line);
        lineScan.useDelimiter(";");

        // Initialisation d'une variable de type Person. L'opérateur
        // new permet de construire une nouvelle valeur.
        Person person = new Person();

        // Lis chaque champ de la ligne
        person.id = lineNumber;
        person.lastname = lineScan.next().trim();
        person.firstname = lineScan.next().trim();
        person.street = lineScan.next().trim();
        person.postalCode = lineScan.next().trim();
        person.city = lineScan.next().trim();
        person.email = lineScan.next().trim();

        // Ferme le scanner (libère les ressources qu'il utilise).
        lineScan.close();

        return person;
    }

    /**
     * Écris des données la sortie avant les données des personnes.
     *
     * @param output le flux de sortie
     */
    private static void printHeader(PrintStream output) {
        output.print("Liste des personnes :\r\n\r\n");
    }

    /**
     * Formate les données de l'objet de type Person passé en paramètre dans
     * le flux de sortie également passé en paramètre.
     *
     * @param person un objet de type Person
     * @param output le flux de sortie
     */
    private static void formatPersonData(Person person, PrintStream output) {
        output.printf("No : %03d\r\n", person.id);
        output.printf("Nom, prénom : %s %s\r\n", person.lastname, person.firstname);
        output.printf("Adresse : %s, %s %s\r\n", person.street, person.postalCode, person.city);
        output.printf("Courriel : %s\r\n", person.email);
        output.print("\r\n");
    }

    /**
     * Écris des données dans la sortie après les données des personnes.
     *
     * @param output le flux de sortie
     */
    private static void printFooter(PrintStream output) {
        output.print("\r\n");
        output.print("Fin du fichier.\r\n");
    }
}
Fig. 6 – Déclaration d'un nouveau type de données

Questions

  1. Quelle est la norme d’encodage utilisée pour coder le fichier de données ? Expliquez la manière dont vous vous êtes pris pour le déterminer.
  2. Quelle norme d’encodage est utilisée par l’objet System.out lorsqu’on fait une redirection de la sortie standard dans un fichier ?
  3. Quels sont les problèmes liés à l’utilisation d’un tableau pour représenter les données d’un enregistrement en Java ?
  4. Dans notre programme, quelle est la partie du code qui déclare le type Person ?
  5. Dans notre programme, que signifie les caractères \r\n que l’on trouve dans le formatage des données ? (utilisez le moteur de recherche SymbolHound)
  6. Modifier les procédures printHeader, formatPersonData, printFooter pour écrire les informations sous forme d’un document XML comme celui de la figure 7.
  7. Que se passe-t-il si une ligne du fichier CSV n’est pas complète ?
  8. Vous pouvez constater une grande différence de taille entre le fichier CSV et le fichier XML. La différence de taille sera-t-elle toujours aussi importante si vous compressez les fichiers au format ZIP par exemple ? Argumentez votre réponse.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<person-list>
    <person id="1">
        <firstname>Adrienne</firstname>
        <lastname>Francillon</lastname>
        <street>chemin des Chatons 6</street>
        <postal-code>1680</postal-code>
        <city>Romont</city>
        <email>francillon1408@zemail.com</email>
    </person>
    <person id="2">
        <firstname>Alix</firstname>
        <lastname>Jacot-Guillarmod</lastname>
        <street>rue de Madretsch 9</street>
        <postal-code>1634</postal-code>
        <city>La Roche</city>
        <email>alix.jacot-guillarmod@blup.com</email>
    </person>
    ...
</person-list>
Fig. 7 – Données au format XML