seo如何根据网站数据做报表,wordpress会员注册怎么搞,导购网站开发,用vs做网站在安装时要勾选目录
CSV文件标准 文件示例RFC 4180简化标准读写CSV文件 使用CsvHelper使用自定义方法总结
项目中经常遇到CSV文件的读写需求#xff0c;其中的难点主要是CSV文件的解析。本文会介绍CsvHelper、TextFieldParser、正则表达式三种解析CSV文件的方法#xff0c;顺带也会介绍一…目录
CSV文件标准 文件示例RFC 4180简化标准读写CSV文件 使用CsvHelper使用自定义方法总结
项目中经常遇到CSV文件的读写需求其中的难点主要是CSV文件的解析。本文会介绍CsvHelper、TextFieldParser、正则表达式三种解析CSV文件的方法顺带也会介绍一下CSV文件的写方法。 CSV文件标准
在介绍CSV文件的读写方法前我们需要了解一下CSV文件的格式。 文件示例
一个简单的CSV文件
? 1 2 3 Test1,Test2,Test3,Test4,Test5,Test6 str1,str2,str3,str4,str5,str6 str1,str2,str3,str4,str5,str6
一个不简单的CSV文件
? 1 2 3 4 5 6 7 8 9 10 11 12 13 Test1 ,,Test2 ,,Test3 ,,Test4 ,,Test5 ,,Test6 , 中文,D23 ,3DFD423412321S2,ASD1,23,,,,213 23F32, ,,asd 中文,D23 ,3DFD423412321S2,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类。代码如下
? 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 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; } } }
生成一些测试数据代码如下
? 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 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 ListTest { test, test }; //写CSV文件 /* *直接把后面的写CSV文件代码复制到此处 */ //读CSV文件 /* *直接把后面的读CSV文件代码复制到此处 */ Console.ReadLine(); } 使用CsvHelper
CsvHelper 是用于读取和写入 CSV 文件的库支持自定义类对象的读写。
github上标星最高的CSV文件读写C#库使用MS-PL、Apache 2.0开源协议。
使用NuGet下载CsvHelper读写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 //写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.GetRecordsTest().ToList(); //逐行读取 //records.Add(csv.GetRecordTest()); }
如果你只想要拿来就能用的库那文章基本上到这里就结束了。 使用自定义方法
为了与CsvHelper区分新建一个CsvFile类存放自定义读写CSV文件的代码最后会提供类的完整源码。CsvFile类定义如下
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /// summary /// CSV文件读写工具类 /// /summary public class CsvFile { #region 写CSV文件 //具体代码... #endregion #region 读CSV文件使用TextFieldParser //具体代码... #endregion #region 读CSV文件使用正则表达式 //具体代码... #endregion }
基于简化标准的写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 65 66 67 68 69 70 71 72 73 74 75 #region 写CSV文件 //字段数组转为CSV记录行 private static string FieldsToLine(IEnumerablestring 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 IEnumerablestring GetObjFieldsT(T obj, bool isTitle) where T : class { IEnumerablestring 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 nameT/typeparam /// param namelist数据列表/param /// param namepath文件路径/param /// param nameappend追加记录/param /// param namefunc字段转换方法/param /// param namedefaultEncoding/param public static void WriteT(ListT list, string path,bool appendtrue, FuncT, bool, IEnumerablestring func null, Encoding defaultEncoding null) where T : class { if (list null || list.Count 0) return; if (defaultEncoding null) { defaultEncoding Encoding.UTF8; } if (func null) { func GetObjFields; } if (!File.Exists(path)|| !append) { var fields func(list[0], true); string title FieldsToLine(fields); File.WriteAllText(path, title, defaultEncoding); } using (StreamWriter sw new StreamWriter(path, true, defaultEncoding)) { list.ForEach(obj { var fields func(obj, false); string line FieldsToLine(fields); sw.Write(line); }); } } #endregion
使用时代码如下
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //写CSV文件 //使用自定义的字段转换方法也是文章开头复杂CSV文件使用字段转换方法 CsvFile.Write(records, path, true, new FuncTest, bool, IEnumerablestring((obj, isTitle) { IEnumerablestring 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; })); //使用默认的字段转换方法 //CsvFile.Write(records, path);
你也可以使用默认的字段转换方法代码如下
? 1 CsvFile.Save(records, path);
使用TextFieldParser解析CSV文件
TextFieldParser是VB中解析CSV文件的类C#虽然没有类似功能的类不过可以调用VB的TextFieldParser来实现功能。
TextFieldParser解析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 #region 读CSV文件使用TextFieldParser /// summary /// 读CSV文件默认第一行为标题 /// /summary /// typeparam nameT/typeparam /// param namepath文件路径/param /// param namefunc字段解析规则/param /// param namedefaultEncoding文件编码/param /// returns/returns public static ListT ReadT(string path, Funcstring[], T func, Encoding defaultEncoding null) where T : class { if (defaultEncoding null) { defaultEncoding Encoding.UTF8; } ListT list new ListT(); using (TextFieldParser parser new TextFieldParser(path, defaultEncoding)) { parser.TextFieldType FieldType.Delimited; //设定逗号分隔符 parser.SetDelimiters(,); //设定不忽略字段前后的空格 parser.TrimWhiteSpace false; bool isLine false; while (!parser.EndOfData) { string[] fields parser.ReadFields(); if (isLine) { var obj func(fields); if (obj ! null) list.Add(obj); } else { //忽略标题行业 isLine true; } } } return list; } #endregion
使用时代码如下
? 1 2 //读CSV文件 records CsvFile.Read(path, Test.Parse);
使用正则表达式解析CSV文件
如果你有一个问题想用正则表达式来解决那么你就有两个问题了。
正则表达式有一定的学习门槛而且学习后不经常使用就会忘记。正则表达式解决的大多数是一些不易变更需求的问题这就导致一个稳定可用的正则表达式可以传好几代。
本节的正则表达式来自 《精通正则表达式第3版》 第6章 打造高效正则表达式——简单的消除循环的例子有兴趣的可以去了解一下表达式说明如下 注这本书最终版的解析CSV文件的正则表达式是Jave版的使用占有优先量词取代固化分组的版本也是百度上经常见到的版本。不过占有优先量词在C#中有点问题本人能力有限解决不了所以使用了上图的版本。不过这两版正则表达式性能上没有差异。
正则表达式解析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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 #region 读CSV文件使用正则表达式 /// summary /// 读CSV文件默认第一行为标题 /// /summary /// typeparam nameT/typeparam /// param namepath文件路径/param /// param namefunc字段解析规则/param /// param namedefaultEncoding文件编码/param /// returns/returns public static ListT Read_RegexT(string path, Funcstring[], T func, Encoding defaultEncoding null) where T : class { ListT list new ListT(); StringBuilder sbr new StringBuilder(100); Regex lineReg new Regex(\); Regex fieldReg new Regex(\\G(?:^|,)(?:\((?[^\]*)(?\\[^\]*)*)\|([^\,]*))); Regex quotesReg new Regex(\\); bool isLine false; string line string.Empty; using (StreamReader sr new StreamReader(path)) { while (null ! (line ReadLine(sr))) { sbr.Append(line); string str sbr.ToString(); //一个完整的CSV记录行它的双引号一定是偶数 if (lineReg.Matches(sbr.ToString()).Count % 2 0) { if (isLine) { var fields ParseCsvLine(sbr.ToString(), fieldReg, quotesReg).ToArray(); var obj func(fields.ToArray()); if (obj ! null) list.Add(obj); } else { //忽略标题行业 isLine true; } sbr.Clear(); } else { sbr.Append(Environment.NewLine); } } } if (sbr.Length 0) { //有解析失败的字符串报错或忽略 } return list; } //重写ReadLine方法只有\r\n才是正确的一行 private static string ReadLine(StreamReader sr) { StringBuilder sbr new StringBuilder(); char c; int cInt; while (-1 ! (cInt sr.Read())) { c (char)cInt; if (c \n sbr.Length 0 sbr[sbr.Length - 1] \r) { sbr.Remove(sbr.Length - 1, 1); return sbr.ToString(); } else { sbr.Append(c); } } return sbr.Length0?sbr.ToString():null; } private static Liststring ParseCsvLine(string line, Regex fieldReg, Regex quotesReg) { var fieldMath fieldReg.Match(line); Liststring fields new Liststring(); while (fieldMath.Success) { string field; if (fieldMath.Groups[1].Success) { field quotesReg.Replace(fieldMath.Groups[1].Value, \); } else { field fieldMath.Groups[2].Value; } fields.Add(field); fieldMath fieldMath.NextMatch(); } return fields; } #endregion
使用时代码如下
? 1 2 //读CSV文件 records CsvFile.Read_Regex(path, Test.Parse);
目前还未发现正则表达式解析有什么bug不过还是不建议使用。
完整的CsvFile工具类
完整的CsvFile类代码如下
? 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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 using Microsoft.VisualBasic.FileIO; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace ConsoleApp4 { /// summary /// CSV文件读写工具类 /// /summary public class CsvFile { #region 写CSV文件 //字段数组转为CSV记录行 private static string FieldsToLine(IEnumerablestring 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 IEnumerablestring GetObjFieldsT(T obj, bool isTitle) where T : class { IEnumerablestring 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 nameT/typeparam /// param namelist数据列表/param /// param namepath文件路径/param /// param nameappend追加记录/param /// param namefunc字段转换方法/param /// param namedefaultEncoding/param public static void WriteT(ListT list, string path,bool appendtrue, FuncT, bool, IEnumerablestring func null, Encoding defaultEncoding null) where T : class { if (list null || list.Count 0) return; if (defaultEncoding null) { defaultEncoding Encoding.UTF8; } if (func null) { func GetObjFields; } if (!File.Exists(path)|| !append) { var fields func(list[0], true); string title FieldsToLine(fields); File.WriteAllText(path, title, defaultEncoding); } using (StreamWriter sw new StreamWriter(path, true, defaultEncoding)) { list.ForEach(obj { var fields func(obj, false); string line FieldsToLine(fields); sw.Write(line); }); } } #endregion #region 读CSV文件使用TextFieldParser /// summary /// 读CSV文件默认第一行为标题 /// /summary /// typeparam nameT/typeparam /// param namepath文件路径/param /// param namefunc字段解析规则/param /// param namedefaultEncoding文件编码/param /// returns/returns public static ListT ReadT(string path, Funcstring[], T func, Encoding defaultEncoding null) where T : class { if (defaultEncoding null) { defaultEncoding Encoding.UTF8; } ListT list new ListT(); using (TextFieldParser parser new TextFieldParser(path, defaultEncoding)) { parser.TextFieldType FieldType.Delimited; //设定逗号分隔符 parser.SetDelimiters(,); //设定不忽略字段前后的空格 parser.TrimWhiteSpace false; bool isLine false; while (!parser.EndOfData) { string[] fields parser.ReadFields(); if (isLine) { var obj func(fields); if (obj ! null) list.Add(obj); } else { //忽略标题行业 isLine true; } } } return list; } #endregion #region 读CSV文件使用正则表达式 /// summary /// 读CSV文件默认第一行为标题 /// /summary /// typeparam nameT/typeparam /// param namepath文件路径/param /// param namefunc字段解析规则/param /// param namedefaultEncoding文件编码/param /// returns/returns public static ListT Read_RegexT(string path, Funcstring[], T func, Encoding defaultEncoding null) where T : class { ListT list new ListT(); StringBuilder sbr new StringBuilder(100); Regex lineReg new Regex(\); Regex fieldReg new Regex(\\G(?:^|,)(?:\((?[^\]*)(?\\[^\]*)*)\|([^\,]*))); Regex quotesReg new Regex(\\); bool isLine false; string line string.Empty; using (StreamReader sr new StreamReader(path)) { while (null ! (line ReadLine(sr))) { sbr.Append(line); string str sbr.ToString(); //一个完整的CSV记录行它的双引号一定是偶数 if (lineReg.Matches(sbr.ToString()).Count % 2 0) { if (isLine) { var fields ParseCsvLine(sbr.ToString(), fieldReg, quotesReg).ToArray(); var obj func(fields.ToArray()); if (obj ! null) list.Add(obj); } else { //忽略标题行业 isLine true; } sbr.Clear(); } else { sbr.Append(Environment.NewLine); } } } if (sbr.Length 0) { //有解析失败的字符串报错或忽略 } return list; } //重写ReadLine方法只有\r\n才是正确的一行 private static string ReadLine(StreamReader sr) { StringBuilder sbr new StringBuilder(); char c; int cInt; while (-1 ! (cInt sr.Read())) { c (char)cInt; if (c \n sbr.Length 0 sbr[sbr.Length - 1] \r) { sbr.Remove(sbr.Length - 1, 1); return sbr.ToString(); } else { sbr.Append(c); } } return sbr.Length0?sbr.ToString():null; } private static Liststring ParseCsvLine(string line, Regex fieldReg, Regex quotesReg) { var fieldMath fieldReg.Match(line); Liststring fields new Liststring(); while (fieldMath.Success) { string field; if (fieldMath.Groups[1].Success) { field quotesReg.Replace(fieldMath.Groups[1].Value, \); } else { field fieldMath.Groups[2].Value; } fields.Add(field); fieldMath fieldMath.NextMatch(); } return fields; } #endregion } }
使用方法如下
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 //写CSV文件 CsvFile.Write(records, path, true, new FuncTest, bool, IEnumerablestring((obj, isTitle) { IEnumerablestring 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文件 records CsvFile.Read(path, Test.Parse); //读CSV文件 records CsvFile.Read_Regex(path, Test.Parse);