2009-03-20 12 views
7

Escribí un programa C# para leer un archivo .xls/.xlsx de Excel y enviarlo a texto CSV y Unicode. Escribí un programa separado para eliminar los registros en blanco. Esto se logra leyendo cada línea con StreamReader.ReadLine(), y luego yendo carácter por carácter a través de la cadena y no escribiendo la línea para mostrar si contiene todas las comas (para el CSV) o todas las pestañas (para el texto Unicode).C# StreamReader.ReadLine() - Necesito elegir los terminadores de línea

El problema se produce cuando el archivo de Excel contiene nuevas líneas incorporadas (\ x0A) dentro de las celdas. Cambié mi conversor de XLS a CSV para encontrar estas nuevas líneas (ya que va de celda en celda) y las escribo como \ x0A, y las líneas normales solo usan StreamWriter.WriteLine().

El problema se produce en el programa separado para eliminar registros en blanco. Cuando leo con StreamReader.ReadLine(), por definición, solo devuelve la cadena con la línea, no con el terminador. Como las nuevas líneas incorporadas aparecen como dos líneas separadas, no puedo decir cuál es un registro completo y cuál es una nueva línea incrustada para cuando las escribo en el archivo final.

Ni siquiera estoy seguro de poder leer en \ x0A porque todo en la entrada se registra como '\ n'. Podría ir personaje por personaje, pero esto destruye mi lógica para eliminar líneas en blanco.

Cualquier idea sería muy apreciada.

Respuesta

13

Le recomendaría que cambie su arquitectura para que funcione más como un analizador en un compilador.

Desea crear un lexer que devuelva una secuencia de tokens, y luego un analizador que lea la secuencia de tokens y haga cosas con ellos.

En su caso las fichas serían:

  1. datos de la columna
  2. comas
  3. final de línea

lo haría con '\ n' ('\ x0a') por como una nueva línea incrustada, y por lo tanto, incluirlo como parte de un token de datos de columna. A '\ r \ n' constituiría un token de final de línea.

Esto tiene las ventajas de:

  1. hacer sólo 1 pase los datos
  2. Sólo almacenar un máximo de 1 líneas por valor de los datos
  3. Reutilización de tanta memoria como sea posible (para el generador de cadenas y la lista)
  4. es fácil cambio debe cambiar sus requisitos

He aquí una muestra de lo que el que está lexer Parece como:

Descargo de responsabilidad: Ni siquiera he compilado, y mucho menos probado, este código, por lo que tendrá que limpiarlo y asegurarse de que funciona.

enum TokenType 
{ 
    ColumnData, 
    Comma, 
    LineTerminator 
} 

class Token 
{ 
    public TokenType Type { get; private set;} 
    public string Data { get; private set;} 

    public Token(TokenType type) 
    { 
     Type = type; 
    } 

    public Token(TokenType type, string data) 
    { 
     Type = type; 
     Data = data; 
    } 
} 

private IEnumerable<Token> GetTokens(TextReader s) 
{ 
    var builder = new StringBuilder(); 

    while (s.Peek() >= 0) 
    { 
     var c = (char)s.Read(); 
     switch (c) 
     { 
      case ',': 
      { 
       if (builder.Length > 0) 
       { 
        yield return new Token(TokenType.ColumnData, ExtractText(builder)); 
       } 
       yield return new Token(TokenType.Comma); 
       break; 
      } 
      case '\r': 
      { 
       var next = s.Peek(); 
       if (next == '\n') 
       { 
        s.Read(); 
       } 

       if (builder.Length > 0) 
       { 
        yield return new Token(TokenType.ColumnData, ExtractText(builder)); 
       } 
       yield return new Token(TokenType.LineTerminator); 
       break; 
      } 
      default: 
       builder.Append(c); 
       break; 
     } 

    } 

    s.Read(); 

    if (builder.Length > 0) 
    { 
     yield return new Token(TokenType.ColumnData, ExtractText(builder)); 
    } 
} 

private string ExtractText(StringBuilder b) 
{ 
    var ret = b.ToString(); 
    b.Remove(0, b.Length); 
    return ret; 
} 

Su código "analizador" tendría el siguiente aspecto:

public void ConvertXLS(TextReader s) 
{ 
    var columnData = new List<string>(); 
    bool lastWasColumnData = false; 
    bool seenAnyData = false; 

    foreach (var token in GetTokens(s)) 
    { 
     switch (token.Type) 
     { 
      case TokenType.ColumnData: 
      { 
       seenAnyData = true; 
       if (lastWasColumnData) 
       { 
        //TODO: do some error reporting 
       } 
       else 
       { 
        lastWasColumnData = true; 
        columnData.Add(token.Data); 
       } 
       break; 
      } 
      case TokenType.Comma: 
      { 
       if (!lastWasColumnData) 
       { 
        columnData.Add(null); 
       } 
       lastWasColumnData = false; 
       break; 
      } 
      case TokenType.LineTerminator: 
      { 
       if (seenAnyData) 
       { 
        OutputLine(lastWasColumnData); 
       } 
       seenAnyData = false; 
       lastWasColumnData = false; 
       columnData.Clear(); 
      } 
     } 
    } 

    if (seenAnyData) 
    { 
     OutputLine(columnData); 
    } 
} 
+0

Gracias ton tone Scott. Esta parece la solución adecuada. Soy un graduado reciente de CompSci, así que es genial ver que haya disponible ayuda como esta. –

4

No puede cambiar StreamReader para devolver los terminadores de línea, y no puede cambiar lo que utiliza para la terminación de línea.

