CSV結合ツールを自作しよう!(バージョンアップ編)

C#入門
この記事は約7分で読めます。

いままでのCSV結合ツールでは、結合させたいCSVファイルの容量に制限がありました。

それは、結合対象ファイル1つ1つを一旦丸ごとメモリに読み込み、出力対象ファイルに書き出すというアルゴリズムが原因でした。

そこで、今回はバージョンアップとして、その制限をなくしてみたいと思います。

具体的には、結合対象ファイルを読み込んでは書き出すという処理に変えたいと思います。

バージョンアップのアルゴリズム

ちょっとややこしくなりましたが、考え方は次と通りです。

  • 出力ファイルを書き込みモードでオープンしておく
  • 結合対象フォルダから1個取り出す
  • 取り出したCSVファイルから順次1行読んでは出力ファイルに書き出すことを繰り返す
  • 取り出したCSVファイルが2個目以降の場合、ヘッダは読み飛ばす(読み込んだヘッダを無視)
  • 出力ファイルをクローズする

1個目か2個目以上かを判定するため、 file_no という変数を使用しています。

また、ヘッダ行を読み飛ばすため、CSVファイルの読み込んだ行数を数え、画面から入力されたヘッダ行数に達するまで読み飛ばすという処理を行っています。

line_cnt を0から始めているので、line_cnt < head_cnt (読み込んだ行数がヘッダ数未満)という条件判断にしています。

line_cnt = 1 にすると、line_cnt <= head_cnt(読み込んだ行数がヘッダ数以下)という書き方ができるのですが、多くのプログラミング言語はカウントを0から開始する習慣があるので、あえてline_cnt=0にしています。 

以上のことを念頭に、フローチャートをざっくり確認してみて下さい。

ソースコード

それでは、全体のソースコードを掲載しておきます。

尚、今回のVisual Studio 2019 プロジェクトファイルは、下記からダウンロード可能です。

UxExecJoin_Clickイベントハンドラの部分しか変更していないので、このイベントハンドラのソースコードを、お手元のソースコードに置き換えて頂ければVerUp完了です。

ファイルを1行ずつ読み書きするクラス

これまで作ってきたプログラムでファイルを読み書きする場合、Fileクラスのメソッド(ReadAllLInes、 WriteAllLines、AppendAllLines)を使ってきました。

これらのメソッドは1行でファイルの読み書きが出来ますが、それは対象となるファイルを一旦メモリに格納する必要がありました。

ことのとにより、「巨大なサイズのファイルはメモリに収まりきらなくなるため扱えない」という制限が生じていました。

こういう場合は、1行ごとに読み書きすることでメモリ量を大幅に減らすことができますが、この時に使うクラスが StreamReader や StreamWrite クラスです。

ファイルのオープン

ファイルに対して読み書きする場合、OSに対して「今からこのファイルを読み込むよ」とか「今からこのファイルに書き込みするよ」という宣言を行い、その許可をもらう必要があります。

この行為をファイルをオープンすると呼びます。

オープンすることで、OSは特定のプログラムにだけ参照や書き込みを許可し、他のプログラムからの要求を跳ね除けたりするのですが、この状態を「ロックされている」とか「掴まれている」という風に表現します。

ファイルに対しては、「読み込みのみ」、「書き込み(追加書き込み含む)のみ」、「読み書き両方」の3パターンが選べるのですが、それぞれにクラスが用意されています。

今回は「読み込みのみ」と「書き込みのみ」の2つ、つまりStreamReader と StreamWriteを使っています。

ファイルのクローズ

プログラムはファイルをオープンした後で、必ずクローズする必要があります。

クローズとは、OSに対して「もう処理が終わりました」という宣言をすることです。

これによりOSは、他のプログラムからアクセスできるような状態に戻すと共に、ファイルスタンプを変更します。

もし、プログラムがオープンしたままクローズせず終了したらどうなるでしょう?

