Faster Aho-Corasick PHP implementation

2.5k views Asked by At

Is there a working implementation of Aho–Corasick in PHP? There is one Aho-Corasick string matching in PHP mentioned on the Wikipedia article:

<?php

/* 

    This class performs a multiple pattern matching by using the Aho-Corasick algorythm, which scans text and matches all patterns "at once".

    This class can:
    - find if any of the patterns occours inside the text
    - find all occourrences of the patterns inside the text
    - substitute all occourrences of the patterns with a specified string (empty as well)

    Example of usage: 

    $words = array{ "ananas", "antani", "assassin" };
    $pm = new PatternsMatcher();    
    $pm->patterns_array = $words;   
    if ( $pm->multipattern_match( "banananassata" ) ) {
        echo "pattern found!!";         
    }


    This class is definitively open-source under no particular license.  
    If you use it, let me know what you think about it and how to improve it: 

        Marco Nobile (Università degli studi di Milano-Bicocca) - [email protected]

    The code wasn't deeply tested, use as your own risk.     

    P.S.: in order to use it as a bad words black-lister, I suggest you to wrap the words with two empty spaces (eg.: "ananas"-->" ananas ") 
    in order to avoid sub-patterns detection. Moreover, better delete the word by substituting it with an empty space instead of the empty string.

*/


class PatternsMatcher {

    var $patterns_array;
    var $text;
    var $finals;
    var $delta;
    var $phi;
    var $container; 
    var $M;
    var $ready;


    /* costruttore */
    function PatternsMatcher() {
        $this->finals = array();
        $this->phi = array();
        $this->container = array();
        $this->delta = array();
        $this->patterns_array = array();
        $this->ready = false;
    }


    /* import new pattern */
    function import_pattern( $p ) {
        $this->patterns_array[]=$p;
    }


    /* shortcuts */
    function deltafun( $indice, $carattere ) {
        return $this->delta[$indice][$carattere];
    }       
    function phifun( $indice ) {
        return $this->phi[$indice+1];
    }   


    /*  chiamata (ricorsiva) che controlla l'esistenza di prefissi uguali a questa stringa. 
        il parametro limita il numero di stati oltre il quale non verificare */
    function check_border( $string , $state ) {


        /* se la stringa è lunga 0 non ho trovato prefissi buoni    */
        if ( strlen($string)==0 )  
            return 0;   

        /* se la stringa è più lunga, controlliamo non sia contenuta in un prefisso
            ovvero in una delle stringhe identificate dagli stati precedenti (<state) */
        for ($j=0; $j<$state; $j++) {

            /* se questo suffisso è uguale ad un pattern, ritorna lo stato corrispondente */
            if ( $string == $this->container[ $j ] )
                return $j+1;
        }

        // trovato nulla, riprovo con la sottostringa
        return $this->check_border( substr( $string, 1 ) , $state );                
    }


    /* costruisce la tabella phi (failure) */
    function build_phi( ) {

        /* valore di partenza */
        $this->phi[0]=0;

        /* foreach stato */
        foreach ( $this->container as $index => $string )  {

            /*  controlla se il prefisso di questo pattern ha un suffisso che è... 
                prefisso di un pattern tra quelli identificati dagli stati 0..index */
            $this->phi[ $index ] = $this->check_border( $string , $index );

        }

        return $this->phi;

    }


    /* costruisce la tabella delta (next) */
    function build_delta( ) {

        /* somma delle lunghezze dei patterns */
        $this->M = 0;

        /* ultimo stato */
        $last_state = 0;

        /* contiamo i caratteri dei patterns */
        foreach( $this->patterns_array as $pattern ) {
            $lunghezza = strlen( $pattern );
            $this->M += $lunghezza;
        }

        /* for each pattern... */
        foreach( $this->patterns_array as $key => $pattern ) {

            /* convertiamo le stringhe in array di caratteri  */        
            $string = $pattern;
            $lun = strlen($pattern);

            /* stati iniziali */
            $asf_state = 0;
            $in_pattern_index = 0;

            /* tengo traccia dei prefissi, mi rende la vita più semplice dopo */
            $temp = "";

            /* finché il pattern non è esaurito e la delta è diversa da NIL... */
            while( ($in_pattern_index < $lun) & ( !is_null( $this->deltafun( $asf_state , $string[$in_pattern_index] ) ) ) ) {

                // segui un percorso pre-esistente 
                $asf_state = $this->deltafun( $asf_state , $string[ $in_pattern_index ] );

                // aggiorna il prefisso fin quì
                $temp.=$string[ $in_pattern_index ];

                // cambia carattere del pattern
                $in_pattern_index++;

            }


            /* crea gli eventuali nuovi stati */
            while( $in_pattern_index<$lun ) {

                // salva i prefissi aggiuntivi
                $temp.=$string[ $in_pattern_index ];                
                $this->container[] = $temp;

                // nuovo stato
                $last_state++;

                // salva in delta
                $this->delta[ $asf_state ][ $string[ $in_pattern_index ] ] = $last_state;

                // prossimo carattere (se c'è)
                $in_pattern_index++;
                $asf_state = $last_state;

            }

            /* è uno stato finale! */
            $this->finals[ $asf_state ] = true;

        }

        return $this->delta;

    }


    /* precalcola le tabelle phi e delta; se già calcolate, le ritorna direttamente */
    function generate() {

        /* cache: se abbiamo già precalcolato le tabelle, ritornale direttamente */
        if ($this->ready)   return;

        /* ordina lessicograficamente */
        sort( $this->patterns_array, SORT_STRING );

        /* precalcula le tabelle */         
        $this->build_delta( );      
        $this->build_phi( );

        /* abbiamo precalcolato */
        $this->ready = true;
    }


