Comment générer un texte autour d’un mot précis en PHP ?

Mathieu Chartier Programmation 13 commentaires

Encore un billet technique sur ce blog. Comme j'en ai pris l'habitude, j'aime varier entre mes différentes passions telles que le référencement web, les réseaux sociaux, les CMS ou encore les astuces techniques (notamment en PHP). Ne pensez pas que je n'aime rien d'autre, mais rien qu'avec ces sujets, nous avons de quoi discuter un moment... :-)

Aujourd'hui, je vous présente une même fonction PHP transcrite en PHP procédural et objet (POO) qui permet de générer un extrait de texte à partir d'un mot précis. Le cas d'école pour ce type de fonction est la génération de description dans un moteur de recherche. Si vous souhaitez créer un snippet (extrait de texte) autour d'un mot recherché, ces fonctions et méthodes sont faites pour vous !

Fonctionnalités générales

La fonction est assez modulaire, même si elle peut être encore améliorée. Au-delà de générer un texte, elle permet plusieurs adaptations :

  • Accepter un mot précis ou un tableau de mot (dans ce cas, l'extrait est généré autour d'un des mots tirés au hasard). Dans la version en POO, il est possible de définir l'indice du mot que nous souhaiterions prioriser.
  • Modifier le nombre de mots à afficher avant et après le mot recherché. Par conséquent, vous pouvez gérer la taille du texte créé dynamiquement.
  • Choisir autour de quelle occurrence du mot générer l'extrait. En effet, il peut arriver qu'un mot soit répété dans un texte, il convient alors de choisir quelle occurrence privilégier pour générer le snippet.
  • Afficher une chaîne au choix avant et après l'extrait (souvent des points de suspension...).

Tous les paramètres sont détaillés dans les fichiers ou les codes présentés ci-dessous, n'hésitez pas à me contacter si vous avez un problème...

N.B. : si cette fonction vous aide, vous pourrez dire merci à Olivier Andrieu, le "Pape" du référencement, car c'est pour lui que j'ai développé cet outil à l'origine. Comme je n'aime pas jeter mes créations par les fenêtres, je préférais vous en faire profiter... :-)

Génération de texte autour d'un mot : la fonction PHP !

Voici la fonction generateWrapText() en PHP procédural. Elle est relativement complète mais peut encore être améliorée comme nous le verrons à la fin de cet article. Si elle vous convient, vous pouvez la recopier ou la télécharger ci-dessous.

Télécharger “GenerateWrapText 1.1 (PHP procédural)”generateWrapText-1.zip – Téléchargé 1692 fois – 1,57 Ko

