Необходимо подобрать терминаторы строки с помощью StreamReader.ReadLine () - PullRequest
8 голосов
/ 20 марта 2009

Я написал программу на C # для чтения файла Excel .xls / .xlsx и вывода в текст CSV и Unicode. Я написал отдельную программу для удаления пустых записей. Это достигается путем чтения каждой строки с StreamReader.ReadLine(), а затем прохождения символа за символом через строку и без записи строки для вывода, если она содержит все запятые (для CSV) или все вкладки (для текста Unicode).

Проблема возникает, когда файл Excel содержит встроенные символы новой строки (\ x0A) внутри ячеек. Я изменил свой конвертер XLS в CSV, чтобы найти эти новые строки (поскольку они идут ячейка за ячейкой) и записать их как \ x0A, а обычные строки просто используют StreamWriter.WriteLine ().

Проблема возникает в отдельной программе для удаления пустых записей. Когда я читаю с StreamReader.ReadLine(), по определению он возвращает только строку со строкой, а не терминатор. Поскольку встроенные новые строки отображаются в виде двух отдельных строк, я не могу сказать, какая из них является полной записью, а какая - встроенной новой строкой, когда я записываю их в окончательный файл.

Я даже не уверен, что могу читать в \ x0A, потому что все на входе регистрируется как '\ n'. Я мог бы идти символ за символом, но это разрушает мою логику, чтобы удалить пустые строки.

Ответы [ 5 ]

13 голосов
/ 21 марта 2009

Я бы порекомендовал вам изменить архитектуру, чтобы она работала больше как парсер в компиляторе.

Вы хотите создать лексер, который возвращает последовательность токенов, а затем синтаксический анализатор, который читает последовательность токенов и что-то с ними делает.

В вашем случае токены будут:

  1. Данные столбца
  2. запятая
  3. Конец строки

Вы бы трактовали '\ n' ('\ x0a') самим собой как встроенную новую строку и, следовательно, включали бы ее как часть токена данных столбца. '\ R \ n' будет означать маркер конца строки.

Это имеет следующие преимущества:

  1. Выполнение только одного прохода над данными
  2. Хранение данных максимум до 1 строки
  3. Повторное использование максимально возможного количества памяти (для строителя строк и списка)
  4. Легко изменить, если ваши требования изменятся

Вот пример того, как будет выглядеть Lexer:

Отказ от ответственности: Я даже не скомпилировал, не говоря уже о тестировании, этот код, поэтому вам нужно его почистить и убедиться, что он работает.

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;
}

Ваш код "парсера" будет выглядеть следующим образом:

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);
    }
}
4 голосов
/ 20 марта 2009

Вы не можете изменить StreamReader, чтобы вернуть терминаторы строки, и вы не можете изменить то, что используется для завершения строки.

Мне не совсем понятна проблема с точки зрения того, что вы спасаетесь, особенно с точки зрения "и запишите их как \ x0A". Пример файла, вероятно, поможет.

Звучит так, как будто вам может нужно работать символ за символом или, возможно, сначала загрузить весь файл и выполнить глобальную замену, например

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

Я уверен, что вы могли бы сделать это с помощью регулярного выражения, и это, вероятно, было бы более эффективным, но я считаю, что долгий путь легче понять :) Хотя это немного хакерская необходимость сделать глобальную замену - надеюсь, с большим количеством информация, мы найдем лучшее решение.

1 голос
/ 20 марта 2009

По сути, жесткий возврат в Excel (shift + enter или alt + enter, я не помню) помещает новую строку, эквивалентную \ x0A, в кодировке по умолчанию, которую я использую для написания CSV. Когда я пишу в CSV, я использую StreamWriter.WriteLine (), который выводит строку плюс символ новой строки (который, я считаю, \ r \ n).

CSV в порядке и выходит именно так, как Excel сохранит его, проблема в том, что когда я читаю его в чистую запись для удаления, я использую ReadLine (), который будет обрабатывать запись со встроенным переводом строки как CRLF.

Вот пример файла после преобразования в 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)

Как вы можете видеть, первая запись имеет встроенную новую строку после al-Numan. Когда я использую ReadLine (), я получаю «1050», «Азиз Салих аль-Нуман», и когда я записываю это, WriteLine () заканчивает эту строку с помощью CRLF. Я теряю исходный терминатор строки. Когда я снова использую ReadLine () Я получаю строку, начинающуюся с '1050a'.

Я мог бы прочитать весь файл и заменить их, но потом мне пришлось бы заменить их обратно потом. По сути, я хочу получить терминатор строки, чтобы определить, является ли он \ x0a или CRLF, а затем, если это \ x0A, я использую Write () и вставлю этот терминатор.

0 голосов
/ 28 сентября 2012

Большое вам спасибо за ваш код и некоторые другие, я придумал следующее решение! Я добавил ссылку внизу к некоторому коду, который я написал, который использовал некоторую логику с этой страницы. Я решил отдать честь, где честь была! Спасибо!

Ниже приведено объяснение того, что мне было нужно: Попробуйте это, я написал это, потому что у меня есть очень большой '|' файлы с разделителями, которые имеют \ r \ n внутри некоторых столбцов, и мне нужно было использовать \ r \ n в качестве конца строки. Я пытался импортировать некоторые файлы, используя пакеты служб SSIS, но из-за некоторых поврежденных данных в файлах я не смог. Файл был более 5 ГБ, поэтому он был слишком большим, чтобы открыть и исправить вручную. Я нашел ответ, просмотрев множество форумов, чтобы понять, как работают потоки, и в итоге нашел решение, которое считывает каждый символ в файле и выплевывает строку на основе определений, которые я добавил в него. это для использования в приложении командной строки, в комплекте с помощью :). Я надеюсь, что это поможет другим людям, я не нашел такого решения, как где-либо еще, хотя идеи были вдохновлены этим форумом и другими.

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

0 голосов
/ 13 марта 2010

Я знаю, что немного опоздал к игре, но у меня возникла та же проблема, и мое решение было намного проще, чем предполагалось.

Если вы можете определить количество столбцов, что должно быть легко сделать, так как в первой строке обычно указываются заголовки столбцов, вы можете сравнить количество столбцов с ожидаемым количеством столбцов. Если количество столбцов не равно ожидаемому количеству столбцов, вы просто объединяете текущую строку с предыдущими несопоставленными строками. Например:

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;
    }
    ......
...