需要使用 StreamReader.ReadLine() 获取行终止符
-
21-08-2019 - |
题
我编写了一个 C# 程序来读取 Excel .xls/.xlsx 文件并输出为 CSV 和 Unicode 文本。我编写了一个单独的程序来删除空白记录。这是通过读取每一行来完成的 StreamReader.ReadLine()
, ,然后逐个字符地遍历字符串,如果该行包含所有逗号(对于 CSV)或所有制表符(对于 Unicode 文本),则不写入要输出的行。
当 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”的条款。该文件的示例可能会有所帮助。
这听起来像你的可以的需要通过文字工作角色,或者可能第一次加载整个文件,然后执行全局替换,e.g。
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(),它输出线加一个新行(我认为是\ 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-努曼后嵌入新行。当我使用的ReadLine(),我得到“1050,“阿齐兹萨利赫AL-努曼”当我写出来,的WriteLine()结束该行以CRLF我。当我使用的ReadLine()再次失去了原有的行终止时,得到线以 '1050A'。
我可以读取整个文件,并替换它们,但后来我不得不更换他们回来之后。基本上我想要做的就是线路终端器,以确定是否其\ X0A或CRLF,然后如果\ X0A,我会用写(),并插入终止。
我知道我有点太迟了这里,但我有同样的问题,我的解决方案比最倾向于是简单了很多。
如果你能够确定哪些应该很容易做到,因为第一行通常是列标题的列数,你可以检查你列数与预期列数。如果列数不等于预期的列数,您只需连接具有无可比拟以前行当前行。例如:
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包的一些文件,但因为文件中有一些损坏的数据我无法。该文件超过500 GB,因此,它太大了打开和手动修复。我找到了答案,通过翻阅大量的论坛,了解流是如何工作的,并最终想出在一个文件中读取每个字符,并吐出了基于我加入到它的定义行的解决方案。这是一个命令行应用程序的使用,完全与帮助:)。我希望这可以帮助一些人出来,我还没有找到很喜欢它在其他位置上的解决方案,虽然想法是通过这个论坛和其他人的启发。