ファイル名が連番であることを確認したい!

自作のマークシートリーダーでは、Windows 用の OpenCV に加え、Python 用の OpenCV も利用して、マークの読み取りを高速化している。

この Python 用の OpenCV を動かすには Python4Delphi(P4D) が必要だ。P4D 使用時はプログラムの仕様として、読み取り対象のマークシート画像ファイル名の末尾は「数値化したら連番として読み取れる半角3桁の数字」でなければならない(例:X_001.jpgなど、MS_Reader.exe Version 1.1.5 から)。

そのことを、つい忘れて実行すると・・・

【コンパイル済みの exe を実行した場合】

最初に表示されるエラーメッセージ


さらに、

2つめのエラーメッセージ


OKをクリックすると、もう一度、

3つめのエラーメッセージ


んで・・・

4つめのエラーメッセージ


泣きたい気持ち T_T で OK をクリックすると・・・

メモリーリークまで発生・・・

うわーん T_T

【実行(F9)では?】

エラーメッセージの形式こそ、違え・・・

泣きたい気持ちは、同じではありませんか。みなさん・・・


ファイル名が「プログラムの仕様と異なっている」ために起きるエラーであるという、言わば「確実に発生を予見できるエラー」なのに、

どうして今まで、
何とかしようと思わなかったのか?

以前から、なんとなく、気づいてはいたけれど・・・

オレはもしかして、
自分で思ってる以上に
バカ
なんじゃないか?

あらためて、そう思ったのであります。みなさん・・・

そこで、この 犯罪に近い プログラムの挙動をなんとかするべく、ようやくと言いますか、今更ではありますが立ち上がり・・・ 悪戦苦闘すること幾年月(実際、半日くらいです)。なので、今回は、このふと思い立ったちいさな夢を実現するまでの お読みいただく価値などまったくない 苦闘の成果の記録です。

【もくじ】

1.そして、悲劇は繰り返される
2.連鎖の終止符は?
3.まとめ
4.お願いとお断り

1.そして、悲劇は繰り返される

人間は、いろいろなことを忘れる生き物です。

むかーし、サーフィンに夢中だった頃、台風の海で大波と一緒に落ちてきたサーフボードが脳天を直撃。溺れて、死ななかったのはよかったけれど、とにかく砂浜までなんとか生還後、確かに見覚えのある風景を感じはするし、自分の名前も、家の住所も思い出せるのに、「僕のおうちまでの帰り道がどうしても思い出せません!」みたいな・・・。うぎゃー

( この道、見覚えだけはあるんだけどなー。はたして、おうちは右だっけ? 左だっけ? )

( 家の玄関の風景も覚えてるんだけどなー。そこへの行き方がまったくわかりましぇん T_T )

あの時はやばかった・・・ まぁ、あの時ほど、困るわけではないが、それでも半年に2回くらい、My マークシートリーダーを使っていて、ファイル名の命名規則をド忘れし、今回、冒頭で紹介したエラーメッセージをくり返し登場させてしまう・・・。

その都度、あわてふためき、もう二度とするまいと固く心に誓い、反省し、失敗の原因の記録まで書き、クラウドにはそのバックアップまでとり、それでも、七転び八起きではなく、七転八倒を身上とするかのごとき私は、果てしない後悔の輪廻、そう苦しみと迷いの連鎖の中で、なお、その悲劇を執拗なまでに繰り返してきたのであった。

そもそも、X_01A.jpg、X_01B.jpg みたいな、連番と紛らわしいファイル名を付けるプログラムを作ったのも、 なので、やはり、この負の連鎖は、自分自身に問題の深すぎる根っこが・・・

ぞーぉさん
ぞーぉさん
おーなかがデカいのね・・・

なんかちがう、みたいな・・・

ファイル名が連番でなければ読めないマークシートリーダーであるとわかっているのに、しかも、作ったのが他ならぬ自分自身であるにも関わらず、なぜか、「 X-01A.jpg, X-01B.jpg, x-02A.jpg, X-02B.jpg ・・・」のような、準連番的な?名前の付いたファイルだと、つい安心して、P4D モードで(しつこいようですが、作者である私自身が) マークの読み取りを実行してしまう・・・ T_T

その場合、プログラムの仕様だから当然のごとく、読み取りエラーが発生し・・・

このエラー、なに?

・・・みたいな・・・、決まって毎回、「驚きと焦り」の方が先走って脳内を占拠、「エラーの真の原因=ファイル名が連番でないこと」に、作者である自分自身がなかなか気づかない・・・

