ToB企服应用市场:ToB评测及商务社交产业平台

标题: C#中的CSV文件读写 [打印本页]

作者: 曹旭辉    时间: 2022-6-20 09:41
标题: C#中的CSV文件读写
目录

项目中经常遇到CSV文件的读写需求,其中的难点主要是CSV文件的解析。本文会介绍CsvHelperTextFieldParser正则表达式三种解析CSV文件的方法,顺带也会介绍一下CSV文件的写方法。
CSV文件标准

在介绍CSV文件的读写方法前,我们需要了解一下CSV文件的格式。
文件示例

一个简单的CSV文件:
  1. Test1,Test2,Test3,Test4,Test5,Test6
  2. str1,str2,str3,str4,str5,str6
  3. str1,str2,str3,str4,str5,str6
复制代码
一个不简单的CSV文件:
  1. "Test1
  2. "",""","Test2
  3. "",""","Test3
  4. "",""","Test4
  5. "",""","Test5
  6. "",""","Test6
  7. "","""
  8. " 中文,D23 ","3DFD4234""""""1232""1S2","ASD1"",""23,,,,213
  9. 23F32","
  10. ",,asd
  11. " 中文,D23 ","3DFD4234""""""1232""1S2","ASD1"",""23,,,,213
  12. 23F32","
  13. ",,asd
复制代码
你没看错,上面两个都是CSV文件,都只有3行CSV数据。第二个文件多看一眼都是精神污染,但项目中无法避免会出现这种文件。
RFC 4180

CSV文件没有官方的标准,但一般项目都会遵守 RFC 4180 标准。这是一个非官方的标准,内容如下:
翻译一下:
简化标准

上面的标准可能比较拗口,我们对它进行一些简化。要注意一下,简化不是简单的删减规则,而是将类似的类似进行合并便于理解。
后面的代码也会使用简化标准,简化标准如下:
读写CSV文件

在正式读写CSV文件前,我们需要先定义一个用于测试的Test类。代码如下:
  1. class Test
  2. {
  3.     public string Test1{get;set;}
  4.     public string Test2 { get; set; }
  5.     public string Test3 { get; set; }
  6.     public string Test4 { get; set; }
  7.     public string Test5 { get; set; }
  8.     public string Test6 { get; set; }
  9.     //Parse方法会在自定义读写CSV文件时用到
  10.     public static Test Parse (string[]fields )
  11.     {
  12.         try
  13.         {
  14.             Test ret = new Test();
  15.             ret.Test1 = fields[0];
  16.             ret.Test2 = fields[1];
  17.             ret.Test3 = fields[2];
  18.             ret.Test4 = fields[3];
  19.             ret.Test5 = fields[4];
  20.             ret.Test6 = fields[5];
  21.             return ret;
  22.         }
  23.         catch (Exception)
  24.         {
  25.             //做一些异常处理,写日志之类的
  26.             return null;
  27.         }
  28.     }
  29. }
复制代码
生成一些测试数据,代码如下:
  1. static void Main(string[] args)
  2. {
  3.     //文件保存路径
  4.     string path = "tset.csv";
  5.     //清理之前的测试文件
  6.     File.Delete("tset.csv");
  7.       
  8.     Test test = new Test();
  9.     test.Test1 = " 中文,D23 ";
  10.     test.Test2 = "3DFD4234"""1232"1S2";
  11.     test.Test3 = "ASD1","23,,,,213\r23F32";
  12.     test.Test4 = "\r";
  13.     test.Test5 = string.Empty;
  14.     test.Test6 = "asd";
  15.     //测试数据
  16.     var records = new List<Test> { test, test };
  17.     //写CSV文件
  18.     /*
  19.     *直接把后面的写CSV文件代码复制到此处
  20.     */
  21.     //读CSV文件
  22.      /*
  23.     *直接把后面的读CSV文件代码复制到此处
  24.     */
  25.    
  26.     Console.ReadLine();
  27. }
复制代码
使用CsvHelper

