【问题标题】:How to read CSV file with cell that has multiple lines using C#如何使用 C# 读取具有多行的单元格的 CSV 文件
【发布时间】:2014-11-07 19:30:22
【问题描述】:

我正在尝试读取包含多行单元格的 CSV 文件。

这是 CSV 的样子:

第 1 行,“详细信息”列有多行。

当我尝试使用ReadLine() 方法阅读时:

private void buttonBrowse_Click(object sender, EventArgs e)
        {
            openFileDialog.Filter = "Excel Worksheets|*.csv";
            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                String filePathAndName = openFileDialog.FileName;
                StreamReader reader = new StreamReader(filePathAndName);
                String line = reader.ReadLine();
                Console.WriteLine(line);
                do
                {
                     line = reader.ReadLine();
                     Console.WriteLine(line);
                } while (line != null);
            }
        }

它将具有多行的单元格拆分为行数:

[1]"Time of Day","Process Name","PID","Operation","Path","Result","Detail","Image Path"
[2]"22:52:24.2905182","notepad.exe","4828","Process Start","","SUCCESS","Parent PID: 2484, Command line: ""C:\Windows\system32\notepad.exe"" , Current directory: C:\Users\User\, Environment: 
[3];    =::=::\
[4];    ALLUSERSPROFILE=C:\ProgramData
[5];    APPDATA=C:\Users\User\AppData\Roaming
[6];    asl.log=Destination=file
[7];    CommonProgramFiles=C:\Program Files\Common Files
...

"22:52:24.2905201","notepad.exe","4828","Thread Create","","SUCCESS","Thread ID: 8008","C:\Windows\system32\notepad.exe"
"22:52:24.2915842","notepad.exe","4828","Load Image","C:\Windows\System32\notepad.exe","SUCCESS","Image Base: 0x6f0000, Image Size: 0x30000","C:\Windows\system32\notepad.exe"

在上面的日志中,第 2-7 行应该是一行。

我想像 powershell 在这里使用 import-csv 函数做得很好一样阅读它:

您可以使用以下命令轻松地从特定单元格的行和列中提取数据(示例):

$csvContent[0] |select -expand Detail

示例:

