关于c#:CSV字符串处理

CSV string handling

创建csv字符串(伪代码)的典型方法:

  • 创建一个csv容器对象(就像c中的StringBuilder)。
  • 循环遍历要添加的字符串,在每个字符串后添加一个逗号。
  • 在循环之后,删除最后一个多余的逗号。
  • 代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public string ReturnAsCSV(ContactList contactList)
    {
        StringBuilder sb = new StringBuilder();
        foreach (Contact c in contactList)
        {
            sb.Append(c.Name +",");
        }

        sb.Remove(sb.Length - 1, 1);
        //sb.Replace(",","", sb.Length - 1, 1)

        return sb.ToString();
    }

    我喜欢通过检查容器是否为空来添加逗号的想法,但这是否意味着需要进行更多的处理,因为它需要检查每次出现时字符串的长度?

    我觉得应该有一种更容易/更干净/更有效的方法来删除最后一个逗号。有什么想法吗?


    可以使用Linq to对象:

    1
    2
    string [] strings = contactList.Select(c => c.Name).ToArray();
    string csv = string.Join(",", strings);

    很明显,这一切都可以在一行中完成,但在两行中就更清楚了。


    您的代码不符合完整的csv格式。如果您只是从没有逗号、前导/尾随空格、制表符、换行符或引号的数据生成csv,那么应该可以。然而,在大多数实际数据交换场景中,您确实需要完整的信息。

    要生成正确的csv,可以使用以下命令:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    public static String EncodeCsvLine(params String[] fields)
    {
        StringBuilder line = new StringBuilder();

        for (int i = 0; i < fields.Length; i++)
        {
            if (i > 0)
            {
                line.Append(DelimiterChar);
            }

            String csvField = EncodeCsvField(fields[i]);
            line.Append(csvField);
        }

        return line.ToString();
    }

    static String EncodeCsvField(String field)
    {
        StringBuilder sb = new StringBuilder();
        sb.Append(field);

        // Some fields with special characters must be embedded in double quotes
        bool embedInQuotes = false;

        // Embed in quotes to preserve leading/tralining whitespace
        if (sb.Length > 0 &&
            (sb[0] == ' ' ||
             sb[0] == '\t' ||
             sb[sb.Length-1] == ' ' ||
             sb[sb.Length-1] == '\t' ))
        {
            embedInQuotes = true;
        }

        for (int i = 0; i < sb.Length; i++)
        {
            // Embed in quotes to preserve: commas, line-breaks etc.
            if (sb[i] == DelimiterChar ||
                sb[i]=='
    '
    ||
                sb[i]=='
    '
    ||
                sb[i] == '"')
            {
                embedInQuotes = true;
                break;
            }
        }

        // If the field itself has quotes, they must each be represented
        // by a pair of consecutive quotes.
        sb.Replace(""","""");

        String rv = sb.ToString();

        if (embedInQuotes)
        {
            rv ="
    "" + rv +""";
        }

        return rv;
    }

    可能不是世界上最有效的代码,但它已经过测试。与快速示例代码相比,现实世界很糟糕:)


    为什么不使用一个开源的csv库呢?

    我知道这听起来像是对一些看起来很简单的东西的过度杀戮,但是正如你从评论和代码片段中所看到的那样,这里有很多东西值得一看。除了处理完整的csv遵从性之外,您最终还需要处理读写csv…您可能需要文件操作。

    我以前在我的一个项目中使用过open-csv(但还有很多其他项目可供选择)。这确实让我的生活更轻松了。;)


    别忘了我们的老朋友"为了"。它不像foreach那么好看,但是它的优点是能够从第二个元素开始。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public string ReturnAsCSV(ContactList contactList)
    {
        if (contactList == null || contactList.Count == 0)
            return string.Empty;

        StringBuilder sb = new StringBuilder(contactList[0].Name);

        for (int i = 1; i < contactList.Count; i++)
        {
            sb.Append(",");
            sb.Append(contactList[i].Name);
        }

        return sb.ToString();
    }

    您还可以将第二个附加包装在"if"中,该"if"用于测试name属性是否包含双引号或逗号,如果包含双引号或逗号,则相应地对其进行转义。


    您还可以创建一个c.name数据数组,并使用string.join方法创建行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public string ReturnAsCSV(ContactList contactList)
    {
        List<String> tmpList = new List<string>();

        foreach (Contact c in contactList)
        {
            tmpList.Add(c.Name);
        }

        return String.Join(",", tmpList.ToArray());
    }

    这可能不如StringBuilder方法的性能好,但看起来确实更干净。

    另外,您可能还需要考虑使用.currentCulture.textinfo.listSparator,而不是硬编码的逗号——如果要将输出导入其他应用程序,则可能会遇到问题。ListSeparator可能在不同的文化中有所不同,而MS至少擅长这种设置。所以:

    1
    2
    3
    return String.Join(
        System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator,
        tmpList.ToArray());

    你可以把逗号作为你前臂里的第一个东西。

    if (sb.Length > 0) sb.Append(",");


    我为这个写了一个小班,以防别人发现它有用…

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    public class clsCSVBuilder
    {
        protected int _CurrentIndex = -1;
        protected List<string> _Headers = new List<string>();
        protected List<List<string>> _Records = new List<List<string>>();
        protected const string SEPERATOR =",";

        public clsCSVBuilder() { }

        public void CreateRow()
        {
            _Records.Add(new List<string>());
            _CurrentIndex++;
        }

        protected string _EscapeString(string str)
        {
            return string.Format(""{0}"", str.Replace(""","""")
                                                .Replace("


    ","")
                                                .Replace("

    ","")
                                                .Replace("

    ",""));
        }

        protected void _AddRawString(string item)
        {
            _Records[_CurrentIndex].Add(item);
        }

        public void AddHeader(string name)
        {
            _Headers.Add(_EscapeString(name));
        }

        public void AddRowItem(string item)
        {
            _AddRawString(_EscapeString(item));
        }

        public void AddRowItem(int item)
        {
            _AddRawString(item.ToString());
        }

        public void AddRowItem(double item)
        {
            _AddRawString(item.ToString());
        }

        public void AddRowItem(DateTime date)
        {
            AddRowItem(date.ToShortDateString());
        }

        public static string GenerateTempCSVPath()
        {
            return Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString().ToLower().Replace("
    -","") +".csv");
        }

        protected string _GenerateCSV()
        {
            StringBuilder sb = new StringBuilder();

            if (_Headers.Count > 0)
            {
                sb.AppendLine(string.Join(SEPERATOR, _Headers.ToArray()));
            }

            foreach (List<string> row in _Records)
            {
                sb.AppendLine(string.Join(SEPERATOR, row.ToArray()));
            }

            return sb.ToString();
        }

        public void SaveAs(string path)
        {
            using (StreamWriter sw = new StreamWriter(path))
            {
                sw.Write(_GenerateCSV());
            }
        }
    }

    我以前用过这种方法。StringBuilder的长度属性不是只读的,因此用一种方法减去它将截断最后一个字符。但是您必须确保您的长度不是零(如果您的列表是空的,则会发生这种情况),因为将长度设置为小于零是一个错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public string ReturnAsCSV(ContactList contactList)
    {
        StringBuilder sb = new StringBuilder();

        foreach (Contact c in contactList)      
        {
            sb.Append(c.Name +",");      
        }

        if (sb.Length > 0)  
            sb.Length -= 1;

        return sb.ToString();  
    }

    只是一个想法,但是记住在字段值中处理逗号和引号("),否则您的csv文件可能会破坏消费者阅读器。


    I like the idea of adding the comma by checking if the container is empty, but doesn't that mean more processing as it needs to check the length of the string on each occurrence?

    你过早的优化,性能的影响可以忽略不计。


    我使用csvhelper-它是一个伟大的开源库,允许您一次生成一个元素或自定义映射类的兼容csv流:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public string ReturnAsCSV(ContactList contactList)
    {
        StringBuilder sb = new StringBuilder();
        using (StringWriter stringWriter = new StringWriter(sb))
        {
            using (var csvWriter = new CsvHelper.CsvWriter(stringWriter))
            {
                csvWriter.Configuration.HasHeaderRecord = false;
                foreach (Contact c in contactList)
                {
                    csvWriter.WriteField(c.Name);
                }
            }
        }
        return sb.ToString();
    }

    或者如果你画这样的地图:csvWriter.WriteRecords(contactList);


    修剪一下怎么样?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public string ReturnAsCSV(ContactList contactList)
    {
        StringBuilder sb = new StringBuilder();

        foreach (Contact c in contactList)
        {
            sb.Append(c.Name +",");
        }

        return sb.ToString().Trim(',');
    }

    如何跟踪你是否在第一个项目上,如果不是第一个项目,只在项目前面加一个逗号。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public string ReturnAsCSV(ContactList contactList)
    {
        StringBuilder sb = new StringBuilder();
        bool isFirst = true;

        foreach (Contact c in contactList) {
            if (!isFirst) {
              // Only add comma before item if it is not the first item
              sb.Append(",");
            } else {
              isFirst = false;
            }

            sb.Append(c.Name);
        }

        return sb.ToString();
    }