web-dev-qa-db-fra.com

Moyen efficace de rechercher une chaîne dans un flux

Supposons qu'il existe un flux de texte (ou Reader en Java) sur lequel j'aimerais vérifier une chaîne particulière. Le flux de texte peut être très volumineux, donc dès que la chaîne de recherche est trouvée, j'aimerais retourner la valeur true et éviter également de stocker la totalité de l'entrée en mémoire.

Naïvement, je pourrais essayer de faire quelque chose comme ça (en 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;
}

Bien sûr, cela ne détecte pas la chaîne de recherche donnée si elle se produit à la limite du tampon 1k:

Texte de recherche: "stackoverflow"
Stream buffer 1: "abc ......... stack"
Flux tampon 2: "débordement ....... xyz"

Comment puis-je modifier ce code afin qu'il trouve correctement la chaîne de recherche donnée dans les limites du tampon, mais sans charger le flux entier en mémoire?

Edit: Note lors de la recherche d'une chaîne sur un flux, nous essayons de minimiser le nombre de lectures dans le flux (pour éviter la latence sur un réseau/disque) et de maintenir l'utilisation de la mémoire constante indépendamment de la quantité de données dans le flux. L'efficacité réelle de algorithme de correspondance de chaîne } est secondaire, mais il serait évidemment agréable de trouver une solution utilisant l'un des plus efficaces de ces algorithmes.

48
Alex Spurling

J'ai apporté quelques modifications à l'algorithme Knuth Morris Pratt pour les recherches partielles. Étant donné que la position de comparaison réelle est toujours inférieure ou égale à la suivante, aucune mémoire supplémentaire n'est nécessaire. Le code avec un Makefile est également disponible sur github et il est écrit en Haxe pour cibler plusieurs langages de programmation à la fois, y compris Java.

J'ai également écrit un article connexe: recherche de sous-chaînes dans les flux: une légère modification de l'algorithme de Knuth-Morris-Pratt dans Haxe . L'article mentionne le Jakarta RegExp , maintenant à la retraite et reposant dans Apache Attic. La méthode « match » de la bibliothèque Jakarta Regexp dans la classe RE utilise un CharacterIterator en tant que paramètre.

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"));

    }
}
9
sw.

Il y a trois bonnes solutions ici:

  1. Si vous voulez quelque chose qui est facile et raisonnablement rapide, optez pour une mémoire tampon et installez plutôt une simple machine à états finis non déterministe. Votre état sera une liste d'index dans la chaîne que vous recherchez et votre logique ressemble à ceci (pseudocode):

    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
    

    Cela trouvera la chaîne si elle existe et vous n’aurez jamais besoin d’un tampon

  2. Un peu plus de travail, mais aussi plus rapide: effectuez une conversion NFA vers DFA qui détermine à l’avance quelles listes d’index sont possibles, puis affectez-les à un petit nombre entier. (Si vous lisez à propos de la recherche de chaînes sur Wikipedia, cela s'appelle la construction de powerset .) Ensuite, vous avez un seul état et vous effectuez une transition d'état à état sur chaque caractère entrant. Le NFA que vous voulez est simplement le DFA de la chaîne précédée d'un état qui supprime de manière non déterministe un caractère ou tente de consommer le caractère actuel. Vous voudrez également un état d'erreur explicite.

  3. Si vous voulez quelque chose de plus rapide, créez un tampon dont la taille est au moins deux fois n et que l'utilisateur Boyer-Moore compile une machine d'état à partir de needle. Vous aurez beaucoup de tracas en plus parce que Boyer-Moore n'est pas trivial à implémenter (même si vous trouverez du code en ligne) et qu'il vous faudra faire en sorte de faire glisser la chaîne dans le tampon. Vous devrez construire ou trouver un circular buffer pouvant "glisser" sans copier; sinon, vous obtiendrez probablement les gains de performances que vous obtiendrez de Boyer-Moore.

14
Norman Ramsey

L'algorithme de recherche Knuth-Morris-Pratt ne sauvegarde jamais; c'est juste la propriété que vous voulez pour votre recherche de flux. Je l'ai déjà utilisé pour résoudre ce problème, bien qu'il soit peut-être plus simple d'utiliser les bibliothèques Java disponibles. (Quand cela m'est arrivé, je travaillais en C dans les années 90.)

Le KMP est par essence un moyen rapide de créer un DFA adapté aux chaînes, comme le suggère la suggestion n ° 2 de Norman Ramsey.

9
Darius Bacon

Cette réponse s'appliquait à la version initiale de la question où la clé ne devait lire le flux que dans la mesure nécessaire pour correspondre à une chaîne, si cette chaîne était présente. Cette solution ne satisferait pas à l'exigence de garantir une utilisation fixe de la mémoire, mais elle pourrait être utile si vous avez trouvé cette question et que vous n'êtes pas lié par cette contrainte.

Si vous êtes lié par la contrainte d'utilisation constante de la mémoire, Java stocke des tableaux de tout type sur le segment de mémoire et, en tant que tel, l'annulation de la référence ne libère pas la mémoire de quelque manière que ce soit. Je pense que toute solution impliquant des tableaux dans une boucle consomme de la mémoire sur le tas et nécessite GC. 


Pour une implémentation simple, le Scanner de Java 5 qui peut accepter un InputStream et utiliser un Java.util.regex.Pattern pour rechercher l’entrée peut vous éviter de vous inquiéter des détails de la mise en oeuvre.

Voici un exemple d'implémentation potentielle:

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;
      }
}

Je pense regex parce que cela ressemble à un travail pour un automate à états finis, quelque chose qui commence dans un état initial, en changeant d'état caractère par caractère jusqu'à ce qu'il rejette la chaîne (aucune correspondance) ou parvienne à un état d'acceptation.

