バイナリファイルで文字列を検索するには?

解決


かえで  2004-04-03 23:16:31  No: 8166

ファイルが特定の文字列を含んでいるかどうかを判定したいのですが、お知恵をお貸しください。
ファイルはテキストファイルだけでなくバイナリファイルもあります。
テキストファイルだけならTStringListとAnsiPosで判定できるのですが、バイナリファイルはお手上げです。
テキストファイルでもバイナリファイルでも使える汎用的な方法はないでしょうか?
文字コードはシフトJISです。
どうぞよろしくお願いします。


たかみちえ  URL  2004-04-04 00:01:19  No: 8167

TMemoryStreamなんかどうでしょう?
TStringStreamでもいいかもしれませんが、失敗する可能性があります。


かえで  2004-04-04 07:02:04  No: 8168

たかみちえさん、どうもありがとうございます。

> TMemoryStreamなんかどうでしょう?
TMemoryStreamにファイルの内容を読み込んでからはどうしたらよいのでしょう?
がんばって考えてみたのですが、わかりませんでした。(^_^;)


たかみちえ  URL  2004-04-04 08:02:22  No: 8169

えーと、どんなファイルを読みたいかによりますのが、
MIDIファイルを例にとれば、一バイトずつ読んで、メタイベントの開始部まで移動します。
開始部を見つけたら、次のバイトから続く文字列長を取得し、
その文字列長のぶん一気に読み込み、文字列とします。

  他も同じく、文字列長を取得(固定長ならばいいですが)し、文字列を得るという流れになると思います。
他のバイナリファイルの仕様はあんまり知りませんが、
大体はちゃんとした文字列領域に入っているものと思いますので、
仕様を理解して読み進めば、問題ないかと。


頑固ひとスジふたスジ  2004-04-04 10:31:33  No: 8170

園長先生:ひとすじクン、おうだんほどうを渡るときにはどうするの?
ひとスジ:は〜い、手を上げてわたりま〜す。
園長先生:そうね。じゃぁ、ふたすじクン、おうだんほどうを渡る前にはどうするの?
ふたスジ:は〜い、手をあらいま〜す♪

var
  p : PChar;
begin
  with TMemoryStream.Create do begin
    LoadFromFile('ほにゃらら.bin');
    p := AnsiStrPos(Memory, 'ほにゃらら');
    if p <> nil then begin
      (p + Length('ほにゃらら'))^ := #0;
      ShowMessage(IntToStr(Integer(p-Memory))+':'+ p); //めっけたで〜
    end;
    Free;
  end;
end;


たかみちえ  URL  2004-04-04 15:42:02  No: 8171

ええと、ご存じのとおり、コンピュータ上のデータには、
テキストだのバイナリだのなんて区別はありません。
故に、プログラマが"これは文字列である"といってしまえば、
たとえバイナリデータの(文字としてみると)無意味な文字の羅列であったとしても、
文字列と見なすことができてしまいます。
(メモ帳あたりで実行ファイルを開いてみると、わかると思います、Terapadやサクラエディタなども可です)

  そのため、データ全域をいっぺんに検索しようとすると、
そんな意味のない文字列の中に偶然の一致でヒットしてしまうことがあります。
逆に、偶然の一致でヒットしない(表現が変ですけど)こともあります。
最初に"TStringStreamでは失敗するかも"といったのは、このためです。

  確実な方法は、一個一個文字列を抜き出し、その中で検索をかけることだと思います。


HPW  2004-04-04 18:05:07  No: 8172

ピーマコ: 「あの〜、バイナリファイルにあるはずの文字列がStrPos関数でヒットしないことってあるんですか?」
なかざわ: 「あるで〜、バイナリっちゅうのがくせものやな。」
オジャマ: 「バイナリファイルって、文字列を隠しちゃうことあるんですかぁ?」
なかざわ: 「考えてみ〜や。PChar型の文字列はな、最後が #0 やろ? 」
ピーマコ: 「そうですよ、モチロン。ヌルで終わる文字列ですから」
オジャマ: 「あっ、そうか! バイナリファイルには それ以外にも #0 が含まれてることあるんですよね」
なかざわ: 「そや、 #0 がないバイナリファイルなん滅多にあらへんで〜」
ピーマコ: 「ナルホド、じゃ〜StrPos関数だと #0 の後にある文字列ヒットしないですよね〜」
なかざわ: 「#0の位置がファイルの末尾やない時は、またその次の位置からStrPos関数で探すの繰り返したら、でけんこともないやろ。けどそれやったらポインタを一つずつ移動して文字の一致を調べてくのとあんまり変わらんやろな」
ピーマコ: 「やっぱり、ジミチにコツコツやらないとダメですか〜。ミソジまでに終わるかなぁ…」
なかざわ: 「ンなに かかるわけないやろ!」


