Datos en archivos
En resumen: CSV es un archivo de texto en el que cada línea es un registro y los valores van separados por comas; el programa divide las líneas en partes y las convierte en objetos.
Problema
Texto, pero necesitas números
El almacén exporta sus productos a un archivo. Cada fila es un producto: nombre, precio y existencias, separados por comas. La primera fila dice qué hay en cada columna:
Nombre,Precio,ExistenciasPan,1.20,30Leche,2.10,18Café en grano,18.90,12Ya sabes leer el archivo. Pero para el programa, la fila es solo texto:
String line = "Pan,1.20,30";System.out.println(line.length());System.out.println(line.charAt(4));111No sabe que 1.20 es un precio y que 30 es una cantidad. Para calcular cuánto vale la mercancía, tienes que convertir cada fila en algo con campos: un objeto Product.
La idea
CSV: de fila a objeto
Un archivo así se llama CSV (comma-separated values, «valores separados por comas»). Lo lee Excel, lo leen las bases de datos, lo lee cualquier programa. Una fila se convierte en objeto en tres pasos: split, convertir cada parte al tipo que necesitas y el constructor.
class Product { String name; double price; int stock;
Product(String name, double price, int stock) { this.name = name; this.price = price; this.stock = stock; }
static Product fromCsv(String line) { String[] f = line.split(","); if (f.length != 3) { throw new IllegalArgumentException("esperaba 3 columnas, pero hay " + f.length); } return new Product(f[0].strip(), Double.parseDouble(f[1].strip()), Integer.parseInt(f[2].strip())); }
String toCsv() { return name + "," + price + "," + stock; }}Product p = Product.fromCsv("Café en grano,18.90,12");System.out.printf("%s: %.2f € x %d = %.2f €%n", p.name, p.price, p.stock, p.price * p.stock);Café en grano: 18.90 € x 12 = 226.80 €fromCsv es el único lugar que sabe cómo es la fila. Si el formato cambia, cambias solo eso. La comprobación del número de columnas lanza IllegalArgumentException con un mensaje claro, en lugar de que el programa se detenga más tarde con un índice fuera del array.
Qué hace fromCsv con la fila, paso a paso, con su cuerpo desarrollado en main:
String line = "Café en grano,18.90,12";String[] f = line.split(",");String name = f[0];double price = Double.parseDouble(f[1]);int stock = Integer.parseInt(f[2]);Product p = new Product(name, price, stock);System.out.println(p.name + ": " + p.price + " € x " + p.stock);System.out.printf("%.2f €%n", p.price * p.stock);Paso a paso, en palabras
- La fila del archivo es una sola cadena. Para el programa, el 18.90 que hay en ella son solo cuatro caracteres.
- split corta la fila en cada coma: un array de tres cadenas.
- El nombre sigue siendo una cadena.
- parseDouble convierte la cadena "18.90" en el número 18.9. Ya se puede calcular con él.
- parseInt saca de "12" el número entero 12.
- El constructor pone los tres valores en los campos de un nuevo Product, y p apunta a él.
- p.price se imprime como 18.9: el cero del final solo estaba en el texto.
- El precio por las existencias: ahora es un cálculo con números.
Míralo
El archivo completo
Path file = Path.of("products.csv");try { Files.writeString(file, "Nombre,Precio,Existencias\nPan,1.20,30\nLeche,2.10,18\nCafé en grano,18.90,12\n"); List<String> lines = Files.readAllLines(file); Product[] products = new Product[lines.size() - 1]; int count = 0; boolean header = true; for (String line : lines) { if (header) { header = false; continue; } products[count] = Product.fromCsv(line); count++; } double value = 0; for (int i = 0; i < count; i++) { value += products[i].price * products[i].stock; } System.out.printf("%d productos, existencias por valor de %.2f €%n", count, value);} catch (IOException e) { System.out.println("No puedo leer " + file + ": " + e.getMessage());}3 productos, existencias por valor de 300.60 €- El array tiene un elemento menos que las filas, porque la primera fila es el encabezado.
headerestruesolo en la primera vuelta. Entonces la fila se salta concontinue.countcuenta cuántos productos hay en el array hasta ahora, y es el índice para el siguiente.
En el módulo «Colecciones» verás una lista que crece sola, y no te hará falta contar con count.
La idea
Filas incorrectas
Los archivos los escriben personas y otros programas. Tarde o temprano llega una fila a la que le falta una columna, con una palabra en lugar de un número, o una fila vacía:
Nombre,Precio,ExistenciasPan,1.20,30Leche,2.10Café,siete,4
Té,3.50,25Un error no debe detener la carga de todo lo demás. Cada fila va en su propio try, y el mensaje da el número de fila, para que alguien pueda corregirla:
Path file = Path.of("products.csv");try { Files.writeString(file, "Nombre,Precio,Existencias\nPan,1.20,30\nLeche,2.10\nCafé,siete,4\n\nTé,3.50,25\n"); List<String> lines = Files.readAllLines(file); Product[] products = new Product[lines.size()]; int count = 0; int row = 0; for (String line : lines) { row++; if (row == 1 || line.isBlank()) { continue; } try { products[count] = Product.fromCsv(line); count++; } catch (NumberFormatException e) { System.out.println("Fila " + row + ": número incorrecto (" + e.getMessage() + ")"); } catch (IllegalArgumentException e) { System.out.println("Fila " + row + ": " + e.getMessage()); } } System.out.println("Cargados: " + count);} catch (IOException e) { System.out.println("No puedo leer " + file + ": " + e.getMessage());}Fila 3: esperaba 3 columnas, pero hay 2Fila 4: número incorrecto (For input string: "siete")Cargados: 2- Las filas vacías se saltan con
isBlank(). NumberFormatExceptiones una subclase deIllegalArgumentException, así que va primero. Si no, elcatchgeneral la atraparía también.- El programa dice «Cargados: 2» y sigue con ellos.
Pruébalo
Columnas vacías
¿Qué imprimirá el programa?
System.out.println("Café,,4".split(",").length);System.out.println("Café,7.50,".split(",").length);System.out.println("Café,7.50,".split(",", -1).length);,,) se queda como "". split quita las partes vacías del final, así que "Café,7.50," da solo 2 partes, y f[2] lanzaría una excepción. Con un segundo argumento -1, las partes vacías del final se quedan. En fromCsv esto significa: una fila con la última columna vacía cuenta como una fila con 2 columnas.La idea
De objetos a CSV
La otra dirección es un método que arma la fila:
String toCsv() { return name + "," + price + "," + stock;}Escribes la fila de encabezado y después una fila por cada objeto:
Product[] stock = { new Product("Pan", 1.2, 30), new Product("Leche", 2.1, 18) };Path file = Path.of("stock.csv");try { try (BufferedWriter out = Files.newBufferedWriter(file)) { out.write("Nombre,Precio,Existencias"); out.newLine(); for (Product p : stock) { out.write(p.toCsv()); out.newLine(); } } System.out.print(Files.readString(file));} catch (IOException e) { System.out.println("No puedo escribir " + file + ": " + e.getMessage());}Nombre,Precio,ExistenciasPan,1.2,30Leche,2.1,18fromCsv puede volver a leer este archivo. Guardar y leer tienen que usar el mismo formato.
Cuidado si guardas los precios con String.format. Sigue la configuración de idioma de la computadora, y en español, alemán y muchos otros idiomas la marca decimal es una coma:
String es = String.format(Locale.of("es"), "%s,%.2f,%d", "Pan", 1.2, 30);System.out.println(es + " -> " + es.split(",").length + " columnas");String root = String.format(Locale.ROOT, "%s,%.2f,%d", "Pan", 1.2, 30);System.out.println(root + " -> " + root.split(",").length + " columnas");Pan,1,20,30 -> 4 columnasPan,1.20,30 -> 3 columnas1,20 se convierte en dos columnas, y las filas se desplazan. Para archivos que leerá otro programa, pasa Locale.ROOT (java.util.Locale). Siempre da un punto, en cualquier computadora.
Pruébalo
Encuentra el error
El programa debería cargar los tres productos de products.csv, pero se detiene ya en la primera fila:
Exception in thread "main" java.lang.NumberFormatException: For input string: "Precio"Elige la línea con el error.
row pasa a ser 1 ya en la primera vuelta, antes de la comprobación, así que row == 0 nunca es verdadero. La fila de encabezado no se salta, y fromCsv intenta convertir la palabra «Precio» en un número. La corrección es row == 1.
La idea
Columnas por nombre
Otro programa exporta los mismos datos, pero con las columnas en otro orden y con una más. Si escribes f[1] para el precio, con un formato nuevo leerás la columna equivocada y no te darás cuenta. Es más seguro buscar la columna por su nombre en la fila de encabezado:
static int column(String[] header, String name) { for (int i = 0; i < header.length; i++) { if (header[i].strip().equalsIgnoreCase(name)) { return i; } } throw new IllegalArgumentException("No existe la columna " + name);}
Path file = Path.of("export.csv");try { Files.writeString(file, "Existencias,Nombre,Código,Precio\n30,Pan,1001,1.20\n18,Leche,1002,2.10\n"); int nameCol = -1; int priceCol = -1; for (String line : Files.readAllLines(file)) { String[] f = line.split(","); if (nameCol == -1) { nameCol = column(f, "nombre"); priceCol = column(f, "precio"); continue; } System.out.println(f[nameCol] + ": " + f[priceCol] + " €"); }} catch (IOException e) { System.out.println(e.getMessage());}Pan: 1.20 €Leche: 2.10 €La primera vuelta solo busca dónde están las columnas. Después, cada fila se lee por esos índices. Si la columna no existe, column lanza una excepción enseguida, en lugar de leer datos equivocados.
Más precisamente, para curiosos
- Una coma en el valor. «Queso, curado» lleva una coma. En CSV un valor así va entre comillas:
"Queso, curado",9.80,5, y una comilla dentro se escribe doble:"". Entoncessplit(",")ya no basta. En proyectos reales se usa una biblioteca como OpenCSV o Apache Commons CSV, que conoce todas las reglas. - Otro separador. Excel en una computadora con la configuración en español o en alemán suele guardar el CSV con
;, precisamente porque la coma es la marca decimal. - Serialización. Java puede escribir un objeto entero en un archivo con
ObjectOutputStream, si la clase implementaSerializable. El archivo es entonces binario: una persona no puede leerlo, y se rompe cuando cambia la clase. Los formatos de texto como CSV y JSON son más predecibles, y por eso se usan más.
Ejercicio
Datos que cobran vida
Resuelve las tareas en tu editor. El programa crea primero él mismo el archivo de entrada.
1. Análisis de un CSV. Crea people.csv con el contenido de abajo. Imprime la edad media (división entera), quién es la persona de más edad y cuántas personas hay de cada ciudad, en el orden en que aparecen las ciudades. Guarda las ciudades en un array String[], y las cantidades en un int[] del mismo tamaño.
Nombre,Edad,CiudadCarlos,34,MadridMaría,28,SevillaJorge,41,MadridElena,25,ValenciaEdad media: 32De más edad: Jorge, 41Personas de Madrid: 2Personas de Sevilla: 1Personas de Valencia: 12. Guardar al héroe. Una clase Hero con nombre, nivel, vida y oro. El método save(Path file) guarda al héroe como líneas clave=valor, y el método estático load(Path file) crea un Hero nuevo a partir de un archivo así. Crea a «Arin» con nivel 6, 100 de vida y 120 de oro, súbele el nivel en 1, quítale 15 de vida, dale 220 de oro, guárdalo, imprime el archivo y vuelve a cargarlo.
name=Arinlevel=7health=85gold=340Cargado: Arin, nivel 7, vida 85, oro 3403. Texto duplicado. Crea text.txt con el texto de abajo. Divídelo en oraciones por el punto (y los espacios que lo siguen), e imprime una sola vez cada oración que aparece más de una vez.
Esto es texto. Esto también es texto. Esto es texto.Texto duplicado: "Esto es texto."Más tareas
- El producto más caro. Carga
products.csvde la lección, e imprime el producto con el precio más alto y cuántos productos tienen menos de 20 en existencias. - Filas corregidas. Carga el archivo con las filas incorrectas, y escribe las buenas en
products-clean.csv, y las incorrectas, con el número de fila y el motivo, enrejected.txt. - Informe de ventas. Un archivo
sales.csvcon las columnasFecha,Producto,Cantidad,Precio. Las filas están ordenadas por fecha. Imprime los ingresos de cada día con String.format en columnas.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Lanzar excepciones y crear las tuyas · Cadenas · Expresiones regulares · Archivos
- Continúa en: Bases de datos
- Ejercicios sobre el tema: Texto y archivos