.NET Frameworkには、カンマやタブ文字で区切られたテキストテキストを読み込むための便利なクラスが用意されています。
Microsoft.VisualBasic.FileIO名前空間にあるTextFieldParserクラスを使えば区切り文字で分割されたテキストを読み込むことができます。
ただし、TextFieldParserクラスの仕様として以下の制限があります。
- 空の行が削除されてしまう
- ダブルクオーテーションで囲んだフィールド内の空白や改行が削除されてしまう。
ですので、カンマやタブ文字で区切られたテキストを読み込むクラスライブラリを自作したいと思います。
目次
文字区切りテキスト読み込みクラス
文字で区切られたテキストを読み込むクラスは、文字エンコーディング、区切り文字(カンマ「,」またはタブ文字「\t」)、ダブルクォートの有無を自動で判別して区切り文字で分割した値(フィールド)を読み込みます。
文字エンコーディングと区切り文字はプロパティで指定することも可能にします。
プロジェクト名、クラス名はVisual Basicのクラスと同じ「TextFieldParser」にします。
文字エンコーディングの自動判定
文字エンコーディングの判定にはReadJEncを使います。
ReadJEnc GitHub
ReadJEncへの参照はNugetで追加します。
ソリューションエクスプローラーのプロジェクトを右クリックすると表示されるコンテキストメニューの「Nuget パッケージの管理」をクリックします。
参照で「ReadJEnc」を入力します。
「変更のプレビュー」ダイアログボックスが表示されたら「OK」ボタンをクリックします。
ReadJEncへの参照が追加されます。
ソースコード
文字区切りを読み込むクラスのプロジェクトには、TextFieldParserクラスの他に1行ずつテキストを読み込んでフィールドにパースするCharacterSeparatedValueReaderクラスと、解析時の例外をスローする際に使用するMalformedLineExceptionクラスを作成します。
TextFieldParser クラス
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 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using Hnx8.ReadJEnc; namespace TextFieldParser { public class TextFieldParser { /// <summary> /// コンストラクタ /// </summary> /// <param name="stream">ストリーム</param> public TextFieldParser(Stream stream) { _encoding = null; _delimiter = ""; _stream = stream; _fileName = null; } /// <summary> /// コンストラクタ /// </summary> /// <param name="fileName">ファイル名(ファイルパス)</param> public TextFieldParser(string fileName) { _encoding = null; _delimiter = ""; _stream = null; _fileName = fileName; } private Stream _stream; private static string _fileName; private bool isLoadFile; private Encoding _encoding; /// <summary> /// 文字エンコーディング /// </summary> public Encoding Encoding { get { return _encoding; } set { _encoding = value ?? throw new MalformedLineException("文字エンコーディングに null は設定できません。\r\nパラメーター名: Encoding"); } } private string _delimiter; /// <summary> /// 区切り文字 /// </summary> public string Delimiter { get { return _delimiter; } set { if (string.IsNullOrEmpty(value)) { throw new MalformedLineException("区切り文字に null または 空文字列は設定できません。\r\nパラメーター名: Delimiter"); } else if (value.Length > 1) { throw new MalformedLineException("区切り文字に 1 桁を超える文字列は設定できません。\r\nパラメーター名: Delimiter"); } _delimiter = value; } } /// <summary> /// すべての行のフィールドリスト /// </summary> public List<string> AllLine { get; private set; } = new List<string>(); /// <summary> /// テキストを読み込む。 /// </summary> public List<List<string>> Parse() { isLoadFile = false; if (_stream == null) { if (string.IsNullOrEmpty(_fileName)) { throw new MalformedLineException("ストリームまたはファイル名が指定されていないのでフィールドを読み取れません。"); } _stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.None); isLoadFile = true; } // 行ごとのフィールドリスト List<List<string>> fieldsPerLine = new List<List<string>>(); try { Encoding enc = GetEncoding(_stream, out string text); // エンコーディングの取得 if (_encoding == null) { _encoding = enc ?? throw new MalformedLineException("文字エンコーディングが null または自動で判別できないため、フィールドを読み取れません。\r\nパラメーター名: Encoding"); } if (string.IsNullOrEmpty(_delimiter)) { // 区切り文字の取得 string delim = GetDelimiter(text); _delimiter = delim ?? throw new MalformedLineException("区切り文字が null または空文字列、または自動で判別できないため、区切り文字で分けられたフィールドを読み取れません。\r\nパラメーター名: Delimiter"); } using (StreamReader streamReader = new StreamReader(_stream, _encoding)) { CharacterSeparatedValueReader csvReader = new CharacterSeparatedValueReader(streamReader, _encoding, _delimiter); int lineNo = 0; // 1行ごとに処理する while (!streamReader.EndOfStream) { lineNo++; // 行ごとのフィールドを読み込む List<string> fields = csvReader.ReadLine(out string line, out bool doubleQuotationError); // 行ごとのフィールドリストを追加 fieldsPerLine.Add(fields); // 行テキストを追加 AllLine.Add(line); if (doubleQuotationError) { // ダブルクォートエラー throw new MalformedLineException($"現在の区切り文字を使用して、行 {lineNo} を解析できません。"); } } } // 行ごとのフィールドリストを返す return fieldsPerLine; } finally { if (isLoadFile) { _stream.Close(); _stream = null; } } } /// <summary> /// 文字エンコーディングを判定して取得 /// </summary> /// <param name="stream">ストリーム</param> /// <param name="text">テキスト</param> /// <returns></returns> private Encoding GetEncoding(Stream stream, out string text) { text = null; _stream.Position = 0; byte[] data = new byte[stream.Length]; stream.Read(data, 0, data.Length); var charCode = ReadJEnc.JP.GetEncoding(data, data.Length, out text); Encoding encoding = charCode.GetEncoding(); // UTF8でない場合はSJIS if (encoding.EncodingName != Encoding.UTF8.EncodingName) { encoding = Encoding.GetEncoding("SJIS"); } _stream.Position = 0; return encoding; } /// <summary> /// 区切り文字を判定して取得 /// </summary> /// <param name="text"></param> /// <returns></returns> private string GetDelimiter(string text) { // 前後の改行を削除 string target = text.Trim(new char[] { '\r', '\n' }); // 空文字の場合はカンマを返す if (string.IsNullOrEmpty(target)) return ","; TokenState state = TokenState.Empty; // 1文字ずつ読み取り最初の区切り文字(カンマORタブ)を判別 foreach (var c in target) { if (state != TokenState.QuotedField && c == '\n') { // 改行で終了 break; } switch (state) { case TokenState.QuotedField: // ダブルクォート中 if (c == '"') { // 一旦終了と判定 state = TokenState.EndQuote; } continue; case TokenState.EndQuote: if (c == '"') { // ダブルクォート内のダブルクォート(2重") state = TokenState.QuotedField; continue; } state = TokenState.NormalField; break; case TokenState.Empty: case TokenState.AfterSeparator: if (c == '"') { state = TokenState.QuotedField; continue; } state = TokenState.NormalField; break; default: state = TokenState.NormalField; break; } if (c == ',') { // カンマ return ","; } else if (c == '\t') { // タブ return "\t"; } } return null; } /// <summary> /// 読み取った文字の状態 /// </summary> private enum TokenState { /// <summary> /// フィールド読み取り前 /// </summary> Empty, /// <summary> /// 区切り文字を読み取った直後 /// </summary> AfterSeparator, /// <summary> /// 通常のフィールドを読み取り中 /// </summary> NormalField, /// <summary> /// ダブルクォートフィールドを読み取り中 /// </summary> QuotedField, /// <summary> /// ダブルクォート終了かフィールド内のダブルクォートかを判定 /// </summary> EndQuote } } } |
CharacterSeparatedValueReader クラス
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 236 237 238 239 240 241 242 243 244 245 246 247 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using System.Collections; namespace TextFieldParser { /// <summary> /// 文字区切り値読み取りクラス /// </summary> internal class CharacterSeparatedValueReader { /// <summary> /// コンストラクタ /// </summary> /// <param name="streamReader">ストリームリーダー</param> /// <param name="encoding">エンコーディング</param> /// <param name="delimiter">区切り文字</param> public CharacterSeparatedValueReader( StreamReader streamReader, Encoding encoding, string delimiter) { _streamReader = streamReader; _encoding = encoding; _delimiter = delimiter; } #region 定数 /// <summary> /// ダブルクォート /// </summary> public static string DoubleQuote = "\""; #endregion #region フィールド /// <summary> /// ストリームリーダー /// </summary> protected StreamReader _streamReader = null; /// <summary> /// 区切り文字。 /// </summary> protected string _delimiter = string.Empty; /// <summary> /// CSVファイルの文字エンコーディング。 /// このSystem.Text.Encodingクラスで表される文字エンコーディングを用いて、ファイルの読み書 /// きを行う。 /// </summary> protected Encoding _encoding; #endregion #region メソッド /// <summary> /// CSVファイル読込(1行を区切り文字で分割して、文字列配列で取得)。 /// 読込ファイルの現在位置から1行を読み込み、CSV形式の書式に則って1行の文字列を文字列配列 /// に分割して取得する。 /// 取得できない場合nullを返す。 /// </summary> /// <param name="lineText">1行分のテキスト</param> /// <param name="doubleQuoteError">ダブルクォートのエラーがあるかどうか</param> /// <returns>読込文字列配列</returns> //public virtual string[] CsvReadStrings() public virtual List<string> ReadLine(out string lineText, out bool doubleQuoteError) { lineText = string.Empty; doubleQuoteError = false; if (_streamReader.Peek() < 0) { return null; } // 1行読み込む string line = _streamReader.ReadLine(); // 呼び出し元に返す行データをいったん設定する lineText = line; // テキストを区切り文字ごとに格納するためのリスト List<string> valueList = new List<string>(); string valueString = string.Empty; // 作業フラグ(0:ダブルクォート中以外の文字処理時、1:ダブルクォート中の文字処理時) int workType = 0; // 空データの場合は空文字列を返す if (line.Length == 0) { return valueList; } // 行の最後の改行記号を削除する line = line.TrimEnd(new char[] { '\r', '\n' }); // 区切り文字の直後かどうか bool isAfterDelimiter = false; // 読み込んだ1行の文字列を1文字ずつ検証して分割する for (int i = 0; i < line.Length; i++) { // 文字列取得 string charString = line[i].ToString(); /*************************************************** * 区切り文字後のスペースをとる場合は有効にする // 区切り文字の後のスペース if (isAfterDelimiter && charString.Equals(" ")) { continue; } ***************************************************/ // ダブルクォート中で最後の文字がダブルクォートでない場合 // または最後が開始ダブルクォートの場合 if ((workType == 1 && i == line.Length - 1 && !DoubleQuote.Equals(charString)) || (workType == 0 && i == line.Length - 1 && DoubleQuote.Equals(charString) && (isAfterDelimiter || i == 0))) { // 次の行を読み込む string nextLine = _streamReader.ReadLine(); if (nextLine != null) { // 行を追加(複数行を1行として読み込む) nextLine = nextLine.TrimEnd(new char[] { '\r', '\n' }); line += "\r\n" + nextLine; lineText = line; } } // 文字がダブルクォートの場合 if (DoubleQuote.Equals(charString) == true) { try { // 次の文字を取得 string nextCharStr = ""; if ((i + 1) < line.Length) { nextCharStr = line[i + 1].ToString(); } // 次の文字がダブルクォートかどうかを判定する if (DoubleQuote.Equals(nextCharStr) == true && workType == 1) { // ダブルクォートの場合は「"」文字列と設定する(ダブルクォートの連続は文字列と判断する為) valueString += charString; i++; continue; } // 開始時のダブルクォート処理 if (workType == 0 && (isAfterDelimiter || i == 0)) { // 作業タイプ設定をダブルクォート中の文字処理と設定する workType = 1; } // 終了時のダブルクォート処理 else if (workType == 1 && (_delimiter.Equals(nextCharStr) || i == line.Length - 1)) { // 次の文字が区切り文字またはダブルクォートが行末の場合 // 作業タイプ設定をダブルクォート中以外の文字処理と設定する workType = 0; } else if (workType == 0) { valueString += charString; } else if (workType == 1) { // ダブルクォート中の場合は無視してエラーにする doubleQuoteError = true; } } finally { isAfterDelimiter = false; } } // 区切り文字かつダブルクォート中以外の場合 else if (_delimiter.Equals(charString) && workType == 0) { isAfterDelimiter = true; // 改行文字しか無い場合は空文字を返す if (IsCheckNewLineOnly(valueString)) { valueString = string.Empty; } // リストに追加 valueList.Add(valueString); // 次の文字列を扱う為初期化 valueString = string.Empty; } // 通常文字の場合 else { isAfterDelimiter = false; // 1文字づつ格納する valueString += charString; } } // 残りを追加 valueList.Add(valueString); // この時点でダブルクォート中(閉じていない場合)はエラーにする if (workType == 1) { // ダブルクォート中の場合は無視してエラーにする doubleQuoteError = true; } return valueList; } /// <summary> /// 指定した文字列が改行のみの文字列かどうか判定する /// </summary> /// <param name="targetString">判定する文字列</param> /// <returns>true:改行のみ false:他の文字も含む(空文字列の場合もfalse)</returns> public virtual bool IsCheckNewLineOnly(string targetString) { if (targetString == "\r\n" || targetString == "\n") { return true; } return false; } #endregion } } |
MalformedLineException クラス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace TextFieldParser { /// <summary> /// 解析例外クラス。 /// </summary> public class MalformedLineException : Exception { public MalformedLineException(string message) : base(message) { } public MalformedLineException(string message, Exception inner) : base(message, inner) { } } } |
TextFieldParserのコーディングが終わったらビルドしてdllを作成しておきます。
TextFieldParserを使用したサンプルプログラム
作成したTextFieldParserのdllを使用してテキストファイルのデータを読み込むサンプルプログラムを作成します。
ユーザーインターフェース
ユーザーインターフェースは次のようなWindows フォームを作成します。
フォームにはファイル名を入力するテキストボックスと、ファイルを参照するボタンと、ファイルの読み込みを実行するボタンと、読み込んだデータを表示するデータグリッドビューを配置します。
ソースコード
「ファイル名」テキストボックスの右にある「…」ボタンをクリックした時のイベント処理でファイルを開くダイアログボックスを表示してファイルを参照できるようにします。「読込」ボタンがクリックされた時のイベントで指定されたファイル名のテキストファイルを読み込んで、フィールドをデータグリッドビューに設定(表示)します。
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 |
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 WindowsFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } // ファイル参照ボタンクリックイベントの処理 private void button1_Click(object sender, EventArgs e) { try { // ファイルを開くダイアログボックスを開いてファイルを参照する using (OpenFileDialog dialog = new OpenFileDialog()) { // ファイル名テキストボックスのテキストを取得する string fileName = textBox1.Text; if (!string.IsNullOrEmpty(fileName)) { // ファイル名が入力されている場合はディレクトリをファイルを開くダイアログボックスに設定する FileInfo fileInfo = new FileInfo(fileName); dialog.InitialDirectory = fileInfo.DirectoryName; } // ファイルを開くダイアログボックスを表示する DialogResult result = dialog.ShowDialog(this); if (result == DialogResult.Cancel) { // キャンセル return; } // OKの場合はファイル名をテキストボックスに設定する textBox1.Text = dialog.FileName; } } catch (Exception ex) { MessageBox.Show(ex.Message); } } // テキストファイルを読み込む private void button2_Click(object sender, EventArgs e) { try { string fileName = textBox1.Text; if (!File.Exists(fileName)) { throw new ApplicationException("ファイル名が見つかりません。"); } // ファイル名を指定してTextFieldParserクラスのインスタンスを生成 TextFieldParser.TextFieldParser textFieldParser = new TextFieldParser.TextFieldParser(fileName); // テキストファイルのフィールドを読み込む List<List<string>> fields = textFieldParser.Parse(); // データグリッドのデータソースに読み込んだフィールドを設定 dataGridView1.DataSource = fields; } catch (Exception ex) { MessageBox.Show(ex.Message); } } } } |
プログラムの実行
プロジェクトをビルドして実行します。
読込テストを行うテキストとして、次のようなデータを入力したテキストファイルを用意します。
1 2 3 4 5 |
コード,名称,データ aaa,名称A,abcdefg bbb,名称B,"改行と ダブルクォート「""」ありのデータ" ccc,名称C,以上3行 |
ファイルを参照して「読込」ボタンをクリックします。
文字エンコーディング、区切り文字を自動判定して、データグリッドにデータが設定されました。
データグリッド上では改行がわかりづらいですが、セルを選択してコピーすると
1 2 3 4 5 |
コード 名称 データ aaa 名称A abcdefg bbb 名称B 改行と ダブルクォート「"」ありのデータ ccc 名称C 以上3行 |
以上のようなデータが読み込めていることが確認できます。