web-dev-qa-db-ja.com

文字列をストリームから検索する効率的な方法

特定の文字列をチェックしたいテキストのストリーム(またはJavaのReader)があるとします。テキストのストリームが非常に大きくなる可能性があるため、検索文字列が見つかったらすぐにtrueを返し、入力全体をメモリに格納しないようにします。

単純に、私はこのようなことをしようとするかもしれません(Javaで):

public boolean streamContainsString(Reader reader, String searchString) throws IOException {
    char[] buffer = new char[1024];
    int numCharsRead;
    while((numCharsRead = reader.read(buffer)) > 0) {
        if ((new String(buffer, 0, numCharsRead)).indexOf(searchString) >= 0)
            return true;
    }
    return false;
}

もちろん、1kバッファの境界で発生した場合、これは指定された検索文字列を検出できません。

検索テキスト:「stackoverflow」
ストリームバッファ1:「abc .........スタック」
ストリームバッファ2:「オーバーフロー....... xyz」

このコードを変更して、ストリーム全体をメモリに読み込まずに、バッファの境界を越えて指定された検索文字列を正しく見つけるにはどうすればよいですか?

Edit:ストリームで文字列を検索するときは、からの読み取り数を最小限に抑えようとしていることに注意してくださいstream(ネットワーク/ディスクでの遅延を回避するため)およびデータの量に関係なくメモリ使用量を一定に保つストリームで。 文字列マッチングアルゴリズム の実際の効率は二次的ですが、明らかに、これらのアルゴリズムのより効率的なものの1つを使用するソリューションを見つけるのは良いことです。

49
Alex Spurling

部分検索用にKnuth Morris Prattアルゴリズムにいくつかの変更を加えました。実際の比較位置は常に次の比較位置以下であるため、追加のメモリは必要ありません。 Makefileを含むコードは github でも利用でき、Javaを含む複数のプログラミング言語を一度にターゲットにするためにHaxeで記述されています。

私も関連記事を書きました: ストリーム内の部分文字列の検索:HaxeのKnuth-Morris-Prattアルゴリズムのわずかな変更 。この記事は Jakarta RegExp について言及しており、現在は廃止され、Apache Atticで休んでいます。 REクラスのJakarta Regexpライブラリ“ match ”メソッドは、CharacterIteratorをパラメーターとして使用します。

class StreamOrientedKnuthMorrisPratt {
    var m: Int;
    var i: Int;
    var ss:
    var table: Array<Int>;

    public function new(ss: String) {
        this.ss = ss;
        this.buildTable(this.ss);
    }

    public function begin() : Void {
        this.m = 0;
        this.i = 0;
    }

    public function partialSearch(s: String) : Int {
        var offset = this.m + this.i;

        while(this.m + this.i - offset < s.length) {
            if(this.ss.substr(this.i, 1) == s.substr(this.m + this.i - offset,1)) {
                if(this.i == this.ss.length - 1) {
                    return this.m;
                }
                this.i += 1;
            } else {
                this.m += this.i - this.table[this.i];
                if(this.table[this.i] > -1)
                    this.i = this.table[this.i];
                else
                    this.i = 0;
            }
        }

        return -1;
    }

    private function buildTable(ss: String) : Void {
        var pos = 2;
        var cnd = 0;

        this.table = new Array<Int>();
        if(ss.length > 2)
            this.table.insert(ss.length, 0);
        else
            this.table.insert(2, 0);

        this.table[0] = -1;
        this.table[1] = 0;

        while(pos < ss.length) {
            if(ss.substr(pos-1,1) == ss.substr(cnd, 1))
            {
                cnd += 1;
                this.table[pos] = cnd;
                pos += 1;
            } else if(cnd > 0) {
                cnd = this.table[cnd];
            } else {
                this.table[pos] = 0;
                pos += 1;
            }
        }
    }

    public static function main() {
        var KMP = new StreamOrientedKnuthMorrisPratt("aa");
        KMP.begin();
        trace(KMP.partialSearch("ccaabb"));

        KMP.begin();
        trace(KMP.partialSearch("ccarbb"));
        trace(KMP.partialSearch("fgaabb"));

    }
}
11
sw.

