如何拆分包含逗号的CSV文件列

142

给定

2,1016,7/31/2008 14:22,Geoff Dalgas,6/5/2011 22:21,http://stackoverflow.com,"Corvallis, OR",7679,351,81,b437f461b3fd27387c5d8ab47a293d35,34

如何使用C#将上述信息分割为以下字符串:

2
1016
7/31/2008 14:22
Geoff Dalgas
6/5/2011 22:21
http://stackoverflow.com
Corvallis, OR
7679
351
81
b437f461b3fd27387c5d8ab47a293d35
34

正如您所看到的,其中一列包含 , <= (Corvallis, OR)

基于C# Regex Split - commas outside quotes

string[] result = Regex.Split(samplestring, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");

1
虽然在Java中,类似的问题:https://dev59.com/y3I-5IYBdhLWcg3woJ5I - Saurabh Gokhale
1
使用正则表达式来完成这个任务是错误的建议。.NET Framework已经内置了解析CSV的支持。请查看这个答案,这是您应该接受的答案。否则我将关闭此问题,因为它与https://dev59.com/WHA75IYBdhLWcg3ws7ci一样同样错误。 - Kev
1
请问您能否详细说明一下.NET对于解析包含逗号的CSV文件的内置支持是什么?您是指Microsoft.VisualBasic.FileIO.TextFieldParser类吗? - AllSolutions
这个回答解决了你的问题吗?使用C#读取CSV文件 - Dour High Arch
这里有另一个基于Regex的解决方案链接,并提供了一些不错的示例。 - minus one
9个回答

215

使用 Microsoft.VisualBasic.FileIO.TextFieldParser 类,它可以处理分隔符文件、TextReaderStream,其中一些字段被引号包围而其他字段则不是。

例如:

using Microsoft.VisualBasic.FileIO;

string csv = "2,1016,7/31/2008 14:22,Geoff Dalgas,6/5/2011 22:21,http://stackoverflow.com,\"Corvallis, OR\",7679,351,81,b437f461b3fd27387c5d8ab47a293d35,34";

TextFieldParser parser = new TextFieldParser(new StringReader(csv));

// You can also read from a file
// TextFieldParser parser = new TextFieldParser("mycsvfile.csv");

parser.HasFieldsEnclosedInQuotes = true;
parser.SetDelimiters(",");

string[] fields;

while (!parser.EndOfData)
{
    fields = parser.ReadFields();
    foreach (string field in fields)
    {
        Console.WriteLine(field);
    }
} 

parser.Close();

这应该会得到以下输出:

2
1016
7/31/2008 14:22
Geoff Dalgas
6/5/2011 22:21
http://stackoverflow.com
Corvallis, OR
7679
351
81
b437f461b3fd27387c5d8ab47a293d35
34

请查看Microsoft.VisualBasic.FileIO.TextFieldParser获取更多信息。

您需要在“添加引用” .NET选项卡中添加对 Microsoft.VisualBasic 的引用。


14
哥们,非常感谢你提供的解决方案。我有大约 50 万行 CSV 数据需要加载到表格中,而这些数据里面包含了带引号的逗号,但是你的解决方案让我成功加载了数据。如果我们有机会见面的话,我会请你喝你喜欢的成人饮料。 - Mark Kram
@tim 我使用了这个,但是注意到它跳过了所有偶数行号,只处理了一个有1050行的文件中的奇数行号。你有什么想法吗? - Smith
@Smith - 没有看到你的代码或样本输入,我不知道。我建议发布一个新问题。也许文件在偶数行上缺少回车符或其他行尾标记? - Tim
3
我们能不能因为微软没有提供一个接受字符串的构造函数让我们不得不先将其转换成流而把它绞死?否则,回答很好。 - Loren Pechtel
@sln - 那就不要用它。我已经使用过很多次了,它适合我的需求。OP提出了一个问题,我给了他一个适合他的答案。你个人是否会对你使用和推荐的库和框架进行全面测试,以考虑到每种可能的情况呢?如果你不喜欢微软的东西,那就不要用它 - 有很多其他选择 :) - Tim
显示剩余10条评论

