C#でCSVファイルを読み書きする方法を詳しく解説

目次
  • CSVファイル規格
    • ファイル例
    • RFC 4180
    • 簡略化基準
  • CSV ファイルの読み取りと書き込み
    • CsvHelper の使用
    • カスタムメソッドを使用する
  • 要約する

プロジェクトでは CSV ファイルの読み取りと書き込みが必要になることがよくありますが、主な困難は CSV ファイルの解析です。この記事では、 CSV ファイルを解析する 3 つの方法 ( CsvHelperTextFieldParser正規表現 )を紹介し、ついでに 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 ","3DFD4234""""""1232""1S2","ASD1"",""23,,,,213

23F32","

",,asd

" 中文,D23 ","3DFD4234""""""1232""1S2","ASD1"",""23,,,,213

23F32","

",,asd

上の 2 つは両方とも CSV ファイルであり、どちらも CSV データは 3 行しかありません。2 番目のファイルを 2 回見るのは精神的汚染ですが、そのようなファイルがプロジェクトに登場するのは避けられません。

RFC 4180

CSV ファイルには公式の標準はありませんが、一般的なプロジェクトは RFC 4180 標準に準拠します。これは非公式の規格であり、その内容は次のとおりです。

各レコードは、改行 (CRLF) で区切られた個別の行に配置されます。

ファイルの最後のレコードには、終了改行がある場合とない場合があります。

通常のレコード行と同じ形式で、ファイルの最初の行としてオプションのヘッダー行が表示される場合があります。このヘッダーには、ファイル内のフィールドに対応する名前が含まれ、ファイルの残りの部分のレコードと同じ数のフィールドが含まれている必要があります(ヘッダー行の有無は、このヘッダーのオプションの「header」パラメーターを介して示される必要があります) MIME タイプ)。

ヘッダーと各レコード内には、カンマで区切られた 1 つ以上のフィールドが存在する場合があります。ファイル全体で各行に同じ数のフィールドが含まれている必要があります。スペースはフィールドの一部とみなされ、無視しないでください。レコードの最後のフィールドの後にカンマを付けることはできません。

各フィールドは二重引用符で囲むことも、囲まないこともできます (ただし、Microsoft Excel などの一部のプログラムでは二重引用符をまったく使用しません)。フィールドが二重引用符で囲まれていない場合、フィールド内に二重引用符が表示されないことがあります。

改行 (CRLF)、二重引用符、カンマを含むフィールドは二重引用符で囲む必要があります。

フィールドを囲むために二重引用符が使用されている場合、フィールド内に現れる二重引用符は、その前に別の二重引用符を付けてエスケープする必要があります。

翻訳する:

  • 各レコードは別の行にあり、改行文字 (CRLF) で区切られています。
  • ファイルの最後のレコードには、終了改行文字がある場合とない場合があります。
  • オプションのヘッダー行が、通常のレコード行と同じ形式でファイルの最初の行として表示される場合があります。このヘッダーには、ファイル内のフィールドに対応する名前が含まれ、ファイルの残りの部分のレコードと同じ数のフィールドが含まれている必要があります (ヘッダー行の有無は、オプションの「header」パラメーターによって示される必要があります)この MIME タイプの)。
  • ヘッダーと各レコードには、カンマで区切られた 1 つ以上のフィールドが存在する場合があります。ファイル全体で、各行に同じ数のフィールドが含まれている必要があります。空白はフィールドの一部とみなされ、無視しないでください。レコードの最後のフィールドの後にカンマを入れることはできません。
  • 各フィールドは二重引用符で囲むことも、囲まないこともできます (ただし、Microsoft Excel などの一部のプログラムでは二重引用符をまったく使用しません)。フィールドが二重引用符で囲まれていない場合、フィールド内に二重引用符が表示されない可能性があります。
  • 改行 (CRLF)、二重引用符、およびコンマを含むフィールドは二重引用符で囲む必要があります。
  • 二重引用符を使用してフィールドを囲む場合は、フィールドに表示される二重引用符の前に別の二重引用符を置く必要があります。

簡略化基準

上記の基準は発音が少し難しいかもしれないので、少し簡略化して説明します。単純化とは単にルールを削除するのではなく、理解しやすくするために類似したルールをマージすることであることに注意してください。
次のコードでは、次のような単純化基準も使用します。

  • 各レコードは別の行にあり、改行文字 (CRLF) で区切られています。
  • 注: ここでの行は、通常のテキストの意味での行ではなく、CSV ファイル形式に準拠したレコード (以下、CSV 行と呼びます)を指し、テキスト内で複数行を占める場合があります。
  • ファイルの最後のレコードには終了改行文字が必要で、ファイルの最初の行はヘッダー行である必要があります (ヘッダー行にはフィールドに対応する名前が含まれており、ヘッダーの数はフィールドの数と同じです)記録にあります)。
  • 注: 元の標準のオプション オプションは、後の分析を容易にするために必須として一律に指定されており、他の人がデータを参照できるようにするためのヘッダー行はありません。
  • ヘッダーと各レコードには、カンマで区切られた1 つ以上のフィールドが存在する場合があります。ファイル全体を通じて、各行には同じ数のフィールドが含まれている必要があります空白はフィールドの一部とみなされ、無視すべきではありませんレコードの最後のフィールドの後にカンマを入れることはできません
  • 注: この規格は簡略化されていません。スペースやタブなどで区切る規格は他にもありますが、カンマで区切られていないファイルもカンマ区切りファイルと呼ばれますか?
  • 各フィールドは二重引用符で囲まれており、フィールドに表示される二重引用符の前には別の二重引用符が必要です。
  • 注: 元の標準には、二重引用符を使用する必要がある場合と二重引用符がオプションである場合があるため、すべて二重引用符を使用しても間違いはありません。*

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 List<Test> { 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.GetRecords<Test>().ToList();

    //逐行读取

    //records.Add(csv.GetRecord<Test>());

}

如果你只想要拿来就能用的库,那文章基本上到这里就结束了。

使用自定义方法

为了与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(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

使用时,代码如下:

?

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 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);

你也可以使用默认的字段转换方法,代码如下:

?

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 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

使用时,代码如下:

?

1

2

//读CSV文件

records = CsvFile.Read(path, Test.Parse);

使用正则表达式解析CSV文件

如果你有一个问题,想用正则表达式来解决,那么你就有两个问题了。

正则表达式有一定的学习门槛,而且学习后不经常使用就会忘记。正则表达式解决的大多数是一些不易变更需求的问题,这就导致一个稳定可用的正则表达式可以传好几代。

本节的正则表达式来自 《精通正则表达式(第3版)》 第6章 打造高效正则表达式——简单的消除循环的例子,有兴趣的可以去了解一下,表达式说明如下:

注: 本書で使用される CSV ファイル解析用の正規表現の最終バージョンは、固定化されたグループ化の代わりに所有量指定子を使用する Java バージョンであり、Baidu でよく見られるバージョンでもあります。ただし、C# の所有優先度数量指定子に問題があり、私の能力では解決できないため、上記のバージョンを使用しました。ただし、正規表現の 2 つのバージョン間でパフォーマンスに違いはありません。

正規表現を解析する 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 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

使用時のコードは次のとおりです。

?

1

2

//读CSV文件

records = CsvFile.Read_Regex(path, Test.Parse);

これまでのところ、正規表現解析のバグは見つかっていませんが、その使用はまだ推奨されていません。

完全な 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(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

    }

}

それの使い方:

?

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 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);

Guess you like

Origin blog.csdn.net/qq_15509251/article/details/131994316