string.Split を利用してCSVファイルやTSVファイルを読み込む - C#

string.Split を利用してCSVファイルを読み込みパーシングするコードを紹介します。

概要

こちらの記事では、Micorosoft.VisualBasic.TextFieldParserクラスを利用してCSVファイルを読み込むコードを紹介しました。 一般的な利用では問題はありませんがアセンブリの参照をなるべく減らしたい場合や、 VisualBasic のアセンブリの参照をしたくない場合もあります。 この記事では、Micorosoft.VisualBasic.TextFieldParserを利用せずに、StreamReaderとString.Splitメソッドを用いてCSVファイルを読み込むコードを紹介します。

プログラム

UI

下図のUIを作成します。
フォームにボタンを1つ、ラベルを1つ、複数行のテキストボックスを1つ配置します。

コード

下記のコードを記述します。実際はbutton1のClickイベント部分の実装になります。
FormMain.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;

namespace CSVParserWinForm
{
  public partial class FormMain : Form
  {
    public FormMain()
    {
      InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
      if (openFileDialog1.ShowDialog() == DialogResult.OK) {
        label1.Text = openFileDialog1.FileName;

        StreamReader sr = new StreamReader(openFileDialog1.FileName, Encoding.GetEncoding("Shift_JIS"));
        try { 
          while (sr.EndOfStream == false) {
            string line = sr.ReadLine();
            string[] fields = line.Split(',');
            //string[] fields = line.Split('\t'); //TSVファイルの場合

            for (int i = 0; i < fields.Length; i++) {
              textBox1.Text += fields[i] + "\r\n";
            }
            textBox1.Text += "------\r\n";

          }
        }finally{ 
          sr.Close();
        }
      }
    }
  }
}

解説

  if (openFileDialog1.ShowDialog() == DialogResult.OK) {
    ...
  }
上記コードにより、ファイル選択ダイアログボックスを開きます。ファイル選択ダイアログボックスでファイルが指定された場合にifブロック内を実行します。
  label1.Text = openFileDialog1.FileName;

  StreamReader sr = new StreamReader(openFileDialog1.FileName, Encoding.GetEncoding("Shift_JIS"));
ラベルに、選択したファイルのフルパスを表示します。また、StreamReaderを用いて、選択したファイルを開きます。今回、ファイルはShift-JISの文字コードのファイルとして開きます。

  try { 
    while (sr.EndOfStream == false) {
      string line = sr.ReadLine();
      string[] fields = line.Split(',');
      //string[] fields = line.Split('\t'); //TSVファイルの場合

      for (int i = 0; i < fields.Length; i++) {
        textBox1.Text += fields[i] + "\r\n";
      }
      textBox1.Text += "------\r\n";
    }
  }finally{ 
    sr.Close();
  }
whileループでファイルの末尾まで繰り返します。ファイルの末尾はStrreamReaderのEndOfStream がtrueになることで検出します。
ファイルの読み込みは、StrreamReaderのReadLine()メソッドにより読み出します。ファイルの読み出し内容は、ReadLine()メソッドの戻り値として取得します。ReadLineメソッドで1行分の内容が読み込まれるため、さらにカンマでの区切り処理をします。カンマでの区切り処理はStringオブジェクトのSplitメソッドを利用します。Splitメソッドの動作の詳細はこちらの記事を参照してください。
Splitメソッドの引数に','カンマを与えることで、指定した文字列をカンマで区切ります。カンマで区切られた結果はstring型の配列としてSplitメソッドの戻り値で返ります。
Splitメソッドの戻り値をテキストボックスに表示します。
すべての処理が完了したのち、StreamReaderをクローズします。

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


button1をクリックします。ファイルを開くダイアログボックスが表示されます。読み込むCSVファイルを選択します。


今回選択したCSVファイルは下記の内容となります。
CSVファイル (sample.txt)
1,Penguin,200
2,Duck,100
3,にわとり,240
4,くじら,120
5,麒麟,580


ファイルを開くと処理が始まり、結果がテキストボックスに表示されます。CSVファイルの内容がパースされて表示できていることが確認できます。


参考:2文字以上の区切り文字の場合