CsvHelper 是用于读取和写入 CSV 文件的库,支持自定义类对象的读写。
github上标星最高的CSV文件读写C#库,使用MS-PLApache 2.0开源协议。
使用NuGet下载CsvHelper,读写CSV文件的代码如下:
  1. //写CSV文件
  2. using (var writer = new StreamWriter(path))
  3. using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
  4. {
  5.     csv.WriteRecords(records);
  6. }
  7. using (var writer = new StreamWriter(path,true))
  8. using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
  9. {
  10.     //追加
  11.     foreach (var record in records)
  12.     {
  13.         csv.WriteRecord(record);
  14.     }
  15. }
  16. //读CSV文件
  17. using (var reader = new StreamReader(path))
  18. using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
  19. {
  20.     records = csv.GetRecords<Test>().ToList();
  21.     //逐行读取
  22.     //records.Add(csv.GetRecord<Test>());
  23. }
复制代码
如果你只想要拿来就能用的库,那文章基本上到这里就结束了。
使用自定义方法

为了与CsvHelper区分,新建一个CsvFile类存放自定义读写CSV文件的代码,最后会提供类的完整源码。CsvFile类定义如下:
  1. /// <summary>
  2. /// CSV文件读写工具类
  3. /// </summary>
  4. public class CsvFile
  5. {
  6.     #region 写CSV文件
  7.     //具体代码...
  8.     #endregion
  9.     #region 读CSV文件(使用TextFieldParser)
  10.     //具体代码...
  11.     #endregion
  12.     #region 读CSV文件(使用正则表达式)
  13.     //具体代码...
  14.     #endregion
  15. }
复制代码
基于简化标准的写CSV文件

