C# と VB.NET の質問掲示板

わんくま同盟

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

ツリー一括表示

C# csvファイル読み込みの質問です /普通のサラリーマン (18/01/12(Fri) 20:00) #86281
Re[1]: C# csvファイル読み込みの質問です /魔界の仮面弁士 (18/01/12(Fri) 22:06) #86283
Re[1]: C# csvファイル読み込みの質問です /WebSurfer (18/01/13(Sat) 12:53) #86284
│└ Re[2]: C# csvファイル読み込みの質問です /魔界の仮面弁士 (18/01/13(Sat) 15:47) #86286
Re[1]: C# csvファイル読み込みの質問です /魔界の仮面弁士 (18/01/13(Sat) 17:03) #86287


親記事 / ▼[ 86283 ] ▼[ 86284 ] ▼[ 86287 ]
■86281 / 親階層)  C# csvファイル読み込みの質問です
□投稿者/ 普通のサラリーマン (1回)-(2018/01/12(Fri) 20:00:02)

分類:[C#] 

複数行、複数列のセルに数値が入力されているcsvファイルがあります。
(厳密には数値が入っていなくて空欄のセルも存在。)

C#プログラムでボタンを押してcsvファイルを読み込んで
ReadToData[i,j]の二次元配列に各セル位置に対応する数値を格納させたいのですが、
空欄のセルが存在するところでインデックス境界外というエラーで読み込めません。
(読み込んだcsvファイル、全て数値が埋まっていればうまくいきます。)
セルが空欄のときは0を入れる、などの処理をさせるにはどうしたら良いでしょうか?

        private void button1_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                StreamReader sr1 = new StreamReader(openFileDialog1.FileName, Encoding.GetEncoding("Shift_JIS"));
                string strStream = sr1.ReadToEnd();
                System.StringSplitOptions option1 = StringSplitOptions.RemoveEmptyEntries;

                string[] lines1 = strStream.Split(new char[] { '\r', '\n' }, option1);
                char[] spliter1 = new char[1] { ',' };

                int heightLength1 = lines1.Length;
                int widthLength1 = lines1[0].Split(spliter1, option1).Length;

                label10.Text = heightLength1.ToString();
                label8.Text = widthLength1.ToString();

                ReadToData = new double[heightLength1, widthLength1];

                for (int i = 0; i < heightLength1; i++)
                {
                    for (int j = 0; j < widthLength1; j++)
                    {
                        string[] readStrData1 = lines1[i].Split(spliter1, option1);

                        if (double.TryParse(readStrData1[j], out ReadToData[i,j]))
                        {

                        }
                        else
                        {
                            ReadToData[i, j] = 0;
                        }
                    }
                }

            }
            else
            {
            }

            Refresh();
        }

[ □ Tree ] 返信 編集キー/

▲[ 86281 ] / 返信無し
■86283 / 1階層)  Re[1]: C# csvファイル読み込みの質問です
□投稿者/ 魔界の仮面弁士 (1530回)-(2018/01/12(Fri) 22:06:46)
No86281 (普通のサラリーマン さん) に返信
> 複数行、複数列のセルに数値が入力されているcsvファイルがあります。

改行が「各レコードの末尾」に付与された CSV ファイルを生成する処理系と
string strStream = "111,222,333\r\n444,555,666\r\n777,888,999\r\n";

改行が「レコードとレコードの間」に付与される CSV ファイルを生成する処理系があります。
string strStream = "111,222,333\r\n444,555,666\r\n777,888,999";


今回の場合、データとして「空欄」があるとのことですので、
上記のどちらの仕様で進めるのかも、事前に確認しておいてください。

というのも、1 列のみで構成された
 string strStream = "111\r\n";
という CSV ファイルがあった場合、
前者(行末に改行)ルールでは 1 レコードのファイルと判定されねばなりませんし、
後者(行間で改行)ルールでは 2 レコードのファイルと判定されねばならないわけです(2行目は空欄)。