91

虽然已经晚了,但这对于某些人可能仍有帮助。我们可以使用以下正则表达式。

Regex CSVParser = new Regex(",(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))");
String[] Fields = CSVParser.Split(Test);

8
太完美了!比起导入整个其它库,我更愿意使用这个。太棒了! - TheGeekYouNeed
1
匹配 asdf, "", "as,"df", - user557597
1
这个解决方案不正确 - 它没有考虑到引号,这意味着在阅读过程中会有很多引号出现在错误的位置。 - AidanH
1
这对我有用,并且考虑到了引号。有三千万行数据,非常好,代码量很少。 - GBGOLC
1
"RG","RG.jpg",350.0000,"PK, LEMON, LIME"," ","RG","FSH","FIVE",09/04/2012,T,"","","MC","WW",""“RG”,“RG.jpg”,350.0000,“PK,LEMON,LIME”,“ ”,“RG”,“FSH”,“FIVE”,09/04/2012,T,“”,“”,“MC”,“WW”, - Aidan Hakimian
显示剩余5条评论

5

我看到如果你将csv分隔的文本粘贴到Excel中并执行“文本分列”操作,它会要求你输入“文本限定符”。默认情况下,它设置为双引号,以使双引号内的文本被视为字面值。我想像Excel通过逐个字符地处理来实现这一点,如果遇到“文本限定符”,则继续查找下一个“限定符”。您可以使用for循环和布尔值来实现自己的限定符,并指示是否在文字限定符内部。

public string[] CsvParser(string csvText)
{
    List<string> tokens = new List<string>();

    int last = -1;
    int current = 0;
    bool inText = false;

    while(current < csvText.Length)
    {
        switch(csvText[current])
        {
            case '"':
                inText = !inText; break;
            case ',':
                if (!inText) 
                {
                    tokens.Add(csvText.Substring(last + 1, (current - last)).Trim(' ', ',')); 
                    last = current;
                }
                break;
            default:
                break;
        }
        current++;
    }

    if (last != csvText.Length - 1) 
    {
        tokens.Add(csvText.Substring(last+1).Trim());
    }

    return tokens.ToArray();
}

仍存在一个单个逗号的问题。逗号应该产生两个空字段,但它没有。 - Maximiliano Rios

4

3
@q0987 - 这不是正确的答案。框架中已经内置了对此的支持:https://dev59.com/ZWw15IYBdhLWcg3wi8Zv#6543418 - Kev
@Kev - 这也是不正确的。你链接的答案是针对 Microsoft.VisualBasic.FileIO.TextFieldParser 类的,它并没有内置在 .NET Framework 中。这就是为什么你必须引用 Microsoft.VisualBasic 程序集才能在你的项目中使用它。 - Blair Allen
@BlairAllen 只是因为它存在于不同的命名空间和程序集中,并不意味着它不是.NET Framework功能之一,其来源可以是微软。如果我说它是基础类库的一部分,那么你可能有自己的看法。 - Kev

3