K  2004-04-05 00:44:08  No: 8173

>TMemoryStreamにファイルの内容を読み込んでからはどうしたらよいのでしょう?
>がんばって考えてみたのですが、わかりませんでした。(^_^;)
Pascalの作者にしてアルゴリズム+データ構造=プログラムの著者でもある
Wirthが草葉の陰で(かどうか知らんけど)泣いてますよ…

"文字列" "検索" "アルゴリズム"  Pascalあたりで検索をかけるといくつも見つかると思います。通常は文字列用のコードですがバイナリへ対応させるのは容易です。

広大なファイルを読み出し専用で扱う場合メモリマップトファイルを使うとファイルを一つの配列・ポインタとして扱えるのでファイルからの読み出しやバッファの区切り等を考えずにすみます。


つっか  2004-04-06 01:29:35  No: 8174

> 通常は文字列用のコードですがバイナリへ対応させるのは容易です。

そうかなぁ。これが容易なら簡単なんですけどね。

たとえば 'A' を検索したとき、文字列の一部なのか、バイトコードの一部なのか
どうやって判断したらいいのだろう?  これは単なる検索コードではできないと
おもうよ。だから、汎用用途での検索のためには、簡単にはコードをしめすこと
はできない。


かえで  2004-04-06 03:38:54  No: 8175

みなさん、どうもありがとうございます。

> Pascalの作者にしてアルゴリズム+データ構造=プログラムの著者でもある
> Wirthが草葉の陰で(かどうか知らんけど)泣いてますよ…
そんなに簡単な質問なんですか?(ショック)

> "文字列" "検索" "アルゴリズム"  Pascalあたりで検索をかけるといくつも見つかると思います。
> 通常は文字列用のコードですがバイナリへ対応させるのは容易です。
容易なんですか? ^_^;;
何度もGoogleで検索してみたのですが、利用できそうな情報を見つけられませんでした。
コードを書いていただけるととてもうれしいのですが。
よろしくお願いします。


K  2004-04-06 03:51:39  No: 8176

>> 通常は文字列用のコードですがバイナリへ対応させるのは容易です。
>
>そうかなぁ。これが容易なら簡単なんですけどね。
お題が
>ファイルが特定の文字列を含んでいるかどうかを判定したいのですが、
なので単純な検索で十分だと判断しましたが、どうでしょう?>かえでさん

>汎用用途での検索のためには、簡単には
そもそもこんなアルゴリズムは簡単かどうか以前に存在しません。
厳密にやるならフォーマットごとにケース分けするしかありません。


かえで  2004-04-06 04:13:04  No: 8177

> お題が
> >ファイルが特定の文字列を含んでいるかどうかを判定したいのですが、
> なので単純な検索で十分だと判断しましたが、どうでしょう?>かえでさん
はい、単純な検索で十分です。(2バイト文字は考慮したいですが)
でも、それがわからないのです。T_T

> 厳密にやるならフォーマットごとにケース分けするしかありません。
フォーマットを考えないで単純に判断したいと考えています。
検索する文字列はある程度の長さがありますので、もしたまたまヒットしたときは
それはそれで致し方ないと思っています。


つっか  2004-04-06 04:47:23  No: 8178

>>ファイルが特定の文字列を含んでいるかどうかを判定したいのですが、
> なので単純な検索で十分だと判断しましたが、どうでしょう?>かえでさん

そうなんですか? 特定の「文字列」がどうやって「文字列」であることを
判断するんでしょうか? それがKさんには簡単なんでしょうか?
google で検索すると見つかるのでしょうか。


つっか  2004-04-06 04:56:29  No: 8179

> 検索する文字列はある程度の長さがありますので、もしたまたまヒットしたときは
> それはそれで致し方ないと思っています。

これは、'A' なら、間違う確率の方が大きいかも知れません。'ALGOL' だったら
かなり誤認識の確率は小さくなります。'ほにゃらほにゃら' だったらまず間違いは無視
できるほど小さくなります。バイトの列が並んでいるなかで、この文字コードのパターンを
発見するのは、速度を考えなければ、単純に1バイトごとにずらしながら、比較
するだけです。


