El CSV no es «solo texto plano»: guía práctica del manejo de CSV en aplicaciones empresariales con C# (codificación de caracteres, compatibilidad con Excel, protección contra inyección)

· · CSharp, .NET, CSV, Excel, Codificación de caracteres, Integración de archivos, Desarrollo Windows, Consultoría técnica

La integración de datos mediante CSV sigue siendo, hoy en día, uno de los protagonistas indiscutibles en el mundo de las aplicaciones empresariales. Y precisamente porque parece «solo texto unido por comas», es también un terreno donde se repiten incidentes: un parser casero que se rompe con una coma dentro de un campo, corrupción de caracteres al abrir el archivo en Excel, o la desaparición del cero inicial en un número de teléfono.

En este artículo, partiendo del supuesto de una aplicación empresarial en C# que lee y escribe CSV, organizamos -en el orden en que suelen surgir las dudas en la práctica- la especificación del CSV (RFC 4180) y sus dialectos reales, cómo decidir la codificación de caracteres, los problemas de compatibilidad con Excel, las medidas contra la inyección CSV y las trampas operativas.

1. La conclusión primero (tabla de decisión)

Las decisiones de diseño sobre CSV se derivan, en sentido inverso, de «quién (o qué) va a abrir ese archivo».

Uso Primera opción de codificación Puntos clave de diseño
Una persona lo abre haciendo doble clic en Excel UTF-8 (con BOM) Sin BOM, hay entornos en los que Excel interpreta mal la codificación y se produce corrupción de caracteres. El problema de la pérdida de ceros iniciales y la conversión a fecha no se puede evitar mediante la codificación (capítulo 5)
Integración nueva entre sistemas UTF-8 (sin BOM) Especificar en el documento de especificaciones la codificación, las comillas, el código de salto de línea y si hay fila de encabezado. Indicar también explícitamente si hay o no BOM
Integración con un sistema heredado Seguir la especificación de la contraparte (a menudo Shift_JIS) En .NET (Core) es necesario registrar CodePagesEncodingProvider para usar Shift_JIS (capítulo 4). Acordar de antemano el tratamiento de los caracteres dependientes del equipo
Integración por lotes con grandes volúmenes de datos Seguir la especificación Procesar en streaming línea a línea sin cargar todo en memoria (capítulo 7). Diseñar también el control de exclusión y el tratamiento de archivos a medio escribir
El propósito principal es que una persona lo vea o lo imprima en Excel Considerar directamente generar en xlsx en lugar de CSV (ver FAQ)

Adelantamos las conclusiones.

  • El parseo de CSV mediante String.Split(',') se rompe según la especificación, aunque parezca funcionar. Esto se debe a que no puede procesar comas, saltos de línea ni comillas dentro de un campo. Dentro del estándar de .NET, use TextFieldParser (capítulo 3).
  • Los incidentes de codificación de caracteres ocurren por «cómo los interpreta Excel». Para un CSV que una persona abrirá en Excel, la elección segura actual es UTF-8 con BOM. Además, existe una trampa: en .NET, si se añade o no el BOM depende de cómo se especifique el Encoding (capítulo 4).
  • En .NET (Core), Shift_JIS no se puede usar sin un paquete adicional. Solo tras registrar CodePagesEncodingProvider del paquete System.Text.Encoding.CodePages tiene éxito Encoding.GetEncoding("shift_jis"). Es un punto clásico donde un procesamiento de CSV migrado desde .NET Framework lanza una excepción por primera vez en producción.
  • La pérdida de ceros iniciales y la conversión a fecha no se pueden evitar desde el lado del CSV. Un hecho importante es que ni siquiera encerrar el valor entre comillas dobles lo evita (capítulo 5).
  • Las aplicaciones que generan CSV con entradas del usuario necesitan protección contra la inyección CSV. Excel interpreta como fórmula cualquier campo que empiece con = o + (capítulo 6).

2. La «especificación» del CSV: RFC 4180 y los dialectos reales

CSV cuenta, de facto, con un estándar: RFC 41801. En la práctica basta con tener presentes estas cuatro reglas.

  1. El separador de registros es el salto de línea (CRLF) y el separador de campos es la coma.
  2. Si un campo contiene una coma, un salto de línea o una comilla doble, todo el campo debe encerrarse entre comillas dobles.
  3. Una comilla doble dentro de un campo se escapa duplicándola: "".
  4. Un campo que no necesita ser encerrado puede encerrarse o no, indistintamente.

Es decir, la siguiente línea es un registro de tres campos.