这个问题及其重复问题都有很多答案。我尝试了看起来很有前途的这个答案,但是发现其中有一些错误。我对它进行了大量修改,以便通过我的所有测试。

    /// <summary>
    /// Returns a collection of strings that are derived by splitting the given source string at
    /// characters given by the 'delimiter' parameter.  However, a substring may be enclosed between
    /// pairs of the 'qualifier' character so that instances of the delimiter can be taken as literal
    /// parts of the substring.  The method was originally developed to split comma-separated text
    /// where quotes could be used to qualify text that contains commas that are to be taken as literal
    /// parts of the substring.  For example, the following source:
    ///     A, B, "C, D", E, "F, G"
    /// would be split into 5 substrings:
    ///     A
    ///     B
    ///     C, D
    ///     E
    ///     F, G
    /// When enclosed inside of qualifiers, the literal for the qualifier character may be represented
    /// by two consecutive qualifiers.  The two consecutive qualifiers are distinguished from a closing
    /// qualifier character.  For example, the following source:
    ///     A, "B, ""C"""
    /// would be split into 2 substrings:
    ///     A
    ///     B, "C"
    /// </summary>
    /// <remarks>Originally based on: https://dev59.com/W2865IYBdhLWcg3whO7E#43284485</remarks>
    /// <param name="source">The string that is to be split</param>
    /// <param name="delimiter">The character that separates the substrings</param>
    /// <param name="qualifier">The character that is used (in pairs) to enclose a substring</param>
    /// <param name="toTrim">If true, then whitespace is removed from the beginning and end of each
    /// substring.  If false, then whitespace is preserved at the beginning and end of each substring.
    /// </param>
    public static List<String> SplitQualified(this String source, Char delimiter, Char qualifier,
                                Boolean toTrim)
    {
        // Avoid throwing exception if the source is null
        if (String.IsNullOrEmpty(source))
            return new List<String> { "" };

        var results = new List<String>();
        var result = new StringBuilder();
        Boolean inQualifier = false;

        // The algorithm is designed to expect a delimiter at the end of each substring, but the
        // expectation of the caller is that the final substring is not terminated by delimiter.
        // Therefore, we add an artificial delimiter at the end before looping through the source string.
        String sourceX = source + delimiter;

        // Loop through each character of the source
        for (var idx = 0; idx < sourceX.Length; idx++)
        {
            // If current character is a delimiter
            // (except if we're inside of qualifiers, we ignore the delimiter)
            if (sourceX[idx] == delimiter && inQualifier == false)
            {
                // Terminate the current substring by adding it to the collection
                // (trim if specified by the method parameter)
                results.Add(toTrim ? result.ToString().Trim() : result.ToString());
                result.Clear();
            }
            // If current character is a qualifier
            else if (sourceX[idx] == qualifier)
            {
                // ...and we're already inside of qualifier
                if (inQualifier)
                {
                    // check for double-qualifiers, which is escape code for a single
                    // literal qualifier character.
                    if (idx + 1 < sourceX.Length && sourceX[idx + 1] == qualifier)
                    {
                        idx++;
                        result.Append(sourceX[idx]);
                        continue;
                    }
                    // Since we found only a single qualifier, that means that we've
                    // found the end of the enclosing qualifiers.
                    inQualifier = false;
                    continue;
                }
                else
                    // ...we found an opening qualifier
                    inQualifier = true;
            }
            // If current character is neither qualifier nor delimiter
            else
                result.Append(sourceX[idx]);
        }

        return results;
    }

