使用C#将数据写入CSV文件

264

我正在尝试使用C#语言逐行向一个csv文件写入数据,以下是我的函数:

string first = reader[0].ToString();
string second=image.ToString();
string csv = string.Format("{0},{1}\n", first, second);
File.WriteAllText(filePath, csv);

整个函数运行在一个循环内,每一行都应该写入到csv文件中。在我的情况下,下一行会覆盖现有的行,在最后,我只得到一个单独的记录在csv文件中,那就是最后一个记录。如何把所有的行都写入到csv文件中?


最好使用 StringBuilder 然后再保存一次吗? - Johan
1
如果这不是您每天需要完成的任务,我建议使用 LinqPad,它带有一个方便的函数,可以将数据写入 csv:Util.WriteCsv(mydatacollection, @"c:\temp\data.csv"); - Marco
3
顺便提一下,请确保您的 CSV 值已经编码。例如,如果其中一个包含逗号或换行符,则可能会破坏您的文件。通常我会使用第三方库处理 CSV 相关的事情。 - Matthijs Wessels
@Marco 为什么不每天执行那个任务很重要? - Vandrey
2
只需使用File.AppendAllText - 您需要追加文本,而不是写入。 - Vincent
显示剩余3条评论
16个回答

400

更新

在我年轻无知的时候,我建议手动处理CSV文件(这是一个简单问题的简单解决方案),然而由于这变得越来越流行,我建议使用库CsvHelper,它可以执行所有安全检查等。

CSV比问题/答案所建议的要复杂得多。

最初的回答

既然你已经有了一个循环,考虑像这样处理:

//before your loop
    var csv = new StringBuilder();

//in your loop
    var first = reader[0].ToString();
    var second = image.ToString();
    //Suggestion made by KyleMit
    var newLine = string.Format("{0},{1}", first, second);
    csv.AppendLine(newLine);  

//after your loop
    File.WriteAllText(filePath, csv.ToString());

或者类似的内容。

我的理由是:您不需要为每个项目写入文件,您只需要打开流一次,然后写入它。

您可以替换

Original Answer翻译成"最初的回答"

File.WriteAllText(filePath, csv.ToString());

with

File.AppendAllText(filePath, csv.ToString());

如果你想在同一个文件中保留之前的csv版本

C# 6

如果你使用的是C# 6.0,那么可以按照以下步骤进行操作

var newLine = $"{first},{second}"

编辑

这里是一个问题的链接,解释了Environment.NewLine的作用。

Environment.NewLine是一个字符串常量,表示当前操作系统的换行符。在Windows上,它等同于"\r\n",而在Unix和Mac OS X上,则为"\n"。


5
您可以摒弃 {2}Environment.NewLine,并采用 AppendLine 替代 Append - KyleMit
63
当CSV内容中有需要转义的逗号时,需要使用引号。当引号需要转义时呢?正确构建CSV文件比此答案所示要复杂得多。 - MgSam
1
我得到了一个"exceptionType": "System.UnauthorizedAccessException"。 - Chit Khine
2
大家会很高兴知道,我已经更新了答案,以正确的方式完成事情。 - Johan
3
我尝试了你的解决方案,但对于我(法国文化),使用 System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator 要比逗号更有效。 - A.Pissicat
显示剩余10条评论

110

我强烈建议您选择更加繁琐的方式。特别是当您的文件大小很大时。

using(var w = new StreamWriter(path))
{
    for( /* your loop */)
    {
        var first = yourFnToGetFirst();
        var second = yourFnToGetSecond();
        var line = string.Format("{0},{1}", first, second);
        w.WriteLine(line);
        w.Flush();
    }
}

File.AppendAllText() 方法会打开一个新文件,写入内容,再关闭文件。相比于向已经打开的流中写入数据,打开文件是一种更加消耗资源的操作。在循环内部不断打开和关闭文件会导致性能下降。

Johan 建议的方法通过将所有输出存储在内存中,然后一次性写入解决了这个问题。然而,对于大文件,你的程序将占用大量 RAM,并可能崩溃并出现 OutOfMemoryException

我的解决方案的另一个优点是,您可以通过保存输入数据中的当前位置来实现暂停/恢复功能。