1001,"株式会社サンプル 営業部","住所は""東京都○○区1-2-3""です,と入力された備考"

Y RFC 4180 es un «estándar de facto»: en la realidad existen infinidad de dialectos de CSV. Saltos de línea solo con LF, ausencia de procesamiento de comillas, separadores de tabulación o punto y coma, presencia o no de fila de encabezado… Por eso, especificar en el documento de la integración la frase «conforme a RFC 4180» junto con la codificación de caracteres, el BOM, el código de salto de línea y si hay fila de encabezado es el trabajo con mejor relación costo-beneficio para evitar problemas futuros.

3. No hay que hacerlo casero con Split(‘,’): la práctica de la lectura

Teniendo en cuenta las reglas del capítulo anterior, queda claro por qué String.Split(',') se rompe: se divide de más por una coma dentro de un campo, y una línea se corta a mitad de camino por un salto de línea dentro de unas comillas. El hecho de que «funcione porque el archivo que llega ahora no tiene datos con comas» no significa nada: puede romperse en cualquier momento según los datos.

Si quiere leer correctamente dentro del estándar de .NET, puede usar Microsoft.VisualBasic.FileIO.TextFieldParser. El espacio de nombres es de Visual Basic, pero se usa con total normalidad desde C#.

using Microsoft.VisualBasic.FileIO; // Microsoft.VisualBasic.Core (incluido en el SDK para escritorio de Windows)

using var parser = new TextFieldParser(path, System.Text.Encoding.UTF8)
{
    TextFieldType = FieldType.Delimited,
    HasFieldsEnclosedInQuotes = true, // Procesa correctamente los campos encerrados entre comillas
    TrimWhiteSpace = false, // El valor predeterminado es true (elimina silenciosamente los espacios en blanco al inicio y al final). Desactivarlo en datos donde el espacio tiene significado
};
parser.SetDelimiters(",");

while (!parser.EndOfData)
{
    try
    {
        string[]? fields = parser.ReadFields();
        if (fields is null) break;
        // Procesar fields[0], fields[1], ...
    }
    catch (MalformedLineException ex)
    {
        // Línea corrupta: se puede registrar el contenido y el número de línea con ErrorLine / ErrorLineNumber
        logger.LogWarning("La línea {Line} del CSV tiene un formato incorrecto: {Content}",
            ex.LineNumber, parser.ErrorLine);
    }
}

La ventaja práctica de TextFieldParser es que, además del procesamiento de comillas (HasFieldsEnclosedInQuotes), permite detectar una línea corrupta como MalformedLineException junto con su número de línea. En la importación de CSV en un entorno empresarial, decidir si «al encontrar una línea corrupta se rechaza todo el archivo, o solo se omite esa línea y se informa» es un punto donde se bifurca la especificación, así que este diseño del manejo de excepciones se puede trasladar directamente a la especificación.

Si necesita además mapeo entre columnas y objetos, conversión de tipos y rendimiento a escala de millones de líneas, considere incorporar una librería de CSV con trayectoria contrastada (de las ampliamente usadas en NuGet). Sea cual sea la librería que elija, los puntos que hay que verificar son cuatro: «procesamiento de comillas según RFC 4180», «especificación de codificación de caracteres», «tratamiento de líneas corruptas» y «posibilidad de lectura en streaming».

En el lado de la escritura, como la especificación es más simple, desarrollarlo internamente es realista, pero asegúrese de concentrar el procesamiento de escape en una única función.

// Concentrar el escape del lado de la escritura en esta única función
static string ToCsvField(string? value)
{
    value ??= string.Empty;
    bool needsQuoting = value.Contains(',') || value.Contains('"')
        || value.Contains('\r') || value.Contains('\n');
    return needsQuoting ? $"\"{value.Replace("\"", "\"\"")}\"" : value;
}

Si queda aunque sea un solo punto donde se hace un string.Join manual solo para la columna del importe, ese punto se convertirá en el futuro escenario del incidente.

4. Codificación de caracteres: el comportamiento de Excel y las trampas de .NET

4.1 Para abrir en Excel, UTF-8 con BOM

La mayor parte de la corrupción de caracteres en CSV se produce porque Excel interpreta mal la codificación del archivo. Si se abre con doble clic un CSV en UTF-8 sin BOM (la marca de unos pocos bytes al inicio del archivo), en algunos entornos se interpreta como ANSI (Shift_JIS en entornos japoneses) y el texto en japonés queda completamente destruido. Con UTF-8 con BOM se reconoce correctamente como UTF-8, así que para un CSV que una persona abrirá con doble clic en Excel, la elección segura actual es UTF-8 con BOM.