【问题讨论】:

    标签: c# csv


    【解决方案1】:

    您可以使用 CsvHelper 之类的库,而不是手动读取行,这将消除解析 csv 的许多麻烦。

    【讨论】:

    • 我进来是为了说这个。 csv 解析有很多不可预见的问题,以至于简单地尝试用逗号(或任何分隔符)进行拆分是不可靠的。这就是人们创建像 CsvHelper 这样的库的原因。
    【解决方案2】:

    我知道这不是一个很好的方法,但它适用于我的情况:

    lineCounter = 0;
    while (!reader.EndOfStream)
    {
         var line = reader.ReadLine();
         var values = line.Split(',');
    
         if(values.Length == 1)
         {
            list4[lineCounter-1] += values[0];
         }
         else
         {
              list1.Add(values[0]);
              list2.Add(values[1]);
              list3.Add(values[2]);
              list4.Add(values[3]);
              lineCounter++;
         }
    
    }
    

    【讨论】:

    • 那么当列值中有逗号时会发生什么?
    【解决方案3】:

    .Net 尚未提供读取 CSV 的标准库。

    虽然CSV specification比较简单, 用多行数据解析 csv 并非易事。

    有些人"cheat"用正则表达式, 但是你需要将整个文件读入字符串, 由于正则表达式不能按需引入更多行, 你仍然需要检测和处理换行符。 那是在我们衡量它的性能、一致性或your new problem之前。

    标准建议是使用经过良好测试的解析器包。 CsvHelper 非常全面,如果您只想读取原始数据,我建议您使用 NReco.Csv


    话虽如此,有时您可能不喜欢套餐或选项受限。 不管是什么原因,我已经用一些静态方法编写了一个 csv 解析器,您可以将它们复制并粘贴到您的项目中并开始运行。

    用法:

    using ( var r = new StreamReader( filePath, Encoding.UTF8, true ) ) {
       while ( r.TryReadCsvRow( out var row ) ) {
          foreach ( string cell in row ) {
             // Your code here.
          }
       }
    }
    
    using ( var r = new StringReader( csvString ) ) {
       while ( r.TryReadCsvRow( out var row ) ) {
          string[] cells = row.ToArray();
          // `cells` is reusable and random-accessible
       }
    }
    

    解析器代码:

    /**
     * <summary>Try read a csv row from a Reader.  May consume multiple lines.  Linebreaks in cells will become \n</summary>
     * <param name="source">Reader to get line data from.</param>
     * <param name="row">Cell data enumeration (forward-only), or null if no more rows.</param>
     * <param name="quoteBuffer">Thread-local buffer for quote parsing. If null, one will be created on demand.</param>
     * <returns>True on success, false on no more rows.</returns>
     * <see cref="StreamReader.ReadLine"/>
     */
    public static bool TryReadCsvRow ( this TextReader source, out IEnumerable<string> row, StringBuilder quoteBuffer = null ) {
       row = ReadCsvRow( source, quoteBuffer );
       return row != null;
    }
    
    /**
     * <summary>Read a csv row from a Reader.  May consume multiple lines.  Linebreaks in cells will become \n</summary>
     * <param name="source">Reader to get line data from.</param>
     * <param name="quoteBuffer">Thread-local buffer for quote parsing. If null, one will be created on demand.</param>
     * <returns>Cell data enumeration (forward-only), or null if no more rows.</returns>
     * <see cref="StreamReader.ReadLine"/>
     */
    public static IEnumerable<string> ReadCsvRow ( this TextReader source, StringBuilder quoteBuffer = null ) {
       var line = source.ReadLine();
       if ( line == null ) return null;
       return ReadCsvCells( source, line, quoteBuffer );
    }
    
    private static IEnumerable<string> ReadCsvCells ( TextReader source, string line, StringBuilder buf ) {
       for ( var pos = 0 ; line?.Length >= pos ; )
          yield return ReadCsvCell( source, ref line, ref pos, ref buf );
    }
    
    private static string ReadCsvCell ( TextReader source, ref string line, ref int pos, ref StringBuilder buf ) {
       var len = line.Length;
       if ( pos >= len ) { // EOL
          pos = len + 1;
          return "";
       }
    
       // Unquoted cell.
       if ( line[ pos ] != '"' ) {
          var end = line.IndexOf( ',', pos );
          var head = pos;
          // Last cell in this row.
          if ( end < 0 ) {
             pos = len + 1;
             return line.Substring( head );
          }
          // Empty cell.
          if ( end == pos ) {
             pos++;
             return "";
          }
          pos = end + 1;
          return line.Substring( head, end - head );
       }
    
       // Quoted cell.
       if ( buf == null )
          buf = new StringBuilder();
       else
          buf.Clear();
       var start = ++pos; // Drop opening quote.
       while ( true ) {
          var end = pos < len
             ? line.IndexOf( '"', pos )
             : -1;
          var next = end + 1;
    
          // End of line.  Append and read next line.
          if ( end < 0 ) {
             buf.Append( line, start, len - start );
             if ( ( line = source.ReadLine() ) == null )
                return buf.ToString();
             buf.Append( '\n' );
             start = pos = 0; len = line.Length;
    
           // End of cell.
          } else if ( next == len || line[ next ] == ',' ) {
             pos = end + 2;
             return buf.Append( line, start, end - start ).ToString();
    
          // Two double quotes.
          } else if ( line[ next ] == '"' ) {
             buf.Append( line, start, end - start + 1 );
             pos = start = end + 2;
    
          // One double quote not followed by EOL or comma.
          } else
             pos++;
       }
    }
    

    优点

    • 低开销,适用于大文件。 (例如 800mb 的人口普查数据)
    • 适用于所有换行符,解析引用的单元格。
    • 提高报价解析速度的可选缓冲区。
    • 线程安全,如果缓冲区未在线程之间共享。没有锁定。
    • 无依赖性。没有Nuget。适用于所有现代 .Net。

    缺点

    • 所有换行符都将转换为\n。
    • 输出是只进/使用一次。使用ToArrayToList 求解。
    • 缓冲区(如果提供)在读取后不会被清除。

    【讨论】:

    • 可能性能很高,但代码对我来说似乎不可读(因此无法维护)。重新格式化可能会解决问题 ;-) 另外 - 为什么不把它放在一个类中?
    • @MarkusSafar 我承认我不明白。 VS(C) 根据你的喜好格式化我的代码是微不足道的,你可以完全控制它。我知道我急切地格式化复制的代码以在更少的滚动中阅读更多内容。 :D 至于类,你最清楚你想把它们放在你的代码中的什么地方——你不会喜欢我用编译器标志滥用它们的方式。 :) 目的是尽量减少占用空间,拥有可以在任何地方粘贴和使用的轻量级代码,无需任何花里胡哨,包括课程。
    • 我不同意,因为在我看来,创建一堆静态方法需要在使用它们之前进行重构。创建一个类 reuqires 只需复制该类并使用(实例化)它。此外,编译器无论如何都需要编译代码,所以为什么不将其粘贴为人类可读和可维护的形式。
    • 当然每个人都可以重新格式化您在此处粘贴的内容,但问题是您为什么要强迫读者阅读?为什么不提供一个“准备好接受”的好答案,而不是提供一些不是的东西呢?或者您会像现在一样将此代码粘贴到您的 OOP 项目中吗?
    • 这里有很多——关于依赖项和 Nuget 的注释,无数关于高性能和低开销的声明,CSV 写入能力——这不是问题的关注点,但没有真正提到除了隐含的“只需复制并粘贴此代码”之外,这实际上是如何回答问题的。如果你把所有额外的东西都去掉,在我看来,这将成为一个纯代码的答案。我也同意@MarkusSafar 关于格式的看法,似乎对换行符的厌恶使这更难阅读和遵循。
    猜你喜欢
    • 1970-01-01
    • 2018-05-10
    • 1970-01-01
    • 1970-01-01
    • 2021-11-19
    • 1970-01-01
    • 2021-02-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多