更新:已将使用放置在正确位置


2
你应该将for循环放在using语句的内部。否则,你会一遍又一遍地重新打开文件。 - Oliver
2
好的答案。如果您从格式字符串中删除“{2}”,并在同一行中将“image”替换为“second”,则是正确的答案。此外,不要忘记使用w.Close()关闭写入器;w.Flush()不需要。 - movAX13h
11
此回答忽略了转义字符的需求。 - MgSam
我们可以补充一下,设置编写器的方式如下:new StreamWriter(path, false, Encoding.UTF8) - Charkins12
如果你有几亿行代码,你应该每行都刷新吗? - Ardianto Suhendar

43

手动编写csv文件可能很困难,因为您的数据中可能包含逗号和换行符。我建议您使用现有的库。

这个问题提到了几个选择。

是否有C#中的CSV读写库?


6
这是唯一合理正确的答案。可惜其他试图手动计算的错误答案获得了很多赞。 - MgSam

17

可以考虑只打开一次文件,然后一次性写入所有内容,而不是每次调用AppendAllText()

var file = @"C:\myOutput.csv";

using (var stream = File.CreateText(file))
{
    for (int i = 0; i < reader.Count(); i++)
    {
        string first = reader[i].ToString();
        string second = image.ToString();
        string csvRow = string.Format("{0},{1}", first, second);

        stream.WriteLine(csvRow);
    }
}

这个答案包括了扩展名必须是csv的信息。如果是xls,数据将全部出现在一列中...而csv则会正确地出现在每一列中。 - gman

17

我采用两个解析方案,因为这样非常容易维护。

// Prepare the values
var allLines = (from trade in proposedTrades
                select new object[] 
                { 
                    trade.TradeType.ToString(), 
                    trade.AccountReference, 
                    trade.SecurityCodeType.ToString(), 
                    trade.SecurityCode, 
                    trade.ClientReference, 
                    trade.TradeCurrency, 
                    trade.AmountDenomination.ToString(), 
                    trade.Amount, 
                    trade.Units, 
                    trade.Percentage, 
                    trade.SettlementCurrency, 
                    trade.FOP, 
                    trade.ClientSettlementAccount, 
                    string.Format("\"{0}\"", trade.Notes),                             
                }).ToList();

// Build the file content
var csv = new StringBuilder();
allLines.ForEach(line => 
{
    csv.AppendLine(string.Join(",", line));            
});

File.WriteAllText(filePath, csv.ToString());

2
当构建大文件时,您可能会发现使用此方法会遇到内存压力。如果您删除 .ToList,则 allLines 将是一个 IEnumerbale<object[]>。然后,您可以在其上进行选择而不是“for eaching”,即 allines.Select(line => csv.AppendLine(string.Join(",", line))),这将为您提供一个 IEnumerable<string>。现在,这只需传递给 File 而不是 WriteAllLines 方法。这现在意味着整个过程都是惰性的,您不需要将所有内容都拉入内存,但仍然可以获得您满意的语法。 - RhysC
我正在使用您的方法将数据写入CSV文件,非常好用!我的最新实现需要我使用File.AppendAllLines(filePath, lines, Encoding.ASCII);而不是File.WriteAllText(filePath, csv.ToString());所以我在做一些非常笨拙的事情。在使用StringBuilder构建csv之后,我将其转换为List<string>以获取一个IEnumerable,然后调用AppendAllLines,因为它不接受csv.ToString()作为参数:List<string> lines = new List<string>(); lines.Add(csv.ToString(0, csv.Length));你有更好的方法吗? - Patricia

13

使用现有的库可以避免重复造轮子。 CsvHelper 是一个很棒的用于创建和读取 csv 文件的库,它的读写操作基于流实现,因此也支持处理大量数据。


你可以像以下方式编写你的 csv 文件。

using(var textWriter = new StreamWriter(@"C:\mypath\myfile.csv"))
{
    var writer = new CsvWriter(textWriter, CultureInfo.InvariantCulture);
    writer.Configuration.Delimiter = ",";

    foreach (var item in list)
    {
        writer.WriteField( "a" );
        writer.WriteField( 2 );
        writer.WriteField( true );
        writer.NextRecord();
    }
}

