8

我编写了一个 C# 程序来读取 Excel .xls/.xlsx 文件并输出为 CSV 和 Unicode 文本。我编写了一个单独的程序来删除空白记录。这是通过读取每一行来完成的StreamReader.ReadLine(),然后逐个字符地遍历字符串,如果它包含所有逗号(对于 CSV)或所有制表符(对于 Unicode 文本),则不将行写入输出。

当 Excel 文件在单元格内包含嵌入的换行符 (\x0A) 时,就会出现此问题。我将我的 XLS 更改为 CSV 转换器以找到这些新行(因为它逐个单元格)并将它们写为 \x0A,而普通行只使用 StreamWriter.WriteLine()。

该问题出现在单独的程序中以删除空白记录。当我读入 with 时StreamReader.ReadLine(),根据定义,它只返回带有行的字符串,而不是终止符。由于嵌入的换行符显示为两个单独的行,因此当我将它们写入最终文件时,我无法分辨哪个是完整记录,哪个是嵌入的换行符。

我什至不确定我是否可以读取 \x0A,因为输入上的所有内容都注册为“\n”。我可以一个字一个字地去,但这破坏了我删除空行的逻辑。

4

5 回答 5

13

我建议您更改架构,使其更像编译器中的解析器。

您想创建一个返回标记序列的词法分析器,然后创建一个解析器来读取标记序列并使用它们进行处理。

在您的情况下,令牌将是:

  1. 列数据
  2. 逗号
  3. 行结束

您会将 '\n' ('\x0a') 本身视为嵌入的新行,因此将其作为列数据标记的一部分。'\r\n' 将构成行尾标记。

这具有以下优点:

  1. 仅对数据进行 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);
    }
}
于 2009-03-20T21:04:48.767 回答
4

您无法更改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

我相信你可以用正则表达式来做到这一点,它可能会更有效,但我发现还有很长的路要走更容易理解 :) 不过,不得不进行全局替换有点麻烦 - 希望我们能提供更多信息会想出更好的解决方案。

于 2009-03-20T20:12:24.120 回答
1

本质上,Excel 中的硬返回(shift+enter 或 alt+enter,我不记得了)在我用来编写 CSV 的默认编码中放置了一个相当于 \x0A 的换行符。当我写入 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,"Aziz Salih al-Numan',当我写出它时,WriteLine() 以 CRLF 结束该行。我丢失了原始行终止符。当我再次使用 ReadLine() ,我得到以'1050a'开头的行。

我可以读入整个文件并替换它们,但之后我必须将它们替换回来。基本上我想要做的是让行终止符来确定它是 \x0a 还是 CRLF,然后如果它是 \x0A,我将使用 Write() 并插入该终止符。

于 2009-03-20T20:24:01.347 回答
0

我知道我在这里玩游戏有点晚了,但我遇到了同样的问题,我的解决方案比大多数给出的要简单得多。

如果您能够确定应该很容易做到的列数,因为第一行通常是列标题,您可以根据预期的列数检查列数。如果列数不等于预期的列数,您只需将当前行与之前不匹配的行连接起来。例如:

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;
    }
    ......
于 2010-03-12T21:41:34.617 回答
0

非常感谢您的代码和其他一些我想出了以下解决方案!我在底部添加了一个链接,指向我编写的一些代码,这些代码使用了此页面中的一些逻辑。我想我会在应该获得荣誉的地方给予荣誉!谢谢!

下面是关于我需要什么的解释:试试这个,我写这个是因为我有一些非常大的“|” 在某些列中有 \r\n 的分隔文件,我需要使用 \r\n 作为行尾分隔符。我试图使用 SSIS 包导入一些文件,但由于文件中的一些损坏的数据,我无法导入。该文件超过 5 GB,因此太大而无法打开和手动修复。我通过查看大量论坛以了解流的工作原理并最终提出了一个解决方案,该解决方案读取文件中的每个字符并根据我添加到其中的定义吐出该行,从而找到了答案。这是在命令行应用程序中使用的,带有帮助:)。我希望这可以帮助其他人,我在其他任何地方都没有找到类似的解决方案,

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

于 2012-09-28T13:26:10.703 回答