我写了一个C#程序来读取Excel .xls/.xlsx文件并输出到CSV和Unicode文本.我写了一个单独的程序来删除空白记录.这是通过读取每一行来完成的StreamReader.ReadLine()
,然后通过字符串逐个字符地进行,如果它包含所有逗号(对于CSV)或所有选项卡(对于Unicode文本),则不将该行写入输出.
当Excel文件在单元格内包含嵌入的换行符(\ x0A)时,会发生此问题.我将我的XLS更改为CSV转换器以找到这些新行(因为它逐个单元格)并将它们写为\ x0A,而普通行只使用StreamWriter.WriteLine().
在单独的程序中发生问题以删除空白记录.当我读入时StreamReader.ReadLine()
,根据定义它只返回带有行的字符串,而不是终止符.由于嵌入的换行符显示为两个单独的行,我无法分辨哪个是完整记录,哪个是我将它们写入最终文件时的嵌入式换行符.
我甚至不确定我能读到\ x0A,因为输入上的所有内容都注册为'\n'.我可以逐字逐句,但这会破坏我删除空行的逻辑.
我建议您将架构更改为更像编译器中的解析器.
您想要创建一个返回标记序列的词法分析器,然后创建一个解析标记序列并使用它们完成任务的解析器.
在你的情况下,令牌将是:
列数据
逗号
行结束
您可以将'\n'('\ x0a')作为嵌入的新行处理,因此将其作为列数据标记的一部分包含在内.'\ r \n'将构成行尾令牌.
这具有以下优点:
只对数据进行一次传递
仅存储最多1行的数据
尽可能多地重用内存(对于字符串生成器和列表)
如果您的要求发生变化,很容易改变
以下是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 IEnumerableGetTokens(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(); 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); } }