> System.StringSplitOptions option1 = StringSplitOptions.RemoveEmptyEntries;
> string[] lines1 = strStream.Split(new char[] { '\r', '\n' }, option1);

空欄が許容されているのですから、RemoveEmptyEntries を指定してはマズイです。

今回は、「空」であることも重要な情報なのですから、
StringSplitOptions は None のままにしておくべきかと思いますよ。


ここでは仮に、「行と行の間で改行」する CSV ルールであると仮定し、
下記のような「4行1列」の CSV があったとしてみましょう。

2 行目に空欄のデータがあります。
 string strStream = "111\r\n\r\n333\r\n444";

この場合、Split 処理を
 string[] lines1 = strStream.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
 string[] lines2 = strStream.Split(new char[] { '\r', '\n' }, StringSplitOptions.None);
 string[] lines3 = strStream.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
としてみるとどうなるでしょうか。

lines3 なら、正しく 4 行に分解してくれますが、
lines1 だと、空行が削除されて 3 行になってしまいます。
lines2 に至っては、\r と \n の間でも区切ってしまい、7 行扱いですね。



ここまでは行単位に区切る場合の話をしてきましたが、
もちろん、列方向に区切る場合も同様です。

RemoveEmptyEntries を指定してしまうと、
"111,,333" が 3 列ではなく、2 列になってしまいますよね。
[ 親 86281 / □ Tree ] 返信 編集キー/

▲[ 86281 ] / ▼[ 86286 ]
■86284 / 1階層)  Re[1]: C# csvファイル読み込みの質問です
□投稿者/ WebSurfer (1413回)-(2018/01/13(Sat) 12:53:16)
No86281 (普通のサラリーマン さん) に返信

問題の原因や解決策の提案はすでに魔界の仮面弁士さんが No86283 でされていますの
で、直接の回答ではないですが、別の案(CSV パーサーの利用)を提案させてください。

そんな提案は不要ということでしたら以下はスルーでお願いします。

質問者さんが最初の質問でアップされたコードでは、ファイルを読んで文字列を作り、
String.Split メソッドでその文字列を区切るといった方法を取られてます。

でも、改行コードやデリミタ(コンマのような区切り文字)がフィールド値の中に
あったり、改行コードが異なったり(例: Windows は CR + LF、Unix は LF)する
CSV ファイルの場合そのような単純な方法は使えません。

詳しくは以下の記事の最初のセクション「CSV形式とは?」を見てください。

CSV形式のファイルをDataTableや配列等として取得する
http://dobon.net/vb/dotnet/file/readcsvfile.html

改行コードやデリミタがフィールド値の中にある場合、フィールド値をダブルクォー
ト (") で囲むと言った約束を設けて対応しますが、そのような CSV ファイルの処置
が可能なパーサーを自力で作るのは簡単ではありません。

なので、TextFieldParser などの既存のパーサーを使うのがよさそうです。上に紹介
した記事にコード例がありますので見てください。

TextFieldParser は Microsoft が提供している Visual Basic .NET 用のクラスライ
ブラリです。C# でも Microsoft.VisualBasic.dll を参照に追加してやれば使えます。

何といっても Microsoft のライブラリなので、.NET アプリならこれを使うのが無難
そうな気がします。

[ 親 86281 / □ Tree ] 返信 編集キー/

▲[ 86284 ] / 返信無し
■86286 / 2階層)  Re[2]: C# csvファイル読み込みの質問です
□投稿者/ 魔界の仮面弁士 (1532回)-(2018/01/13(Sat) 15:47:46)
No86284 (WebSurfer さん) に補足
> 別の案(CSV パーサーの利用)を提案させてください。

CsvHelper なども有名ですね。
https://www.nuget.org/packages/CsvHelper/
http://joshclose.github.io/CsvHelper/reading#getting-all-records


> なので、TextFieldParser などの既存のパーサーを使うのがよさそうです。

Microsoft.VisualBasic.FileIO.TextFieldParser は確かに高機能ですが、
『空行を無視する』仕様になっていることにご注意ください。