だから、バカだと、さっき

さすがに最近はそんなことはないが、以前はコレでさんざん悩んだこともあったのです・・・みなさん。

その My マークシートリーダーで、数学の解答用紙を読み取り、別プログラムで処理(受験者に返却する答案や資料を作成)する方向で、現在、既存のプログラムを改良しているのですが・・・

とある休日の朝、シャワーを浴びながら、なぜか、ふと

(そうだ。この際、アレも何とかしておこう)

と、ようやく思い立ったのです。みなさん。

アレとは、もちろん、P4D 使用時に「ファイル名が連番でないとエラーになること」であります。みなさん。

エラーになって(なぜか?毎回のようにその真の原因を忘れ)あわてふためく前に、予め、読み取り指定フォルダ内の拡張子を小文字に変換すると「jpg」or 「jpeg」になるファイルだけ抽出して、そのファイル名の末尾3桁の半角数字が完全に連番であるか・どうかを調べ、もし、問題がある場合はユーザーに通知して、エラーを未然に防止する、そんなプログラムは・・・ ぎゃはは、Delphi さえあれば、わーらっちゃうくらいカンタンに・・・

すぐ出来る・・・ )

そう軽く考えて、朝から始めた「ファイル名が完全に連番であることを確認する関数」作りに、なんと半日以上、費やしてしまったのであります。みなさん。

たぁーくさんサンプルがあると思ってあちこち調べてみたが(私が調べた範囲では)、Web上にその方法を解説している資料も、サンプル・プログラムも、ついに見つけることができなかったのであります。みなさん。

( もしかして・・・ そんな関数作りは「カンタンすぎる」から、サンプルがないのかなー? )

・・・などと思いつつ、でも、実際にそれを書くとなると、誰も話題にしてないって・・・ なんで? いや、それにしちゃ、なんだかんだ、結構・・・ それなりに難しいぞ、と半日ほど、あーでもない・こーでもないをくり返して・・・ なんとか、自分の環境では、期待通りに動作するものが書けたので、もしかしたら、将来、同じことを実現したくて悩んでおられる方の参考になるかも?しれないと思い、ここに書いておくことにしたわけであります。みなさん。

まず、どなたの役にも立たないカモ・・・ですが。とりあえず、核心部分は、次の通り。

implementation

uses
  //  (略)
  System.RegularExpressions,
  Generics.Collections;

  //System.RegularExpressionsはP4D使用時にファイル名が連番であるかどうかを確認するために追加
  //Generics.Collectionsは上と同じ目的でTListを使うために追加

上記ライブラリを2つ、uses しておいて、プログラム全体で使いまわすわけではないので、Formのメンバーにせず、マークシート画像ファイルを読みだす手続き内から呼び出して使う形で次の関数を記述。

procedure TFormMSReader.ProcDataRead(Sender: TObject);
var
  //  (略)
  strMsg:string;
  Ext1, Ext2: string;
  Extension:string;

  //jpg とjpeg が同一フォルダ内に混在していないことを確認する_20250302追加
  function HasMixedExtensions(const FolderPath: string): Boolean;
  var
    SearchRec: TSearchRec;
    JPGFound, JPEGFound: Boolean;
  begin
    JPGFound := False;
    JPEGFound := False;

    if FindFirst(FolderPath + '\*.jpg', faAnyFile, SearchRec) = 0 then
    begin
      JPGFound := True;
      FindClose(SearchRec);
    end;

    if FindFirst(FolderPath + '\*.jpeg', faAnyFile, SearchRec) = 0 then
    begin
      JPEGFound := True;
      FindClose(SearchRec);
    end;

    Result := JPGFound and JPEGFound;
  end;

  //ファイル名が連番であるかどうか、確認
  function IsSequentialFileNames(const DirPath: String;
    var Extension1, Extension2: String): Boolean;
  var
    FileList: TStringList;
    FileNumbers: TList<Integer>;
    i, j, numStart: Integer;
    tempFileName, fileName, fileNum: string;
  begin

    //Falseで初期化
    Result := False;

    //指定されたディレクトリ内から、指定された拡張子のファイル名を抽出する
    FileList := TStringList.Create;
    FileNumbers := TList<Integer>.Create;

    try

      for j := 0 to 1 do
      begin

        //小文字に変換して拡張子を指定
        case j of
          0:Extension:= LowerCase(Extension1);
          1:Extension:= LowerCase(Extension2);
        end;

        for tempFileName in TDirectory.GetFiles(DirPath, '*' + Extension) do
        begin
          // ファイル名からパスと拡張子を除去
          fileName := TPath.GetFileNameWithoutExtension(tempFileName);
          //数値部分を抽出
          numStart := TRegEx.Match(fileName, '\d+$').Index;
          if numStart <= 0 then
            Exit; // 数値部分がない場合はFalseを返す
          fileNum := Copy(fileName, numStart, Length(fileName) - numStart + 1);
          if TryStrToInt(fileNum, i) then
            FileNumbers.Add(i);
        end;

        //数値部分があるファイルのみ抽出し、比較する
        if FileNumbers.Count > 0 then
        begin
          FileNumbers.Sort;
          for i := 1 to FileNumbers.Count - 1 do
          begin
            if FileNumbers[i] <> FileNumbers[i - 1] + 1 then
              Exit; //連番でない場合はFalseを返す
          end;
          Result := True; //連番である場合はTrueを返す
        end;

      end;

    finally
      FileList.Free;
      FileNumbers.Free;
    end;

  end;