ここには3つの優れたソリューションがあります。

  1. 簡単で適度に高速なものが必要な場合は、バッファなしで、代わりに単純な非決定的有限状態マシンを実装します。状態は、検索している文字列へのインデックスのリストであり、ロジックは次のようになります(疑似コード)。

    String needle;
    n = needle.length();
    
    for every input character c do
      add index 0 to the list
      for every index i in the list do
        if c == needle[i] then
          if i + 1 == n then
            return true
          else
            replace i in the list with i + 1
          end
        else
          remove i from the list
        end
      end
    end
    

    これは、文字列が存在する場合はその文字列を検索し、バッファは必要ありません。

  2. 作業は少し多くなりますが、処理も速くなります。NFAからDFAへの変換を実行して、可能なインデックスのリストを事前に特定し、それぞれを小さな整数に割り当てます。 (ウィキペディアで文字列検索について読んだ場合、これはパワーセット構成と呼ばれます。)次に、単一の状態があり、各着信文字で状態から状態への遷移を行います。必要なNFAは、文字を削除するか、現在の文字を消費しようとする状態が非決定的に続く文字列のDFAだけです。明示的なエラー状態も必要です。

  3. より高速なものが必要な場合は、サイズがnの2倍以上のバッファーを作成し、ユーザーBoyer-Mooreがneedleから状態マシンをコンパイルするようにします。 Boyer-Mooreは実装が簡単ではないため(コードをオンラインで見つけることができます)、バッファーを介して文字列をスライドするように調整する必要があるため、多くの余分な手間がかかります。コピーせずに「スライド」できるcircularバッファーを作成または検索する必要があります。そうしないと、ボイヤー・ムーアから得られるパフォーマンスの向上が見込めます。

14
Norman Ramsey

Knuth-Morris-Pratt検索アルゴリズム はバックアップしません。これは、ストリーム検索に必要なプロパティです。以前にこの問題で使用しましたが、利用可能なJavaライブラリを使用するより簡単な方法があるかもしれません。これが思いついたとき、私は90年代にCで作業していました。)

本質的にKMPは、Norman Ramseyの提案#2のように、文字列マッチングDFAを構築するための高速な方法です。

9
Darius Bacon

この回答は、文字列が存在する場合、その文字列に一致するために必要な範囲でのみストリームを読み取ることがキーであった質問の最初のバージョンに適用されました。このソリューションは、固定メモリの使用を保証する要件を満たしませんが、この質問を見つけてその制約に拘束されない場合は、検討する価値があります。

一定のメモリ使用量の制約に縛られている場合、Javaはヒープに任意のタイプの配列を格納するため、参照をnullにしてもメモリの割り当ては解除されません。配列を含む解決策はあると思いますループでは、ヒープ上のメモリを消費し、GCが必要になります。


単純な実装の場合、多分Java 5's Scanner これは、InputStreamを受け入れ、 Java.util.regex.Pattern を使用して入力を検索できます実装の詳細について心配する必要がなくなるからです。

潜在的な実装の例を次に示します。

public boolean streamContainsString(Reader reader, String searchString)
            throws IOException {
      Scanner streamScanner = new Scanner(reader);
      if (streamScanner.findWithinHorizon(searchString, 0) != null) {
        return true;
      } else {
        return false;
      }
}

正規表現は、有限状態オートマトンの仕事のように聞こえるので、正規表現を考えています。初期状態で始まり、文字列を拒否する(一致しない)か、受け入れ状態になるまで、文字ごとに状態を変更します。

これはおそらく、使用できる最も効率的なマッチングロジックであり、情報の読み取りをどのように整理するかは、パフォーマンスチューニング用のマッチングロジックから切り離すことができます。

また、正規表現が機能する方法でもあります。

5
brabster

バッファーを配列にする代わりに、循環バッファーを実装する抽象化を使用します。インデックス計算はbuf[(next+i) % sizeof(buf)]となり、一度に半分ずつバッファをいっぱいにするように注意する必要があります。しかし、検索文字列がバッファの半分に収まる限り、それを見つけることができます。

4
Norman Ramsey

この問題の最善の解決策は、シンプルに保つことです。覚えておいてください、私はストリームから読み込んでいるので、使用されるメモリの量を一定に保ちながら(ネットワークが遅延する可能性があるため)、ストリームからの読み込みの数を最小限に抑えたいです(ネットワークまたはディスクの遅延が問題になる可能性があるため)サイズが非常に大きい)。文字列照合の実際の効率は、一番の目標ではありません(それが 死に至るまでの調査 であるので)。

AlbertoPLの提案に基づいて、バッファを検索文字列と文字ごとに比較する簡単なソリューションを次に示します。重要なのは、検索は一度に1文字しか実行されないため、バックトラッキングが必要ないため、循環バッファーや特定のサイズのバッファーが必要ないことです。

さて、誰かが Knuth-Morris-Pratt検索アルゴリズム に基づく同様の実装を思い付くことができれば、ニースの効率的なソリューションが得られます;)

public boolean streamContainsString(Reader reader, String searchString) throws IOException {
    char[] buffer = new char[1024];
    int numCharsRead;
    int count = 0;
    while((numCharsRead = reader.read(buffer)) > 0) {
        for (int c = 0; c < numCharsRead; c++) {
            if (buffer[c] == searchString.charAt(count))
                count++;
            else
                count = 0;
            if (count == searchString.length()) return true;
        }
    }
    return false;
}
4
Alex Spurling

ストリームの非常に高速な検索は、UjormフレームワークのRingBufferクラスに実装されています。サンプルを見る:

 Reader reader = RingBuffer.createReader("xxx ${abc} ${def} zzz");

 String Word1 = RingBuffer.findWord(reader, "${", "}");
 assertEquals("abc", Word1);

 String Word2 = RingBuffer.findWord(reader, "${", "}");
 assertEquals("def", Word2);

 String Word3 = RingBuffer.findWord(reader, "${", "}");
 assertEquals("", Word3);