Aquí se suma una trampa del lado de .NET. Que se añada o no el BOM depende de cómo se pase el Encoding.

// UTF-8 sin BOM: el valor predeterminado cuando se omite la especificación de codificación
File.WriteAllText(path, csv);

// UTF-8 con BOM: Encoding.UTF8 (propiedad estática) está configurado para emitir el BOM
File.WriteAllText(path, csv, Encoding.UTF8);

// Si quiere dejar la intención explícita, especificarlo mediante el constructor evita confusiones
File.WriteAllText(path, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));

Que el archivo resultante cambie según se pase Encoding.UTF8 o se omita la especificación es una particularidad que, si no se conoce, pasa desapercibida. Una vez que decida como especificación «sin BOM para la integración entre sistemas» y «con BOM para el destino en Excel», se recomienda dejar la intención explícita también en el código mediante new UTF8Encoding(...).

4.2 Para usar Shift_JIS en .NET (Core) hace falta un registro

En la integración con sistemas heredados, el CSV en Shift_JIS (página de códigos 932) sigue vigente hoy en día. Lo importante aquí es que, de manera predeterminada, .NET (Core) / .NET 5 en adelante solo admite las codificaciones Unicode y un pequeño conjunto como ASCII. Encoding.GetEncoding("shift_jis") lanza una excepción hasta que se agrega el paquete System.Text.Encoding.CodePages y se registra CodePagesEncodingProvider.

using System.Text;

// Registrar una sola vez al iniciar la aplicación (Program.cs, etc.)
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

// Con esto ya se puede obtener Shift_JIS (página de códigos 932)
Encoding sjis = Encoding.GetEncoding("shift_jis");

Es un punto clásico de incidente: una aplicación cuyo procesamiento de CSV se migró de la era de .NET Framework a .NET recibe por primera vez en producción un archivo en Shift_JIS y lanza una excepción. Los puntos de verificación al migrar se resumen en Lista de verificación previa a la migración de .NET Framework a .NET.

Además, mientras se use Shift_JIS, es inevitable lidiar con los caracteres dependientes del equipo (como ① o ㈱), el problema de la virgulilla (nami dash) y la pérdida de caracteres que solo existen en Unicode. Los fundamentos de la codificación de caracteres en esta materia se explican en detalle en Windows: codificación de caracteres y el mecanismo de la corrupción de texto y en Windows: la práctica de la codificación de caracteres y los códigos de salto de línea.

5. Datos que se corrompen al abrirlos en Excel: pérdida de ceros iniciales, conversión a fecha, notación exponencial

Incluso corrigiendo la codificación de caracteres, la visualización en Excel presenta otro grupo de problemas. Excel “interpreta” los valores al abrir un CSV.

Dato original Visualización en Excel Qué ocurrió
090123456xx (número de teléfono) 90123456xx Se interpreta como número y desaparece el cero inicial
007 (código de producto) 7 Lo mismo
1-1 (número de sufijo) 1 de enero Se interpreta como fecha
1234567890123456 (número de control de 16 dígitos) 1.23457E+15 Pasa a notación exponencial y se pierden los dígitos inferiores

Y lo importante es que esto no se evita ni siquiera encerrando el campo entre comillas dobles. Las comillas solo indican la separación sintáctica del CSV; Excel vuelve a interpretar el contenido de todas formas.

En la práctica, hay tres opciones de tratamiento.

  1. Que se abra siguiendo el procedimiento correcto de Excel. Si en lugar de doble clic se usa «Obtener datos (desde texto/CSV)» de Excel y se especifica el tipo de columna como «texto» al importar, no se corrompe. Vale la pena incluirlo en el manual de operación.
  2. Si el propósito principal es la visualización, generar en xlsx. Como el formato de celda se puede controlar desde el lado de la generación, este grupo de problemas desaparece de raíz. Para la forma de construir la generación de archivos de Excel, consulte Cómo construir la salida de informes en Excel. Además, la automatización de Excel mediante COM en servidores o servicios no se recomienda, ya que -como se explicó en El problema de que EXCEL.EXE permanece en ejecución- es un foco frecuente de procesos que quedan colgados, así que elija una generación basada en librerías.
  3. Generar en formato ="007" es el último recurso. Es un truco para que se muestre como texto al abrirlo en Excel, pero como CSV se convierte en un «archivo no estándar con fórmulas» que rompe cualquier destino de importación distinto de Excel. No lo use en archivos que se comparten con otros sistemas.

6. Inyección CSV: no convertir la entrada del usuario en una fórmula