begin

  //文字列型変数 Path に画像ファイルを入れたフォルダへのパスを指定する

  //jpg とjpeg が同一フォルダ内に混在していないことを確認する_20250302追加
  if HasMixedExtensions(Path) then
  begin
    strMsg:='jpg とjpeg の2種類の拡張子が混在しています。'+#13#10+
      '拡張子はjpg か jpeg のどちらかに統一してください。'+#13#10+
      '処理を中止します。';
    Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
    Exit;
  end else begin
    //確認用
    //strMsg:='拡張子の混在はありません!';
    //Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
  end;

  //画像ファイルを読み込む処理でファイル名が連番であるかどうか、確認する
  try
    Ext1:='jpg';
    Ext2:='jpeg';
    if IsSequentialFileNames(Path, Ext1, Ext2) then
    begin
      //確認用
      //strMsg:='ファイル番号は連番です!';
      //Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
      //Blog用に実験
      //raise Exception.Create('T_T');
    end else begin
      strMsg:='ファイル番号が連番ではありません!';
      Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
      Exit;
    end;
  except
    on E: Exception do
    begin
      strMsg:='大変です。本物のエラーが発生しました: ' + E.Message;
      Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
    end;
  end;

end;

なんで、こんなイイことに今まで気づかなかったのか???

だから、バカだと、さっき

*(^_^)*♪

2.連鎖の終止符は?

任意のフォルダに連番でないファイル名を付けたマークシート画像を入れてテスト。

五十音的には「連番」と言えるのだろうか?


MS_Reader.exe を起動して、プログラムが期待通りに動作するか、確認。

読み込む画像が入ったフォルダとして、上の「連番じゃない画像フォルダ」を指定し、画像ファイルを読み込もうとすると・・・

やった! やった!!


MS_Reader.exe が、この世に誕生して5年(くらいかな?)。
ようやく、悲しみの連鎖に終止符が打たれたのであります。みなさん。

あとは、正真正銘のエラーが発生しないことを祈るのみであります。みなさん。

こっちのエラーは、マジでやばい >_<

これだけは見たくないのであります。
みなさん。

でも、よく考えたら(考えなくても)
エラーの連鎖を断ち切るためのメッセージが、

エラーメッセージだった

・・・ということは、

連鎖が断ち切れてるどころか、
これは、むしろ、立派な連鎖ではないでしょうか。みなさん。

私は、
ここに、運命を感じたのであります。
みなさん。

僕のじんせいはー *(^_^)*♪

3.まとめ

一部、変数の宣言が足りないカモですが、フォルダを開く処理まで入れた一連のプログラムコードは、次の通りです。

procedure TFormMSReader.ProcDataRead(Sender: TObject);
const
  //ディレクトリ(フォルダ)の存在を確認 -> なければ作成する
  DataPath='ProcData';