単一クラスの実装は SourceForge で利用できます。詳細については、 link を参照してください。

2
pop

スライディングウィンドウを実装します。バッファーを動かし、バッファー内のすべての要素を1つ前に移動し、最後にバッファーに新しい文字を1つ入力します。バッファが検索されたワードと等しい場合、それは含まれています。

もちろん、これをより効率的にしたい場合は、たとえば、循環バッファーと、同じ方法でバッファーを「循環」する文字列の表現を用意することにより、バッファー内のすべての要素の移動を防ぐ方法を検討できます。そのため、コンテンツが等しいかどうかを確認するだけで済みます。これにより、バッファ内のすべての要素の移動が節約されます。

1
Tetha

バッファーの境界で少量をバッファーする必要があると思います。

たとえば、バッファサイズが1024で、SearchStringの長さが10の場合、1024バイトの各バッファを検索するだけでなく、2つのバッファ間の18バイトの遷移も検索する必要があります(前のバッファの最後から9バイト)次のバッファーの先頭から9バイトで連結されます)。

1
ChrisW

私は文字ごとのソリューションに切り替えます。その場合、ターゲットテキストの最初の文字をスキャンし、その文字が見つかったら、カウンターをインクリメントして次の文字を探します。次の連続するキャラクターが見つからない場合は、カウンターを再起動してください。次のように機能します。

public boolean streamContainsString(Reader reader, String searchString) throws IOException {
char[] buffer = new char[1024];
int numCharsRead;
int count = 0;
while((numCharsRead = reader.read(buffer)) > 0) {
    if (buffer[numCharsRead -1] == searchString.charAt(count))
        count++;
    else
        count = 0;

    if (count == searchString.size())    
     return true;
}
return false; 
}

唯一の問題は、文字を調べている最中です...この場合、カウント変数を記憶する方法が必要です。クラス全体のプライベート変数として以外は、簡単な方法はないと思います。この場合、このメソッド内でカウントをインスタンス化しません。

1
AlbertoPL

リーダーの使用に縛られていない場合は、JavaのNIO APIを使用してファイルを効率的にロードできます。例(テストされていませんが、作業に近いはずです):

public boolean streamContainsString(File input, String searchString) throws IOException {
    Pattern pattern = Pattern.compile(Pattern.quote(searchString));

    FileInputStream fis = new FileInputStream(input);
    FileChannel fc = fis.getChannel();

    int sz = (int) fc.size();
    MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz);

    CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
    CharBuffer cb = decoder.decode(bb);

    Matcher matcher = pattern.matcher(cb);

    return matcher.matches();
}

これは基本的にmmap()が検索するファイルであり、オペレーティングシステムに依存して、キャッシュとメモリの使用に関して正しいことを行います。ただし、約10 KiB未満のファイルの大きなバッファーにファイルを読み込むだけで、map()の方がコストが高くなることに注意してください。

1
deverton

高速フーリエ変換を使用して非常に高速なソリューションを実装できる場合があります。これを適切に実装すると、時間O(nlog(m))で文字列照合を行うことができます。ここで、nは照合する長い文字列の長さです。 mは短い文字列の長さです。たとえば、長さmのストリーム入力を受け取ったらすぐにFFTを実行し、一致する場合は戻ることができます。一致しない場合は、ストリーム入力の最初の文字を破棄して待機できます。新しい文字がストリームを介して表示されるようにしてから、もう一度FFTを実行します。

1
rboling

正規表現ではなく定数の部分文字列を探している場合は、ボイヤー・ムーアをお勧めします。インターネット上にはたくさんのソースコードがあります。

また、バッファの境界についてあまり考えすぎないように、循環バッファを使用してください。

マイク。

0
Mike

いくつかの 文字列検索アルゴリズム を使用すると、非常に大きな文字列の検索速度を上げることができます。

0
Alex

私も同様の問題がありました:指定された文字列(またはバイト配列)までInputStreamからバイトをスキップしました。これは循環バッファに基づく単純なコードです。それは非常に効率的ではありませんが、私のニーズに応えます:

  private static boolean matches(int[] buffer, int offset, byte[] search) {
    final int len = buffer.length;
    for (int i = 0; i < len; ++i) {
      if (search[i] != buffer[(offset + i) % len]) {
        return false;
      }
    }
    return true;
  }

  public static void skipBytes(InputStream stream, byte[] search) throws IOException {
    final int[] buffer = new int[search.length];
    for (int i = 0; i < search.length; ++i) {
      buffer[i] = stream.read();
    }

    int offset = 0;
    while (true) {
      if (matches(buffer, offset, search)) {
        break;
      }
      buffer[offset] = stream.read();
      offset = (offset + 1) % buffer.length;
    }
  }
0
dmitriykovalev