2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

2種類のフォーマットを含むCSVファイルの読み込み方

Last updated at Posted at 2020-05-06

(※2020/5/07時点の CSVHelper 14.x では動作していましたが、2021年度2月時点の 23.x では仕様が変わり手直しが必要です)

はじめに

 1つの CSV ファイルには、1種類のフォーマットが一般的だと思います。データの途中でデータのフォーマットが変わってしまうと、読み込み処理も書き込み処理も煩雑になるためです。しかし、たまに複数のフォーマットを含む CSV ファイルがあり、どうしても読み込んで処理しなければならない場合があります。(汎用機からのデータで多いような気がします)

 CsvHelper は、1つのCSVファイルに2つ以上のレコードフォーマットが定義されている場合にも対応しています。読み込み時の csv.Configuration.RegisterClassMap は、複数定義することができ、読み込んだヘッダの項目名を判別させることで、異なるフォーマットの読み込みを実現させます。

1種類のフォーマットの場合の CSV ファイルの読み込み方と、2種類のフォーマットが含まれる CSV ファイルの読み込み方を、サンプルで見比べてみます。

1種類のフォーマットの場合

 下のサンプルでは、test.csv ファイルに入っているデータを、TestDataMap が指定する順番に従って TestData クラスにマッピングし、records に登録していきます。

using System;
using System.Globalization;
using System.IO;
using System.Text;
using CsvHelper;
using CsvHelper.Configuration;

namespace MyProject
{
  class Program
  {
    static void Main(string[] args)
    {
      using (var sr = new StreamReader("test.csv", Encoding.GetEncoding("SHIFT_JIS")))
      {
        using (var csv = new CsvReader(sr, CultureInfo.InvariantCulture))
        {
          csv.Configuration.RegisterClassMap<TestDataMap>();
          var records = csv.GetRecords<TestData>();

          foreach(var l in records)
          {
            Console.WriteLine(l.Name, l.Param1);
          }
        }
      }
    }
  }
  //データクラス
  public class TestData
  {
    public string Name { get; set; }
    public string Param1 { get; set; }
  }
  //マップクラス 
  class TestDataMap : ClassMap<TestData>
  { 
    public TestDataMap()
    {
      Map(x => x.Name).Name("Name");
      Map(x => x.Param1);
    }
  }
}

見慣れてしまうと、難しいところは特にありません。

2種類のフォーマットが混在する場合

 2種類のフォーマットの混在の仕方に特に共通にルールがあるわけではないので、CsvHelper を使ったとしても、仕様に従ってある程度の実装が必要です。

とりあえずの例として、次のようなフォーマットだったとします。

Id,Name
1,A社
2,B社

Name,Param1,Param2
A社,2019,20
A社,2020,19
B社,2019,10

1つめのフォーマットは、Id、Name の2つの項目です。2つめのフォーマットは、Name、Param1、Param2 の3つの項目によるフォーマットで、1つ目の Name 項目に関するパラメータを扱っています。
なお、フォーマット間は空行で区分けされているとします。ただし、空行は1行のみとします。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using CsvHelper;
using CsvHelper.Configuration;

namespace MyProject
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var reader = new StreamReader("test2.csv", Encoding.GetEncoding("SHIFT_JIS")))
            {
                using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
                {
                    csv.Configuration.IgnoreBlankLines = false;
                    csv.Configuration.RegisterClassMap<TestData1Map>();
                    csv.Configuration.RegisterClassMap<TestData2Map>();
                    var TestData1Records = new List<TestData1>();
                    var TestData2Records = new List<TestData2>();
                    var isHeader = true;
                    while (csv.Read())
                    {
                        if (isHeader)
                        {
                            csv.ReadHeader();
                            isHeader = false;
                            continue;
                        }

                        if (string.IsNullOrEmpty(csv.GetField(0)))
                        {
                            isHeader = true;
                            continue;
                        }

                        switch (csv.Context.HeaderRecord[0])
                        {
                            case "Id":
                               TestData1Records.Add(csv.GetRecord<TestData1>());
                               break;
                            case "Name":
                               TestData2Records.Add(csv.GetRecord<TestData2>());
                               break;
                            default:
                               throw new InvalidOperationException("Unknown record type.");
                        }
                    }
                    foreach (var l in TestData1Records)
                    {
                        Console.WriteLine($"Id = {l.Id}, {l.Name}");
                        foreach (var m in TestData2Records)
                        {
                            if (l.Name == m.Name)
                            {
                                Console.WriteLine($"Param1 = {m.Param1}, Param2 = {m.Param2}");
                            }
                        }
                    }
                }
            }
        }
        public class TestData1
        {
            public int Id { get; set; }
            public string Name { get; set; }
        }

        public class TestData2
        {
            public string Name { get; set; }
            public string Param1 { get; set; }
            public string Param2 { get; set; }
        }

        public sealed class TestData1Map : ClassMap<TestData1>
        {
            public TestData1Map()
            {
                Map(m => m.Id).Name("Id");
                Map(m => m.Name);
            }
        }

        public sealed class TestData2Map : ClassMap<TestData2>
        {
            public TestData2Map()
            {
                Map(m => m.Name).Name("Name");
                Map(m => m.Param1);
                Map(m => m.Param2);
            }
        }
    }
}

簡単に解説します。

IgnoreBlankLines = false とすることで、空行の読み飛ばしを無効にします。これでフォーマットの仕切りを認識できるようになります。

csv.Configuration.IgnoreBlankLines = false;

RegisterClassMap に2種類のマッピングクラスを登録します。

csv.Configuration.RegisterClassMap<TestData1Map>();
csv.Configuration.RegisterClassMap<TestData2Map>();

それぞれのフォーマットの保存先を用意します。

var TestData1Records = new List<TestData1>();
var TestData2Records = new List<TestData2>();

ヘッダ行かどうかを判断するフラグを定義します。1行目はヘッダでしょうから、初期値は true です。

var isHeader = true;

空行を判断した場合の処理です。空行の次はヘッダですので、isHeader を true にして、次のループに移ります。

if (string.IsNullOrEmpty(csv.GetField(0)))
{
  isHeader = true;
  continue;
}

現在のヘッダ情報から、フォーマットを判別して、それぞれの保存先に格納します。想定していないヘッダだと、throw で例外が発生します。

switch (csv.Context.HeaderRecord[0])
{
  case "Id":
    TestData1Records.Add(csv.GetRecord<TestData1>());
    break;
  case "Name":
    TestData2Records.Add(csv.GetRecord<TestData2>());
    break;
  default:
    throw new InvalidOperationException("Unknown record type.");
}

読み込んだデータを出力します。1つめのフォーマットで定義された Name を使って、2つ目のフォーマットで定義される Param1 と Param2 を表示しています。

foreach (var l in TestData1Records)
{
  Console.WriteLine($"Id = {l.Id}, {l.Name}");
  foreach (var m in TestData2Records)
  {
    if (l.Name == m.Name)
    {
      Console.WriteLine($"Param1 = {m.Param1}, Param2 = {m.Param2}");
    }
  }
}

出力結果は次のようになります。

出力結果
Id = 1, A社
Param1 = 2019, Param2 = 20
Param1 = 2020, Param2 = 19
Id = 2, B社
Param1 = 2019, Param2 = 10
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?