Leer y escribir archivos CSV en Kotlin con Apache Commons

En este tutorial, repasaremos cómo leer y escribir archivos CSV en Kotlin, utilizando la biblioteca Apache Commons, con ejemplos de lectura y escritura de objetos personalizados.

Introducción

En este artículo, veremos cómo leer y escribir archivos CSV en Kotlin, específicamente, usando Apache Commons.

Dependencia de Apache Commons

Ya que estamos trabajando con una biblioteca externa, avancemos e importémosla a nuestro proyecto Kotlin. Si está utilizando Maven, simplemente incluya la dependencia commons-csv:

1
2
3
4
5
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-csv</artifactId>
    <version>1.5</version>
</dependency>

O, si estás usando Gradle:

1
implementation 'org.apache.commons:commons-csv:1.5'

Finalmente, con la biblioteca agregada a nuestro proyecto, definamos el archivo CSV que vamos a leer: students.csv:

1
2
3
101,John,Smith,90
203,Mary,Jane,88
309,John,Wayne,96

Estará ubicado en /resources/students.csv.

Además, dado que leeremos estos registros en objetos personalizados, hagamos una clase de datos:

1
2
3
4
5
6
data class Student (
    val studentId: Int,
    val firstName: String,
    val lastName: String,
    val score: Int
)

Leer un archivo CSV en Kotlin

Primero leamos este archivo usando un BufferedReader, que acepta una Ruta al recurso que nos gustaría leer:

1
val bufferedReader = new BufferedReader(Paths.get("/resources/students.csv"));

Luego, una vez que hayamos leído el archivo en el búfer, podemos usar el propio búfer para inicializar una instancia de CSVParser:

1
val csvParser = CSVParser(bufferedReader, CSVFormat.DEFAULT);

Dado lo volátil que puede ser el formato CSV, para eliminar las conjeturas, deberá especificar CSVFormat al inicializar el analizador. Este analizador, inicializado de esta manera, solo se puede usar para este formato CSV.

Dado que estamos siguiendo el ejemplo del libro de texto del formato CSV, y estamos usando el separador predeterminado, una coma (,), pasaremos CSVFormat.DEFAULT como segundo argumento.

Ahora, el CSVParser es un Iterable, que contiene instancias de CSVRecord. Cada línea es un registro CSV. Naturalmente, podemos iterar sobre la instancia csvParser y extraer registros de ella:

1
2
3
4
5
6
7
for (csvRecord in csvParser) {
    val studentId = csvRecord.get(0);
    val studentName = csvRecord.get(1);
    val studentLastName = csvRecord.get(2);
    var studentScore = csvRecord.get(3);
    println(Student(studentId, studentName, studentLastName, studentScore));
}

Para cada CSVRecord, puede obtener sus respectivas celdas usando el método get() y pasando el índice de la celda, comenzando en 0. Luego, podemos simplemente usarlos en el constructor de nuestra clase de datos Student.

Este código da como resultado:

1
2
3
Student(studentId=101, firstName=John, lastName=Smith, score=90)
Student(studentId=203, firstName=Mary, lastName=Jane, score=88)
Student(studentId=309, firstName=John, lastName=Wayne, score=96)

Sin embargo, este enfoque no es genial. Necesitamos saber el orden de las columnas, así como cuántas columnas hay para usar el método get(), y cambiar cualquier cosa en la estructura del archivo CSV rompe totalmente nuestro código.

Leer un archivo CSV con encabezados en Kotlin

Es razonable saber qué columnas existen, pero un poco menos en qué orden están.

Por lo general, los archivos CSV tienen una línea de encabezado que especifica los nombres de las columnas, como StudentID, FirstName, etc. Al construir la instancia de CSVParser, siguiendo el [Patrón de diseño de constructor](/el-patron -de-diseno-del-constructor-en-java/), podemos especificar si el archivo que estamos leyendo tiene una fila de encabezado o no, en el CSVFormat.

Por defecto, CSVFormat asume que el archivo no tiene un encabezado. Primero agreguemos una fila de encabezado a nuestro archivo CSV:

1
2
3
4
StudentID,FirstName,LastName,Score
101,John,Smith,90
203,Mary,Jane,88
309,John,Wayne,96

Ahora, vamos a inicializar la instancia CSVParser y establecer un par de opciones opcionales en CSVFormat a lo largo del camino:

1
2
3
4
5
6
val bufferedReader = new BufferedReader(Paths.get("/resources/students.csv"));