No tengo muy claro el problema en términos de qué escapes estás haciendo, particularmente en términos de "y escríbelos como \ x0A". Una muestra del archivo probablemente sea útil.

Parece que puede necesita trabajar carácter por carácter, o posiblemente cargar todo el archivo primero y hacer una sustitución global, p.

x.Replace("\r\n", "\u0000") // Or some other unused character 
.Replace("\n", "\\x0A") // Or whatever escaping you need 
.Replace("\u0000", "\r\n") // Replace the real line breaks 

Estoy seguro de que podría hacer eso con una expresión regular y probablemente sería más eficiente, pero me parece el camino más largo más fácil de entender :) Es un poco de un truco que tiene que hacer un reemplazo global, aunque - con suerte, con más información obtendremos una mejor solución.

+0

+1 para "puede que tenga que trabajar carácter por carácter" – eglasius

+0

me gusta mi solución (que se puede ver a continuación), pero me gusta mucho el desarrollo de compiladores, por lo que podría ser uno de esos "para alguien con un martillo todo parece un clavo" tipo de cosas –

1

Básicamente, un retorno difícil en Excel (shift + enter o alt + enter, no recuerdo) pone una nueva línea que es equivalente a \ x0A en la codificación predeterminada que uso para escribir mi CSV. Cuando escribo en CSV, uso StreamWriter.WriteLine(), que da salida a la línea más una nueva línea (que creo que es \ r \ n).

El CSV está bien y sale exactamente cómo Excel lo guardaría, el problema es que cuando lo leí en el removedor de registros en blanco, estoy usando ReadLine() que tratará un registro con una nueva línea incrustada como CRLF .

He aquí un ejemplo del archivo después convierto a CSV ...

Reference,Name of Individual or Entity,Type,Name Type,Date of Birth,Place of Birth,Citizenship,Address,Additional Information,Listing Information,Control Date,Committees 
1050,"Aziz Salih al-Numan 
",Individual,Primary Name,1941 or 1945,An Nasiriyah,Iraqi,,Ba’th Party Regional Command Chairman; Former Governor of Karbala and An Najaf Former Minister of Agriculture and Agrarian Reform (1986-1987),Resolution 1483 (2003),6/27/2003,1518 (Iraq) 
1050a,???? ???? ???????,Individual,Original script,1941 or 1945,An Nasiriyah,Iraqi,,Ba’th Party Regional Command Chairman; Former Governor of Karbala and An Najaf Former Minister of Agriculture and Agrarian Reform (1986-1987),Resolution 1483 (2003),6/27/2003,1518 (Iraq) 

Como se puede ver, el primer registro tiene una nueva línea incorporado tras al-Numan. Cuando uso ReadLine(), obtengo '1050,' Aziz Salih al-Numan 'y cuando lo escribo, WriteLine() termina esa línea con un CRLF. Pierdo el terminador de línea original. Cuando uso ReadLine() nuevamente , Obtengo la línea que comienza con '1050a'.

Pude leer todo el archivo y reemplazarlos, pero luego tendría que volver a colocarlos después. Básicamente lo que quiero hacer es obtener el terminador de línea para determinar si es \ x0a o un CRLF, y luego si es \ x0A, usaré Write() e insertaré ese terminador.

0

Sé que soy un poco tarde para el juego aquí, pero yo estaba teniendo el mismo problema y mi solución era una mucho más simple que la mayoría dada.

Si puede determinar el conteo de columnas que debería ser fácil de hacer dado que la primera línea suele ser los títulos de las columnas, puede verificar el recuento de columnas con el recuento de columnas esperado. Si el recuento de columnas no es igual al recuento de columnas esperado, simplemente concatenará la línea actual con las líneas no coincidentes anteriores. Por ejemplo:

string sep = "\",\""; 
int columnCount = 0; 
while ((currentLine = sr.ReadLine()) != null) 
{ 
    if (lineCount == 0) 
    { 
     lineData = inLine.Split(new string[] { sep }, StringSplitOptions.None); 
     columnCount = lineData.length; 
     ++lineCount; 
     continue; 
    } 
    string thisLine = lastLine + currentLine; 

    lineData = thisLine.Split(new string[] { sep }, StringSplitOptions.None); 
    if (lineData.Length < columnCount) 
    { 
     lastLine += currentLine; 
     continue; 
    } 
    else 
    { 
     lastLine = null; 
    } 
    ...... 
0

Muchas gracias con su código y algunos otros ¡se me ocurrió la siguiente solución! He agregado un enlace en la parte inferior de algún código que escribí que utilizó parte de la lógica de esta página. ¡Pensé que iba a dar honor donde merecía honor! ¡Gracias!

A continuación hay una explicación acerca de lo que necesitaba: Pruebe esto, escribí esto porque tengo algunos '|' archivos delimitados que tienen \ r \ n dentro de algunas de las columnas y necesitaba usar \ r \ n como el final del delimitador de línea. Estaba tratando de importar algunos archivos usando paquetes SSIS pero debido a algunos datos corruptos en los archivos no pude. El archivo tenía más de 5 GB, por lo que era demasiado grande para abrirlo y corregirlo manualmente. Encontré la respuesta mirando muchos Foros para entender cómo funcionan las secuencias y terminé con una solución que lee cada carácter en un archivo y escupe la línea en función de las definiciones que agregué.esto es para usar en una aplicación de línea de comandos, completar con ayuda :). Espero que esto ayude a otras personas, no he encontrado una solución como en ningún otro lado, aunque las ideas fueron inspiradas por este foro y otros.

https://stackoverflow.com/a/12640862/1582188

Cuestiones relacionadas