前提
pngファイルのバイナリ(16進数)から指定した2つの文字列を探し、
その文字列にはさまれた文字列を取得する、という処理を実装しました。
しかし、ファイル数・ファイルサイズが膨大になると、処理途中で以下のエラーが発生します。
実現したいこと
- 任意のディレクトリにあるpngファイルのバイナリを取得
- 取得したバイナリ上にAAAXXXCCC という文字列があるとする(XXXは文字数も含めファイルによって変わるため不明、AAA,CCCは固定)
- XXXを知りたいので、AAAとCCCを探し、位置を取得する
- AAAの末尾から、CCCの先頭の間にある文字列を取得する(この場合はXXX)
- 1~4をファイル数だけ繰り返す。
- 番号リスト
発生している問題・エラーメッセージ
以下のエラーが発生します。 OutOfMemoryException
該当のソースコード
static private byte[] bytes = new byte[300000000] /* 300MBのファイルまで対応 */ private async void ButtonExecution_Click_1(object sender, EventArgs e) { string[] l_file_dir; /* pngファイルのディレクトリを取得 */ l_file_dir = Directory.GetFiles(textBox1.Text, "*.png", SearchOption.AllDirectories); /* ファイル選別処理を実行*/ await Task.Run(() => GetFileInfo_inf(l_file_dir)); } private void GetFileInfo_inf(string[] path) { int XXX; int l_length; int l_start = 0; int l_end = 0; using (FileStream fs = File.OpenRead(path[num])) { // バイナリファイルの読込 先頭(0)から末尾(fs.Length)まで fs.Read(bytes, 0, (int)fs.Length); l_length = (int)fs.Length; } for (int i = 0; i < l_length; i++) { /* まずAAAの位置を取得 */ if (0 == l_start) { /* AAA のバイナリ */ if (0x64 == bytes[i] && 0x75 == bytes[i + 1] && 0x72 == bytes[i + 2] && 0x61 == bytes[i + 3] && 0x74 == bytes[i + 4] && 0x69 == bytes[i + 5] ) { /* AAAの終了位置(XXXの開始位置)を取得 */ l_start = i + 6; } } /* AAAの位置がわかったらCCCの位置を取得 */ else { /* CCC のバイナリ */ if (0x22 == bytes[i] && 0x20 == bytes[i + 1] && 0x62 == bytes[i + 2] && 0x6C == bytes[i + 3] ) { /* CCCの開始位置(XXXの終了位置)を取得 */ l_end = i - 1; break; } } } /* AAA ~ CCC の間がXXXに該当する */ l_time_len = l_end - l_start + 1; /* バイナリファイルからXXXを取得する */ byte[] l_time_len_arr = new byte[l_time_len]; Array.Copy(bytes, l_start, l_time_len_arr, 0, l_time_len); /* Shift_JISに変換 */ System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); l_time_len = Encoding.GetEncoding("Shift_JIS").GetString(l_time_len_arr); MessageBox.Show(l_time_len); }
試したこと
もともと、バイナリをバイト配列に代入することを関数内で行っておりメモリリークが発生していました。
静的領域に大きな単位で確保するように変更した結果、メモリが増え続けるエラーは減りましたが、
ファイルが多くなると再びエラーが出てしまう状況です。
見よう見まねでC#の勉強を始めたため稚拙なコードかと思いますが、よろしければアドバイスいただきたいです。よろしくお願いいたします。
このコード本当に動きますか?
何か別の物を走らせているのでは?
l_start が見るからにスコープ外からアクセスされているので、動くとしたらどこか別の場所でも定義されているのでは?
データが大量で想定外のデータが含まれる事によるロジックの問題という事も考えられますので、特定のデータで発生していないかも確認された方がよろしいかと。
> ファイル数・ファイルサイズが膨大になると、処理途中で以下のエラーが発生します。
非同期に開けるだけファイルを開いて見た目で並列に処理してたらいずれそうなると思います。1バイトずつ見てるし。見よう見真似でやるのではなく、理解してやりましょう。最初は普通に同期式、逐次で処理したらいかがですか。
抜粋したコードでしょうけど文字列が見つからなかったらどうするんでしょうか?
あと、Encoding.GetString するために配列を作成していますが、GetString(Byte[], Int32, Int32) を使うと static フィールドの bytes を使用できます。
https://docs.microsoft.com/ja-jp/dotnet/api/system.text.encoding.getstring?view=net-6.0#system-text-encoding-getstring(system-byte()-system-int32-system-int32)
dodox86 さんのコメントを見て気になりました。まさか並列処理していないでしょうね?
というかボタンをポチポチすると並列処理されちゃいますね。byes が複数のスレッドから使われるのでまずいです。
@Zuishin さん
ご指摘ありがとうございます。ソースコードに誤植がありました。失礼いたしました。
@sazi さん
ご指摘ありがとうございます。思い当たる節があります。エラーが出るファイルのバイナリで確認してみます。
@dodox86 さん
ご指摘ありがとうございます。もともと逐次処理で記述していたのですが、プログレスバーが固まってしまうため、処理ごと並行処理に変更した経緯がありました。一旦逐次処理にもどし、同じエラーが出るか動作確認をしてみます。
@KOZ6.0 さん
ご指摘ありがとうございます。文字列がない場合は、このループには入らない処理にしています。メソッドの紹介、大変助かります。ありがとうございます。並列処理はこの関数以外では行っていません。プログレスバーが応答なしになるのを回避するためだけのために、async/awaitを使って処理完了まで待つ。ようにしています。
> プログレスバーが応答なしになるのを回避するためだけのために、async/awaitを使って処理完了まで待つ。ようにしています
処理中はボタンを押せないようにする等、並列処理を抑止する処理が入っていれば大丈夫です。
Encoding.RegisterProvider を呼び出しているので .NET6 かと思いますが、バイト型配列を確保する際、System.Buffers.ArrayPool<T> の使用を検討してください。
https://docs.microsoft.com/ja-jp/dotnet/api/system.buffers.arraypool-1?view=net-6.0
https://ikorin2.hatenablog.jp/entry/2020/07/25/113904
ディレクトリを検索して見つかったファイルを全部読み込んでから処理しようとしていますね。
一つずつしないと、大量のファイルがあった場合にメモリが足りなくなると思います。
あなたの回答
tips
プレビュー