c#实现csv文件读写的示例详解
项目中经常遇到csv文件的读写需求,其中的难点主要是csv文件的解析。本文会介绍csvhelper、textfieldparser、正则表达式三种解析csv文件的方法,顺带也会介绍一下csv文件的写方法。
csv文件标准
在介绍csv文件的读写方法前,我们需要了解一下csv文件的格式。
文件示例
一个简单的csv文件:
test1,test2,test3,test4,test5,test6 str1,str2,str3,str4,str5,str6 str1,str2,str3,str4,str5,str6
一个不简单的csv文件:
"test1 "",""","test2 "",""","test3 "",""","test4 "",""","test5 "",""","test6 "",""" " 中文,d23 ","3dfd4234""""""1232""1s2","asd1"",""23,,,,213 23f32"," ",,asd " 中文,d23 ","3dfd4234""""""1232""1s2","asd1"",""23,,,,213 23f32"," ",,asd
你没看错,上面两个都是csv文件,都只有3行csv数据。第二个文件多看一眼都是精神污染,但项目中无法避免会出现这种文件。
rfc 4180
csv文件没有官方的标准,但一般项目都会遵守rfc 4180标准。这是一个非官方的标准,内容如下:
each record is located on a separate line, delimited by a line break (crlf).
the last record in the file may or may not have an ending line break.
there maybe an optional header line appearing as the first line of the file with the same format as normal record lines. this header will contain names corresponding to the fields in the file and should contain the same number of fields as the records in the rest of the file (the presence or absence of the header line should be indicated via the optional "header" parameter of this mime type).
within the header and each record, there may be one or more fields, separated by commas. each line should contain the same number of fields throughout the file. spaces are considered part of a field and should not be ignored. the last field in the record must not be followed by a comma.
each field may or may not be enclosed in double quotes (however some programs, such as microsoft excel, do not use double quotes at all). if fields are not enclosed with double quotes, then double quotes may not appear inside the fields.
fields containing line breaks (crlf), double quotes, and commas should be enclosed in double-quotes.
if double-quotes are used to enclose fields, then a double-quote appearing inside a field must be escaped by preceding it with another double quote.
翻译一下:
- 每条记录位于单独的行上,由换行符 (crlf) 分隔。
- 文件中的最后一条记录可能有也可能没有结束换行符。
- 可能有一个可选的标题行出现在文件的第一行,格式与普通记录行相同。此标题将包含与文件中的字段对应的名称,并且应包含与文件其余部分中的记录相同数量的字段(标题行的存在或不存在应通过此 mime 类型的可选“标头”参数指示)。
- 在标题和每条记录中,可能有一个或多个字段,以逗号分隔。在整个文件中,每行应包含相同数量的字段。空格被视为字段的一部分,不应忽略。记录中的最后一个字段后面不能有逗号。
- 每个字段可以用双引号括起来,也可以不用双引号(但是某些程序,例如 microsoft excel,根本不使用双引号)。如果字段没有用双引号括起来,那么双引号可能不会出现在字段内。
- 包含换行符 (crlf)、双引号和逗号的字段应该用双引号括起来。
- 如果使用双引号将字段括起来,则出现在字段中的双引号必须在其前面加上另一个双引号。
简化标准
上面的标准可能比较拗口,我们对它进行一些简化。要注意一下,简化不是简单的删减规则,而是将类似的类似进行合并便于理解。
后面的代码也会使用简化标准,简化标准如下:
- 每条记录位于单独的行上,由换行符 (crlf) 分隔。
- 注:此处的行不是普通文本意义上的行,是指符合csv文件格式的一条记录(后面简称为csv行),在文本上可能占据多行。
- 文件中的最后一条记录需有结束换行符,文件的第一行为标题行(标题行包含字段对应的名称,标题数与记录的字段数相同)。
- 注:原标准中可有可无的选项统一规定为必须有,方便后期的解析,而且没有标题行让别人怎么看数据。
- 在标题和每条记录中,可能有一个或多个字段,以逗号分隔。在整个文件中,每行应包含相同数量的字段。空格被视为字段的一部分,不应忽略。记录中的最后一个字段后面不能有逗号。
- 注:此标准未做简化,虽然也有其它标准使用空格、制表符等做分割的,但不使用逗号分割的文件还叫逗号分隔值文件吗。
- 每个字段都用双引号括起来,出现在字段中的双引号必须在其前面加上另一个双引号
注:原标准有必须使用双引号和可选双引号的情况,那全部使用双引号肯定不会出错。*
读写csv文件
在正式读写csv文件前,我们需要先定义一个用于测试的test类。代码如下:
class test { public string test1{get;set;} public string test2 { get; set; } public string test3 { get; set; } public string test4 { get; set; } public string test5 { get; set; } public string test6 { get; set; } //parse方法会在自定义读写csv文件时用到 public static test parse (string[]fields ) { try { test ret = new test(); ret.test1 = fields[0]; ret.test2 = fields[1]; ret.test3 = fields[2]; ret.test4 = fields[3]; ret.test5 = fields[4]; ret.test6 = fields[5]; return ret; } catch (exception) { //做一些异常处理,写日志之类的 return null; } } }
生成一些测试数据,代码如下:
static void main(string[] args) { //文件保存路径 string path = "tset.csv"; //清理之前的测试文件 file.delete("tset.csv"); test test = new test(); test.test1 = " 中文,d23 "; test.test2 = "3dfd4234\"\"\"1232\"1s2"; test.test3 = "asd1\",\"23,,,,213\r23f32"; test.test4 = "\r"; test.test5 = string.empty; test.test6 = "asd"; //测试数据 var records = new list<test> { test, test }; //写csv文件 /* *直接把后面的写csv文件代码复制到此处 */ //读csv文件 /* *直接把后面的读csv文件代码复制到此处 */ console.readline(); }</test>
使用csvhelper
csvhelper是用于读取和写入 csv 文件的库,支持自定义类对象的读写。
github上标星最高的csv文件读写c#库,使用ms-pl、apache 2.0开源协议。
使用nuget下载csvhelper,读写csv文件的代码如下:
//写csv文件 using (var writer = new streamwriter(path)) using (var csv = new csvwriter(writer, cultureinfo.invariantculture)) { csv.writerecords(records); } using (var writer = new streamwriter(path,true)) using (var csv = new csvwriter(writer, cultureinfo.invariantculture)) { //追加 foreach (var record in records) { csv.writerecord(record); } } //读csv文件 using (var reader = new streamreader(path)) using (var csv = new csvreader(reader, cultureinfo.invariantculture)) { records = csv.getrecords<test>().tolist(); //逐行读取 //records.add(csv.getrecord<test>()); }</test></test>
如果你只想要拿来就能用的库,那文章基本上到这里就结束了。
使用自定义方法
为了与csvhelper区分,新建一个csvfile类存放自定义读写csv文件的代码,最后会提供类的完整源码。csvfile类定义如下:
/// <summary> /// csv文件读写工具类 /// </summary> public class csvfile { #region 写csv文件 //具体代码... #endregion #region 读csv文件(使用textfieldparser) //具体代码... #endregion #region 读csv文件(使用正则表达式) //具体代码... #endregion }
基于简化标准的写csv文件
根据简化标准(具体标准内容见前文),写csv文件代码如下:
#region 写csv文件 //字段数组转为csv记录行 private static string fieldstoline(ienumerable<string> fields) { if (fields == null) return string.empty; fields = fields.select(field => { if (field == null) field = string.empty; //简化标准,所有字段都加双引号 field = string.format("\"{0}\"", field.replace("\"", "\"\"")); //不简化标准 //field = field.replace("\"", "\"\""); //if (field.indexofany(new char[] { ',', '"', ' ', '\r' }) != -1) //{ // field = string.format("\"{0}\"", field); //} return field; }); string line = string.format("{0}{1}", string.join(",", fields), environment.newline); return line; } //默认的字段转换方法 private static ienumerable<string> getobjfields<t>(t obj, bool istitle) where t : class { ienumerable<string> fields; if (istitle) { fields = obj.gettype().getproperties().select(pro => pro.name); } else { fields = obj.gettype().getproperties().select(pro => pro.getvalue(obj)?.tostring()); } return fields; } /// <summary> /// 写csv文件,默认第一行为标题 /// </summary> /// <typeparam name="t"></typeparam> /// <param name="list" />数据列表 /// <param name="path" />文件路径 /// <param name="append" />追加记录 /// <param name="func" />字段转换方法 /// <param name="defaultencoding" /> public static void write<t>(list<t> list, string path,bool append=true, func<t, bool,="" ienumerable
- C#操作JSON(序列化与反序列化)的方法详解
- C#使用kernel32.dll读写INI文件的案例详解
- C# 读写编辑INI文件的操作
- 利用C#实现可以继承的"枚举"
- C#实现系统休眠或静止休眠的方法
- C# 计算DataTime的4种时间差的方法(相差天数、相差小时、相差分钟、相差秒)
- C#控制台程序的开发与打包为一个exe文件实例详解
- C#实现CSV文件读写的示例详解
- 基于WPF实现拟物音量控件
- 一文掌握C#JSON(2023最新整理)
- C#之Socket客户端全过程
- C#SynchronizationContext以及Send和Post使用解读
- C#之如何实现真正的四舍五入
- C#四舍五入MidpointRounding.AwayFromZero解析
- c#中如何去除字符串左边的0
- C#字符串如何提取数值(带小数点)
- C#正则表达式之Ismatch()的用法解读
- Unity游戏开发中的中介者模式的应用与实现
- 利用C#/VB.NET实现PPT转换为HTML
- UnityShader片段着色器使用基础详解