2.1. CSV#

El formato CSV es un formato relativamente sencillo del que casi podríamos improvisar una biblioteca para su soporte nosotros mismos. Sin embargo, tiene algunos casos especiales (p.ej. valores entre comillas que pueden contener el propio carácter separador), que aconsejan el uso de una biblioteca de terceros. Una muy apropiada es Apache Commons CSV de la que podemos obtener fácilmente un JAR listo para su uso.

Prudencia

Esta biblioteca, a su vez, necesita:

Algunas distribuciones de Linux traen paquete para su instalación directa en el sistema (p.e. este paquete de Debian).

Nota

Una alternativa interesante es OpenCSV.

2.1.1. Lectura#

Veamos cómo leer un CSV que se obtiene como flujo de entrada[1]:

Path ruta = Path.of(System.getProperty("user.home"), "caca.csv");

try (
   InputStream st = Files.newInputStream(ruta);
   CSVParser parse = new CSVParser(new InputStreamReader(st), CSVFormat.DEFAULT);
) {
   for(CSVRecord registro: parse) {
      // Tratamos cada registro del archivo. Por ejemplo:
      long num = parse.getRecordNumber(); // Número de registro.
      int cantidad = registro.size(); // Cantidad de campos en el registro.
      // Unimos los campos con " -- ".
      String contenido = StreamSupport.stream(registro.spliterator(), false).collect(Collectors.joining(" -- "));
      System.out.printf("%d: [%d]  %s\n", num, cantidad, contenido);
   }
}
catch(IOException err) {
   // Lo puede provocar la apertura del flujo o los lectores.
}

Esta es la lectura más simple que podemos hacer:

  • Como los objetos CSVParser son iterables usamos un bucle for-each (aunque también podríamos haber usado el método .forEach de los iterables). También dispone de un método getRecords que devuelve una lista.

  • Cada elemento de la iteración es un objeto CSVRecord. Podemos acceder a cada elemento por separado (con el método .get), pero es a su vez también iterable, lo que hemos preferido usar en este caso.

  • Al procesar el CSV hemos declarado que el archivo cumple estrictamente el RFC 4180 con la salvedad de que permite líneas vacías. CSVFormat lista otras variantes del estándar predefinidas indicando cuál es su definición. En cualquier caso, podemos definir nosotros mismos el formato:

    CSVFormat formato = CSVFormat.Builder.create(CSVFormat.DEFAULT)
        .setIgnoreSurroundingSpaces(true)
        .setHeaderName("Nombre", "Edad")  // Define los nombres de las columnas.
        .get();
    

    En este caso, hemos tomado como base el formato anterior y hemos añadido que no se tengan en consideración los espacios que pueda haber alrededor del carácter separador[2]. Además definimos los nombres de las columnas, con lo que podremos acceder a los valores de los campos usando get no sólo con el índice (registro.get(0)), sino también con el propio nombre (registro.get("Nombre")).

    Nota

    En realidad, lo recomendable es que la primera línea del CSV no sean valores, sino los nombres de las columnas, por lo que muy comúnmente conviene redefinir el formato así:

    CSVFormat formato = CSVFormat.Builder.create(CSVFormat.DEFAULT)
        .setIgnoreSurroundingSpaces(true)
        .setHeader()                // Respetamos los nombres incluidos.
        .setSkipHeaderRecord(true)  // Saltamos la primera línea al leer.
        .get();
    

2.1.2. Escritura#

La creación de archivos CSV también es bastante sencilla utilizando CSVPrinter:

// Datos que se han generado de alguna manera.
String[][] datos = {
   {"Pepe", "13"},
   {"Manolo", "22,5"}
};

Path ruta = Path.of(System.getProperty("user.home"), "caca.csv");
CSVFormat formato = CSVFormat.Builder.create(CSVFormat.DEFAULT)
   .setHeader("Nombre", "Edad")  // Incluirá los nombres en la salida.
   .get();

try (
   OutputStream st = Files.newOutputStream(ruta);
   CSVPrinter printer = formato.print(new OutputStreamWriter(st));
) {
   for(String[] registro: datos) {
      //printer.printRecord(registro[0], registro[1]);
      printer.printRecord(registro); // Puede ser cualquier objeto iterable.
   }
   printer.flush();
}
catch(IOException err) {
   // Lo puede provocar la apertura del flujo o los lectores.
}

Consejo

No es necesario escribir registro a registro. Si disponemos los datos en un flujo, una colección o una lista, podemos usar printRecords:

printer.printRecords(Arrays.stream(datos));

Nota

Los Enum no presentan ningún problema al ser almacenados o leídos.

Notas al pie