OSがファイルを掴んだままになるため、他のプログラムがアクセスしようとしても出来くなってしまいます。

また、 StreamReaderやStreamWriteを new した時に確保されたメモリも暫くは残ったままになるため、ループ処理の中で数多くのファイルを処理する場合、メモリ不測に陥る可能性も否めません。

以上のことから、ファイルをオープンしたら、必ずクローズするという処理が必要になります。

クローズするには Closeメソッドを呼ぶだけなので、ファイルを扱う場合は是非このメソッドを呼ぶことを心がけてください。

自動でCloseしてくれる using 文

C#には便利な機能があって、明示的にCloseメソッドを呼ばなくても、自動的にクローズするように記述が出来ます。

それが using です。

ソースコードの冒頭に using System; とかいう記述が数行続きますが、これとは全く違います。

その点が少しややこしいのですが、全く別物です。

書き方は

using( クラスのインスタンス生成 ) { 処理 }

です。

では、実際の例を見てみましょう。

以上の様に、using( ) { } で括ることで、明示的に Closeメソッドを呼ばなくとも、自動的にクローズ処理をしてくれるようになります。

ファイルの終わりまでループ

ファイルを1行ずつ、最後まで読み込むには次のように記述します。

これは こちらの記事の最後にも登場しましたね。

ファイル読み込みの処理をGoogle検索すると、次の様な書き方を目にすることもあります。

こちらは、ファイルの終わりに達した状態でReadLineメソッドを呼ぶと null が返ってくるので、それを利用した方法になります。

これは好みの問題なので、どちらでも良いと思います。

2個目以降とヘッダ行数の判定

最後に要となるヘッダを読み飛ばす際の判定条件です。

1個目のファイルか2個目以降のファイルかを判定するために、ファイル1個を処理するたびに file_noをカウントアップしています。

これを利用すると、file_no が0の時は1個目、0以外の時(0より大きい時)は2個目以降であると判断できます。

そして、line_cnt は1行読む度にカウントアップしていますので、行数が head_cnt より小さい場合はヘッダだと判断して、読み飛ばし(continue により、forループの先頭に無条件にジャンプ)を行っている訳です。

ちょっとだけ注意する点としては、line_cnt は int 型で定義しているので 、 -2147483648~2147483648(プラスマイナス2億1千)の値が扱える範囲になります。

つまり、2億1千行を超えるCSVを扱う場合、途中でline_cntの値がマイナスになって挙動がおかしくなるという危険性はあります。

実質そんな巨大なファイルを結合することは無いと思いますが、もし気になるようでしたら long line_cnt とすれば -9223372036854775808~9223372036854775808というくらいとてつもない行数に対応できるようになります。

ちなみに、file_no > 0 と書いていますが、 file_no != 0 と書いても同じです。

それから、 line_cnt ++ と書いていますが、これはC言語っぽい書き方です。

++演算子

++ は演算子で、変数の値に1加えるという記述です。

C言語には次のような記述方法が用意されていて、 1を加えるだけなら ++ を使います。

++ は変数の後だけではなく、前にも記述できます。

1行の中に単独で書く場合、どちらの記述でも結果は同じです。

しかし条件判断の中で使う場合は意味が変わってきます。

++を変数の後に書く場合、条件判断を実行した後で変数を加算します。
逆に、変数の前に記述すると、変数に値を加算してから条件判断します

条件判断で用いる場合、この違いは大きいのでご注意ください。

まとめ

如何でしたでしょうか。

StreadReader とStreamWrite を使って1行毎に読み込みと書き込みを繰り返すことで、メモリ消費量を減らしました。

これでメモリにファイルを丸ごと読み込む必要がなくなったので、実質メモリ不測という事は無くなります。

数G単位のファイルを扱う場合はこの方法が有効になりますので、機会があれば是非ご活用下さい。

タイトルとURLをコピーしました