つっか  2004-04-06 05:51:42  No: 8180

具体的にコードを示します。単純パターン検索で、見つかったらファイルの先頭
からのオフセットを示します。みつからなかったら -1 を返します。

procedure TForm1.Button1Click(Sender: TObject);
var
  ms:TMemoryStream;
  i,offset:integer;
  p:PByte;
  s:string;
begin
  Randomize;
  ms := TMemoryStream.Create;
  try
    ms.SetSize(3000);
    p := ms.Memory;
    for i := 1 to 3000 do begin // ランダムパターンを生成
      p^ := Random(256);
      Inc(p);
    end;

    s := 'ほにゃらほにゃら';    // 文字列をランダム位置に書き込む
    offset := Random(2900);
    ms.Position := offset;
    //ms.Write(s[1],Length(s));
    Label1.Caption := IntToStr(offset);

    ms.SaveToFile('c:\Test.bin');

  finally
    ms.Free;
  end;
end;

これは3000バイトのランダムバイトパターンを生成してファイルにします。
//ms.Write(s[1],Length(s)); の部分のコメントアウトをとると、ランダム
な位置に 'ほにゃらほにゃら' の文字列を書き込みます。

function FindPattern(const Filename,str:string):integer;
var
  ms:TMemoryStream;
  p:PByte;

  function ComparePattern(p1,p2:PByte;Count:integer):Boolean;
  var
    i:integer;
  begin
    result := false;
    for i := 1 to Count do begin
      if p1^ <> p2^ then exit;
      Inc(p1); Inc(p2);
    end;
    result := true;
  end;
begin
  result := -1;
  ms := TMemoryStream.Create;
  ms.LoadFromFile(Filename);
  p := ms.Memory;
  while (integer(p)-integer(ms.Memory)) < (ms.Size-Length(str)+1) do
  begin
    if ComparePattern(p,PByte(str),Length(str)) then
    begin
      result := integer(p)-integer(ms.Memory);
      exit;
    end;
    Inc(p)
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  offset:integer;
begin
  offset := FindPattern('c:\Test.bin','ほにゃらほにゃら');
  Label2.Caption := IntToStr(offset);
end;

FindPattern() が今回つくった関数で、文字列と同じバイトパターンを検索して
見つかったらそのオフセットを返します。何回かテストしたところではうまく
いっています。


つっか  2004-04-06 05:54:21  No: 8181

上のコードで FindPattern() で ms.Free を忘れてました。以下のように訂正
します。

function FindPattern(const Filename,str:string):integer;
var
  ms:TMemoryStream;
  p:PByte;

  function ComparePattern(p1,p2:PByte;Count:integer):Boolean;
  var
    i:integer;
  begin
    result := false;
    for i := 1 to Count do begin
      if p1^ <> p2^ then exit;
      Inc(p1); Inc(p2);
    end;
    result := true;
  end;
begin
  result := -1;
  ms := TMemoryStream.Create;
  ms.LoadFromFile(Filename);
  p := ms.Memory;
  while (integer(p)-integer(ms.Memory)) < (ms.Size-Length(str)+1) do
  begin
    if ComparePattern(p,PByte(str),Length(str)) then
    begin
      result := integer(p)-integer(ms.Memory);
      ms.Free;
      exit;
    end;
    Inc(p)
  end;
  ms.Free;
end;


かえで  2004-04-06 07:26:07  No: 8182

つっかさん、どうもありがとうございました。
こちらでもうまくいっています。
PByteを使うのですね。とても勉強になりました。
ところで、質問なのですが、中で使われているComparePattern関数の代わりに
DelphiのCompareMem関数を使ってもよいのでしょうか?


つっか  2004-04-06 07:35:41  No: 8183

> DelphiのCompareMem関数を使ってもよいのでしょうか?

おおっ、まさにこれですね。知りませんでした。そっちを使ってください。


かえで  2004-04-06 07:51:17  No: 8184

> おおっ、まさにこれですね。知りませんでした。そっちを使ってください。
はい、わかりました。

つっかさん、このたびは大変お世話になりました。
どうもありがとうございました。
レスしてくださったみなさんにもお礼を申し上げます。
おかげさまで解決できました。


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加