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. Escritura#

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

// Supongamos que tenemos un clase Persona que almacena nombre y edad.
Persona[] personas = new Persona[] {
   new Persona("Pepe", 13),
   new Persona("Manolo", 25)
};

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

try (
   OutputStream st = Files.newOutputStream(ruta);
   CSVPrinter printer = formato.print(new OutputStreamWriter(st));
) {
   for(Persona persona: personas) {
      printer.printRecord(
         persona.getNombre(),
         // Si la transformación a String es sencilla, no hay problema
         persona.getEdad()
      );
   }
   printer.flush();
}
catch(IOException err) {
   // Tratar el error.
}

Nota

Los Enum no presentan ningún problema al ser almacenados.

2.1.2. Lectura#

Para leer un CSV que se obtiene como flujo de entrada[1]:

// El archivo de antes
Path ruta = Path.of(System.getProperty("user.home"), "personas.csv");
CSVFormat formato = CSVFormat.Builder.create(CSVFormat.DEFAULT)
   .setHeader()                 // Nos valen los nombres de las columnas incluidos.
   .setSkipHeaderRecord(true)   // La primera columna no son datos.
   .get();

try (
   InputStream st = Files.newInputStream(ruta);
   CSVParser parser = CSVParser.parse(new InputStreamReader(st), CSVFormat.DEFAULT);
) {
   for(CSVRecord registro: parser) {
      // 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.
      String nombre = registro.get("Nombre");   // También .get(0)
      int edad = Integer.parseInt(registro.get("Edad"));
      Persona persona = new Persona(nombre, edad);
      System.out.printf("%d: [%d]  %s\n", num, cantidad, persona);
   }
}
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.

  • Como es iterable, además de un bucle, podríamos haber usado un enfoque funcional:

    Persona[] personas = StreamSupport.stream(registro.spliterator(), false)
       .map(registro -> {
             String nombre = registro.get("Nombre");
             int edad = Integer.parseInt(registro.get("Edad"));
             return new Persona(nombre, edad);
       }).toArray(Persona[]::new);
    
  • CSVFormat.DEFAULT significa que el formato cumple estrictamente el RFC 4180 con la salvedad de que se permiten 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)
        .setHeaderName("nombre", "edad")  // Define otros nombres de columna
        .setSkipHeaderRecord(true)
        .setIgnoreSurroundingSpaces(true)
        .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")).

Notas al pie