以下是证明它有效的测试方法:
    [TestMethod()]
    public void SplitQualified_00()
    {
        // Example with no substrings
        String s = "";
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "" }, substrings);
    }
    [TestMethod()]
    public void SplitQualified_00A()
    {
        // just a single delimiter
        String s = ",";
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "", "" }, substrings);
    }
    [TestMethod()]
    public void SplitQualified_01()
    {
        // Example with no whitespace or qualifiers
        String s = "1,2,3,1,2,3";
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2", "3", "1", "2", "3" }, substrings);
    }
    [TestMethod()]
    public void SplitQualified_02()
    {
        // Example with whitespace and no qualifiers
        String s = " 1, 2 ,3,  1  ,2\t,   3   ";
        // whitespace should be removed
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2", "3", "1", "2", "3" }, substrings);
    }
    [TestMethod()]
    public void SplitQualified_03()
    {
        // Example with whitespace and no qualifiers
        String s = " 1, 2 ,3,  1  ,2\t,   3   ";
        // whitespace should be preserved
        var substrings = s.SplitQualified(',', '"', false);
        CollectionAssert.AreEquivalent(
            new List<String> { " 1", " 2 ", "3", "  1  ", "2\t", "   3   " },
            substrings);
    }
    [TestMethod()]
    public void SplitQualified_04()
    {
        // Example with no whitespace and trivial qualifiers.
        String s = "1,\"2\",3,1,2,\"3\"";
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2", "3", "1", "2", "3" }, substrings);

        s = "\"1\",\"2\",3,1,\"2\",3";
        substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2", "3", "1", "2", "3" }, substrings);
    }
    [TestMethod()]
    public void SplitQualified_05()
    {
        // Example with no whitespace and qualifiers that enclose delimiters
        String s = "1,\"2,2a\",3,1,2,\"3,3a\"";
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2,2a", "3", "1", "2", "3,3a" },
                                substrings);

        s = "\"1,1a\",\"2,2b\",3,1,\"2,2c\",3";
        substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1,1a", "2,2b", "3", "1", "2,2c", "3" },
                                substrings);
    }
    [TestMethod()]
    public void SplitQualified_06()
    {
        // Example with qualifiers enclosing whitespace but no delimiter
        String s = "\" 1 \",\"2 \",3,1,2,\"\t3\t\"";
        // whitespace should be removed
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2", "3", "1", "2", "3" },
                                substrings);
    }
    [TestMethod()]
    public void SplitQualified_07()
    {
        // Example with qualifiers enclosing whitespace but no delimiter
        String s = "\" 1 \",\"2 \",3,1,2,\"\t3\t\"";
        // whitespace should be preserved
        var substrings = s.SplitQualified(',', '"', false);
        CollectionAssert.AreEquivalent(new List<String> { " 1 ", "2 ", "3", "1", "2", "\t3\t" },
                                substrings);
    }
    [TestMethod()]
    public void SplitQualified_08()
    {
        // Example with qualifiers enclosing whitespace but no delimiter; also whitespace btwn delimiters
        String s = "\" 1 \", \"2 \"  ,  3,1, 2 ,\"  3  \"";
        // whitespace should be removed
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2", "3", "1", "2", "3" },
                                substrings);
    }
    [TestMethod()]
    public void SplitQualified_09()
    {
        // Example with qualifiers enclosing whitespace but no delimiter; also whitespace btwn delimiters
        String s = "\" 1 \", \"2 \"  ,  3,1, 2 ,\"  3  \"";
        // whitespace should be preserved
        var substrings = s.SplitQualified(',', '"', false);
        CollectionAssert.AreEquivalent(new List<String> { " 1 ", " 2   ", "  3", "1", " 2 ", "  3  " },
                                substrings);
    }
    [TestMethod()]
    public void SplitQualified_10()
    {
        // Example with qualifiers enclosing whitespace and delimiter
        String s = "\" 1 \",\"2 , 2b \",3,1,2,\"  3,3c  \"";
        // whitespace should be removed
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2 , 2b", "3", "1", "2", "3,3c" },
                                substrings);
    }
    [TestMethod()]
    public void SplitQualified_11()
    {
        // Example with qualifiers enclosing whitespace and delimiter; also whitespace btwn delimiters
        String s = "\" 1 \", \"2 , 2b \"  ,  3,1, 2 ,\"  3,3c  \"";
        // whitespace should be preserved
        var substrings = s.SplitQualified(',', '"', false);
        CollectionAssert.AreEquivalent(new List<String> { " 1 ", " 2 , 2b   ", "  3", "1", " 2 ", "  3,3c  " },
                                substrings);
    }
    [TestMethod()]
    public void SplitQualified_12()
    {
        // Example with tab characters between delimiters
        String s = "\t1,\t2\t,3,1,\t2\t,\t3\t";
        // whitespace should be removed
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2", "3", "1", "2", "3" }, substrings);
    }
    [TestMethod()]
    public void SplitQualified_13()
    {
        // Example with newline characters between delimiters
        String s = "\n1,\n2\n,3,1,\n2\n,\n3\n";
        // whitespace should be removed
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2", "3", "1", "2", "3" }, substrings);
    }
    [TestMethod()]
    public void SplitQualified_14()
    {
        // Example with qualifiers enclosing whitespace and delimiter, plus escaped qualifier
        String s = "\" 1 \",\"\"\"2 , 2b \"\"\",3,1,2,\"  \"\"3,3c  \"";
        // whitespace should be removed
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "\"2 , 2b \"", "3", "1", "2", "\"3,3c" },
                                substrings);
    }
    [TestMethod()]
    public void SplitQualified_14A()
    {
        // Example with qualifiers enclosing whitespace and delimiter, plus escaped qualifier
        String s = "\"\"\"1\"\"\"";
        // whitespace should be removed
        var substrings = s.SplitQualified(',', '"', true);
        CollectionAssert.AreEquivalent(new List<String> { "\"1\"" },
                                substrings);
    }


    [TestMethod()]
    public void SplitQualified_15()
    {
        // Instead of comma-delimited and quote-qualified, use pipe and hash

        // Example with no whitespace or qualifiers
        String s = "1|2|3|1|2,2f|3";
        var substrings = s.SplitQualified('|', '#', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2", "3", "1", "2,2f", "3" }, substrings);
    }
    [TestMethod()]
    public void SplitQualified_16()
    {
        // Instead of comma-delimited and quote-qualified, use pipe and hash

        // Example with qualifiers enclosing whitespace and delimiter
        String s = "# 1 #|#2 | 2b #|3|1|2|#  3|3c  #";
        // whitespace should be removed
        var substrings = s.SplitQualified('|', '#', true);
        CollectionAssert.AreEquivalent(new List<String> { "1", "2 | 2b", "3", "1", "2", "3|3c" },
                                substrings);
    }
    [TestMethod()]
    public void SplitQualified_17()
    {
        // Instead of comma-delimited and quote-qualified, use pipe and hash

        // Example with qualifiers enclosing whitespace and delimiter; also whitespace btwn delimiters
        String s = "# 1 #| #2 | 2b #  |  3|1| 2 |#  3|3c  #";
        // whitespace should be preserved
        var substrings = s.SplitQualified('|', '#', false);
        CollectionAssert.AreEquivalent(new List<String> { " 1 ", " 2 | 2b   ", "  3", "1", " 2 ", "  3|3c  " },
                                substrings);
    }

3

使用类似于LumenWorks的库来进行CSV文件的读取。它可以处理带引号的字段,并且由于已经存在很长时间,因此通常比您的自定义解决方案更加健壮。


3

解析.csv文件时,如果.csv文件既可以是逗号分隔的字符串、又可以是逗号分隔的带引号字符串,或混合这两种情况,则会变得棘手。我提出的解决方案允许这三种可能性。

我创建了一个名为ParseCsvRow()的方法,它从csv字符串返回一个数组。我首先通过在双引号上拆分字符串并创建名为quotesArray的数组来处理字符串中的双引号。仅当有偶数个双引号时,带引号的字符串.csv文件才有效。列值中的双引号应该替换为一对双引号(这是Excel的方法)。只要.csv文件满足这些要求,就可以期望分隔符逗号仅出现在双引号对的外部。双引号对内部的逗号是列值的一部分,在将.csv拆分为数组时应忽略它们。

我的方法将通过仅查看quotesArray的偶数索引来测试双引号对外的逗号。它还从列值的开头和末尾删除双引号。

    public static string[] ParseCsvRow(string csvrow)
    {
        const string obscureCharacter = "ᖳ";
        if (csvrow.Contains(obscureCharacter)) throw new Exception("Error: csv row may not contain the " + obscureCharacter + " character");

        var unicodeSeparatedString = "";

        var quotesArray = csvrow.Split('"');  // Split string on double quote character
        if (quotesArray.Length > 1)
        {
            for (var i = 0; i < quotesArray.Length; i++)
            {
                // CSV must use double quotes to represent a quote inside a quoted cell
                // Quotes must be paired up
                // Test if a comma lays outside a pair of quotes.  If so, replace the comma with an obscure unicode character
                if (Math.Round(Math.Round((decimal) i/2)*2) == i)
                {
                    var s = quotesArray[i].Trim();
                    switch (s)
                    {
                        case ",":
                            quotesArray[i] = obscureCharacter;  // Change quoted comma seperated string to quoted "obscure character" seperated string
                            break;
                        default:
                            if (s.All(chars => chars == ','))
                            {
                                quotesArray[i] = "";
                                for (int j = 0; j < s.Count(); j++)
                                {
                                    quotesArray[i] += obscureCharacter;
                                } 
                            }
                            break;
                    }
                }
                // Build string and Replace quotes where quotes were expected.
                unicodeSeparatedString += (i > 0 ? "\"" : "") + quotesArray[i].Trim();
            }
        }
        else
        {
            // String does not have any pairs of double quotes.  It should be safe to just replace the commas with the obscure character
            unicodeSeparatedString = csvrow.Replace(",", obscureCharacter);
        }

        var csvRowArray = unicodeSeparatedString.Split(obscureCharacter[0]); 

        for (var i = 0; i < csvRowArray.Length; i++)
        {
            var s = csvRowArray[i].Trim();
            if (s.StartsWith("\"") && s.EndsWith("\""))
            {
                csvRowArray[i] = s.Length > 2 ? s.Substring(1, s.Length - 2) : "";  // Remove start and end quotes.
            }
        }
        
        return csvRowArray;
    }

我方法的一个缺点是,我临时用一个不常见的Unicode字符替换了分隔符逗号。这个字符需要非常冷门,以免在您的.csv文件中出现。您可能希望对此进行更多处理。

1

我遇到了一个包含引号字符的CSV文件的问题,所以我使用TextFieldParser,得出了以下结果:

private static string[] parseCSVLine(string csvLine)
{
  using (TextFieldParser TFP = new TextFieldParser(new MemoryStream(Encoding.UTF8.GetBytes(csvLine))))
  {
    TFP.HasFieldsEnclosedInQuotes = true;
    TFP.SetDelimiters(",");

    try 
    {           
      return TFP.ReadFields();
    }
    catch (MalformedLineException)
    {
      StringBuilder m_sbLine = new StringBuilder();

      for (int i = 0; i < TFP.ErrorLine.Length; i++)
      {
        if (i > 0 && TFP.ErrorLine[i]== '"' &&(TFP.ErrorLine[i + 1] != ',' && TFP.ErrorLine[i - 1] != ','))
          m_sbLine.Append("\"\"");
        else
          m_sbLine.Append(TFP.ErrorLine[i]);
      }

      return parseCSVLine(m_sbLine.ToString());
    }
  }
}

仍然使用StreamReader逐行读取CSV,如下所示:

using(StreamReader SR = new StreamReader(FileName))
{
  while (SR.Peek() >-1)
    myStringArray = parseCSVLine(SR.ReadLine());
}

1

使用Cinchoo ETL - 一个开源库,它可以自动处理包含分隔符的列值。

string csv = @"2,1016,7/31/2008 14:22,Geoff Dalgas,6/5/2011 22:21,http://stackoverflow.com,""Corvallis, OR"",7679,351,81,b437f461b3fd27387c5d8ab47a293d35,34";

using (var p = ChoCSVReader.LoadText(csv)
    )
{
    Console.WriteLine(p.Dump());
}

输出:

Key: Column1 [Type: String]
Value: 2
Key: Column2 [Type: String]
Value: 1016
Key: Column3 [Type: String]
Value: 7/31/2008 14:22
Key: Column4 [Type: String]
Value: Geoff Dalgas
Key: Column5 [Type: String]
Value: 6/5/2011 22:21
Key: Column6 [Type: String]
Value: http://stackoverflow.com
Key: Column7 [Type: String]
Value: Corvallis, OR
Key: Column8 [Type: String]
Value: 7679
Key: Column9 [Type: String]
Value: 351
Key: Column10 [Type: String]
Value: 81
Key: Column11 [Type: String]
Value: b437f461b3fd27387c5d8ab47a293d35
Key: Column12 [Type: String]
Value: 34

如需更多信息,请访问codeproject文章。

希望能对你有所帮助。


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