Las aplicaciones que generan CSV con valores introducidos por el usuario (nombre, observaciones, dirección, etc.) para que se abran en Excel tienen una vía de ataque llamada inyección CSV. Excel interpreta como fórmula cualquier campo que empiece por =, +, - o @, por lo que una entrada maliciosa (por ejemplo, una fórmula orientada a lanzar un proceso externo como =cmd|'/C calc'!A0) puede llegar a ejecutarse en el entorno de otro usuario que abra el CSV.

La base de la protección consiste en neutralizar los caracteres iniciales peligrosos al generar el archivo.

// Añade una comilla simple al inicio de los campos que podrían interpretarse como fórmula.
// En entornos en japonés, los caracteres de ancho completo =+-@ también pueden interpretarse como inicio de fórmula, así que se incluyen
static string SanitizeForExcel(string value)
{
    if (value.Length > 0 && value[0]
        is '=' or '+' or '-' or '@' or '\t' or '\r' or '\n'
        or '=' or '+' or '-' or '@')
    {
        return "'" + value;
    }
    return value;
}

La comilla simple al inicio es la marca en Excel para «tratar como texto». Tenga en cuenta que la lista de caracteres iniciales peligrosos puede variar según la implementación y la versión de la hoja de cálculo, por lo que diseñar esta neutralización en el sentido de «dejar pasar tal cual solo si el inicio coincide con esta lista permitida (alfanuméricos, etc.)» resulta más robusto ante casos no previstos. No obstante, si otro sistema importa ese CSV, hay que tener presente que la comilla simple queda como parte del dato, por lo que también es posible optar por separar los archivos «para abrir en Excel» y «para importar a otro sistema». En cuanto a si conviene combinarlo con la validación del valor de entrada (por ejemplo, no permitir directamente un = inicial), consulte Lista de verificación mínima de seguridad para aplicaciones Windows para el diseño de seguridad de la aplicación en su conjunto.

7. Trampas operativas: dónde ocurren los incidentes más allá del formato

Por último, resumimos los puntos donde ocurren incidentes prácticos al margen de la propia sintaxis del CSV.

  • No depender de la configuración regional del sistema operativo para el formato de números y fechas. value.ToString() se ve afectado por la configuración regional del sistema operativo. En una configuración regional donde el separador decimal es la coma, se generaría 1,5, lo que choca con el separador del CSV. En las conversiones para la integración de archivos, especifique explícitamente CultureInfo.InvariantCulture y use un formato fijo para las fechas, como yyyy-MM-dd. El diseño de fechas se trata en detalle en Diseño de fecha/hora y zona horaria en aplicaciones empresariales.
  • Procesar los grandes volúmenes de datos en streaming, aunque si conviene hacerlo «línea a línea» depende de la especificación. File.ReadAllLines carga todas las líneas en memoria como un arreglo, por lo que no es adecuado para archivos grandes. Sustituirlo por File.ReadLines (enumeración diferida) o por lectura línea a línea con StreamReader solo es válido cuando la especificación indica explícitamente que no se permiten saltos de línea dentro de un campo. En un CSV conforme a RFC 4180 que sí permite saltos de línea dentro de comillas, dividir por líneas corta un registro a la mitad. En ese caso, use TextFieldParser leyendo directamente desde la ruta del archivo (puede procesar sin cargar todo en memoria) o la lectura en streaming de una librería de CSV.
  • No dejar leer -ni leer- un «archivo a medio escribir». El lado que genera el archivo debe escribirlo con un nombre temporal y luego renombrarlo; el lado que lo importa debe verificar la exclusión antes de leer. Esta forma básica de la integración de archivos se resume en Conocimientos básicos sobre el control de exclusión en la integración de archivos, y las precauciones al importar mediante monitoreo de carpetas se resumen en Guía práctica de FileSystemWatcher.
  • Prepararse para el aumento o disminución de columnas. Especifique en la especificación si se hará «correspondencia por el nombre de columna de la fila de encabezado» o «orden de columnas fijo, con verificación del número de columnas que rechaza si no coincide». Una referencia implícita con números mágicos como fields[7] se rompe silenciosamente ante una simple adición de columna por parte de la contraparte de la integración.
  • Decidir de antemano el tratamiento del salto de línea final y de las líneas vacías. Si hay o no un salto de línea después de la última línea, y si se omiten o se rechazan las líneas vacías, es un punto donde el comportamiento varía según la implementación del parser. TextFieldParser omite las líneas vacías.

Resumen