<?php
// Fonction de génération d'un texte autour d'un mot précis
// Paramètre 1 : $texte correspond au texte dans lequel rechercher et générer un extrait
// Paramètre 2 : $mot correspond au mot autour duquel va être dessiné l'extrait
// Paramètre 3 : $limite est le nombre de mot à récupérer autour du mot recherché
// Paramètre 4 : $numOccurrence équivaut au numéro de l'occurrence du mot trouvé dans le texte ("5" étant la 5e présence du mot dans le texte, pour générer l'extrait plus loin dans le texte)
// Paramètre 5 : $strong = true signifie que le mot recherché (central) sera mis en gras (nouveau depuis la version 1.1)
// Paramètre 6 : $chainePrec correspond au texte à écrire avant l'extrait
// Paramètre 7 : $chaineSuiv correspond au texte à écrire après l'extrait
function generateWrapText($texte = '', $mot = '', $limite = 15, $numOccurrence = 5, $strong = true, $chainePrec = "(...)", $chaineSuiv = "...") {
    // Dans le cas où malgré la suppression des accents $mot reste un tableau
    if(is_array($mot)) {
        $indice = rand(0, count($mot)); // On prend un des mots au hasard
        $mot = $mot[$indice];
    }
    
    // Coupe la chaîne mot à mot
    $chaineTexte = mb_split("([[:space:]]|[\(\)\[\]\{\},;:!?<>])+", strip_tags(trim($texte)));
    
    // Nettoyage de la chaine (optionnel)
    foreach($chaineTexte as $cle => $motUnique) {
        $cleanWord = trim($motUnique);
        $motUnique = preg_replace("#(\\n|\\r|&nbsp;)#i", " ", $motUnique);
        
        // Tableau nettoyé reconstitué
        $chaineTab[] = $motUnique;
    }
    
    // Récupère la clé du mot recherché
    $cleMot = array_keys($chaineTab, $mot); // Toutes les occurrences
    
    // On compte le nombre d'occurrences pour assurer de générer un snippet autour du mot cherché
    $nbOcc = count($cleMot, 1);

    if($numOccurrence > $nbOcc) {
        $numOccurrence = $nbOcc - 1; // On prend l'avant-dernière occurrence par exemple
    }
    
    // Paramètres par défaut
    $debutIncomplet = false;
    $finIncomplete = false;
    $chaineCreee = "";
    $chaine = "";

    // Extraction des mots suivants
    if(isset($cleMot[$numOccurrence])) {
        foreach($chaineTab as $cle => $valeur) {    
            if($cle == $cleMot[$numOccurrence]) {
                // "n" Mots précédents
                for($i = $limite; $i >= 1; $i--) {
                    $posMotPrecedent = $cleMot[$numOccurrence]-$i;
                    if(isset($chaineTab[$posMotPrecedent])) {
                        $chaineCreee.= " ".$chaineTab[$posMotPrecedent];
                    } else {
                        $debutIncomplet = true;
                    }
                }

                // Mot recherché (affiché en gras ou non)
                if($strong == true) {
                    $chaineCreee.= " <strong>".$chaineTab[$cleMot[$numOccurrence]]."</strong>";
                } else {
                    $chaineCreee.= " ".$chaineTab[$cleMot[$numOccurrence]];
                }
                
                // "n" Mots suivants
                for($i = 1; $i <= $limite; $i++) {
                    $posMotSuivant = $cleMot[$numOccurrence]+$i;
                    if(isset($chaineTab[$posMotSuivant])) {
                        $chaineCreee.= " ".$chaineTab[$posMotSuivant];
                    } else {
                        $finIncomplete = true;
                    }
                }
            }
        }

        // Ajout des caractères de début (... par exemple) si le découpage n'atteint pas le début du texte.
        if($debutIncomplet == false) {
            $chaine.= $chainePrec;
        }
        $chaine.= $chaineCreee; // Ajout de la chaîne reconstituée
        // Ajout des caractères de fin (... par exemple) si la phrase n'est pas au bout.
        if($finIncomplete == false) {
            $chaine.= $chaineSuiv;
        }
    }
    
    // Retourne le résultat
    return $chaine;
}
?>

Version plus complète en PHP Objet (POO)

Juste pour le plaisir, j'ai développé une version en PHP Objet de ce générateur de texte autour d'un mot précis. Au fond, rien de bien neuf, la Class PHP peut encore être améliorée mais elle profite désormais de getters (accesseurs) et setters (mutateurs) pour effectuer plus facilement des opérations sur les extraits à générer.

Les méthodes principales de la Class GenerateWrapText sont les suivantes :

  • retournerTexte("texte général", "mot recherché") qui retourne le résultat (return).
  • afficherTexte("texte général", "mot recherché") qui affiche le résultat (echo).

Vous pouvez la télécharger ci-dessous ou la recopier dans le cadre qui suit.

Télécharger “GenerateWrapText 1.1 (PHP objet)”generateWrapText-POO.zip – Téléchargé 1360 fois – 2,02 Ko

<?php
class GenerateWrapText {
    private $chaine;
    public $texte = '';                // Texte dans lequel généré un extrait
    public $mot = '';                // Mot recherché (peut être un tableau de mots !)
    public $limite = 15;            // Nombre de mots avant et après le mot recherché
    public $numOccurrence = 5;        // Numéro de l'occurrence du mot à récupérer dans le texte
    public $randOcc = 1;            // Si l'occurrence du mot dépasse sa présence, on prend $randOcc en moins
    public $strong = true;            // Mettre le mot recherché en gras
    public $chainePrec = "(...)";    // Chaine affichée au début
    public $chaineSuiv = "...";        // Chaine de fin
    