2文字以上の区切り文字の場合はSplitメソッドの引数にstringの配列を渡します。
string[] fields = line.Split(new string[] {"--","::",";;"}, StringSplitOptions.None);

ダブルクォーテーション「"」で囲まれた「,」のある項目に対応する

下記のCSVのように項目内に「,」が含まれる場合は、項目全体を「"」でくくる書式を利用します。先のプログラムでは、下記のCSVは正しく読み込むことができません。
CSVファイル (sample-dq.txt)
1,Penguin,200
2,Duck,"100,000"
3,"白い,にわとり",240

上記のコードを先のプログラムで読み込むと下図の結果となります。「"」で囲まれている内部の「,」も区切り文字と判定され、文字列が分割されてしまっていることが確認できます。

対策コード : 配列での実装

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;

namespace CSVParserWinForm
{
  public partial class FormMain : Form
  {
    public FormMain()
    {
      InitializeComponent();
    }

    private void button3_Click(object sender, EventArgs e)
    {
      if (openFileDialog1.ShowDialog() == DialogResult.OK) {
        label1.Text = openFileDialog1.FileName;

        StreamReader sr = new StreamReader(openFileDialog1.FileName, Encoding.GetEncoding("Shift_JIS"));
        try {
          while (sr.EndOfStream == false) {
            string line = sr.ReadLine();
            string[] fields = line.Split(',');
            //string[] fields = line.Split('\t');    //tsvファイルの場合
          
            List<string> fieldsList = new List<string>(fields);

            for (int i = 0; i < fieldsList.Count; i++) {
              if (fieldsList[i].Length > 0 && fieldsList[i].TrimStart()[0] == '"') {
                fieldsList[i] = fieldsList[i].TrimStart();

                if (fieldsList[i].TrimEnd()[fieldsList[i].TrimEnd().Length - 1] == '"') {
                  fieldsList[i] = fieldsList[i].TrimEnd();
                  fieldsList[i] = fieldsList[i].Remove(0, 1);
                  fieldsList[i] = fieldsList[i].Remove(fieldsList[i].Length - 1, 1);
                  continue;
                }

                while (true) {
                  fieldsList[i] = fieldsList[i] + "," + fieldsList[i + 1];
                  fieldsList.RemoveAt(i + 1);

                  if (fieldsList[i].TrimEnd()[fieldsList[i].TrimEnd().Length - 1] == '"') {
                    fieldsList[i] = fieldsList[i].TrimEnd();
                    fieldsList[i] = fieldsList[i].Remove(0, 1);
                    fieldsList[i] = fieldsList[i].Remove(fieldsList[i].Length - 1, 1);
                    break;
                  }
                }
              }          
            }
          
            for (int i = 0; i < fieldsList.Count; i++) {
              textBox1.Text += fieldsList[i] + "\r\n";
            }
            textBox1.Text += "------\r\n";

          }
        }
        finally {
          sr.Close();
        }
      }
    }
  }
}

解説

Splitメソッドによりカンマで文字列が分割された後、分割された文字列を確認し、先頭の文字が「"」であった場合には、次の要素の文字列をカンマを追加して、自身の文字列の後に追加します。また、以降の配列の内容を手前に一つずらします。追加後の文字列を確認し、文字列の末尾が「"」であれば、処理が完了したとみなし、次の要素の確認をします。この処理を繰り返すことで、「"」で囲まれた文字列を正しく抜き出します。

処理例

ペンギン,"120,000","サクサク,うまうま,クッキー"
の場合を考えます。

インデックス
0ペンギン
1"120
2000"
3"サクサク
4うまうま
5クッキー"

となっています。0の要素は「"」が無いため、次の要素に移ります。1の要素は先頭に「"」があります。一つ次の要素を1の要素の文字列の後ろに、カンマを含めて追加します、以降の要素は手前に一つずらします。

インデックス
0ペンギン
1"120,000"
2"サクサク
3うまうま
4クッキー"
5

変更後、1の要素の末尾を確認すると「"」の文字がありますので、処理を終了し次の項目に移ります。2の要素を確認すると、先頭に「"」がありますので、次の項目の要素を2の要素の文字列の後ろに、カンマを含めて追加します。また、後ろの要素を手前に一つずらします。
処理後の状態が下の表です。2の要素の値の末尾は「"」でないため、さらに次の要素を2の要素の末尾にカンマを追加して足します。

