如何将 DataTable 转换为 CSV?

137

请问为什么以下代码无法工作。数据被保存到了csv文件中,但是数据没有分隔。它们全部存在于每行的第一个单元格中。

StringBuilder sb = new StringBuilder();

foreach (DataColumn col in dt.Columns)
{
    sb.Append(col.ColumnName + ',');
}

sb.Remove(sb.Length - 1, 1);
sb.Append(Environment.NewLine);

foreach (DataRow row in dt.Rows)
{
    for (int i = 0; i < dt.Columns.Count; i++)
    {
        sb.Append(row[i].ToString() + ",");
    }

    sb.Append(Environment.NewLine);
}

File.WriteAllText("test.csv", sb.ToString());
感谢。

您可以查看此链接:https://gist.github.com/riyadparvez/4467668 - user
我开发了高性能扩展。请查看此答案 - Nigje
19个回答

261

下面这个更短的版本在Excel中可以正常打开,也许你的问题是结尾有逗号

.net = 3.5

StringBuilder sb = new StringBuilder(); 

string[] columnNames = dt.Columns.Cast<DataColumn>().
                                  Select(column => column.ColumnName).
                                  ToArray();
sb.AppendLine(string.Join(",", columnNames));

foreach (DataRow row in dt.Rows)
{
    string[] fields = row.ItemArray.Select(field => field.ToString()).
                                    ToArray();
    sb.AppendLine(string.Join(",", fields));
}

File.WriteAllText("test.csv", sb.ToString());

.net >= 4.0

正如Tim所指出的,如果你在使用.net>=4,你可以让它变得更短:

StringBuilder sb = new StringBuilder(); 

IEnumerable<string> columnNames = dt.Columns.Cast<DataColumn>().
                                  Select(column => column.ColumnName);
sb.AppendLine(string.Join(",", columnNames));

foreach (DataRow row in dt.Rows)
{
    IEnumerable<string> fields = row.ItemArray.Select(field => field.ToString());
    sb.AppendLine(string.Join(",", fields));
}

File.WriteAllText("test.csv", sb.ToString());

正如Christian所建议的那样,如果您想处理字段中的特殊字符转义,请将循环块替换为:

foreach (DataRow row in dt.Rows)
{
    IEnumerable<string> fields = row.ItemArray.Select(field => 
      string.Concat("\"", field.ToString().Replace("\"", "\"\""), "\""));
    sb.AppendLine(string.Join(",", fields));
}

最后一个建议是,您可以逐行编写 CSV 内容,而不是作为整个文档,以避免在内存中拥有大型文档。


