En este artículo se muestra una función para convertir los datos de un 'array' en una cadena con formato CSV, y al contrario, dada una cadena CSV, devolver un Array cargado con los datos que contiene la cadena. El análisis de una cadena en formato CSV puede parecer trivial, pero es un autentico infierno de programación. La mejor solución es utilizar Expresiones Regulares. ¡¡Todo un hallazgo!!
CSV [Ver CSV] es uno de los primeros formatos de intercambio de datos. Permite volcar en un fichero información, de forma que pueda ser leída por otro ordenador, independientemente del sistema que implemente el receptor
A pesar de que ha sido y es una forma bastante común de mover información entre ordenadores, hasta hace muy poco no ha existido una norma que lo defina. En la actualidad el documento Common Format and MiME Type for Comma-Separated Values (CSV) [Ver RFC-4180] Files del año 2005, es lo más parecido que existe a un estándar sobre este formato aunque sigue sin ser nada oficial
En un trabajo que he estado realizando, se me ha presentado el problema de volcar la información de un DataTable a formato CSV, para mandarlo por internet, y en el destino, recuperar la información del formato CSV y volverlo a colocar en una DataTable.
Aunque en principio, el asunto no representaba ningún problema, me he dedicado a investigar y a buscar algún código que resuelva el problema, y para mi sorpresa, me he encontrado que en realidad no existe, -o por lo menos no lo he encontrado- ninguna solución genérica a este problema, todo son códigos más o menos específicos para mover/recuperar una información pero realizadas específicamente para resolver un problema determinado.
Por esa razón, y como disponía de tiempo, he escrito dos funciones que realizan el trabajo de conversión de un registro de datos a y desde un formato CSV, pero antes de entrar en la descripción del código vamos a ver un poco de información (y/o culturilla)
• Un documento CSV es un documento Tabular, es decir está formado por líneas y columnas.
• Las líneas se separan con un carácter de separación (o terminación) de líneas
• Las líneas de todo el documento tienen todas las mismas columnas. Cada línea contiene los datos separados por un separador de campos, en este caso es una coma (,) pero también puede ser cualquier otro carácter, por ejemplo en España es muy normal emplear un punto y coma (;).
• Un poco de culturilla: Últimamente se está desarrollando un formato de intercambio de datos [Ver CTX] que es un hijo directo del formato CSV que emplea como carácter separador el carácter pipe ["|", (0x7c)]
• Las condiciones que se deben cumplir a la hora de escribir un registro (una línea) codificada según el formato CSV son las siguientes:
• Cada registro es una línea del documento, finalizado por el carácter de fin de línea (CRLF)
Por ejemplo: aaa,bbb,ccc CRLF zzz,yyy,xxx CRLF
• El último registro del fichero, puede tener o no el carácter terminador de fin de línea (CRLF)
Por Ejemplo: aaa,bbb,ccc CRLF zzz,yyy,xxx
• El primer registro de todos puede dedicarse a contener los nombres correspondientes de los campos del fichero. No existe ninguna diferencia en cuanto al formato del registro que permita identificar si son datos o son los nombres de los campos. Y además, siguen las mismas reglas de formación que el resto de los datos.
Por Ejemplo Nombres, apellido_1, apellido_2 CRLF aaa,bbb,ccc CRLF zzz,yyy,xxx CRLF
• El documento CSV puede contener el número de líneas que deseemos, no existe ninguna limitación.
• Cada línea debe contener siempre el mismo número de datos. Si un campo de la línea está vacío se mantiene como vacio
Por ejemplo Valor inicial -> {nombre} {vacio} {edad} Valor Final -> nombre,,edad (observa las dos comas juntas)
• Los espacios se consideran como parte del campo y no deben ser ignorados ni eliminados, sino que tienen que conservarse
• Cada campo puede estar o no rodeado de comillas dobles. Si un campo de una línea está rodeado de comillas no obliga al que el campo de la línea siguiente también este rodeado, puede rodearse o no
Por Ejemplo "aaa","bbb","ccc" CRLF zzz ,"yyy",xxx CRLF
• Los campos que contengan en su interior el carácter de fin de línea (CR, LF, o ambos a la vez), y/o el carácter separador de campos -es decir la coma (,)- se rodearan con comillas dobles
Por ejemplo Valor inicial -> salto de línea CR interno Valor Final -> "salto de línea CR interno" Por Ejemplo Valor inicial -> 25,96 Valor Final -> "25,96"
• Los campos que contengan en su interior comillas dobles, primero se duplican las comillas y después se rodea todo el campo con comillas dobles
Por ejemplo Valor inicial - > Ford "Capri" Primer paso - > Duplicar las comillas --> Ford ""Capri"" Segindo paso - > Rodear el campo con comillas dobles - > --> "Ford ""Capri"""
• Aunque no es necesario, creo que es conveniente que aquellos campos que contengan caracteres en blanco, también se rodeen con comillas
Por Ejemplo Valor inicial -> aquí hay cuatro palabras Valor Final -> "aquí hay cuatro palabras"
He desarrollado dos funciones que realizan la conversión de datos desde y hacia CSV, las he llamado "ToLineaCsv" y "LineaCsvToArray",
La Función "ToLineaCsv" recibe una matriz de cadenas (String) con los datos y monta con ellos una línea con formato CSV
Es un poco larga, pero no tiene ningún problema. Consiste en un bucle que recorre los elementos de la matriz, los analiza y decide que tratamiento hay que darles, (rodearlas con comillas, duplicar las comillas interiores, etc.)
''' --------------------------------------------------------------------- ''' <summary> ''' Devuelve una cadena creada a partir de la combinación de ''' varias subcadenas contenidas en una matriz. ''' </summary> ''' <param name="caracterSeparadorCampos"> ''' El carácter que se va a utilizar como separador de campos ''' </param> ''' <param name="matrizElementos"> ''' La matriz cuyos datos se van a montar como una línea ''' de un documento CSV ''' </param> ''' <returns> ''' Objeto String formado por los elementos de matrizElementos ''' intercalados con la cadena separador. ''' </returns> ''' <remarks> ''' <para>Por ejemplo, si <paramref name="caracterSeparadorCampos" > ''' caracterSeparadorCampos</paramref> es ", " ''' y los elementos de <paramref name="matrizElementos" > ''' matrizElementos</paramref> son ''' "manzana", "naranja", "uva" y "pera", ''' <c>ToLineaCsv(caracterSeparadorCampos, matrizElementos)</c> ''' devuelve una cadena con la siguiente información ''' [manzana, naranja, uva, pera CRLF]. ''' </para> ''' <para>Si <paramref name="caracterSeparadorCampos" > ''' caracterSeparadorCampos</paramref> es ''' null (Nothing en Visual Basic), se dispara una ''' excepción [ArgumentNullException] ''' </para> ''' <para> Cada elemento puede ir rodeado o no de comillas dobles, ''' pero siempre va rodeado si se cumplen cualquiera de las ''' siguientes condiciones: ''' </para> ''' <para> Existe un carácter separador dentro del elemento ''' P.E. [52.45] === . ["52.45"]</para> ''' <para> Existe un salto de línea dentro del elemento ''' (caracteres CrLf) ''' </para> ''' <para> Existen (uno o más) espacios en blanco en el elemento ''' P.E:[ciudad lineal] === :["ciudad lineal"]</para> ''' <para> Existen (una o más) comillas dobles dentro del elemento ''' P.E [pegamento "maravilloso"]. En este caso, cada una de las ''' comillas dobles existentes se duplican, y a continuación, ''' se rodea el elemento con comillas dobles</para> ''' <para> Ejemplo:</para> ''' <para> Valor inicial = [pegamento "maravilloso"].</para> ''' <para> Paso Uno: Duplicando comillas [pegamento ""maravilloso""].</para> ''' <para> Paso Dos: Rodeando con comillas ["pegamento ""maravilloso"""].</para> '''</remarks> ''' <exception cref="ArgumentNullException"> ''' <para> El valor de <paramref name="matrizElementos" > ''' matrizElementos</paramref> </para> ''' <para> o bien</para> ''' <para> El valor de <paramref name="caracterSeparadorCampos" > ''' caracterSeparadorCampos</paramref></para> ''' <para> es null (Nothing en Visual Basic</para> ''' </exception> ''' <exception cref="ArgumentException"> ''' <para> El valor de <paramref name="caracterSeparadorCampos" > ''' caracterSeparadorCampos</paramref> ''' es uno de los caracteres que no son válidos para actuar ''' como separador de campos ''' </para> ''' </exception> ''' <Copyright> ''' <para> Copyright: Joaquin Medina Serrano [joaquin@medina.name]</para> ''' <para> Version .: [2009/03/15] </para> ''' </Copyright> Public Overloads Shared Function ToLineaCsv( _ ByVal caracterSeparadorCampos As Char, _ ByVal ParamArray matrizElementos() As String) _ As String '----------------------------------- ' Control de parametros If matrizElementos Is Nothing Then Throw New ArgumentNullException( _ "matrizElementos", _ "El valor de la [matrizElementos()] es null" & _ " (Nothing en Visual Basic)") End If ' si solo tiene un elemento y tiene el valor Nothing ' se devuelve una cadena vacia If matrizElementos.Length = 1 Then If matrizElementos(0) = Nothing Then Return String.Empty End If End If ' -------------------------------------- 'Control de los valores CORRECTOS mas frecuentes ' una coma(,) un punto y coma(;), un Pipe(|) If Not (caracterSeparadorCampos = ","c OrElse _ caracterSeparadorCampos = ";"c OrElse _ caracterSeparadorCampos = "|"c) Then Throw New ArgumentException( _ "El [caracterSeparadorCampos] no es valido ", _ "caracterSeparadorCampos") End If '----------------------------------- ' Definiendo variables Const QUOTE As Char = """"c Dim salida As String = String.Empty Dim aux As String = String.Empty Dim caracterAux As Char = Nothing Dim rodearConComillas As Boolean = False '----------------------------------- ' Proceso Try '----------------------------------- ' Recorrer la matriz para montar la cadena For i As Integer = matrizElementos.GetLowerBound(0) To _ matrizElementos.GetUpperBound(0) '------- ' en principio el texto va tal cual rodearConComillas = False '------- ' obtener unelemento de la matriz aux = matrizElementos(i) '----------------------------- 'MODIFICACIONES DEL TEXTO '----------------------------- ' caso uno ' El elemento tiene el carácter separador, ' rodear todo el texto con comillas dobles 'ejemplo 5,34 --> "5,34" If aux.IndexOf(caracterSeparadorCampos) <> -1 Then ' poner las comillas rodearConComillas = True End If '----------------- ' caso dos ' Si el elemento tiene dobles comillas hay que 'escaparlas' ' poniendo otra doble comilla junto a ella, y ' después rodear todo el texto con comillas dobles ' ejemplo ' Venture "Extended Edition" ' "Venture ""Extended Edition""" If aux.IndexOf(QUOTE) <> -1 Then ' no uso replace porque me da problemas ' aux = aux.Replace(QUOTE, QUOTE & QUOTE) Dim remplaza As String = String.Empty For Each caracter As Char In aux remplaza = remplaza & caracter 'doblar las comillas If caracter = QUOTE Then remplaza = remplaza & QUOTE End If Next ' Valor modificado aux = remplaza ' poner las comillas rodearConComillas = True End If '----------------- ' caso tres ' si el elemento contiene roturas de línea ' rodear todo el texto con comillas dobles ' http://es.wikipedia.org/wiki/ASCII caracterAux = CType(Char.ConvertFromUtf32(10), Char) ' LF If aux.IndexOf(caracterAux) <> -1 Then ' poner las comillas rodearConComillas = True End If caracterAux = CType(Char.ConvertFromUtf32(13), Char) ' CR If aux.IndexOf(caracterAux) <> -1 Then ' poner las comillas rodearConComillas = True End If '----------------- ' caso cuatro ' si el elemento contiene espacios dentro de la cadena ' rodear todo el texto con comillas dobles If aux.IndexOf(" "c) <> -1 Then ' poner las comillas rodearConComillas = True End If '--------------------------------- '--------------------------------- 'Rodear con comillas el texto If rodearConComillas = True Then '----------------------------- ' Si hay comillas rodeando no se hace nada ' en caso contrario se ponen las comillas ' ejemplo 5,34 --> "5,34" ' ejemplo "5,34" --> no se hace nada '----------------------------- If Not (aux.Trim.StartsWith( _ QUOTE, _ StringComparison.CurrentCulture) AndAlso _ aux.Trim.EndsWith( _ QUOTE, _ StringComparison.CurrentCulture)) Then ' poner las comillas ' Observa que no elimino los espacios al poner las comillas ' LOS ESPACIOS DE CADA ELEMENTO HAY QUE MANTENERLOS ' ' Sin embargo, y únicamente en la interrogación, ' elimino los espacios inciales y finales de la cadena ' para ver si el elemento está rodeado o no de comillas aux = QUOTE & aux & QUOTE End If End If '----------------------------- ' OJO, el último elemento no lleva una coma (carácter separador) ' si solo hay un elemento tampoco hay coma (carácter separador) ' El carácter separador de líneas es el salto de línea (CrLf) '----------------------------- If i = matrizElementos.GetUpperBound(0) Then ' es el ultimo , Meter un fin de linea salida = salida & aux & Environment.NewLine Else salida = salida & aux & caracterSeparadorCampos End If Next Catch ex As Exception Throw Finally aux = Nothing End Try '----------------------------------- ' Devolver resultado obtenido Return salida End Function
Esta función es un poco más complicada, lo que hace es recibir una línea en formato CSV y separar sus componentes devolviéndolos cargados en una matriz de caracteres.
Ha resultado bastante más difícil de escribir de lo que parece porque el tratamiento de las comillas "huérfanas" o "duplicadas" puede llegar a convertirse en un autentico rompecabezas, y al final siempre hay algún caso que no lo resuelve. Un ejemplo, a modo de ejemplo, como separo un numero rodeado de comillas [ datos ,"25,36", "más datos"]
El problema con el que me he enfrentado es que la función tiene que responder a la siguiente baterías de pruebas sin equivocarse
Esta tabla está copiada. La tabla original está en el documento siguiente: http://xbeat.net/vbspeed/c_ParseCSV.php |
|||||
# | CSV | Valor 1 | Valor 2 | Valor 3 | |
1 | a,b,c | a | b | c | |
2 | "a",b,c | a | b | c | |
3 | 'a',b,c | 'a' | b | c | |
4 | a , b , c | a | b | c | |
5 | aa,bb;cc | aa | bb;cc | ||
6 | |||||
7 | a | a | |||
8 | ,b, | b | |||
9 | ,,c | c | |||
10 | ,, | ||||
11 | "",b | b | |||
12 | " ",b | b | |||
13 | "a,b" | a,b | |||
14 | "a,b",c | a,b | c | ||
15 | " a , b ", c | a , b | c | ||
16 | a b,c | a b | c | ||
17 | a"b,c | a"b | c | ||
18 | "a""b",c | a"b | c | ||
19 | a""b,c | a""b | c | ||
20 | a,b",c | a | b" | c | |
21 | a,b"",c | a | b"" | c | |
22 | a,"B: ""Hi, I'm B""",c | a | B: "Hi, I'm B" | c |
Al final, después de desesperarme varias veces, encontré en [esta página] una solución muy elegante usando expresiones regulares. Y después de estudiarla y de una pequeña adaptación ha funcionado sin ningún problema
''' <summary> ''' Devuelve una matriz de cadenas con las subcadenas de la ''' cadena pasada por parámetro que están delimitadas por el ''' caracterSeparadorCampos especificado. ''' </summary> ''' <param name="caracterSeparadorCampos"> ''' El carácter que se va a utilizar como separador de campos ''' </param> ''' <param name="lineaConFormatoCsv"> ''' Cadena a analizar, la cadena que vamos a partir ''' </param> ''' <returns> ''' Una matriz cuyos elementos contienen las subcadenas de ''' <paramref name="lineaConFormatoCsv" > ''' lineaConFormatoCsv</paramref> ''' que están delimitadas ''' por el <paramref name="caracterSeparadorCampos" > ''' caracterSeparadorCampos</paramref>. ''' </returns> ''' <remarks> ''' <para> ''' El [caracterSeparadorCampos] no se incluye en los ''' elementos de la matriz devuelta. ''' </para> ''' <para> ''' Si la cadena no contiene ningun [caracterSeparadorCampos], ''' la matriz devuelta estará formada por un solo elemento ''' que contiene la cadena. ''' </para> ''' <para> ''' Si la [lineaParaAnalizar] esta vacía (empty), ''' la matriz devuelta estará formada por un solo ''' elemento que contiene el valor [Empty]. ''' </para> ''' <para> ''' Si dos delimitadores son adyacentes o el delimitador ''' se encuentra al principio o al final de la [lineaParaAnalizar], ''' el elemento de matriz correspondiente contiene Empty. ''' </para> ''' <para> ''' Si hay comillas rodeando una subcadenas, hay que ''' quitarlas. Ejemplo ["5,34"] --> 5,34 ''' </para> ''' <para> ''' Si una subcadenas contiene dos comillas dobles ''' seguidas, se sustituyen por una sola ''' </para> ''' <para>ejemplo</para> ''' <para> subcadena de entrada ..= Venture ""Extended Edition""</para> ''' <para> salida generada .......= Venture "Extended Edition"</para> ''' <example> ''' '''</example> ''' <code> ''' Esta función esta inspirada en el código que ''' se encuentra en la página ''' http://xbeat.net/vbspeed/c_ParseCSV.php ''' </code> '''</remarks> ''' <exception cref="ArgumentNullException"> ''' <para> El valor de <paramref name="lineaConFormatoCsv" > ''' lineaConFormatoCsv</paramref> </para> ''' <para> o bien</para> ''' <para> El valor de <paramref name="caracterSeparadorCampos" > ''' caracterSeparadorCampos</paramref></para> ''' <para> es null (Nothing en Visual Basic</para> ''' </exception> ''' <exception cref="ArgumentException"> ''' <para> El valor de <paramref name="caracterSeparadorCampos" > ''' caracterSeparadorCampos</paramref> ''' es uno de los caracteres que no son válidos para actuar ''' como separador de campos ''' </para> ''' </exception> ''' <Copyright> ''' <para> Copyright: Joaquin Medina Serrano [joaquin@medina.name]</para> ''' <para> Version .: [2009/03/15] </para> ''' </Copyright> Public Overloads Shared Function LineaCsvToArray( _ ByVal caracterSeparadorCampos As Char, _ ByVal lineaConFormatoCsv As String) _ As String() '----------------------------------- ' Control de parámetros If lineaConFormatoCsv Is Nothing Then Throw New ArgumentNullException( _ "lineaConFormatoCsv", _ "El valor de la [matrizElementos()] " & _ "es null (Nothing en Visual Basic)") End If If lineaConFormatoCsv.Length = 0 Then Dim auxBorrar As String() = {String.Empty} Return auxBorrar End If ' -------------------------------------- 'Control de los valores CORRECTOS mas frecuentes ' una coma(,) un punto y coma(;), un Pipe(|) If Not (caracterSeparadorCampos = ","c OrElse _ caracterSeparadorCampos = ";"c OrElse _ caracterSeparadorCampos = "|"c) Then Throw New ArgumentException( _ "El [caracterSeparadorCampos] no es valido ", _ "caracterSeparadorCampos") End If '----------------------------------- ' Definiendo variables '----------------------- Const QUOTE As Char = """"c Dim arrayDatosIndividuales As String() = Nothing '----------------------- Dim objRegEx As _ System.Text.RegularExpressions.Regex = Nothing Dim objMatchCollection As _ System.Text.RegularExpressions.MatchCollection = Nothing Dim objMatch As _ System.Text.RegularExpressions.Match = Nothing '----------------------- Dim indice As Integer = 0 Dim Aux As String = String.Empty '------------------------------------------------------------- ' Cadena para separar utilizando la coma como separador ' Dim patron As String = "(\s*""[^""]*""\s*,)|(\s*[^,]*\s*,)" '------------------------------------------------------------- ' Cadena modificada para que acepte cualquier separador Dim patron As String = _ "(\s*""[^""]*""\s*" & caracterSeparadorCampos & _ ")|(\s*[^" & caracterSeparadorCampos & "]*\s*" & _ caracterSeparadorCampos & ")" '----------------------------------- ' Proceso Try objRegEx = New System.Text.RegularExpressions.Regex( _ patron, _ System.Text.RegularExpressions.RegexOptions.IgnoreCase) objMatchCollection = _ objRegEx.Matches(lineaConFormatoCsv & caracterSeparadorCampos) ' Definir la matriz para los datos de salida ReDim arrayDatosIndividuales(objMatchCollection.Count - 1) For Each objMatch In objMatchCollection '----- ' Obtener un elemento Aux = objMatch.Value.Substring(0, objMatch.Length - 1) '----------------------------- ' ---> Si hay comillas rodeando el valor hay que quitarlas ' ejemplo ["5,34"] --> 5,34 ' Problema = Que haya un espacio delante o detrás de la comilla ' ejemplo [ " 5,34" ] --> 5,34 '----------------------------- If Aux.Trim.StartsWith( _ QUOTE, _ StringComparison.CurrentCulture) AndAlso _ Aux.Trim.EndsWith( _ QUOTE, _ StringComparison.CurrentCulture) Then Aux = Aux.Trim Aux = Aux.Substring(1, Aux.Length - 2) ' ---- >Segundo asunto ' Dos comillas dobles seguidas, sustituirlas por una sola ' ejemplo ' Venture ""Extended Edition"" ' Venture "Extended Edition" Aux = Aux.Replace(QUOTE & QUOTE, QUOTE) End If '----- ' Añadirlo a la matriz de salida arrayDatosIndividuales(indice) = Aux indice += 1 Next objMatch Catch ex As Exception Throw Finally If objRegEx IsNot Nothing Then objRegEx = Nothing End If If objMatchCollection IsNot Nothing Then objMatchCollection = Nothing End If If objMatch IsNot Nothing Then objMatch = Nothing End If indice = Nothing patron = Nothing Aux = Nothing End Try '----------------------------------- ' Devolver resultado obtenido Return arrayDatosIndividuales End Function
Una vez que el código funcionaba, he hecho una pequeña modificación para que acepte cualquier carácter como separador ( puede verse la modificación de la cadena [patrón] de la expresión regular, porque la cadena original esta comentada), y entonces aparece el problema de la consistencia de datos, es decir, si estoy volcando texto a formato CSV, no puedo emplear letras ni números como separadores de datos, por esa razón, he escrito una función que limita los caracteres de separación a los que me han parecido mas lógicos. La limitación la he realizado de forma excluyente, es decir, no permito caracteres de control, aquellos cuyo valor ASCII sea inferior a 32, excepto el carácter tabulador horizontal que tiene el valor ASCII = 8. Tampoco permito letras, ni números, ni el carácter [Esc] . Puedes ver dicha modificación en el código que acompaña a este artículo.
© 1997 - - La Güeb de Joaquín | |||||
Joaquin Medina Serrano
|
|||||
|