インデックス
0ペンギン
1"120,000"
2"サクサク,うまうま
3クッキー"
4
5

処理が終わった状態が下の表です。2の要素の末尾の文字が「"」になっているため、処理を完了し次の要素に移りますが、3,4,5の要素は空欄のため、処理は無く以上オデ処理が完了となります。

インデックス
0ペンギン
1"120,000"
2"サクサク,うまうま,クッキー"
3
4
5

対策コード : リストでの実装

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;

namespace CSVParserWinForm
{
  public partial class FormMain : Form
  {
    public FormMain()
    {
      InitializeComponent();
    }

    private void button2_Click(object sender, EventArgs e)
    {
      if (openFileDialog1.ShowDialog() == DialogResult.OK) {
        label1.Text = openFileDialog1.FileName;

        StreamReader sr = new StreamReader(openFileDialog1.FileName, Encoding.GetEncoding("Shift_JIS"));
        try {
          while (sr.EndOfStream == false) {
            string line = sr.ReadLine();
            string[] fields = line.Split(',');
            //string[] fields = line.Split('\t'); //TSVファイルの場合

            for (int i = 0; i < fields.Length-1; i++) {

              if (fields[i].Length>0 && fields[i].TrimStart()[0] == '"') {
                fields[i] = fields[i].TrimStart();

                if (fields[i].TrimEnd()[fields[i].TrimEnd().Length - 1] == '"') {
                  fields[i] = fields[i].TrimEnd();
                  fields[i] = fields[i].Remove(0, 1);
                  fields[i] = fields[i].Remove(fields[i].Length - 1, 1);
                  continue;
                }

                while (true){
                  if (i < fields.Length) {
                    fields[i] = fields[i] + "," + fields[i+1];

                    for (int k = i+1; k < fields.Length-1; k++) {
                      fields[k] = fields[k+1];
                    }
                    fields[fields.Length-1] = "";

                    if (fields[i][fields[i].Length - 1] == '"') {
                      fields[i] = fields[i].TrimEnd();
                      fields[i] = fields[i].Remove(0, 1);
                      fields[i] = fields[i].Remove(fields[i].Length-1, 1);
                      break;

                    }
                  }
                }                
              }
            }

            for (int i = 0; i < fields.Length; i++) {
              textBox1.Text += fields[i] + "\r\n";
            }
            textBox1.Text += "------\r\n";

          }
        }
        finally {
          sr.Close();
        }
      }
    }

  }
}

解説

先のコードの配列の処理をリストで実装したコードです。リストの場合は要素の削除ができるため、後続の要素を手前にずらす処理が不要になります。

処理例

ペンギン,"120,000","サクサク,うまうま,クッキー"
の場合を考えます。

ペンギン
"120
000"
"サクサク
うまうま
クッキー"

となっています。1番目の要素は「"」が無いため、次の要素に移ります。2番目の要素は先頭に「"」があります。一つ次の要素を2番目の要素の文字列の後ろに、カンマを含めて追加します。手前に追加した要素である2の要素は削除します。

処理後の状態が下の表です。2番目の要素の値の末尾は「"」になっているため次の要素の処理に移ります。
3番目の要素の文字列は先頭に「"」があるため、次の要素を手前の要素の末尾にカンマを付加して足し込みます。

ペンギン
"120,000"
"サクサク
うまうま
クッキー"

文字列をカンマを追加して手前に追加し、追加した項目を削除した状態が下の表です。3番目の要素の文字列の末尾を確認すると「"」ではありませんので、さらに次の要素を手前の文字列の末尾にカンマを含めて追加します。

ペンギン
"120,000"
"サクサク,うまうま
クッキー"

処理後の状態が下の表です。3目の要素の値の末尾は「"」になっているため次の要素の処理に移ります。が、以降の要素は存在しないため以上で処理の完了となります。

ペンギン
"120,000"
"サクサク,うまうま,クッキー"

以上で「"」に囲まれた「,」(カンマにも対応できるようになりました。

著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
最終更新日: 2020-10-26
作成日: 2010-08-13
iPentec all rights reserverd.