根据简化标准(具体标准内容见前文),写CSV文件代码如下:
  1. #region 写CSV文件
  2. //字段数组转为CSV记录行
  3. private static string FieldsToLine(IEnumerable<string> fields)
  4. {
  5.     if (fields == null) return string.Empty;
  6.     fields = fields.Select(field =>
  7.     {
  8.         if (field == null) field = string.Empty;
  9.         //简化标准,所有字段都加双引号
  10.         field = string.Format(""{0}"", field.Replace(""", """"));
  11.         //不简化标准
  12.         //field = field.Replace(""", """");
  13.         //if (field.IndexOfAny(new char[] { ',', '"', ' ', '\r' }) != -1)
  14.         //{
  15.         //    field = string.Format(""{0}"", field);
  16.         //}
  17.         return field;
  18.     });
  19.     string line = string.Format("{0}{1}", string.Join(",", fields), Environment.NewLine);
  20.     return line;
  21. }
  22. //默认的字段转换方法
  23. private static IEnumerable<string> GetObjFields<T>(T obj, bool isTitle) where T : class
  24. {
  25.     IEnumerable<string> fields;
  26.     if (isTitle)
  27.     {
  28.         fields = obj.GetType().GetProperties().Select(pro => pro.Name);
  29.     }
  30.     else
  31.     {
  32.         fields = obj.GetType().GetProperties().Select(pro => pro.GetValue(obj)?.ToString());
  33.     }
  34.     return fields;
  35. }
  36. /// <summary>
  37. /// 写CSV文件,默认第一行为标题
  38. /// </summary>
  39. /// <typeparam name="T"></typeparam>
  40. /// <param name="list">数据列表</param>
  41. /// <param name="path">文件路径</param>
  42. /// <param name="append">追加记录</param>
  43. /// <param name="func">字段转换方法</param>
  44. /// <param name="defaultEncoding"></param>
  45. public static void Write<T>(List<T> list, string path,bool append=true, Func<T, bool, IEnumerable<string>> func = null, Encoding defaultEncoding = null) where T : class
  46. {
  47.     if (list == null || list.Count == 0) return;
  48.     if (defaultEncoding == null)
  49.     {
  50.         defaultEncoding = Encoding.UTF8;
  51.     }
  52.     if (func == null)
  53.     {
  54.         func = GetObjFields;
  55.     }
  56.     if (!File.Exists(path)|| !append)
  57.     {
  58.         var fields = func(list[0], true);
  59.         string title = FieldsToLine(fields);
  60.         File.WriteAllText(path, title, defaultEncoding);
  61.     }
  62.     using (StreamWriter sw = new StreamWriter(path, true, defaultEncoding))
  63.     {
  64.         list.ForEach(obj =>
  65.         {
  66.             var fields = func(obj, false);
  67.             string line = FieldsToLine(fields);
  68.             sw.Write(line);
  69.         });
  70.     }
  71. }
  72. #endregion
复制代码
使用时,代码如下:
  1. //写CSV文件
  2. //使用自定义的字段转换方法,也是文章开头复杂CSV文件使用字段转换方法
  3. CsvFile.Write(records, path, true, new Func<Test, bool, IEnumerable<string>>((obj, isTitle) =>
  4. {
  5.     IEnumerable<string> fields;
  6.     if (isTitle)
  7.     {
  8.         fields = obj.GetType().GetProperties().Select(pro => pro.Name + Environment.NewLine + "","");
  9.     }
  10.     else
  11.     {
  12.         fields = obj.GetType().GetProperties().Select(pro => pro.GetValue(obj)?.ToString());
  13.     }
  14.     return fields;
  15. }));
  16. //使用默认的字段转换方法
  17. //CsvFile.Write(records, path);
复制代码
你也可以使用默认的字段转换方法,代码如下:
  1. CsvFile.Save(records, path);
复制代码
使用TextFieldParser解析CSV文件

TextFieldParser是VB中解析CSV文件的类,C#虽然没有类似功能的类,不过可以调用VB的TextFieldParser来实现功能。
TextFieldParser解析CSV文件的代码如下:
  1. #region 读CSV文件(使用TextFieldParser)
  2. /// <summary>
  3. /// 读CSV文件,默认第一行为标题
  4. /// </summary>
  5. /// <typeparam name="T"></typeparam>
  6. /// <param name="path">文件路径</param>
  7. /// <param name="func">字段解析规则</param>
  8. /// <param name="defaultEncoding">文件编码</param>
  9. /// <returns></returns>
  10. public static List<T> Read<T>(string path, Func<string[], T> func, Encoding defaultEncoding = null) where T : class
  11. {
  12.     if (defaultEncoding == null)
  13.     {
  14.         defaultEncoding = Encoding.UTF8;
  15.     }
  16.     List<T> list = new List<T>();
  17.     using (TextFieldParser parser = new TextFieldParser(path, defaultEncoding))
  18.     {
  19.         parser.TextFieldType = FieldType.Delimited;
  20.         //设定逗号分隔符
  21.         parser.SetDelimiters(",");
  22.         //设定不忽略字段前后的空格
  23.         parser.TrimWhiteSpace = false;
  24.         bool isLine = false;
  25.         while (!parser.EndOfData)
  26.         {
  27.             string[] fields = parser.ReadFields();
  28.             if (isLine)
  29.             {
  30.                 var obj = func(fields);
  31.                 if (obj != null) list.Add(obj);
  32.             }
  33.             else
  34.             {
  35.                 //忽略标题行业
  36.                 isLine = true;
  37.             }
  38.         }
  39.     }
  40.     return list;
  41. }
  42. #endregion
复制代码
使用时,代码如下:
  1. //读CSV文件
  2. records = CsvFile.Read(path, Test.Parse);
复制代码
使用正则表达式解析CSV文件

如果你有一个问题,想用正则表达式来解决,那么你就有两个问题了。
正则表达式有一定的学习门槛,而且学习后不经常使用就会忘记。正则表达式解决的大多数是一些不易变更需求的问题,这就导致一个稳定可用的正则表达式可以传好几代。
本节的正则表达式来自 《精通正则表达式(第3版)》 第6章 打造高效正则表达式——简单的消除循环的例子,有兴趣的可以去了解一下,表达式说明如下:

注:这本书最终版的解析CSV文件的正则表达式是Jave版的使用占有优先量词取代固化分组的版本,也是百度上经常见到的版本。不过占有优先量词在C#中有点问题,本人能力有限解决不了,所以使用了上图的版本。不过,这两版正则表达式性能上没有差异。
正则表达式解析CSV文件代码如下:
  1. #region 读CSV文件(使用正则表达式)
  2. /// <summary>
  3. /// 读CSV文件,默认第一行为标题
  4. /// </summary>
  5. /// <typeparam name="T"></typeparam>
  6. /// <param name="path">文件路径</param>
  7. /// <param name="func">字段解析规则</param>
  8. /// <param name="defaultEncoding">文件编码</param>
  9. /// <returns></returns>
  10. public static List<T> Read_Regex<T>(string path, Func<string[], T> func, Encoding defaultEncoding = null) where T : class
  11. {
  12.     List<T> list = new List<T>();
  13.     StringBuilder sbr = new StringBuilder(100);
  14.     Regex lineReg = new Regex(""");
  15.     Regex fieldReg = new Regex("\\G(?:^|,)(?:"((?>[^"]*)(?>""[^"]*)*)"|([^",]*))");
  16.     Regex quotesReg = new Regex("""");
  17.     bool isLine = false;
  18.     string line = string.Empty;
  19.     using (StreamReader sr = new StreamReader(path))
  20.     {
  21.         while (null != (line = ReadLine(sr)))
  22.         {
  23.             sbr.Append(line);
  24.             string str = sbr.ToString();
  25.             //一个完整的CSV记录行,它的双引号一定是偶数
  26.             if (lineReg.Matches(sbr.ToString()).Count % 2 == 0)
  27.             {
  28.                 if (isLine)
  29.                 {
  30.                     var fields = ParseCsvLine(sbr.ToString(), fieldReg, quotesReg).ToArray();
  31.                     var obj = func(fields.ToArray());
  32.                     if (obj != null) list.Add(obj);
  33.                 }
  34.                 else
  35.                 {
  36.                     //忽略标题行业
  37.                     isLine = true;
  38.                 }
  39.                 sbr.Clear();
  40.             }
  41.             else
  42.             {
  43.                 sbr.Append(Environment.NewLine);
  44.             }                  
  45.         }
  46.     }
  47.     if (sbr.Length > 0)
  48.     {
  49.         //有解析失败的字符串,报错或忽略
  50.     }
  51.     return list;
  52. }
  53. //重写ReadLine方法,只有\r\n才是正确的一行
  54. private static string ReadLine(StreamReader sr)
  55. {
  56.     StringBuilder sbr = new StringBuilder();
  57.     char c;
  58.     int cInt;
  59.     while (-1 != (cInt =sr.Read()))
  60.     {
  61.         c = (char)cInt;
  62.         if (c == '\n' && sbr.Length > 0 && sbr[sbr.Length - 1] == '\r')
  63.         {
  64.             sbr.Remove(sbr.Length - 1, 1);
  65.             return sbr.ToString();
  66.         }
  67.         else
  68.         {
  69.             sbr.Append(c);
  70.         }
  71.     }
  72.     return sbr.Length>0?sbr.ToString():null;
  73. }
  74. private static List<string> ParseCsvLine(string line, Regex fieldReg, Regex quotesReg)
  75. {
  76.     var fieldMath = fieldReg.Match(line);
  77.     List<string> fields = new List<string>();
  78.     while (fieldMath.Success)
  79.     {
  80.         string field;
  81.         if (fieldMath.Groups[1].Success)
  82.         {
  83.             field = quotesReg.Replace(fieldMath.Groups[1].Value, """);
  84.         }
  85.         else
  86.         {
  87.             field = fieldMath.Groups[2].Value;
  88.         }
  89.         fields.Add(field);
  90.         fieldMath = fieldMath.NextMatch();
  91.     }
  92.     return fields;
  93. }
  94. #endregion
复制代码
使用时代码如下:
  1. //读CSV文件
  2. records = CsvFile.Read_Regex(path, Test.Parse);
复制代码
目前还未发现正则表达式解析有什么bug,不过还是不建议使用。
完整的CsvFile工具类

完整的CsvFile类代码如下:
  1. using Microsoft.VisualBasic.FileIO;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Text.RegularExpressions;
  8. namespace ConsoleApp4
  9. {
  10.     /// <summary>
  11.     /// CSV文件读写工具类
  12.     /// </summary>
  13.     public class CsvFile
  14.     {
  15.         #region 写CSV文件
  16.         //字段数组转为CSV记录行
  17.         private static string FieldsToLine(IEnumerable<string> fields)
  18.         {
  19.             if (fields == null) return string.Empty;
  20.             fields = fields.Select(field =>
  21.             {
  22.                 if (field == null) field = string.Empty;
  23.                 //所有字段都加双引号
  24.                 field = string.Format(""{0}"", field.Replace(""", """"));
  25.                 //不简化
  26.                 //field = field.Replace(""", """");
  27.                 //if (field.IndexOfAny(new char[] { ',', '"', ' ', '\r' }) != -1)
  28.                 //{
  29.                 //    field = string.Format(""{0}"", field);
  30.                 //}
  31.                 return field;
  32.             });
  33.             string line = string.Format("{0}{1}", string.Join(",", fields), Environment.NewLine);
  34.             return line;
  35.         }
  36.         //默认的字段转换方法
  37.         private static IEnumerable<string> GetObjFields<T>(T obj, bool isTitle) where T : class
  38.         {
  39.             IEnumerable<string> fields;
  40.             if (isTitle)
  41.             {
  42.                 fields = obj.GetType().GetProperties().Select(pro => pro.Name);
  43.             }
  44.             else
  45.             {
  46.                 fields = obj.GetType().GetProperties().Select(pro => pro.GetValue(obj)?.ToString());
  47.             }
  48.             return fields;
  49.         }
  50.         /// <summary>
  51.         /// 写CSV文件,默认第一行为标题
  52.         /// </summary>
  53.         /// <typeparam name="T"></typeparam>
  54.         /// <param name="list">数据列表</param>
  55.         /// <param name="path">文件路径</param>
  56.         /// <param name="append">追加记录</param>
  57.         /// <param name="func">字段转换方法</param>
  58.         /// <param name="defaultEncoding"></param>
  59.         public static void Write<T>(List<T> list, string path,bool append=true, Func<T, bool, IEnumerable<string>> func = null, Encoding defaultEncoding = null) where T : class
  60.         {
  61.             if (list == null || list.Count == 0) return;
  62.             if (defaultEncoding == null)
  63.             {
  64.                 defaultEncoding = Encoding.UTF8;
  65.             }
  66.             if (func == null)
  67.             {
  68.                 func = GetObjFields;
  69.             }
  70.             if (!File.Exists(path)|| !append)
  71.             {
  72.                 var fields = func(list[0], true);
  73.                 string title = FieldsToLine(fields);
  74.                 File.WriteAllText(path, title, defaultEncoding);
  75.             }
  76.             using (StreamWriter sw = new StreamWriter(path, true, defaultEncoding))
  77.             {
  78.                 list.ForEach(obj =>
  79.                 {
  80.                     var fields = func(obj, false);
  81.                     string line = FieldsToLine(fields);
  82.                     sw.Write(line);
  83.                 });
  84.             }
  85.         }
  86.         #endregion
  87.         #region 读CSV文件(使用TextFieldParser)
  88.         /// <summary>
  89.         /// 读CSV文件,默认第一行为标题
  90.         /// </summary>
  91.         /// <typeparam name="T"></typeparam>
  92.         /// <param name="path">文件路径</param>
  93.         /// <param name="func">字段解析规则</param>
  94.         /// <param name="defaultEncoding">文件编码</param>
  95.         /// <returns></returns>
  96.         public static List<T> Read<T>(string path, Func<string[], T> func, Encoding defaultEncoding = null) where T : class
  97.         {
  98.             if (defaultEncoding == null)
  99.             {
  100.                 defaultEncoding = Encoding.UTF8;
  101.             }
  102.             List<T> list = new List<T>();
  103.             using (TextFieldParser parser = new TextFieldParser(path, defaultEncoding))
  104.             {
  105.                 parser.TextFieldType = FieldType.Delimited;
  106.                 //设定逗号分隔符
  107.                 parser.SetDelimiters(",");
  108.                 //设定不忽略字段前后的空格
  109.                 parser.TrimWhiteSpace = false;
  110.                 bool isLine = false;
  111.                 while (!parser.EndOfData)
  112.                 {
  113.                     string[] fields = parser.ReadFields();
  114.                     if (isLine)
  115.                     {
  116.                         var obj = func(fields);
  117.                         if (obj != null) list.Add(obj);
  118.                     }
  119.                     else
  120.                     {
  121.                         //忽略标题行业
  122.                         isLine = true;
  123.                     }
  124.                 }
  125.             }
  126.             return list;
  127.         }
  128.         #endregion
  129.         #region 读CSV文件(使用正则表达式)
  130.         /// <summary>
  131.         /// 读CSV文件,默认第一行为标题
  132.         /// </summary>
  133.         /// <typeparam name="T"></typeparam>
  134.         /// <param name="path">文件路径</param>
  135.         /// <param name="func">字段解析规则</param>
  136.         /// <param name="defaultEncoding">文件编码</param>
  137.         /// <returns></returns>
  138.         public static List<T> Read_Regex<T>(string path, Func<string[], T> func, Encoding defaultEncoding = null) where T : class
  139.         {
  140.             List<T> list = new List<T>();
  141.             StringBuilder sbr = new StringBuilder(100);
  142.             Regex lineReg = new Regex(""");
  143.             Regex fieldReg = new Regex("\\G(?:^|,)(?:"((?>[^"]*)(?>""[^"]*)*)"|([^",]*))");
  144.             Regex quotesReg = new Regex("""");
  145.             bool isLine = false;
  146.             string line = string.Empty;
  147.             using (StreamReader sr = new StreamReader(path))
  148.             {
  149.                 while (null != (line = ReadLine(sr)))
  150.                 {
  151.                     sbr.Append(line);
  152.                     string str = sbr.ToString();
  153.                     //一个完整的CSV记录行,它的双引号一定是偶数
  154.                     if (lineReg.Matches(sbr.ToString()).Count % 2 == 0)
  155.                     {
  156.                         if (isLine)
  157.                         {
  158.                             var fields = ParseCsvLine(sbr.ToString(), fieldReg, quotesReg).ToArray();
  159.                             var obj = func(fields.ToArray());
  160.                             if (obj != null) list.Add(obj);
  161.                         }
  162.                         else
  163.                         {
  164.                             //忽略标题行业
  165.                             isLine = true;
  166.                         }
  167.                         sbr.Clear();
  168.                     }
  169.                     else
  170.                     {
  171.                         sbr.Append(Environment.NewLine);
  172.                     }                  
  173.                 }
  174.             }
  175.             if (sbr.Length > 0)
  176.             {
  177.                 //有解析失败的字符串,报错或忽略
  178.             }
  179.             return list;
  180.         }
  181.         //重写ReadLine方法,只有\r\n才是正确的一行
  182.         private static string ReadLine(StreamReader sr)
  183.         {
  184.             StringBuilder sbr = new StringBuilder();
  185.             char c;
  186.             int cInt;
  187.             while (-1 != (cInt =sr.Read()))
  188.             {
  189.                 c = (char)cInt;
  190.                 if (c == '\n' && sbr.Length > 0 && sbr[sbr.Length - 1] == '\r')
  191.                 {
  192.                     sbr.Remove(sbr.Length - 1, 1);
  193.                     return sbr.ToString();
  194.                 }
  195.                 else
  196.                 {
  197.                     sbr.Append(c);
  198.                 }
  199.             }
  200.             return sbr.Length>0?sbr.ToString():null;
  201.         }
  202.       
  203.         private static List<string> ParseCsvLine(string line, Regex fieldReg, Regex quotesReg)
  204.         {
  205.             var fieldMath = fieldReg.Match(line);
  206.             List<string> fields = new List<string>();
  207.             while (fieldMath.Success)
  208.             {
  209.                 string field;
  210.                 if (fieldMath.Groups[1].Success)
  211.                 {
  212.                     field = quotesReg.Replace(fieldMath.Groups[1].Value, """);
  213.                 }
  214.                 else
  215.                 {
  216.                     field = fieldMath.Groups[2].Value;
  217.                 }
  218.                 fields.Add(field);
  219.                 fieldMath = fieldMath.NextMatch();
  220.             }
  221.             return fields;
  222.         }
  223.         #endregion
  224.     }
  225. }
复制代码
使用方法如下:
  1. //写CSV文件CsvFile.Write(records, path, true, new Func((obj, isTitle) =>{    IEnumerable fields;    if (isTitle)    {        fields = obj.GetType().GetProperties().Select(pro => pro.Name + Environment.NewLine + "","");    }    else    {        fields = obj.GetType().GetProperties().Select(pro => pro.GetValue(obj)?.ToString());    }    return fields;}));//读CSV文件
  2. records = CsvFile.Read(path, Test.Parse);//读CSV文件
  3. records = CsvFile.Read_Regex(path, Test.Parse);
复制代码
总结

附录


来源:https://www.cnblogs.com/timefiles/archive/2022/06/15/CsvReadWrite.html
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4