    // Fonction principale pour retourner le texte
    public function retournerTexte($texte = '', $mot = '') {
        // Récupération des variables
        if(!empty($texte)) {
            $this->texte = $texte;
        }
        if(!empty($mot)) {
            $this->setMot($mot);
        }
        
        // Coupe la chaîne mot à mot
        $chaineTexte = mb_split("([[:space:]]|[\(\)\[\]\{\},;:!?<>])+", strip_tags(trim($this->texte)));
        
        // Nettoyage de la chaine (optionnel)
        foreach($chaineTexte as $cle => $motUnique) {
            $cleanWord = trim($motUnique);
            $motUnique = preg_replace("#(\\n|\\r|&nbsp;)#i", " ", $motUnique);
            
            // Tableau nettoyé reconstitué
            $chaineTab[] = $motUnique;
        }
        
        // Récupère la clé du mot recherché
        $cleMot = array_keys($chaineTab, $this->mot);
        
        // On compte le nombre d'occurrences pour assurer de générer un snippet autour du mot cherché
        $nbOcc = count($cleMot, 1);

        if($this->numOccurrence > $nbOcc) {
            $this->numOccurrence = $nbOcc - $this->randOcc; // On prend l'avant-dernière occurrence par exemple
        }
        
        // Paramètres par défaut
        $debutIncomplet = false;
        $finIncomplete = false;
        $chaineCreee = "";
        $chaine = "";

        // Extraction des mots suivants
        if(isset($cleMot[$this->numOccurrence])) {
            foreach($chaineTab as $cle => $valeur) {
                if($cle == $cleMot[$this->numOccurrence]) {
                    // "n" Mots précédents
                    for($i = $this->limite; $i >= 1; $i--) {
                        $posMotPrecedent = $cleMot[$this->numOccurrence]-$i;
                        if(isset($chaineTab[$posMotPrecedent])) {
                            $chaineCreee.= " ".$chaineTab[$posMotPrecedent];
                        } else {
                            $debutIncomplet = true;
                        }
                    }

                    // Mot recherché (affiché en gras ou non)
                    if($this->getStrong() == true) {
                        $chaineCreee.= " <strong>".$chaineTab[$cleMot[$this->numOccurrence]]."</strong>";
                    } else {
                        $chaineCreee.= " ".$chaineTab[$cleMot[$this->numOccurrence]];
                    }
                    
                    // "n" Mots suivants
                    for($i = 1; $i <= $this->limite; $i++) {
                        $posMotSuivant = $cleMot[$this->numOccurrence]+$i;
                        if(isset($chaineTab[$posMotSuivant])) {
                            $chaineCreee.= " ".$chaineTab[$posMotSuivant];
                        } else {
                            $finIncomplete = true;
                        }
                    }
                }
            }

            // Ajout des caractères de début (... par exemple) si le découpage n'atteint pas le début du texte.
            if($debutIncomplet == false) {
                $chaine.= $this->chainePrec;
            }
            $chaine.= $chaineCreee; // Ajout de la chaîne reconstituée
            // Ajout des caractères de fin (... par exemple) si la phrase n'est pas au bout.
            if($finIncomplete == false) {
                $chaine.= $this->chaineSuiv;
            }
        }

        // Retourne le résultat
        $this->chaine = $chaine;
        return $chaine;
    }
    
    // Affiche le texte
    public function afficherTexte($texte = '', $mot = '') {
        // Récupération des variables
        if(!empty($texte)) {
            $this->texte = $texte;
        }
        if(!empty($mot)) {
            $this->mot = $mot;
        }
        echo $this->retournerTexte($this->texte, $this->mot);
    }
    
    // Accesseurs (getters)
    public function getTexte() {
        return $this->texte;
    }
    public function getMot() {
        return $this->mot;
    }
    public function getLimite() {
        return $this->limite;
    }
    public function getNumOccurrence() {
        return $this->numOccurrence;
    }
    public function getRandOcc() {
        return $this->randOcc;
    }
    public function getStrong() {
        return $this->strong;
    }
    public function getChainePrev() {
        return $this->chainePrev;
    }
    public function getChaineSuiv() {
        return $this->chaineSuiv;
    }
    
    // Mutateurs (setters)
    public function setTexte($texte) {
        $this->texte = $texte;
    }
    public function setMot($mot, $i = '') {
        // Si $mot est un tableau
        if(is_array($mot) && empty($i)) {
            // On prend un des mots au hasard dans le tableau
            $indice = rand(0, count($mot));
            $mot = $mot[$indice];
        } else if(is_array($mot) && !empty($i)) {
            $this->mot = $mot[$i];
        } else {
            $this->mot = $mot;
        }
    }
    public function setLimite($limite) {
        if(is_numeric($limite)) {
            $this->limite = $limite;
        } else {
            $this->limite = 15; // Valeur par défaut si $limite n'est pas numérique
        }
    }
    public function setNumOccurrence($numOccurrence) {
        if(is_numeric($numOccurrence)) {
            $this->numOccurrence = $numOccurrence;
        } else {
            $this->numOccurrence = 15; // Valeur par défaut si $numOccurrence n'est pas numérique
        }
    }
    public function setRandOcc($randOcc) {
        if(is_numeric($randOcc)) {
            $this->randOcc = $randOcc;
        } else {
            $this->randOcc = 1; // Valeur par défaut si $randOcc n'est pas numérique
        }
    }
    public function setStrong($bool) {
        if(is_bool($bool)) {
            $this->strong = $bool;
        }
    }
    public function setChainePrec($chainePrec) {
        if(is_string($chainePrec)) {
            $this->chainePrec = $chainePrec;
        } else {
            $this->chainePrec = "(...)"; // Défaut si ce n'est pas une "string"
        }
    }
    public function setChaineSuiv($chaineSuiv) {
        if(is_string($chaineSuiv)) {
            $this->chaineSuiv = $chaineSuiv;
        } else {
            $this->chaineSuiv = "..."; // Défaut si ce n'est pas une "string"
        }
    }
}
?>