この「空行を無視する」という TextFieldParser の制限により、
私が No86283 で例に挙げた CSV 群、たとえば:
>>> string strStream = "111\r\n\r\n333\r\n444";
などは、残念ながら正しく処理することができません。


> 改行コードやデリミタがフィールド値の中にある場合、

空行無視という制限により、TextFieldParser ではたとえば
 string strStream
   = "BARCODE,NAME\r\n"
   + "4901313037508,ソフトサラダ\r\n"
   + "4582409181895,\"選べる!\r\nみんなのスープ\r\n\r\n8袋入\"\r\n"
   + "4901990328544,\"ごつ盛 坦々麺\"\r\n";
のようなデータも正しく読み取れません。


上記の CSV データをパースした場合、たとえば
 var lines = new string[4][];
 lines[0] = new string[2] { "BARCODE", "NAME" };
 lines[1] = new string[2] { "4901313037508", "ソフトサラダ" };
 lines[2] = new string[2] { "4582409181895", "選べる!\r\nみんなのスープ\r\n\r\n8袋入" };
 lines[3] = new string[2] { "4901990328544", "ごつ盛 坦々麺" };
へと解釈されることが期待されるわけですが、
Microsoft.VisualBasic.FileIO.TextFieldParser では空行部が読み飛ばされるため、
たとえ HasFieldsEnclosedInQuotes = true / TrimWhiteSpace = false を
指定したいたとしても、ReadFields メソッドで順次読み込みする限り
 var lines = new string[4][];
 lines[0] = new string[2] { "BARCODE", "NAME" };
 lines[1] = new string[2] { "4901313037508", "ソフトサラダ" };
 lines[2] = new string[2] { "4582409181895", "選べる!\r\nみんなのスープ\r\n8袋入" }; // ★破損行
 lines[3] = new string[2] { "4901990328544", "ごつ盛 坦々麺" };
のように解釈されてしまうことになります。
[ 親 86281 / □ Tree ] 返信 編集キー/

▲[ 86281 ] / 返信無し
■86287 / 1階層)  Re[1]: C# csvファイル読み込みの質問です
□投稿者/ 魔界の仮面弁士 (1533回)-(2018/01/13(Sat) 17:03:47)
No86281 (普通のサラリーマン さん) に返信
> ReadToData[i,j]の二次元配列に各セル位置に対応する数値を格納させたいのですが、
> ReadToData = new double[heightLength1, widthLength1];
> セルが空欄のときは0を入れる、などの処理をさせるにはどうしたら良いでしょうか?

LINQ で処理できるようにするために、
ReadToData[行,列] の二次元配列の代わりに
ReadToData[行][列] のジャグ配列にするというのはどうでしょう。


// ReadToData[行, 列] の二次元配列の代わりに
// ReadToData[行][列] のジャグ配列にしておく
double[][] ReadToData = { };

private void button1_Click(object sender, EventArgs e)
{
 if (openFileDialog1.ShowDialog() == DialogResult.OK)
 {
  // 「セルが空欄のときに 0.0 にする」ための変換関数
  Func<string, double> ToDouble = s => CDbl(s, 0.0);

  // ファイルを読み取ってジャグ配列にする処理
  ReadToData = File.ReadLines(openFileDialog1.FileName, Encoding.GetEncoding("Shift_JIS"))
   .Select(row => row.Split(new string[] { "," }, StringSplitOptions.None).Select(ToDouble).ToArray())
   .Cast<double[]>().ToArray();
 }
}

double CDbl(string s, double defaultValue = default(double))
{
 return double.TryParse(s, out double dbl) ? dbl : defaultValue;
}


どうしても 2 次元配列の方が良い場合には、ジャグ配列に受け取ってから
 int 行数 = ReadToData.Length;
 int 列数 = (行数 == 0) ? 0 : ReadToData[0].Length;
 //int 最大列数 = ReadToData.Max(cols => cols.Length);
で行数・列数を取得してから、改めて詰め込み直すことになってしまいますが…。
[ 親 86281 / □ Tree ] 返信 編集キー/


管理者用

- Child Tree -