    /* Aho-Corasick standard */ 
    function multipattern_match( $text ) {

        // generate tables
        $this->generate();

        // necessario per prendere anche le prime occorrenze della frase (es.: pattern = " ab " in "ab ac ").
        $text = " ".$text;

        $i=0;       
        $stato=0;

        while ( $i<strlen($text) ) {

            $n = $this->delta[ $stato ][ $text[$i] ];

            $stato = 
                is_null($n)?    $this->phi[ $stato ] : $n;

            if ( $this->finals[ $stato] ) {
                return $i;
            }

            $i++;
        }       
        return -1;
    }


    /* Aho-Corasick che trova tutte le occorrenze (ritorna un array di tuple {posizione,stringa} ) */
    function multipattern_match_array( $text ) {

        // generate tables
        $this->generate();

        // necessario per prendere anche le prime occorrenze della frase (es.: pattern = " ab " in "ab ac ").
        $text = " ".$text;

        $i=0;       
        $stato=0;
        $result = array();
        $temp = "";

        while ( $i<strlen($text) ) {

            $n = $this->deltafun( $stato , $text[$i] );

            $stato = 
                is_null($n)?    $this->phi[ $stato ] : $n;

            $temp = 
                $stato == 0?
                    "" : $temp.$text[$i];           

            if ( $this->finals[ $stato] ) {
                $result[] = array($temp,$i);
                // echo $temp;
            }           

            $i++;
        }       

        return $result;
    }


    /*  Aho-Corasick modificato per la cancellazione di pattern (blacklist). 
        Il parametro specifica con quale sequenza sostituire lo spazio vuoto */
    function remove_substrings( $text , $with = "" ) {

        // genera le tabelle
        $this->generate();

        // necessario per prendere anche le prime occorrenze della frase (es.: pattern = " ab " in "ab ac ").
        $text = " ".$text;

        // contatore sul T
        $i=0;

        // contatore sul T' (output)
        $j=0;

        // contatore su P
        $k=0;

        // stato sull'ASF
        $stato=0;

        // non ricalcolare la dimensione del testo tutte le volte!
        $luntext = strlen($text);

        // preallochiamo il testo in uscita T' (necessario per le idiosincrasie di PHP)
        $nuovo = str_repeat( " ", $luntext ) ;

        /* finché ci sono caratteri nel testo... */
        while ( $i<$luntext ) {

            // prox stato su ASF
            $n = $this->deltafun( $stato , $text[$i] );

            // è null? usiamo phi
            $stato = 
                is_null($n)?    $this->phifun( $stato ) : $n;

            // aggiorniamo la posizione nella sottostringa (utile per fare backtrack dopo la sostituzione)
            $k = 
                $stato == 0?
                    0 : $k+1;

            // piazza il nuovo carattere
            $nuovo[$j] = $text[$i];

            /* stato di accettazione! cancella all'indietro e riparti */            
            if ( $this->finals[ $stato] ) {

                // backtracking (equivale a cancellare i caratteri)
                $j -= $k;
                $k=0;

                // abbiamo cancellato della roba. dobbiamo riposizionarci sull'ASF!
                $n = $this->deltafun( $stato , substr($with,-1) );

                $stato = 
                    is_null($n)?    $this->phifun( $stato ) : $n;

                // ci posizioniamo sull'ultimo carattere della stringa con cui abbiamo sostituito il pattern
                $i--;
            }   

            // muoviamo i puntatori
            $j++; $i++;         
        }   

        // non ritorniamo l'intera stringa ma solo quella lunga quanto il risultato
        return substr( $nuovo , 0, $j );
    }

}

However I have hard time using it. It works for a baby example but if I try to load several thousands keywords then the script exceeds the 30 seconds limit for loading.

For other script languages there are wonderful implementation such as http://metacpan.org/pod/Text::Scan for Perl and http://pypi.python.org/pypi/ahocorasick/0.9 for Python. Why not for PHP?

4

There are 4 answers

0
quickshiftin On

PHP is really slooow and that is known. Crunching numbers is better left to compiled languages and in the case of PHP, an extension. You could re-write this code as an extension, but it would likely be better to take an existing C library and wrap that into an extension, something like this library for example:

http://sourceforge.net/projects/multifast/

If you're looking for a quicker solution then use the shell to wrap one of the perl / python implementations you've praised.

0
dhruvbird On

If you use POSIX regexp functions in php and use alternation "|" to separate words, you'll get essentially similar behaviour as Aho-Corasick since the regexp engine will probably generate a DFA to evaluate the regular expression.

e.g. /stackoverflow|serverfault|php|python|c++|javascript/

0
Tim Hawkins On

Aho corasick has two phases, one is building an optimised tree, the second is using that tree to find a match. As you increase the number of items in the dictionary the first phase gets longer and longer. PHP has an issue where every request is a shared nothing execution, which means that every request would have to rebuild the whole dictiinary again.

The easiest solution would be to split the algorythm into its two parts and have a loader that builds a dictionary and places it into either APC (APCU), memcache or other fast shared data store. And then use the preloaded dictionary to perform the searches with the other half.

Alternatively create a small REST server using a language that does not have the shared nothing request limitation so a dictionary can be built in global scope, and have it pwrform serachs via REST.

3
ph4r05 On

There is a great C library for Aho-Corasick, look on project page http://sourceforge.net/projects/multifast/?source=dlp

I also needed Aho Corasick in PHP so I implemented PHP Extension (.so) as a wrapper for this library. Check out my GitHub: https://github.com/ph4r05/php_aho_corasick

Licence: LGPL.