原创C#CSV读写类
郝伟 2021/01/14
网上找了一些C#的第三方的CSV库总觉得不好用,于是自己写了一个 CSV 类,用于处理CSV函数。
/***************** 从文件初始化一个CSV文件 *****************/ // 初始化,可修改编码,如:new CSV(input, Encoding.Default); // 加载后的数据在 csv.Data 中,其数据类型为 List<List<string>> CSV csv = new CSV(@"C:\input\data.csv"); /***************** 从加载好的 csv.Data 中读取数据 *****************/ string v0_0 = csv.Data[0,0] // 为了简化操作,可以用以下方式读取。注意:这两种方法都会对范围验证,如果数据不存在,则返回 null string v3_5 = csv.GetData(3,5); // 将单元格(3,5)的内容为赋给 v3_5 string c0_1 = csv[0,1]; // 将单元格(0,1)的内容为赋给 c3_5 /***************** 从加载好的 csv.Data 中读取数据 *****************/ csv.SetData(2,6,"tom"); // 设置单元格(2,6)的内容为 "tom" csv[1, 5] = "jack"; // 设置单元格(1,5)的内容为 "jack" /***************** 添加一行数据 *****************/ // 向csv.Data追尾追加一行数据,长度为 ColumnCount,每个元素均为 ""。 csv.AddRow(); // 向csv.Data追加一行数据,如果 csv.ColumnCount 长度大于2,则不足部分用""补充。 csv.AddRow(new String[]{"a", "b"}); // 向csv.Data的第5行追加一行数据,如果 csv.ColumnCount 长度大于2,则不足部分用""补充。 // 如果行号小于0,则在首行添加;如果大于最大长度则在末尾添加。 csv.AddRow(new String[]{"a", "b"}, 5); /***************** 删除列数据 *****************/ // 删除第11列数据,如果超出列的范围,则不做任何操作。 csv.RemoveColumn(10) // 删除第1,3,8列数据。当有重复的列时,只删除一次。范围超出的不做操作。 csv.RemoveColumns(1, 3, 8, 3) /***************** 保存数据 *****************/ csv.Save("c:\\output\\data1.csv");
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace NMapMatchingML { /// <summary> /// 用于CSV文件的读写,加载以后数据以二维表的形式返回。 /// 主要用法如下: /// <example> /// CSV csv = new CSV("C:\\input\\data.csv"); // 初始化 /// string c0_1 = csv[0,1]; // 读取 /// csv[1, 5] = "jack"; // 写入 /// csv.Save("c:\\output\\data1.csv"); // 保存 /// </example> /// </summary> public class CSV { /// <summary> /// 文件的编码。 /// </summary> public Encoding CsvEncoding { get; set; } = Encoding.UTF8; /// <summary> /// 表示是否要进行列的裁剪操作。 /// </summary> public bool TrimColumns { get; set; } = false; /// <summary> /// CSV的分隔符,默认为半角逗号(即,)。 /// </summary> public char CsvSeparator { get; set; } = ','; /// <summary> /// 所有的数据都保存在些数组中。 /// </summary> public List<List<string>> Data { get; set; } = new List<List<string>>(); /// <summary> /// CSV是一种通用的使用逗号隔开的二维表的结构文件。 /// </summary> public CSV() { } /// <summary> /// 通过指定的CSV文件进行加载。返回一个二维矩阵。 /// </summary> /// <param name="filepath"></param> public CSV(string filepath) : this(filepath, Encoding.Default) { } /// <summary> /// 通过指定的CSV文件进行加载。 /// </summary> /// <param name="filepath">指定的CSV文件路径。</param> /// <param name="encoding">CSV文本的编码。</param> public CSV(string filepath, Encoding encoding) { CsvEncoding = encoding; Read(filepath); } /// <summary> /// 返回CSV二维数列的总行数。 /// </summary> /// <returns></returns> public int RowCount { get { return Data.Count; } } /// <summary> /// 返回CSV二维数列的总列数。 /// </summary> public int ColumnCount { get { return Data != null && Data.Count > 0 ? Data[0].Count : 0; } } /// <summary> /// 返回指定行列的值,如果超出数据返回值为 null。 /// </summary> /// <param name="row">指定行。</param> /// <param name="column">指定列。</param> /// <returns></returns> public string GetData(int row, int column) { return IsInRange(row, column) ? Data[row][column] : null; } /// <summary> /// 读写CSV指定单元格,下标从0开始。如果不在范围内,则会操作失败,处理方式如下: /// 读=返回null;写=返回false。 /// </summary> /// <param name="row">指定的行。</param> /// <param name="column">指定的列。</param> /// <returns></returns> public string this[int row, int column] { get { return GetData(row, column); } set { SetData(row, column, value); } } /// <summary> /// 在指定行添加一行数据内容为""的字符数列。 /// 输入数据的行数必需与 RowCount 相同,超出部分被裁掉,不足部分用""补充。 /// 要插入的行的位置小于0的按0处理,大于等于RowCount的按RowCount处理。 /// </summary> /// <param name="columnData">输入数据。</param> /// <param name="columnID">需要添加的行的序号。</param> public void AddColumn(string[] columnData = null, int columnID = int.MaxValue) { // 输入数据的长度必需与 ColumnCount相同,超出部分被裁掉,不足部分用""补充。 columnData = columnData ?? new string[RowCount]; List<string> list = new List<string>(); for (int i = 0; i < ColumnCount; i++) list.Add(i < columnData.Length ? columnData[i] : ""); // 要插入的行的位置小于0的按0处理,大于等于ColumnCount的按ColumnCount处理。 columnID = columnID < 0 ? 0 : columnID; columnID = columnID >= ColumnCount ? ColumnCount : columnID; // 将数据插入指定位置。 for (int i = 0; i < RowCount; i++) Data[i].Insert(columnID, columnData[i]); } /// <summary> /// 删除指定的某个列。 /// </summary> /// <param name="columnId">待删除列的ID,范围为[0, ColumnCount)。</param> public void RemoveColumn(int columnId) { if (0 <= columnId && columnId < ColumnCount) { for (int row = 0; row < RowCount; row++) { Data[row].RemoveAt(columnId); } } } /// <summary> /// 删除指定的多个列。 /// </summary> /// <param name="columnIds">待删除列的列的编号,可以是多个列。如果不在范围内,不会处理;如果有重复,只会处理1次。</param> public void RemoveColumns(params int[] columnIds) { columnIds.Distinct().OrderByDescending(c => c).ToList().ForEach(c => RemoveColumn(c)); } /// <summary> /// 在指定行添加一行数据内容为""的字符数列。 /// 输入数据的长度必需与 ColumnCount相同,超出部分被裁掉,不足部分用""补充。 /// 要插入的行的位置小于0的按0处理,大于等于RowCount的按RowCount处理。 /// </summary> /// <param name="strs">输入数据。</param> /// <param name="rowID">需要添加的行的序号。</param> public void AddRow(string[] strs = null, int rowID = int.MaxValue) { // 输入数据的长度必需与 ColumnCount相同,超出部分被裁掉,不足部分用""补充。 strs = strs ?? new string[ColumnCount]; List<string> list = new List<string>(); for (int i = 0; i < ColumnCount; i++) list.Add(i < strs.Length ? strs[i] : ""); // 要插入的行的位置小于0的按0处理,大于等于RowCount的按RowCount处理。 rowID = rowID < 0 ? 0 : rowID; rowID = rowID >= RowCount ? RowCount : rowID; Data.Insert(rowID, list); } /// <summary> /// 在指定行添加一行数据内容为""的字符数列。 /// </summary> /// <param name="rowNo">待插入的行号。</param> /// <param name="rowdata">待插入的数据。</param> public void AddRow(int rowNo, params string[] rowdata) { AddRow(rowdata, rowNo); } /// <summary> /// 判断指定行列的单元格是否在范围内。 /// </summary> /// <param name="row">指定的行编号。</param> /// <param name="column">指定的列编号。</param> /// <returns></returns> public bool IsInRange(int row, int column) { return row >= 0 && row < RowCount && column >= 0 && column < ColumnCount; } /// <summary> /// 为指定编号的行列的单元格赋值。 /// </summary> /// <param name="row">所要设置的行。</param> /// <param name="column">所要设置的列。</param> /// <param name="value">所要设置的值。</param> /// <returns></returns> public bool SetData(int row, int column, string value) { bool valid = IsInRange(row, column); if (valid) Data[row][column] = value; return valid; } /// <summary> /// 读取CSV数据文件, 目前的版本不支持带有分隔符的内容。 /// </summary> /// <param name="csvfile">待加载的CSV文件。</param> public void Read(string csvfile) { Data = GetListCsvData(csvfile); } /// <summary> /// 将数据转换为二维列表。 /// </summary> /// <returns></returns> public List<List<string>> GetListCsvData(string file) { StreamReader reader = new StreamReader(file, Encoding.UTF8); List<List<string>> tempListCsvData = new List<List<string>>(); bool isNotEndLine = false; string tempCsvRowString = reader.ReadLine(); // 对每行进行读写 while (tempCsvRowString != null) { List<string> tempCsvRowList; if (isNotEndLine) { tempCsvRowList = ParseContinueLine(tempCsvRowString); isNotEndLine = (tempCsvRowList.Count > 0 && tempCsvRowList[tempCsvRowList.Count - 1].EndsWith("\r\n")); List<string> myNowContinueList = tempListCsvData[tempListCsvData.Count - 1]; myNowContinueList[myNowContinueList.Count - 1] += tempCsvRowList[0]; tempCsvRowList.RemoveAt(0); myNowContinueList.AddRange(tempCsvRowList); } else { tempCsvRowList = ParseLine(tempCsvRowString); isNotEndLine = (tempCsvRowList.Count > 0 && tempCsvRowList[tempCsvRowList.Count - 1].EndsWith("\r\n")); tempListCsvData.Add(tempCsvRowList); } tempCsvRowString = reader.ReadLine(); } reader.Close(); // 读取完成以后,可能不同的行有不同的数据个数,为了保证所有行的内容一样,添加以下内容: // 找到最大列数。 int maxColumn = 0; for (int i = 0; i < tempListCsvData.Count; i++) maxColumn = tempListCsvData[i].Count > maxColumn ? tempListCsvData[i].Count : maxColumn; // 使所有行的列数都是 maxColumn foreach (List<string> item in tempListCsvData) while (item.Count < maxColumn) item.Add(""); return tempListCsvData; } /// <summary> /// 处理未完成的Csv单行 /// </summary> /// <param name="line">数据源</param> /// <returns>元素列表</returns> private List<string> ParseContinueLine(string line) { StringBuilder _columnBuilder = new StringBuilder(); List<string> Fields = new List<string>(); _columnBuilder.Remove(0, _columnBuilder.Length); if (line == "") { Fields.Add("\r\n"); return Fields; } for (int i = 0; i < line.Length; i++) { char character = line[i]; if ((i + 1) == line.Length)//这个字符已经结束了整行 { if (character == '"') //正常转义结束,且该行已经结束 { Fields.Add(TrimColumns ? _columnBuilder.ToString().TrimEnd() : _columnBuilder.ToString()); return Fields; } else //异常结束,转义未收尾 { _columnBuilder.Append("\r\n"); Fields.Add(_columnBuilder.ToString()); return Fields; } } else if (character == '"' && line[i + 1] == CsvSeparator) //结束转义,且后面有可能还有数据 { Fields.Add(TrimColumns ? _columnBuilder.ToString().TrimEnd() : _columnBuilder.ToString()); i++; //跳过下一个字符 Fields.AddRange(ParseLine(line.Remove(0, i + 1))); break; } else if (character == '"' && line[i + 1] == '"') //双引号转义 { i++; //跳过下一个字符 } else if (character == '"') //双引号单独出现(这种情况实际上已经是格式错误,为了兼容暂时不处理) { throw new Exception("格式错误,错误的双引号转义"); } _columnBuilder.Append(character); } return Fields; } /// <summary> /// 解析一行CSV数据。 /// </summary> /// <param name="line">待分析的行。</param> /// <returns></returns> private List<string> ParseLine(string line) { StringBuilder sb = new StringBuilder(); List<string> Fields = new List<string>(); bool inColumn = false; //是否是在一个列元素里 bool inQuotes = false; //是否需要转义 bool isNotEnd = false; //读取完毕未结束转义 sb.Remove(0, sb.Length); //空行也是一个空元素,一个逗号是2个空元素 if (line == "") Fields.Add(""); // 遍历一个行中的所有元素。 for (int i = 0; i < line.Length; i++) { char c = line[i]; if (!inColumn) { inColumn = true; if (c == '"') { inQuotes = true; continue; } } // 如果有引用 if (inQuotes) { //这个字符已经结束了整行 if ((i + 1) == line.Length) { //正常转义结束,且该行已经结束 if (c == '"') { inQuotes = false; continue; } isNotEnd = true; } //结束转义,且后面有可能还有数据 else if (c == '"' && line[i + 1] == CsvSeparator) { inQuotes = false; inColumn = false; i++; } //双引号转义 else if (c == '"' && line[i + 1] == '"') { i++; } //双引号单独出现(这种情况实际上已经是格式错误,为了兼容可暂时不处理) else if (c == '"') { throw new Exception("格式错误,错误的双引号转义"); } } else if (c == CsvSeparator) { inColumn = false; } //结束该元素时inColumn置为false,并且不处理当前字符,直接进行Add if (!inColumn) { Fields.Add(TrimColumns ? sb.ToString().Trim() : sb.ToString()); sb.Remove(0, sb.Length); } else { sb.Append(c); } } // 标准格式一行结尾不需要逗号结尾,而上面for是遇到逗号才添加的,为了兼容最后还要添加一次 if (inColumn) { if (isNotEnd) sb.Append("\r\n"); Fields.Add(TrimColumns ? sb.ToString().Trim() : sb.ToString()); } //如果inColumn为false,说明已经添加,因为最后一个字符为分隔符,所以后面要加上一个空元素 else { Fields.Add(""); } return Fields; } /// <summary> /// 将数据进行保存,默认使用编号进行保存。 /// </summary> /// <param name="outcsvfile">需要保存的路径。</param> public bool Save(string outcsvfile) { if (!File.Exists(outcsvfile)) throw new Exception("find error in your FilePath"); if (Data == null) throw new Exception("your DataSouse is null"); // 将Data中的数据以非追加的方式写至指定的文件中。 using (StreamWriter sw = new StreamWriter(outcsvfile, false, CsvEncoding)) { foreach (List<string> fields in Data) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < fields.Count; i++) { sb.Append(fields[i].Contains("\"") ? string.Format("\"{0}\"", fields[i].Replace("\"", "\"\"")) : string.Format("\"{0}\"", fields[i])); if (i < fields.Count - 1) sb.Append(CsvSeparator); } sw.WriteLine(sb.ToString()); } } return true; } /// <summary> /// 以二维表的形式返回氖Data的内容,同一行的相邻数据使用\t\t分隔。 /// </summary> /// <returns></returns> public override string ToString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < Data.Count; i++) { sb.AppendLine(string.Join("\t\t", Data[i])); } return sb.ToString(); } } }