由于该库使用反射,它将直接获取任何类型并解析它。

public class CsvRow
{
    public string Column1 { get; set; }
    public bool Column2 { get; set; }

    public CsvRow(string column1, bool column2)
    {
        Column1 = column1;
        Column2 = column2;
    }
}

IEnumerable<CsvRow> rows = new [] {
    new CsvRow("value1", true),
    new CsvRow("value2", false)
};
using(var textWriter = new StreamWriter(@"C:\mypath\myfile.csv")
{
    var writer = new CsvWriter(textWriter, CultureInfo.InvariantCulture);
    writer.Configuration.Delimiter = ",";
    writer.WriteRecords(rows);
}

值1,真

值2,假


如果您想了解有关该库配置和可能性的更多信息,可以在这里阅读。


您可能需要将 CultureInfo 传递给 StreamWriter 才能使其正常工作。 - alif

13

您可以使用AppendAllText替代:

File.AppendAllText(filePath, csv);

根据WriteAllText的文档所述:

如果目标文件已经存在,它将被覆盖。

此外,请注意您当前的代码未使用正确的换行符,例如在记事本中,您将看到它全部显示为一行。请将代码更改为以下内容以获得正确的换行符:

string csv = string.Format("{0},{1}{2}", first, image, Environment.NewLine);

6
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using System.Configuration;
using System.Data.SqlClient;

public partial class CS : System.Web.UI.Page
{
    protected void ExportCSV(object sender, EventArgs e)
    {
        string constr = ConfigurationManager.ConnectionStrings["constr"].ConnectionString;
        using (SqlConnection con = new SqlConnection(constr))
        {
            using (SqlCommand cmd = new SqlCommand("SELECT * FROM Customers"))
            {
                using (SqlDataAdapter sda = new SqlDataAdapter())
                {
                    cmd.Connection = con;
                    sda.SelectCommand = cmd;
                    using (DataTable dt = new DataTable())
                    {
                        sda.Fill(dt);

                        //Build the CSV file data as a Comma separated string.
                        string csv = string.Empty;

                        foreach (DataColumn column in dt.Columns)
                        {
                            //Add the Header row for CSV file.
                            csv += column.ColumnName + ',';
                        }

                        //Add new line.
                        csv += "\r\n";

                        foreach (DataRow row in dt.Rows)
                        {
                            foreach (DataColumn column in dt.Columns)
                            {
                                //Add the Data rows.
                                csv += row[column.ColumnName].ToString().Replace(",", ";") + ',';
                            }

                            //Add new line.
                            csv += "\r\n";
                        }

                        //Download the CSV file.
                        Response.Clear();
                        Response.Buffer = true;
                        Response.AddHeader("content-disposition", "attachment;filename=SqlExport.csv");
                        Response.Charset = "";
                        Response.ContentType = "application/text";
                        Response.Output.Write(csv);
                        Response.Flush();
                        Response.End();
                    }
                }
            }
        }
    }
}

1
为什么这里没有字符串构建器? - Seabizkit
1
你使用了哪个RFC来编写这个代码: .Replace(",", ";") + ',' - vt100
如果您有超过20000行和30列的数据,这种方法需要太长时间。 - Vikrant

4
这是一个关于使用C#创建CSV文件的简单教程,您可以编辑和扩展它以适应自己的需求。
首先,您需要创建一个新的Visual Studio C#控制台应用程序,有步骤可供遵循。示例代码将在您指定的位置创建一个名为MyTest.csv的CSV文件。文件的内容应该是3个命名列,其中前3行为文本。
参考链接:https://tidbytez.com/2018/02/06/how-to-create-a-csv-file-with-c/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace CreateCsv
{
    class Program
    {
        static void Main()
        {
            // Set the path and filename variable "path", filename being MyTest.csv in this example.
            // Change SomeGuy for your username.
            string path = @"C:\Users\SomeGuy\Desktop\MyTest.csv";

            // Set the variable "delimiter" to ", ".
            string delimiter = ", ";

            // This text is added only once to the file.
            if (!File.Exists(path))
            {
                // Create a file to write to.
                string createText = "Column 1 Name" + delimiter + "Column 2 Name" + delimiter + "Column 3 Name" + delimiter + Environment.NewLine;
                File.WriteAllText(path, createText);
            }

            // This text is always added, making the file longer over time
            // if it is not deleted.
            string appendText = "This is text for Column 1" + delimiter + "This is text for Column 2" + delimiter + "This is text for Column 3" + delimiter + Environment.NewLine;
            File.AppendAllText(path, appendText);

            // Open the file to read from.
            string readText = File.ReadAllText(path);
            Console.WriteLine(readText);
        }
    }
}

2
欢迎提供解决方案的链接,但请确保您的答案即使没有链接也是有用的:在链接周围添加上下文,以便其他用户了解它的内容和原因,然后引用您链接的页面中最相关的部分,以防目标页面不可用。仅仅提供链接的答案可能会被删除。 - 4b0
啊,我明白了。感谢您的反馈。我已经进行了突出显示的更改。请点赞。 - Bloggins

4

处理逗号

在使用string.Format(...)时处理值内的逗号,以下方法适用于我:

var newLine = string.Format("\"{0}\",\"{1}\",\"{2}\"",
                              first,
                              second,
                              third                                    
                              );
csv.AppendLine(newLine);

因此,结合Johan的答案,它看起来像这样:

//before your loop
var csv = new StringBuilder();

//in your loop
  var first = reader[0].ToString();
  var second = image.ToString();
  //Suggestion made by KyleMit
  var newLine = string.Format("\"{0}\",\"{1}\"", first, second);
  csv.AppendLine(newLine);  

//after your loop
File.WriteAllText(filePath, csv.ToString());

返回CSV文件

如果您只是想返回文件而不是将其写入到位置,这是我完成它的示例:

从存储过程中

public FileContentResults DownloadCSV()
{
  // I have a stored procedure that queries the information I need
  SqlConnection thisConnection = new SqlConnection("Data Source=sv12sql;User ID=UI_Readonly;Password=SuperSecure;Initial Catalog=DB_Name;Integrated Security=false");
  SqlCommand queryCommand = new SqlCommand("spc_GetInfoINeed", thisConnection);
  queryCommand.CommandType = CommandType.StoredProcedure;

  StringBuilder sbRtn = new StringBuilder();

  // If you want headers for your file
  var header = string.Format("\"{0}\",\"{1}\",\"{2}\"",
                             "Name",
                             "Address",
                             "Phone Number"
                            );
  sbRtn.AppendLine(header);

  // Open Database Connection
  thisConnection.Open();
  using (SqlDataReader rdr = queryCommand.ExecuteReader())
  {
    while (rdr.Read())
    {
      // rdr["COLUMN NAME"].ToString();
      var queryResults = string.Format("\"{0}\",\"{1}\",\"{2}\"",
                                        rdr["Name"].ToString(),
                                        rdr["Address"}.ToString(),
                                        rdr["Phone Number"].ToString()
                                       );
      sbRtn.AppendLine(queryResults);
    }
  }
  thisConnection.Close();

  return File(new System.Text.UTF8Encoding().GetBytes(sbRtn.ToString()), "text/csv", "FileName.csv");
}

从列表中获取

/* To help illustrate */
public static List<Person> list = new List<Person>();

/* To help illustrate */
public class Person
{
  public string name;
  public string address;
  public string phoneNumber;
}

/* The important part */
public FileContentResults DownloadCSV()
{
  StringBuilder sbRtn = new StringBuilder();

  // If you want headers for your file
  var header = string.Format("\"{0}\",\"{1}\",\"{2}\"",
                             "Name",
                             "Address",
                             "Phone Number"
                            );
  sbRtn.AppendLine(header);

  foreach (var item in list)
  {
      var listResults = string.Format("\"{0}\",\"{1}\",\"{2}\"",
                                        item.name,
                                        item.address,
                                        item.phoneNumber
                                       );
      sbRtn.AppendLine(listResults);
    }
  }

  return File(new System.Text.UTF8Encoding().GetBytes(sbRtn.ToString()), "text/csv", "FileName.csv");
}

希望这对你有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接