El CSV tiene la apariencia de «solo separar por comas», pero es un formato de integración lleno de dialectos. Los puntos clave en la práctica son cuatro: para la lectura, usar un componente capaz de procesar comillas (no desarrollarlo internamente con Split); para la codificación de caracteres, decidirla en sentido inverso a partir de quién abrirá el archivo (especialmente Excel) y dejar por escrito en la especificación incluso la presencia o ausencia de BOM; saber que la interpretación de valores de Excel (pérdida de ceros, conversión a fecha) no se puede evitar desde el lado del CSV; e incorporar medidas contra la inyección si se genera contenido con entradas del usuario. Con solo esto, la mayoría de los incidentes en la integración de CSV se pueden eliminar en la etapa de diseño.

La investigación de la corrupción de caracteres o daños en datos de una integración CSV existente, la organización de las especificaciones de integración con sistemas heredados, y la implementación nueva de funciones de importación/exportación de CSV suelen requerir decisiones basadas en los archivos reales y las especificaciones de la contraparte, así que si tiene dudas, no dude en consultarnos.

Artículos relacionados

Áreas de consultoría relacionadas

En Komura Soft LLC (合同会社小村ソフト) atendemos consultorías técnicas sobre el desarrollo de aplicaciones empresariales que incluyen CSV e integración de archivos, la investigación del origen de la corrupción de caracteres o daños en datos de integraciones existentes, y la organización de las especificaciones de integración con sistemas heredados.

Referencias

  1. IETF, RFC 4180 - Common Format and MIME Type for Comma-Separated Values (CSV) Files. Especificación de facto del CSV. Sobre el procesamiento de comillas para comas, saltos de línea y comillas dobles dentro de un campo, y el escape mediante ""

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Qué codificación de caracteres debería usar para el CSV?
La regla general, si el destinatario ya está definido, es ajustarse a las especificaciones de quien recibe el archivo. Si se puede decidir desde cero, un buen punto de partida es especificar en el documento de especificaciones UTF-8 con BOM para los archivos que las personas abrirán en Excel, y UTF-8 sin BOM para la integración entre sistemas. Si una integración con un sistema heredado exige Shift_JIS, es imprescindible acordar de antemano el tratamiento de los caracteres dependientes del equipo y de los caracteres externos, o de lo contrario surgirán después disputas sobre la responsabilidad de la corrupción de caracteres.
¿Conviene incorporar una librería de CSV, o basta con desarrollarlo internamente?
En cuanto a la lectura, dado que un String.Split casero no puede procesar correctamente ciertos casos según la especificación (comas, saltos de línea y comillas dentro de un campo), conviene usar algún componente ya existente. Dentro del propio estándar de .NET, TextFieldParser admite un procesamiento de comillas equivalente al de RFC 4180. Si además necesita mapeo entre columnas y objetos, conversión de tipos o rendimiento con grandes volúmenes de datos, considere incorporar una librería de CSV con trayectoria contrastada. Para la escritura, como la especificación es más simple, desarrollarlo internamente es razonable, pero prepare una única función común para el escape de comillas y haga que todo el código pase por ella.
¿Debería usar tabulaciones (TSV) en lugar de comas como separador?
Si los datos contienen comas con frecuencia (direcciones, nombres, importes monetarios, etc.), pasar a TSV reduce la frecuencia de tener que usar comillas y facilita la verificación visual. Sin embargo, el hecho de que las tabulaciones o los saltos de línea puedan aparecer en los datos sigue siendo el mismo, por lo que el procesamiento de comillas no deja de ser necesario. Tenga cuidado: «al ser TSV, no hace falta preocuparse por el separador» no es correcto. Si puede llegar a un acuerdo con la contraparte de la integración, especificar claramente en el documento de especificaciones la codificación de caracteres, las comillas, el código de salto de línea y si hay o no fila de encabezado es más eficaz para prevenir incidentes que el propio carácter separador.
Si el objetivo es que se abra en Excel, ¿debería generar un archivo de Excel en lugar de un CSV?
Si el propósito principal es que una persona lo vea o lo imprima en Excel, vale la pena considerar la generación en formato xlsx. El CSV es un formato en el que, en el momento de abrirlo en Excel, se produce la pérdida de ceros iniciales o la conversión automática a fecha, y no existe forma de evitarlo por completo desde el lado del CSV. Con xlsx, en cambio, el formato de celda puede controlarse desde el lado de la generación. Por otro lado, si el propósito principal es la importación a otro sistema y la visualización en Excel es secundaria, resulta más realista mantener el CSV e indicar como procedimiento operativo los «pasos de importación» (usar la función de importación de datos).

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog