1. Fundamentos de CSV y TSV
Si alguna vez has abierto Excel/Google Sheets, ya entiendes intuitivamente que son los «datos tabulares»: filas, columnas, valores. CSV/TSV es un intento de guardar una tabla asi en texto plano, para poder enviarla por correo, almacenarla en un repositorio y leerla incluso sin formatos «pesados». En esencia, es una «tabla para pobres», pero a veces eso es exactamente lo que hace falta.
CSV significa comma-separated values — valores separados por comas. TSV significa tab-separated values — valores separados por tabulacion (\t). Y ambos formatos tienen una filosofia simple: una linea del archivo suele corresponder a una fila de la tabla, y dentro de la fila los campos se separan por un unico simbolo elegido.
Por ejemplo, un TSV con datos de libros puede verse asi:
id title author year
1 Clean Code Robert C. Martin 2008
2 The Pragmatic Programmer Andrew Hunt 1999
Con CSV es exactamente lo mismo, solo que el separador es la coma:
id,title,author,year
1,Clean Code,Robert C. Martin,2008
2,The Pragmatic Programmer,Andrew Hunt,1999
2. Cuando CSV/TSV sirven y cuando no
CSV/TSV son muy tentadores: es solo texto, se puede ver facilmente con los ojos, se puede commitear al git sin problema y se puede «parsear rapidamente» con split. Y precisamente por eso los principiantes intentan meter en CSV absolutamente todo: desde objetos complejos hasta «cualquier texto de usuario», incluyendo saltos de linea y comas. A partir de ahi empiezan las historias divertidas y los bugs tristes, y al final descubres de repente que has escrito la mitad de un parser de CSV real... y aun asi sigue siendo incorrecto.
Para mantener el sentido comun, conviene tener en mente una regla simple: CSV/TSV son buenos cuando los datos son realmente tabulares y sencillos. Son adecuados para importacion/exportacion, datasets de prueba, logs en forma de lineas (si controlas el formato) y pequeñas bases de datos de referencia. Se convierten en una mala eleccion cuando aparecen estructuras anidadas, texto arbitrario (donde el usuario puede escribir lo que sea), necesidad de guardar saltos de linea dentro de un solo campo o requisitos estrictos de compatibilidad con los «verdaderos» parsers de CSV del mundo de la contabilidad.
A continuacion hay una pequena tabla-guia para que el cerebro no se sobrecaliente:
| Escenario | CSV/TSV sirven? | Por que |
|---|---|---|
| Conjunto de datos de prueba para desarrollo | Si | Facil de editar y leer con la vista |
| Exportar una lista de libros «para personas» | Si | Una fila = un libro, comodo |
| Importacion/exportacion entre Sheets y CLI | Si (a menudo) | Sheets soporta CSV/TSV de fabrica |
| Los campos contienen texto arbitrario del usuario (incluyendo comas/saltos de linea) | Mas bien no | Empiezan las comillas/escapado/campos multilinea |
| Los datos son complejos: arrays, anidamiento, distintos tipos en un solo campo | No | El modelo tabular se rompe |
| Se necesita una especificacion estricta y compatibilidad con la industria | Con cuidado | «CSV simple» y «CSV real» son bestias distintas |
3. CSV/TSV simplificado para el curso
Reglas del CSV/TSV simple
Para que CSV/TSV sean predecibles, necesitamos un contrato: un conjunto de reglas. Aqui es importante dar un paso atras y decir honestamente: «Soportamos exactamente tanto como podemos explicar y comprobar, y todo lo demas queda fuera».
En esta leccion fijamos un modelo maximo practico y limitado, que encaja bien con un proyecto educativo y con muchas utilidades CLI reales. Suena aburrido, pero funciona. Y si, este es uno de esos casos en los que el aburrimiento es un cumplido.
Reglas del CSV/TSV simple en el curso:
| Regla | Que significa |
|---|---|
| Una linea del archivo = un registro | Por ejemplo, un libro = una linea |
| Los campos se separan por un simbolo | CSV: ,; TSV: \t |
| No hay comillas ni escapado | No soportamos "field,with,comma" como un solo campo |
| Los campos no contienen saltos de linea | Si un campo contiene \n, el formato «se rompe» |
| Permitimos encabezado (header) | La primera linea puede ser id,title,author,year |
| Se pueden recortar los espacios alrededor de los campos | " Bob " lo convertimos en "Bob" |
Por que simplificamos asi? Porque en Swift separar una cadena en partes es realmente facil (split), pero un «CSV correcto» exige rapidamente muchas reglas adicionales. En la biblioteca estandar existe el conveniente split(separator:), y trimmingCharacters(in:) ayuda a normalizar los espacios alrededor de los datos. Pero ningun split te salva si dentro del campo pueden haber comas, comillas y saltos de linea: ahi ya hace falta un analisis completo.
Por que TSV suele ser mas tranquilo que CSV
En papel, CSV parece «natural»: coma, valores, todo bonito. Pero en datos reales la coma aparece constantemente: apellidos-nombre-patronimico, nombres de organizaciones, subtitulos, «Estambul, Turquia». Y si no usas comillas y escapado, CSV empieza a deshacerse en el mismo sitio.
TSV en este sentido es un poco mas «zen»: la tabulacion (\t) aparece raramente en el texto humano normal. Por eso, si hemos acordado no soportar comillas, TSV suele ser mas seguro: hay menos posibilidades de que el separador aparezca de repente dentro del campo.
Tambien hay una desventaja: la tabulacion es visualmente «invisible». Abres el archivo en un editor simple y no siempre entiendes enseguida donde termina un campo y empieza otro. Pero eso se puede resolver: muchos editores muestran los tabs, y TSV tambien se abre muy bien en hojas de calculo.
La conclusion practica para el curso es esta: si controlas el formato, usa TSV para datos donde potencialmente pueda haber comas en el texto. Dejamos CSV como formato «para el mundo exterior», porque les gusta a las tablas y a las integraciones.
4. Practica: analisis y exportacion
Analizar una linea: split, trim y comprobacion de campos
Ahora vamos a construir la microfuncion mas util del dia: «toma una linea → separa en campos → normaliza espacios → comprueba que el numero de campos es el esperado». Es un paso pequeno, pero convierte texto caotico en una estructura con la que se puede trabajar sin miedo a salirte del indice y sin ganas de dejarlo todo e irte a cultivar un huerto.
En Swift, para dividir una cadena es comodo usar split(separator:): devuelve las partes, y luego las convertimos cuidadosamente en String y aplicamos trimmingCharacters(in:) para quitar espacios sobrantes. Lo mas importante es no olvidar la validacion: si no hay tantos campos como esperamos, la linea esta «rota» y hay que omitirla o convertirla en error.
import Foundation
func parseSimpleRow(_ line: String, separator: Character, expectedCount: Int) -> [String]? {
let parts = line.split(separator: separator, omittingEmptySubsequences: false)
let fields = parts.map { String($0).trimmingCharacters(in: .whitespaces) }
guard fields.count == expectedCount else { return nil }
return fields
}
Aqui hay un detalle importante: omittingEmptySubsequences: false significa que la cadena a,,b dara tres campos ("a", "", "b"), y no dos. Para datos tabulares, esto suele ser mas util: un campo vacio sigue siendo un campo, solo que vacio.
Otro pequeno ejemplo con comprobacion y diagnostico mediante print:
import Foundation
let line = "1\tClean Code\tRobert C. Martin\t2008"
if let fields = parseSimpleRow(line, separator: "\t", expectedCount: 4) {
print(fields[1]) // Clean Code
} else {
print("Bad row:", line)
}
TSV con encabezado: “columna → valor”
Cuando aparece un encabezado, la vida se vuelve a la vez mas facil y mas dificil. Mas facil, porque ya no dependes de indices magicos como fields[2], y el codigo empieza a leerse de forma humana. Mas dificil, porque hay que emparejar nombres de columnas con valores y manejar casos en los que el encabezado esta roto o es inesperado.
En proyectos CLI educativos, un encabezado casi siempre compensa: puedes añadir una nueva columna sin romper el codigo antiguo, o al menos obtener un error comprensible. Haremos una funcion sencilla: la primera linea es el header, las demas son datos. El resultado es un array de diccionarios [String: String].
import Foundation
func parseTSVWithHeader(_ text: String) -> [[String: String]] {
let lines = text.split(whereSeparator: { $0.isNewline })
guard let headerLine = lines.first else { return [] }
let header = headerLine.split(separator: "\t").map { String($0) }
var rows: [[String: String]] = []
for line in lines.dropFirst() {
let values = line.split(separator: "\t").map { String($0) }
guard values.count == header.count else { continue }
var row: [String: String] = [:]
for i in 0..<header.count { row[header[i]] = values[i] }
rows.append(row)
}
return rows
}
Esta es una version minimalista: «las lineas rotas se omiten». En una aplicacion real, posiblemente registrarias esas lineas y mostrarias al usuario estadisticas: cuantas lineas se leyeron, cuantas se omitieron. Pero la idea «header → map» ya te da una API agradable.
Importacion en CLI: libros desde TSV
Ahora vamos a enlazar con cuidado CSV/TSV a nuestra aplicacion CLI (al estilo de LibraryCLI). Todavia no estamos construyendo una «arquitectura de almacenamiento ideal»: el objetivo del dia es mas simple: mostrar como el texto se convierte en datos y donde poner las comprobaciones de formato. Imaginemos que nos llega books.tsv, y queremos leerlo y obtener un array de «borradores de libros», que luego se pueden añadir al sistema.
Hagamos un modelo de entrada muy simple: BookDraft. Si, no es un «modelo de dominio ideal», pero para la importacion esta bien: la importacion casi siempre crea primero borradores y luego los valida y los convierte en entidades reales.
import Foundation
struct BookDraft {
let title: String
let author: String
let year: Int
}
func bookDraft(from row: [String: String]) -> BookDraft? {
guard let title = row["title"], !title.isEmpty else { return nil }
guard let author = row["author"], !author.isEmpty else { return nil }
guard let yearStr = row["year"], let year = Int(yearStr) else { return nil }
return BookDraft(title: title, author: author, year: year)
}
Fijate: aqui no hay !. No confiamos en el archivo. El archivo es el mundo exterior, y el mundo exterior a veces trae sorpresas (casi siempre los viernes por la noche).
Ahora unamos esto con la lectura del archivo en una cadena (lo que hiciste en las lecciones anteriores del dia) y apliquemos nuestro parser:
import Foundation
func importBooksTSV(from url: URL) throws -> [BookDraft] {
let text = try String(contentsOf: url, encoding: .utf8)
let rows = parseTSVWithHeader(text)
var result: [BookDraft] = []
for row in rows {
if let draft = bookDraft(from: row) {
result.append(draft)
}
}
return result
}
Si quieres un diagnostico minimo, puedes contar cuantas lineas «se cayeron»:
import Foundation
func importBooksTSVWithStats(from url: URL) throws -> [BookDraft] {
let text = try String(contentsOf: url, encoding: .utf8)
let rows = parseTSVWithHeader(text)
var ok = 0
var drafts: [BookDraft] = []
for row in rows {
if let d = bookDraft(from: row) { drafts.append(d); ok += 1 }
}
print("Imported:", ok, "Skipped:", rows.count - ok) // Imported: X Skipped: Y
return drafts
}
Exportacion a TSV/CSV: “serializacion” sin magia
Importar es la mitad de la vida. La otra mitad es exportar, y a menudo la exportacion la necesitas incluso para ti mismo: para ver rapidamente el estado de los datos, depurar filtros, pasar una tabla a colegas o simplemente hacer una «captura» en forma de texto.
La exportacion en TSV simple se parece casi a unir cadenas con join: tomamos el encabezado, luego para cada libro construimos una linea con \t entre los campos y añadimos \n. Aqui hay una honestidad de ingenieria importante: como no soportamos comillas ni escapado, debemos decidir que hacer si en un campo aparece de repente un tab o un salto de linea. La opcion mas sensata para un formato simple es o bien prohibir esos simbolos (y devolver un error), o bien sustituirlos por un espacio.
Para el proyecto de aprendizaje haremos una normalizacion suave: sustituiremos \t y \n por espacios, para que el archivo no se rompa.
import Foundation
func safeField(_ value: String) -> String {
value
.replacingOccurrences(of: "\t", with: " ")
.replacingOccurrences(of: "\n", with: " ")
}
func exportBooksTSV(_ books: [BookDraft]) -> String {
var lines: [String] = ["title\tauthor\tyear"]
for b in books {
lines.append("\(safeField(b.title))\t\(safeField(b.author))\t\(b.year)")
}
return lines.joined(separator: "\n") + "\n"
}
Y ahora la escritura en archivo (repetimos la idea de las lecciones anteriores del dia, pero ya con texto con sentido):
import Foundation
func saveTSV(_ text: String, to url: URL) {
do {
try text.write(to: url, atomically: true, encoding: .utf8)
print("Saved TSV") // Saved TSV
} catch {
print("Save failed:", error)
}
}
5. Por que el “CSV real” es mas complicado
Da muchas ganas de creer que CSV es «una coma entre valores» y ya esta. Pero el CSV real vive en un mundo donde dentro de un campo puede haber una coma, una comilla e incluso un salto de linea. Para que eso funcione, aparecen reglas: comillas alrededor de los campos, duplicacion de comillas dentro del texto, distintos dialectos y, a veces, expectativas diferentes en programas distintos. Y de repente ya no estas escribiendo «un parser de 10 lineas», sino construyendo un mini compilador para tablas. Divertido, pero eso ya es otro nivel de responsabilidad.
Dentro del dia actual, nuestra meta no es vencer todas las especificaciones del mundo. Nuestra meta es aprender a limitar el formato de forma consciente y escribir el codigo de manera que o bien analice correctamente los datos, o bien se niegue correctamente a analizarlos. En ingenieria, esta es una de las habilidades mas adultas: no «hacer como si todo funcionara», sino fijar explicitamente los limites.
Si necesitas compatibilidad con archivos CSV complejos de sistemas externos, la solucion correcta normalmente no es «anadir un par mas de split», sino usar un parser/biblioteca especializada o acordar de antemano TSV/un formato de exportacion simplificado.
6. Errores tipicos al trabajar con CSV/TSV
Error n.º 1: creer que split es un parser CSV completo.
split(separator:) separa muy bien una cadena por un simbolo, pero no sabe nada de comillas, escapado ni campos multilinea. Si tus datos pueden contener el separador dentro del campo, el «CSV simple» deja de ser simple. La salida correcta es o bien endurecer el formato (TSV sin tabs dentro), o bien usar un parser CSV real.
Error n.º 2: no comprobar el numero de campos y fallar por indice.
Clasico: haces fields[3], y la linea resulto mas corta porque alguien borro una columna o se salto un separador. Esos errores son especialmente desagradables en CLI, porque el usuario recibe una finalizacion anomala en lugar de un mensaje claro. Incluso una comprobacion simple fields.count == expected salva muchas neuronas.
Error n.º 3: ignorar los espacios alrededor de los valores.
En formatos tabulares, los espacios alrededor de los separadores son algo normal: "Bob, 20, London". Si no normalizas los campos, luego se rompen las comparaciones de cadenas y la busqueda: "Bob" y " Bob" son distintos para el programa. trimmingCharacters(in:) es un habito pequeno, pero muy practico.
Error n.º 4: mezclar la lectura del archivo, el analisis de lineas y la validacion de negocio en un solo lugar “monolitico”.
Cuando todo esta en una sola funcion, dejas de entender rapidamente que ha fallado exactamente: ¿no se pudo leer el archivo, la linea esta rota o el año no se pudo analizar? Mucho mas tranquilo es primero obtener el texto, luego convertirlo en lineas/campos y solo despues convertir los campos en tipos y aplicar reglas.
Error n.º 5: no acordar una politica para las “malas lineas”.
Si una linea no se pudo analizar, ¿que haces: la omites, la consideras error, la registras y sigues? Cualquier opcion es valida, pero no lo es la opcion «como salga». En CLI es especialmente importante que el comportamiento sea predecible: el usuario debe entender si se perdieron datos o si la importacion se detuvo.
GO TO FULL VERSION