序文
プロジェクトでは CSV ファイルの読み取りと書き込みが必要になることがよくありますが、主に CSV ファイルの分析が困難になります。
この記事では、CSV ファイルを解析するCsvHelper、TextFieldParser、正規表現の 3 つの方法を紹介し、ついでに 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 ファイルであり、両方の CSV データはわずか 3 行です。2番目のファイルは一見すると霊的汚染ですが、プロジェクトにおいてこの種のファイルは避けられません。
RFC 4180
CSV ファイルには公式の標準はありませんが、一般的なプロジェクトは RFC 4180 標準に従います。これは非公式の標準であり、次のようになります。
各レコードは、改行 (CRLF) で区切られた個別の行に配置されます。
ファイルの最後のレコードには、終了改行がある場合とない場合があります。
通常のレコード行と同じ形式で、ファイルの最初の行としてオプションのヘッダー行が表示される場合があります。このヘッダーには、ファイル内のフィールドに対応する名前が含まれ、ファイルの残りの部分のレコードと同じ数のフィールドが含まれている必要があります(ヘッダー行の有無は、このヘッダーのオプションの「header」パラメーターを介して示される必要があります) MIME タイプ)。
ヘッダーと各レコード内には、カンマで区切られた 1 つ以上のフィールドが存在する場合があります。ファイル全体で各行に同じ数のフィールドが含まれている必要があります。スペースはフィールドの一部とみなされ、無視しないでください。レコードの最後のフィールドの後にカンマを付けることはできません。
各フィールドは二重引用符で囲むことも、囲まないこともできます (ただし、Microsoft Excel などの一部のプログラムでは二重引用符をまったく使用しません)。フィールドが二重引用符で囲まれていない場合、フィールド内に二重引用符が表示されないことがあります。
改行 (CRLF)、二重引用符、カンマを含むフィールドは二重引用符で囲む必要があります。
フィールドを囲むために二重引用符が使用されている場合、フィールド内に現れる二重引用符は、その前に別の二重引用符を付けてエスケープする必要があります。
翻訳:
1. 各レコードは改行文字 (CRLF) で区切られた個別の行にあります。
2. ファイルの最後のレコードには、終了改行がある場合とない場合があります。
3. オプションのヘッダー行が、通常のレコード行と同じ形式でファイルの最初の行として表示される場合があります。このヘッダーには、ファイル内のフィールドに対応する名前が含まれ、ファイルの残りの部分のレコードと同じ数のフィールドが含まれている必要があります (ヘッダー行の有無は、オプションの「header」パラメーターによって示されます)この MIME タイプの)。
4. タイトルと各レコードには、カンマで区切られた 1 つ以上のフィールドが存在する場合があります。ファイル全体で、各行に同じ数のフィールドが含まれている必要があります。空白はフィールドの一部とみなされ、無視しないでください。レコードの最後のフィールドの後にカンマを入れることはできません。
5. 各フィールドは二重引用符で囲むことも、囲まないこともできます (ただし、Microsoft Excel などの一部のプログラムでは二重引用符をまったく使用しません)。フィールドが二重引用符で囲まれていない場合、フィールド内に二重引用符が表示されないことがあります。
6. 改行 (CRLF)、二重引用符、カンマを含むフィールドは二重引用符で囲む必要があります。
7. 二重引用符を使用してフィールドを囲む場合は、フィールド内に表示される二重引用符の前に別の二重引用符を置く必要があります。
簡易規格
上記の基準は長すぎるかもしれないので、少し簡略化します。簡略化は単純な削減ルールではなく、理解を容易にするために類似点を組み合わせたものであることに注意してください。次のコードでも、次のような簡略化された標準が使用されます。
1. 各レコードは改行文字 (CRLF) で区切られた個別の行にあります。注: ここでの行とは、通常のテキストの意味での行ではなく、CSV ファイル形式に準拠したレコード (以下、CSV 行と呼びます) を指し、テキスト内で複数行を占める場合があります。
2. ファイルの最後のレコードには終了改行文字が必要で、ファイルの最初の行はタイトル行です (タイトル行にはフィールドに対応する名前が含まれており、タイトルの数はその数と同じです)レコード内のフィールドの数)。注: 元の標準の必須オプションは一律に必須として規定されており、これは後の分析に便利であり、他の人にデータを確認させるためのヘッダー行はありません。
3. タイトルと各レコードには、カンマで区切られた1 つ以上のフィールドが存在する場合があります。ファイル全体を通じて、各行には同じ数のフィールドが含まれている必要があります。空白はフィールドの一部とみなされ、無視すべきではありません。レコードの最後のフィールドの後にカンマを入れることはできません。注: この規格は簡略化されていません。スペースやタブなどで区切る規格は他にもありますが、カンマ区切りを使用しないファイルもカンマ区切り値ファイルと呼ばれますか?
4. 各フィールドは二重引用符で囲まれています。フィールドに表示される二重引用符の前には別の二重引用符が必要です。注意: 元の標準では、二重引用符を使用する必要があり、オプションの二重引用符も使用されます。また、すべての二重引用符は二重引用符で囲む必要があります。引用符を使用しても間違いはありません。
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();
}
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>());
}
すぐに使えるライブラリだけが必要な場合は、基本的にこの記事はこれで終わりです。
カスタムメソッドを使用する
CsvHelper と区別するには、CSV ファイルの読み取りと書き込みのためのカスタム コードを保存する新しい CsvFile クラスを作成し、最後にクラスの完全なソース コードを提供します。
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>
/// <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 を使用して CSV ファイルを解析する
TextFieldParser はVB で CSV ファイルを解析するためのクラスで、C# には同様の機能を持つクラスはありませんが、VB の TextFieldParser を呼び出すことで機能を実現できます。
CSV ファイルを解析する TextFieldParser のコードは次のとおりです。
#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);
正規表現を使用して CSV ファイルを解析する
1 つの問題があり、それを正規表現で解決したい場合、2 つの問題があります。
正規表現には一定の学習閾値があり、学習後も頻繁に使用しないと忘れてしまいます。ほとんどの正規表現は、変更が難しい問題を解決するため、数世代にわたって受け継がれる安定した使用可能な正規表現につながります。このセクションの正規表現は、 『Mastering Regular Expressions (3rd Edition)』の第 6 章「効率的な正規表現の作成 - サイクルの排除の簡単な例」から引用されています。興味がある場合は、調べてください。式の説明は次のとおりです。: 本書の最終版の CSV ファイルを解析するための正規表現は、固定化されたグループの代わりに所有量指定子を使用する Java バージョンであり、Baidu でよく見られるバージョンでもあります。ただし、C#の所有量指定子に問題があり、私の能力では解決できないため、上の図のバージョンを使用しました。ただし、正規表現の 2 つのバージョン間でパフォーマンスに違いはありません。
正規表現を解析する 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);
正規表現解析のバグはまだ見つかっていませんが、それでも推奨されません。
完全な CsvFile ツール クラス
完全な 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);
要約する
CSV ファイルのRFC 4180標準とその簡易バージョンを導入
CSV ファイルを解析する 3 つの方法、CsvHelper、TextFieldParser、正規表現を導入しました。プロジェクトでは CsvHelper が推奨されています。オープン ソース コンポーネントをあまり導入したくない場合は、TextFieldParser を使用できます。正規表現は推奨されていません。
付録
CsvHelper github:https://github.com/JoshClose/CsvHelper
CsvHelper プロジェクトのバックアップ: https://pan.baidu.com/s/1xDOGgJuw5YaxPZwf8vGyrw 抽出コード: 33j7
RFC 4180 標準: https://datatracker.ietf.org/doc/html/rfc4180
転載元: タイムフライ
リンク: cnblogs.com/timefiles/p/CsvReadWrite.html
-
技術グループ: Xiaobian WeChat を追加し、グループにコメントします
編集者 WeChat: mm1552923
公開番号: dotNet プログラミング Daquan