3
еңЁ.NET 4дёӯпјҢдҪ еҸҜд»ҘзңҒз•Ҙ.ToArray()并дҪҝз”ЁжҺҘеҸ—IEnumerable<T>зҡ„String.JoinйҮҚиҪҪпјҢиҖҢж— йңҖе°ҶItemArrayеӨҚеҲ¶еҲ°ж–°зҡ„String[]дёӯгҖӮ - Tim Schmelter
3
@TimSchmelter,是的,但这些重载是在.NET 4中引入的,如果 OP 使用.NET < 4,代码将无法编译。 - vc 74
26
这种方法没有考虑到列值中包含逗号的情况。 - Christian
2
而不是IEnumerable<string> fields = row.ItemArray.Select(field => field.ToString().Replace(""","""")); sb.AppendLine("""+string.Join("","", fields)+"""); - Christian
2
@Si8 你是什么意思?这个答案只使用了数据库组件,而 &nbsp 是 HTML/XML 文档的典型表示方式。除非表格显式包含 &nbsp;,否则不会产生上述代码。 - vc 74
显示剩余4条评论

54

我将这个功能封装成一个扩展类,使你可以调用:

myDataTable.WriteToCsvFile("C:\\MyDataTable.csv");

在任何数据表上。

public static class DataTableExtensions 
{
    public static void WriteToCsvFile(this DataTable dataTable, string filePath) 
    {
        StringBuilder fileContent = new StringBuilder();

        foreach (var col in dataTable.Columns) 
        {
            fileContent.Append(col.ToString() + ",");
        }

        fileContent.Replace(",", System.Environment.NewLine, fileContent.Length - 1, 1);

        foreach (DataRow dr in dataTable.Rows) 
        {
            foreach (var column in dr.ItemArray) 
            {
                fileContent.Append("\"" + column.ToString() + "\",");
            }

            fileContent.Replace(",", System.Environment.NewLine, fileContent.Length - 1, 1);
        }

        System.IO.File.WriteAllText(filePath, fileContent.ToString());
    }
}

37

基于Paul Grimshaw的答案,我创建了一个新的扩展函数。我对它进行了清理并添加了处理异常数据的能力(空数据、嵌入引号和标题中的逗号...)。

此外,该函数返回一个更加灵活的字符串。如果表对象不包含任何结构,则返回Null。

    public static string ToCsv(this DataTable dataTable) {
        StringBuilder sbData = new StringBuilder();

        // Only return Null if there is no structure.
        if (dataTable.Columns.Count == 0)
            return null;

        foreach (var col in dataTable.Columns) {
            if (col == null)
                sbData.Append(",");
            else
                sbData.Append("\"" + col.ToString().Replace("\"", "\"\"") + "\",");
        }

        sbData.Replace(",", System.Environment.NewLine, sbData.Length - 1, 1);

        foreach (DataRow dr in dataTable.Rows) {
            foreach (var column in dr.ItemArray) {
                if (column == null)
                    sbData.Append(",");
                else
                    sbData.Append("\"" + column.ToString().Replace("\"", "\"\"") + "\",");
            }
            sbData.Replace(",", System.Environment.NewLine, sbData.Length - 1, 1);
        }

        return sbData.ToString();
    }
你可以这样调用它:
var csvData = dataTableOject.ToCsv();

2
这个是最好的,比其他的都要好。干得好。谢谢。 - Fandango68
太棒了!我在本地添加了注释,但是不需要费力就能直接使用。非常感谢。 - j.hull
太棒了!我将它用作非静态方法,只需将我的DataTable作为参数传递。效果很好,谢谢你。 - Kid Koder

10
如果您的调用代码引用了 System.Windows.Forms 程序集,您可以考虑一种根本不同的方法。 我的策略是使用框架已提供的函数,在非常少的代码行中完成此操作,而无需循环遍历列和行。下面的代码所做的是在程序中动态创建一个 DataGridView,并将 DataGridView.DataSource 设置为 DataTable。接下来,我以编程方式选择 DataGridView 中的所有单元格(包括标题)并调用 DataGridView.GetClipboardContent(),将结果放入 Windows Clipboard 中。然后,我将剪贴板的内容“粘贴”到对 File.WriteAllText() 的调用中,确保指定“粘贴”的格式为 TextDataFormat.CommaSeparatedValue。
以下是代码:
public static void DataTableToCSV(DataTable Table, string Filename)
{
    using(DataGridView dataGrid = new DataGridView())
    {
        // Save the current state of the clipboard so we can restore it after we are done
        IDataObject objectSave = Clipboard.GetDataObject();

        // Set the DataSource
        dataGrid.DataSource = Table;
        // Choose whether to write header. Use EnableWithoutHeaderText instead to omit header.
        dataGrid.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableAlwaysIncludeHeaderText;
        // Select all the cells
        dataGrid.SelectAll();
        // Copy (set clipboard)
        Clipboard.SetDataObject(dataGrid.GetClipboardContent());
        // Paste (get the clipboard and serialize it to a file)
        File.WriteAllText(Filename,Clipboard.GetText(TextDataFormat.CommaSeparatedValue));              

        // Restore the current state of the clipboard so the effect is seamless
        if(objectSave != null) // If we try to set the Clipboard to an object that is null, it will throw...
        {
            Clipboard.SetDataObject(objectSave);
        }
    }
}

注意,在开始之前,我还要确保在进行操作之前保存剪贴板的内容,并在完成后恢复它,以便用户下次尝试粘贴时不会遇到意外的垃圾。这种方法的主要限制是:1)您的类必须引用 System.Windows.Forms,这在数据抽象层中可能不是这种情况;2)您的程序集必须针对.NET 4.5框架进行目标设置,因为DataGridView在4.0中不存在;3)如果剪贴板正在被另一个进程使用,则该方法将失败。

无论如何,这种方法可能不适合您的情况,但仍然很有趣,并且可以成为您工具箱中的另一个工具。


1
不需要使用剪贴板。.GetClipboardContent 还处理了一些包含 ,"\t(它将制表符转换为空格)的特殊情况。 - Slai
2
这很好,但是如果有人在关键时刻同时使用机器并将某些内容放入剪贴板怎么办? - Ayo Adesina

7
尝试将sb.Append(Environment.NewLine);更改为sb.AppendLine();
StringBuilder sb = new StringBuilder();          
foreach (DataColumn col in dt.Columns)         
{             
    sb.Append(col.ColumnName + ',');         
}          

sb.Remove(sb.Length - 1, 1);         
sb.AppendLine();          

foreach (DataRow row in dt.Rows)         
{             
    for (int i = 0; i < dt.Columns.Count; i++)             
    {                 
        sb.Append(row[i].ToString() + ",");             
    }              

    sb.AppendLine();         
}          

File.WriteAllText("test.csv", sb.ToString());

那将会产生两个回车。 - Darren Young
@alexl:这就是我最初的想法,但那只是我脑海中的想法,直到VS启动为止 :o) - Neil Knight

7

四行代码:

public static string ToCSV(DataTable tbl)
{
    StringBuilder strb = new StringBuilder();

    //column headers
    strb.AppendLine(string.Join(",", tbl.Columns.Cast<DataColumn>()
        .Select(s => "\"" + s.ColumnName + "\"")));

    //rows
    tbl.AsEnumerable().Select(s => strb.AppendLine(
        string.Join(",", s.ItemArray.Select(
            i => "\"" + i.ToString() + "\"")))).ToList();

    return strb.ToString();
}

请注意,末尾的ToList()很重要;我需要一些东西来强制表达式求值。如果我在编写代码时追求节约代码量,我可以使用Min()代替。
还要注意,由于最后一次调用AppendLine(),结果将在末尾有换行符。你可能不想要它。你可以简单地调用TrimEnd()来移除它。

7
我最近也做了相同的事情,但在我的值周围包含了双引号。
例如,更改这两行:
sb.Append("\"" + col.ColumnName + "\","); 
...
sb.Append("\"" + row[i].ToString() + "\","); 

感谢建议,但是所有数据仍然都在每行的第一个单元格中吗? - Darren Young

5
尝试使用;而不是,,希望能有所帮助。

5
错误在于列表分隔符。
不应该写成sb.Append(something... + ','),而应该写成sb.Append(something... + System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator); 必须将操作系统中配置的列表分隔符字符(如上面的示例)或文件所在客户端机器上的列表分隔符放入。另一种选择是在您的应用程序的app.config或web.config中将其配置为参数。

5
要写入文件,我认为以下方法是最有效和直接的:(如果需要,您可以添加引号)
public static void WriteCsv(DataTable dt, string path)
{
    using (var writer = new StreamWriter(path)) {
        writer.WriteLine(string.Join(",", dt.Columns.Cast<DataColumn>().Select(dc => dc.ColumnName)));
        foreach (DataRow row in dt.Rows) {
            writer.WriteLine(string.Join(",", row.ItemArray));
        }
    }
}

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