var
  iStartFolder: string;
  iDirectories: TArray<string>;
  Path: string;
  SearchPattern: string;
  Option: TSearchOption;
  FileNames:TStringDynArray;
  FileName:string;
  strFN, strCheckFolder:string;
  strMsg:string;
  Ext1, Ext2: string;
  Extension:string;

  //jpg とjpeg が同一フォルダ内に混在していないことを確認する_20250302追加
  function HasMixedExtensions(const FolderPath: string): Boolean;
  var
    SearchRec: TSearchRec;
    JPGFound, JPEGFound: Boolean;
  begin
    JPGFound := False;
    JPEGFound := False;

    if FindFirst(FolderPath + '\*.jpg', faAnyFile, SearchRec) = 0 then
    begin
      JPGFound := True;
      FindClose(SearchRec);
    end;

    if FindFirst(FolderPath + '\*.jpeg', faAnyFile, SearchRec) = 0 then
    begin
      JPEGFound := True;
      FindClose(SearchRec);
    end;

    Result := JPGFound and JPEGFound;
  end;

  //ファイル名が連番であるかどうか、確認
  function IsSequentialFileNames(const DirPath: String;
    var Extension1, Extension2: String): Boolean;
  var
    FileList: TStringList;
    FileNumbers: TList<Integer>;
    i, j, numStart: Integer;
    tempFileName, fileName, fileNum: string;
  begin

    //Falseで初期化
    Result := False;

    //指定されたディレクトリ内から、指定された拡張子のファイル名を抽出する
    FileList := TStringList.Create;
    FileNumbers := TList<Integer>.Create;

    try

      for j := 0 to 1 do
      begin

        //小文字に変換して拡張子を指定
        case j of
          0:Extension:= LowerCase(Extension1);
          1:Extension:= LowerCase(Extension2);
        end;

        for tempFileName in TDirectory.GetFiles(DirPath, '*' + Extension) do
        begin
          // ファイル名からパスと拡張子を除去
          fileName := TPath.GetFileNameWithoutExtension(tempFileName);

          //数値部分を抽出
          numStart := TRegEx.Match(fileName, '\d+$').Index;
          if numStart <= 0 then
            Exit; // 数値部分がない場合はFalseを返す

          fileNum := Copy(fileName, numStart, Length(fileName) - numStart + 1);
          if TryStrToInt(fileNum, i) then
            FileNumbers.Add(i);

        end;

        //数値部分があるファイルのみ抽出し、比較する
        if FileNumbers.Count > 0 then
        begin
          FileNumbers.Sort;
          for i := 1 to FileNumbers.Count - 1 do
          begin
            if FileNumbers[i] <> FileNumbers[i - 1] + 1 then
              Exit; //連番でない場合はFalseを返す
          end;
          Result := True; //連番である場合はTrueを返す
        end;
      end;
    finally
      FileList.Free;
      FileNumbers.Free;
    end;
  end;

begin

  try

    //読み込むファイルの存在するフォルダを選択

    //Win10のフォルダ選択Dialogを使用する
    iStartFolder := ExpandFileName('.\ProcData');
    if SelectDirectory(iStartFolder, iDirectories,
      [sdHidePinnedPlaces, sdNoDereferenceLinks, sdForceShowHidden,
      sdAllowMultiselect], 'フォルダを選択してください', 'Folder', 'Ok') then
    begin

      //カーソルを待機状態に設定
      Screen.Cursor := crHourGlass;

      //読み込むデータのあるフォルダへのPathを取得
      Path:=iDirectories[0];

      //jpg とjpeg が同一フォルダ内に混在していないことを確認する_20250302追加
      if HasMixedExtensions(Path) then
      begin
        strMsg:='jpg とjpeg の2種類の拡張子が混在しています。'+#13#10+
          '拡張子はjpg か jpeg のどちらかに統一してください。'+#13#10+
          '処理を中止します。';
        Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
        Exit;
      end else begin
        //確認用
        //strMsg:='拡張子の混在はありません!';
        //Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
      end;

      //ファイル名が連番であるかどうか、確認
      try
        Ext1:='jpg';
        Ext2:='jpeg';
        if IsSequentialFileNames(Path, Ext1, Ext2) then
        begin
          //確認用
          //strMsg:='ファイル番号は連番です!';
          //Application.MessageBox(PChar(strMsg), PChar('情報'), MB_ICONINFORMATION);
          //Blog用に実験
          //raise Exception.Create('T_T');
        end else begin
          strMsg:='ファイル番号が連番ではありません!';
          Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
          Exit;
        end;
      except
        on E: Exception do
        begin
          strMsg:='大変です。本物のエラーが発生しました: ' + E.Message;
          Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
        end;
      end;

      // (省略)

    end;
  finally
    Screen.Cursor := crDefault;
  end;

end;

4.お願いとお断り

今回掲載したプログラムは、拡張子が jpg と jpeg の画像が同一フォルダ内に混在していないことを正常動作の前提にしています。この点には十分、ご注意・ご留意いただけますよう、お願い申し上げます。

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容(プログラムを含む)利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。