val csvParser = CSVParser(bufferedReader, CSVFormat.DEFAULT
        .withFirstRecordAsHeader()
        .withIgnoreHeaderCase()
        .withTrim());

De esta forma, el primer registro (fila) del archivo se tratará como la fila de encabezado y los valores de esa fila se utilizarán como nombres de columna.

También hemos especificado que el caso del encabezado no significa mucho para nosotros, convirtiendo el formato en uno que no distingue entre mayúsculas y minúsculas.

Finalmente, también le hemos dicho al analizador que recorte los registros, lo que elimina los espacios en blanco redundantes desde el principio y el final de los valores, si los hay. Algunas de las otras opciones con las que puede jugar son opciones como:

1
2
3
4
CSVFormat.DEFAULT
    .withDelimiter(',')
    .withQuote('"')
    .withRecordSeparator("\r\n")

Estos se usan si desea cambiar el comportamiento predeterminado, como establecer un nuevo delimitador, especificar cómo tratar las comillas, ya que a menudo pueden romper la lógica de análisis y especificar el separador de registros, presente al final de cada registro.

Finalmente, una vez que hayamos cargado el archivo y lo hayamos analizado con esta configuración, puede recuperar CSVRecords como se vio anteriormente:

1
2
3
4
5
6
7
for (csvRecord in csvParser) {
    val studentId = csvRecord.get("StudentId");
    val studentName = csvRecord.get("FirstName);
    val studentLastName = csvRecord.get("LastName);
    var studentScore = csvRecord.get("Score);
    println(Student(studentId, studentName, studentLastName, studentScore));
}

Este es un enfoque mucho más indulgente, ya que no necesitamos saber el orden de las columnas. Incluso si se modifican en un momento dado, CSVParser nos tiene cubiertos.

Ejecutar este código también da como resultado:

1
2
3
Student(studentId=101, firstName=John, lastName=Smith, score=90)
Student(studentId=203, firstName=Mary, lastName=Jane, score=88)
Student(studentId=309, firstName=John, lastName=Wayne, score=96)

Escribir un archivo CSV en Kotlin

Similar a la lectura de archivos, también podemos escribir archivos CSV usando Apache Commons. Esta vez, usaremos CSVPrinter.

Así como el CSVReader acepta un BufferedReader, el CSVPrinter acepta un BufferedWriter y el CSVFormat que nos gustaría usar al escribir el archivo.

Vamos a crear un BufferedWriter e instanciar una instancia de CSVPrinter:

1
2
3
4
val writer = new BufferedWriter(Paths.get("/resources/students.csv"));

val csvPrinter = CSVPrinter(writer, CSVFormat.DEFAULT
                     .withHeader("StudentID", "FirstName", "LastName", "Score"));

El método printRecord(), de la instancia CSVPrinter se utiliza para escribir registros. Acepta todos los valores para ese registro y lo imprime en una nueva línea. Llamar al método una y otra vez nos permite escribir muchos registros. Puede especificar cada valor en una lista o simplemente pasar una lista de datos.

No hay necesidad de usar el método printRecord() para la fila del encabezado en sí, ya que ya lo hemos especificado con el método withHeader() de CSVFormat. Sin especificar el encabezado allí, hubiéramos tenido que imprimir la primera fila manualmente.

En general, puede usar csvPrinter así:

1
2
3
csvPrinter.printRecord("123", "Jane Maggie", "100");
csvPrinter.flush();
csvPrinter.close();

No olvide “lavar ()” y “cerrar ()” la impresora después de su uso.

Dado que estamos trabajando con una lista de estudiantes aquí, y no podemos simplemente imprimir el registro de esta manera, recorreremos la lista de estudiantes, pondremos su información en una nueva lista e imprimiremos esa lista de datos usando el método printRecord():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
val students = listOf(
    Student(101, "John", "Smith", 90), 
    Student(203, "Mary", "Jane", 88), 
    Student(309, "John", "Wayne", 96)
);

for (student in students) {
    val studentData = Arrays.asList(
            student.studentId,
            student.firstName,
            student.lastName,
            student.score)

    csvPrinter.printRecord(studentData);
}
csvPrinter.flush();
csvPrinter.close();

Esto da como resultado un archivo CSV, que contiene:

1
2
3
4
StudentID,FirstName,LastName,Score
101,John,Smith,90
203,Mary,Jane,88
309,John,Wayne,96

Conclusión

En este tutorial, hemos repasado cómo leer y escribir archivos CSV en Kotlin, utilizando la biblioteca Apache Commons.