Je pense que c'est probablement la logique de correspondance la plus efficace que vous puissiez utiliser, et la manière dont vous organisez la lecture des informations peut être dissociée de la logique de correspondance pour l'optimisation des performances.

C'est aussi comment fonctionnent les regex.

5
brabster

Au lieu de faire de votre tampon un tableau, utilisez une abstraction qui implémente un circular buffer . Votre calcul d'indice sera buf[(next+i) % sizeof(buf)], et vous devrez faire attention à remplir le tampon une moitié à la fois. Mais tant que la chaîne de recherche correspond à la moitié de la mémoire tampon, vous la trouverez. 

4
Norman Ramsey

Je crois que la meilleure solution à ce problème est d'essayer de garder les choses simples. N'oubliez pas, parce que je lis dans un flux, je veux limiter au minimum le nombre de lectures dans le flux (la latence du réseau ou du disque peut être un problème) tout en maintenant constante la quantité de mémoire utilisée (le flux pouvant être utilisé). de très grande taille). L’efficacité réelle de la mise en correspondance des chaînes n’est pas l’objectif numéro un (comme cela a déjà été étudié à mort déjà).

Sur la base de la suggestion d'AlbertoPL, voici une solution simple qui compare le tampon à la chaîne de recherche caractère par caractère. La clé étant que, comme la recherche est effectuée uniquement un caractère à la fois, aucun suivi en arrière n'est nécessaire et, par conséquent, aucun tampon circulaire ou tampon d'une taille particulière n'est nécessaire.

Maintenant, si quelqu'un peut arriver avec une implémentation similaire basée sur algorithme de recherche Knuth-Morris-Pratt alors nous aurions une solution efficace de Nice;)

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

Une recherche très rapide d'un flux est implémentée dans la classe RingBuffer à partir du framework Ujorm. Voir l'échantillon:

 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);

L'implémentation de classe unique est disponible sur SourceForge : Pour plus d'informations, voir link .

1
pop

Mettre en place une fenêtre coulissante. Avoir votre tampon autour, déplacer tous les éléments du tampon un vers l’avant et entrer un nouveau caractère unique dans le tampon à la fin. Si le tampon est égal à votre mot recherché, il est contenu. 

Bien sûr, si vous voulez rendre cela plus efficace, vous pouvez chercher un moyen d'éviter de déplacer tous les éléments de la mémoire tampon, par exemple en créant une mémoire tampon cyclique et une représentation des chaînes qui "tournent" de la même manière fait, il vous suffit donc de vérifier l'égalité de contenu. Cela évite de déplacer tous les éléments dans le tampon. 

1
Tetha

Je pense que vous devez tamponner une petite quantité à la limite entre les tampons.

Par exemple, si votre tampon a une taille de 1024 et une longueur de SearchString de 10, vous devez également rechercher dans chaque tampon de 1024 octets chaque transition de 18 octets entre deux tampons (9 octets à partir de la fin du tampon précédent). concaténés avec 9 octets à partir du début du tampon suivant).

1
ChrisW

Je dirais de passer à une solution caractère par caractère, auquel cas vous devez rechercher le premier caractère de votre texte cible, puis, lorsque vous le trouvez, incrémentez un compteur et recherchez le caractère suivant. Chaque fois que vous ne trouvez pas le prochain caractère consécutif, relancez le compteur. Cela fonctionnerait comme ceci:

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; 
}

Le seul problème est que vous êtes en train de parcourir des caractères ... Dans ce cas, il doit y avoir un moyen de se souvenir de votre variable de comptage. Je ne vois pas de moyen facile de le faire, sauf en tant que variable privée pour toute la classe. Dans ce cas, vous n'institueriez pas de compte dans cette méthode.

1
AlbertoPL

Si vous n'êtes pas lié à l'utilisation d'un Reader, vous pouvez utiliser l'API NIO de Java pour charger efficacement le fichier. Par exemple (non testé, mais devrait être sur le point de fonctionner):

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 () est le fichier à rechercher et repose sur le système d’exploitation pour prendre les mesures appropriées en ce qui concerne l’utilisation du cache et de la mémoire. Notez cependant que map () est plus coûteux que de simplement lire le fichier dans un grand tampon pour les fichiers de moins de 10 Ko environ.

1
deverton

Si vous recherchez une sous-chaîne constante plutôt qu'une expression régulière, je vous recommanderais Boyer-Moore. Il y a beaucoup de code source sur Internet.

Utilisez également un tampon circulaire pour éviter de trop réfléchir aux limites du tampon.

Mike.

0
Mike

Vous pouvez augmenter la vitesse de recherche de très grandes chaînes en utilisant un algorithme de recherche string

0
Alex

J'ai également eu un problème similaire: ignorer les octets de InputStream jusqu'à la chaîne spécifiée (ou tableau d'octets). C'est le code simple basé sur le tampon circulaire. Ce n'est pas très efficace mais ça répond à mes besoins:

  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

Vous pourrez peut-être mettre en oeuvre une solution très rapide utilisant des transformations rapides de Fourier, qui, si elles sont correctement implémentées, vous permettent de faire une correspondance de chaîne en temps O (nlog (m)), où n est la longueur de la chaîne la plus longue à mettre en correspondance, et m est la longueur de la chaîne la plus courte. Vous pouvez, par exemple, exécuter FFT dès que vous recevez une entrée de flux de longueur m, et si elle correspond, vous pouvez revenir, et si elle ne correspond pas, vous pouvez jeter le premier caractère de l'entrée de flux, attendez pour qu'un nouveau caractère apparaisse dans le flux, puis exécutez à nouveau la FFT. 

0
rboling