项目中经常遇到CSV文件的读写需求,其中的难点主要是CSV文件的解析。本文会介绍CsvHelper、TextFieldParser、正则表达式三种解析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数据。第二个文件多看一眼都是精神污染,但项目中无法避免会出现这种文件。
CSV文件没有官方的标准,但一般项目都会遵守 RFC 4180 标准。这是一个非官方的标准,内容如下:
翻译一下:
上面的标准可能比较拗口,我们对它进行一些简化。要注意一下,简化不是简单的删减规则,而是将类似的类似进行合并便于理解。
后面的代码也会使用简化标准,简化标准如下:
在正式读写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();
}
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>());
}
如果你只想要拿来就能用的库,那文章基本上到这里就结束了。
为了与CsvHelper区分,新建一个CsvFile类存放自定义读写CSV文件的代码,最后会提供类的完整源码。CsvFile类定义如下:
/// <summary>
/// CSV文件读写工具类
/// </summary>
public class CsvFile
{
#region 写CSV文件
//具体代码...
#endregion
#region 读CSV文件(使用TextFieldParser)
//具体代码...
#endregion
#region 读CSV文件(使用正则表达式)
//具体代码...
#endregion
}
根据简化标准(具体标准内容见前文),写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>
/// <param name="path">文件路径</param>
/// <param name="append">追加记录</param>
/// <param name="func">字段转换方法</param>
/// <param name="defaultEncoding"></param>
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
{
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
使用时,代码如下:
//写CSV文件
//使用自定义的字段转换方法,也是文章开头复杂CSV文件使用字段转换方法
CsvFile.Write(records, path, true, new Func<Test, bool, IEnumerable<string>>((obj, isTitle) =>
{
IEnumerable<string> 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);
你也可以使用默认的字段转换方法,代码如下:
CsvFile.Save(records, path);
TextFieldParser是VB中解析CSV文件的类,C#虽然没有类似功能的类,不过可以调用VB的TextFieldParser来实现功能。
TextFieldParser解析CSV文件的代码如下:
#region 读CSV文件(使用TextFieldParser)
/// <summary>
/// 读CSV文件,默认第一行为标题
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path">文件路径</param>
/// <param name="func">字段解析规则</param>
/// <param name="defaultEncoding">文件编码</param>
/// <returns></returns>
public static List<T> Read<T>(string path, Func<string[], T> func, Encoding defaultEncoding = null) where T : class
{
if (defaultEncoding == null)
{
defaultEncoding = Encoding.UTF8;
}
List<T> list = new List<T>();
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
使用时,代码如下:
//读CSV文件
records = CsvFile.Read(path, Test.Parse);
如果你有一个问题,想用正则表达式来解决,那么你就有两个问题了。
正则表达式有一定的学习门槛,而且学习后不经常使用就会忘记。正则表达式解决的大多数是一些不易变更需求的问题,这就导致一个稳定可用的正则表达式可以传好几代。
本节的正则表达式来自 《精通正则表达式(第3版)》 第6章 打造高效正则表达式——简单的消除循环的例子,有兴趣的可以去了解一下,表达式说明如下:
注:这本书最终版的解析CSV文件的正则表达式是Jave版的使用占有优先量词取代固化分组的版本,也是百度上经常见到的版本。不过占有优先量词在C#中有点问题,本人能力有限解决不了,所以使用了上图的版本。不过,这两版正则表达式性能上没有差异。
正则表达式解析CSV文件代码如下:
#region 读CSV文件(使用正则表达式)
/// <summary>
/// 读CSV文件,默认第一行为标题
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path">文件路径</param>
/// <param name="func">字段解析规则</param>
/// <param name="defaultEncoding">文件编码</param>
/// <returns></returns>
public static List<T> Read_Regex<T>(string path, Func<string[], T> func, Encoding defaultEncoding = null) where T : class
{
List<T> list = new List<T>();
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.Length>0?sbr.ToString():null;
}
private static List<string> ParseCsvLine(string line, Regex fieldReg, Regex quotesReg)
{
var fieldMath = fieldReg.Match(line);
List<string> fields = new List<string>();
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
使用时代码如下:
//读CSV文件
records = CsvFile.Read_Regex(path, Test.Parse);
目前还未发现正则表达式解析有什么bug,不过还是不建议使用。
完整的CsvFile类代码如下:
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(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>
/// <param name="path">文件路径</param>
/// <param name="append">追加记录</param>
/// <param name="func">字段转换方法</param>
/// <param name="defaultEncoding"></param>
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
{
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 name="T"></typeparam>
/// <param name="path">文件路径</param>
/// <param name="func">字段解析规则</param>
/// <param name="defaultEncoding">文件编码</param>
/// <returns></returns>
public static List<T> Read<T>(string path, Func<string[], T> func, Encoding defaultEncoding = null) where T : class
{
if (defaultEncoding == null)
{
defaultEncoding = Encoding.UTF8;
}
List<T> list = new List<T>();
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 name="T"></typeparam>
/// <param name="path">文件路径</param>
/// <param name="func">字段解析规则</param>
/// <param name="defaultEncoding">文件编码</param>
/// <returns></returns>
public static List<T> Read_Regex<T>(string path, Func<string[], T> func, Encoding defaultEncoding = null) where T : class
{
List<T> list = new List<T>();
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.Length>0?sbr.ToString():null;
}
private static List<string> ParseCsvLine(string line, Regex fieldReg, Regex quotesReg)
{
var fieldMath = fieldReg.Match(line);
List<string> fields = new List<string>();
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
}
}
使用方法如下:
//写CSV文件
CsvFile.Write(records, path, true, new Func<Test, bool, IEnumerable<string>>((obj, isTitle) =>
{
IEnumerable<string> 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);
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://www.cnblogs.com/timefiles/p/CsvReadWrite.html
内容来源于网络,如有侵权,请联系作者删除!