Необходимо подобрать терминаторы строк с помощью StreamReader.ReadLine().
-
21-08-2019 - |
Вопрос
Я написал программу на C# для чтения файла Excel .xls/.xlsx и вывода его в форматы CSV и Unicode.Я написал отдельную программу для удаления пустых записей.Это достигается путем чтения каждой строки с помощью StreamReader.ReadLine()
, а затем посимвольно пройти по строке и не записывать строку для вывода, если она содержит все запятые (для CSV) или все табуляции (для текста в Юникоде).
Проблема возникает, когда файл Excel содержит встроенные символы новой строки (\x0A) внутри ячеек.Я изменил свой конвертер XLS в CSV, чтобы найти эти новые строки (поскольку они идут ячейка за ячейкой) и записать их как \x0A, а обычные строки просто используют StreamWriter.WriteLine().
Проблема возникает в отдельной программе по удалению пустых записей.Когда я читаю с StreamReader.ReadLine()
, по определению он возвращает только строку со строкой, а не признак конца.Поскольку встроенные символы новой строки отображаются как две отдельные строки, я не могу сказать, какая из них является полной записью, а какая — встроенной новой строкой, когда я записываю их в окончательный файл.
Я даже не уверен, что смогу прочитать \x0A, потому что все входные данные регистрируются как ' '.Я мог бы идти посимвольно, но это разрушает мою логику удаления пустых строк.
Решение
Я бы порекомендовал вам изменить вашу архитектуру, чтобы она работала как парсер в компиляторе.
Вы хотите создать лексер, который возвращает последовательность токенов, а затем синтаксический анализатор, который считывает последовательность токенов и что-то с ними делает.
В вашем случае токены будут:
- Данные столбца
- Запятая
- Конец линии
Вы будете рассматривать ' ' ('\x0a') сам по себе как встроенную новую строку и, следовательно, включать ее как часть токена данных столбца.' ' будет представлять собой токен конца строки.
Это имеет следующие преимущества:
- Выполнение только 1 прохода по данным
- Сохранение максимум 1 строки данных.
- Повторное использование как можно большего объема памяти (для построителя строк и списка)
- Легко изменить, если ваши требования изменятся.
Вот пример того, как будет выглядеть Лексер:
Отказ от ответственности: Я даже не компилировал, не говоря уже о тестировании, этот код, так что вам придется его почистить и убедиться, что он работает.
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);
}
}
Другие советы
Вы не можете изменить 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
Я уверен, что вы могли бы сделать это с помощью регулярного выражения, и это, вероятно, было бы более эффективно, но я считаю, что длинный путь легче понять :) Хотя необходимость глобальной замены требует некоторого взлома - надеюсь, получив больше информации, мы я придумаю лучшее решение.
По сути, жесткий возврат в Excel (shift+enter или alt+enter, я не помню) помещает новую строку, эквивалентную \x0A в кодировке по умолчанию, которую я использую для записи CSV.Когда я пишу в CSV, я использую StreamWriter.WriteLine(), который выводит строку плюс новую строку (я считаю, что это ).
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)
Как видите, в первой записи есть встроенная новая строка после ан-Нумана.Когда я использую ReadLine(), я получаю «1050, «Азиз Салих ан-Нуман», и когда я это записываю, WriteLine() завершает эту строку CRLF.Я теряю исходный терминатор строки.Когда я снова использую ReadLine(), я получаю строку, начинающуюся с «1050a».
Я мог бы прочитать весь файл и заменить их, но потом мне пришлось бы заменять их обратно.По сути, я хочу, чтобы терминатор строки определял, \x0a это или CRLF, а затем, если это \x0A, я использую Write() и вставляю этот терминатор.
Я знаю, что немного опоздал, но у меня была та же проблема, и мое решение было намного проще, чем предлагалось большинством.
Если вы можете определить количество столбцов, что нетрудно сделать, поскольку первая строка обычно представляет собой заголовки столбцов, вы можете сравнить количество столбцов с ожидаемым количеством столбцов.Если количество столбцов не равно ожидаемому количеству столбцов, вы просто объединяете текущую строку с предыдущими несовпадающими строками.Например:
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;
}
......
Большое спасибо за ваш код и некоторые другие. Я нашел следующее решение!Я добавил ссылку внизу на написанный мной код, в котором использовалась часть логики с этой страницы.Я решил, что отдам честь там, где это необходимо!Спасибо!
Ниже приведено объяснение того, что мне нужно:Попробуйте это, я написал это, потому что у меня есть очень большой '|' Разграниченные файлы, которые имеют r n внутри некоторых столбцов, и мне нужно было использовать r n в качестве конца разделителя строки.Я пытался импортировать некоторые файлы с помощью пакетов SSIS, но из-за поврежденных данных в файлах мне это не удалось.Размер файла превышал 5 ГБ, поэтому его нельзя было открыть и исправить вручную.Я нашел ответ, просматривая множество форумов, чтобы понять, как работают потоки, и в итоге нашел решение, которое считывает каждый символ в файле и выдает строку на основе добавленных мной определений.это для использования в приложении командной строки, в комплекте со справкой :).Я надеюсь, что это поможет некоторым другим людям, я больше нигде не нашел подобного решения, хотя идеи были вдохновлены этим форумом и другими.