Variante de création d'extrait de texte à la volée

Il existe une variante pour faire le même type de création d'extrait de texte à la volée autour d'un mot précis. Souvent, les développeurs conseillent de chercher la position exacte du mot (avec strpos ou l'inverse strrpos), d'en récupérer la longueur (avec strlen), puis de boucler autour de cette position pour afficher "n" lettres avant ou après.

J'aime moins cette méthode qui me semble plus lourde et plus complexe pour couper précisément des mots, bien qu'il y ait des moyens d'arriver à nos fins. Il faudrait notamment remonter de "n" caractères avant le mot recherché pour trouver un espace ou idem après ce mot. Toutefois, nous n'aurions jamais la précision et le contrôle sur le nombre de mots réels à afficher (sans parler des calculs parfois lourds pour trouver les positions, etc.)

Conclusion sur la génération de snippets en PHP autour d'un mot précis

Voilà, j'espère que cette fonction pourra rendre service à certains d'entre vous, voire donner des idées d'améliorations.

Je vous glisse une piste par exemple. La fonction découpe les mots à chaque espace, mais il arrive que des "mots" n'en soit pas réellement, il faudrait donc filtrer ces "faux mots" tant que possible. Certes, cela pourrait être un travail de titan mais voici quelques exemples simples d'améliorations possibles :

  • Si on écrit une phrase avec un trait d'union encadré d'espace, ce tiret est considéré comme un mot, il faudrait donc supprimer les "mots" de ce type lors du découpage du texte en tableau de termes. Beaucoup d'exemples sont similaires comme l'esperluette, le signe euro, le pourcentage (...) qui sont souvent espacés des textes. Il faut également supprimer la ponctuation espacée comme les points d'exclamation, d'interrogation, etc. Tout cela permettrait d'avoir un nombre de mots plus réaliste et précis autour du terme ciblé.
  • Si on utilise des encodages variés (fortement déconseillé mais ça arrive...), il pourrait être intéressant de faire un test de détection d'encodage (avec mb_detect_encoding ou mieux) pour adapter l'affichage final.

13 commentaires

  • Zorinho dit :

    Bonjour,

    N'étant pas du tout un technicien, j'ai tenté de mettre le fichier dans mon espace FTP pour y accéder par URL mais la page reste en blanc.

    Comment faut-il faire pour pouvoir tester ce fichier qui à l'ar très intéressant ?

    Cordialement

    • Bonjour,
      C'est normal que ça vous fasse ça, le fichier n'étant que la fonction générique. Il faut donc l'inclure dans le fichier sur lequel vous souhaitez générer votre extrait de texte (avec ). Une fois le fichier "chargé", vous pouvez utiliser la fonction en respectant les paramètres, par exemple en écrivant ceci (n'importe où après l'inclusion) :

      Voilà ! :D

  • Zorinho dit :

    Merci beaucoup pour les infos complémentaires, j'ai fait des tests et ça fonctionne bien, manque plus que le plugin wordpress !

    En revanche, c'est dommage lorsque le texte contient plusieurs occurence de ne pas avoir des extraits correspondants à chaque occurence pour voir quel extrait serait le plus pertinent.

    +1

  • Xavier dit :

    Bonjour,
    D'abord merci pour les ressources que vous dispensez sur votre site (notamment le moteur de recherche !), mais j'ai une question concernant cette fonction. Il me semble qu'il y a un souci si il n'y a pas "n" mots suivants ou précédents. Par exemple si l'occurrence recherchée est en début ou fin de texte. Comment pourrais-je modifier la fonction pour vérifier qu'elle s'arrête si elle arrive en début ou fin de texte ? Merci pour vos conseils.

    • Bonjour,
      Merci pour votre remarque, je n'avais pas pensé à ça quand j'ai codé ça à la va-vite. Téléchargez la nouvelle version (1.1), c'est corrigé et j'ai ajouté une option pour mettre le mot recherché en gras. De même, si c'est le découpage arrive en début ou fin de texte, les chaînes complémentaires de s'affichent pas (par exemple, inutile d'afficher "..." à la fin si c'était bien la fin du texte normal). :D

      • Xavier dit :

        Merci pour cette réactivité ! Toutefois, je dois m'y prendre comme un manche, j'ai toujours des erreurs.
        Exemple :
        $contenu = 'un test de chaine assez court mais pas trop quand même un test de chaine court mais pas trop quand même un test de chaine court mais pas trop quand même un test de chaine court mais pas trop quand même';
        nb : une fois "assez", au début.
        $extrait->afficherTexte($contenu, 'assez');
        résultat : Notice: Undefined offset: 1
        Un avis ?

  • Xavier dit :

    Désolé pour le spam, mais après quelques tests, je m'excuse, votre script ne semble pas être en cause. Mon souci vient probablement du fait que je soumets à votre script une chaine qui ne contient pas systématiquement le mot recherché (ce script est utilisé dans un système de recherche, le mot cherché peut être dans le titre ou dans le contenu). Il va falloir que je me penche plus sérieusement dessus.

    • J'ai résolu ce souci. En effet, je n'avais pas d'erreur, sauf si le mot n'existait pas. Désormais, ça retourne un texte "vide" si le mot recherché n'est pas dans le texte. Vous pouvez donc téléchargez à nouveau la version 1.1, c'est corrigé. :D

  • Alice dit :

    Bonjour,

    Honte à moi, l'article date de 2015 et je ne le vois que maintenant.

    Merci de partager cela, je pense que ça va bien m'aider pour un projet de génération semi automatique de texte.

    Je commence à peine à apprendre le PHP et je me demandais si cette fonction peut s'adapter à une utilisation plus "poussée". Admettons que je souhaite générer un texte autour de cinq variables (ma requête principale, mes deux requêtes secondaires et mes deux ancres), et qu'en fonction de mes requêtes je puisse choisir un type de texte (= secteur d'activité), ai-je besoin de créer une base de données (regroupant mes textes en fonction de différents secteurs d'activité) ?

    Merci d'avance,
    Alice

    • Bonjour,
      Vous serez obligée d'avoir une base de données ici sinon vous aurez trop de calculs à gérer à chaque recherche. Ce n'est pas tant que vous ne pourriez pas faire sans, mais déjà, le programme que je donne ici impose quelques calculs qui prennent du temps quand plusieurs résultats sont générés. Si dans votre cas vous voulez l'appliquer selon 5 variables (une seule pouvant être prise en compte par extrait en revanche), ce sera donc encore pire, donc l'idéal serait d'enregistrer les infos dans une base de données pour chaque requête, afin de ne plus avoir à les reproduire à chaque recherche.
      Après, pour gagner du temps, quand une requête sera effectuée, il suffira de vérifier dans la base si les "cases" sont déjà remplies (donc il y a déjà eu une génération d'extrait) ou non, afin d'éviter d'écraser ce qui est déjà fait.

  • Bruno dit :

    Bonjour,
    peut-être un peux tard, mais comment rendre la recherche insensible au majuscule ?
    Merci

    • Bonjour,
      Comment ça ? Ce code ne fait que produite un extrait autour d'un mot recherché, quelque que soit la casse en effet. Si vous voulez effectuer une recherche uniquement en minuscule, il faut au préalable traiter le texte comme ça (soit en passant tout en minuscule, soit en faisant une recherche, uniquement en minuscule, sensible à la casse). Il s'agit plus d'un problème de recherche ici, mais il y a des solutions en tout cas ! ;-)

      • Bruno dit :

        Pour information, pour que mon code fonctionne j'ai du corriger la partie suivante :

        // Nettoyage de la chaine (optionnel)
        foreach($chaineTexte as $cle => $motUnique) {
        $cleanWord = trim($motUnique);
        $motUnique = preg_replace("#(\\n|\\r| )#i", " ", $motUnique);
        // Tableau nettoyé reconstitué
        $chaineTab[] = $motUnique;
        $chaineLower[] = strtolower($motUnique);
        }
        // Récupère la clé du mot recherché
        $cleMot = array_keys($chaineLower, $mot); // Toutes les occurrences

        Merci pour ton code.

  • Déposer un commentaire

    Répondre à Zorinho Annuler la réponse

    L'adresse de messagerie ne sera pas publiée.* Champs obligatoires