Jump to content

REGEXP : remplacement sélectif


Recommended Posts

Bonjour à tous,

J'espère trouver ici quelques pros pouvant m'aiguiller sur la rédaction d'une expression régulière en PHP somme toute assez simple.

Voilà j'ai donc une chaine de HTML en entrée, dont le texte est exclusivement structuré par des "P", des "UL/OL" des "H1,H2,H3" et des "A". Certaines balises sont assorties d'une classe CSS, et les liens contiennent bien entendu leur href.

J'ai à disposition une liste de mots clés que je souhaiterais détecter dans cette chaine HTML (toutes les occurrences) et convertir le mot en question en lien avec les restrictions suivantes :

  • Ne pas convertir les mots en lien dans les balises H
  • Ne pas convertir les mots déjà en lien, et exclure le remplacement dans les attributs du liens existant (sinon ça fout le HTML en l'air)
  • Ne pas convertir une portion de mot (pas de lien sur "aero" dans le mot "aeronautique" par exemple), seulement un mot entier (défini par (espace)mot(espace) ou tout autre ponctuation (point, point virgule,guillemets collés ou pas, etc...)

Ce qui revient à dire que je ne souhaite le remplacement que dans les paragraphes et les autres balises de formatage, hors titres et liens existants.

Je cherche donc un motif polyvalent pour ce traitement.

Je tacherai de développer le motif si mon HTML devait se complexifier avec des scripts ou d'autres balises.

Ci dessous un cas pratique, qui rend compte du dispositif actuellement en place (traitement du tableau avant preg)

<?php
//
$HTML = '<h1>le gros titre</h1>
<h2>le moyen titre</h2>
<p>un paragraphe contenant des gros mots et aussi un lien <a href="protection.html">protection</a> qui ne doit pas etre modifié</p>
<p>mais ce mot protection doit être converti</p>';
//
function make_pattern_word($n) {
return '//'; // ICI la REGEX
}
$mots = array('gros','mots','protection', 'doit');
$mots_def = array_map("make_pattern_word", $mots);
echo preg_replace($mots_def,'\\1<a href="?word=\\2">\\2</a>\\3',$HTML);
//
?>

Merci d'avance pour votre aide et votre temps !!!

Link to post
Share on other sites

simple?

<?php

// la fonction
function ajouter_liens($html,$mots=array()) {
// les éléments html à exclure
static $balises_exclues=array('head','script','h[1-6]','a');
// format du remplacement
static $remplacement='<a href="?word=%s">%s</a>';
// le traitement regex
if(!is_array($html)) {
if(!is_string($html)||!is_array($mots)||count($mots)==0) return $html;
$balises=implode('|',$balises_exclues);
$mots=implode('|',array_map('preg_quote',$mots));
$regex='#<('.$balises.')[\s>].+?</\1>|<[^>]+>|\b('.$mots.')\b#si';
return preg_replace_callback($regex,__FUNCTION__,$html);
}
if(isset($html[2])) return sprintf($remplacement,rawurlencode($html[0]),$html[0]);
return $html[0];
} // ajouter_liens

// tester la fonction
$HTML = '<h1>le gros titre</h1>
<h2>le moyen titre</h2>
<p>un paragraphe contenant des gros mots et aussi un lien <a href="protection.html">protection</a> qui ne doit pas etre modifié</p>
<p>mais ce mot protection doit être converti</p>';

$nouveau_html=ajouter_liens($HTML,array('gros','mots','protection','doit'));

echo '<pre>',htmlentities($nouveau_html),'</pre>';

?>

Link to post
Share on other sites

Merci tisha_carpenter,

Je découvre à l'instant ta solution. J'aurais pas cru qu'elle serait si compliquée. En tout cas elle est jolie.

Je la teste tout de suite dans différentes configurations.

Merci par avance !

****

ça marche merveilleusement bien. Dommage que je pige toujours rien aux REGEX et au sprintf. Je vais me plonger assidument dans ton code !

Merci encore ! :clap:

Edited by Commmint
Link to post
Share on other sites
  • 2 months later...

Merci tisha_carpenter pour ce code,

Mais j'ai juste un problème pour pouvoir l'appliquer à un de mes sites.

Le problème est que chacun de mes mots est attaché à un id et qu'il faut que l'id se trouve dans l'url

Christophe

Link to post
Share on other sites
Le problème est que chacun de mes mots est attaché à un id et qu'il faut que l'id se trouve dans l'url

Même si je ne suis pas certaine de la forme de ton array, j'ai ajouté le paramètre $cle_url qui, à true, fait la transposition sur un array de la forme id=>mot. En espérant que c'est la forme de ton array. Désolée, je ne peux pas éditer le message#2 :(

CODE
<?php

// La fonction *** ajouter_liens ***

// *********************************

// 1) $cle_url à false rend le mot en minuscule (url encodé) dans l'url

// 2) $cle_url à true rend la clé du mot en minuscule (url encodé) dans l'url

// Le array $mots peut avoir une relation id=>mot textuelle

// $cle_url est à false par défaut

function ajouter_liens($html,$mots=array(),$cle_url=false) {

// Les éléments html à exclure

static $balises_exclues=array('head','script','h[1-6]','a');

// Format du remplacement

static $remplacement='<a href="?word=%s">%s</a>';

// Pour le traitement interne id=>mot

static $mots_id=array();

// Le traitement regex, appel d'origine

if(!is_array($html)) {

if(!is_string($html)||!is_array($mots)||count($mots)==0) return $html;

$mots_id=array(); // S'assurer que la liste statique est vidée

if($cle_url) { // Générer $mots_id si id=>mot est désiré

$mots_id=array_map('strtolower',$mots);

}

$balises=implode('|',$balises_exclues);

$mots=implode('|',array_map('preg_quote',$mots));

$regex='#<('.$balises.')[\s>].+?</\1>|<[^>]+>|\b('.$mots.')\b#si';

return preg_replace_callback($regex,__FUNCTION__,$html); // Appel récursif

}

// Traitement php de l'appel récursif sur preg_replace_callback

if(isset($html[2])) { // un mot a été trouvé pour remplacement

$mot_texte=$html[0];

$mot_url=strtolower($mot_texte);

if(count($mots_id)) { // S'il y a une relation id=>mot alors transposer

$mot_url=array_search($mot_url,$mots_id,true);

if(!$mot_url) return $html[0]; // Couvrir un échec (peu probable)

$mot_url=strtolower($mot_url);

}

return sprintf($remplacement,rawurlencode($mot_url),$mot_texte);

}

return $html[0]; // Cas où aucun remplacement

} // ajouter_liens

// Tester la fonction

$ancien_html = '<h1>le gros titre</h1>

<h2>le moyen titre</h2>

<p>un paragraphe contenant des gros mots et aussi un lien <a href="protection.html">protection</a> qui ne doit pas être modifié</p>

<p>mais ce mot protection doit être converti</p>';

// Test avec valeur par défaut $cle_url=false

echo '<hr><b>$cle_url=false</b> Valeur par défaut<br>';

$liste_mots=array('gros','mots','protection','doit');

$nouveau_html=ajouter_liens($ancien_html,$liste_mots);

echo '<pre>',htmlentities($nouveau_html),'</pre>';

// Test avec valeur $cle_url=true

echo '<hr><b>$cle_url=true</b> clé=>valeur Transposé<br>';

$liste_mots=array(121=>'gros',617=>'mots',22=>'protection','devoir'=>'doit');

$nouveau_html=ajouter_liens($ancien_html,$liste_mots,true);

echo '<pre>',htmlentities($nouveau_html),'</pre>';

?>

Par contre dû à l'id, je me demande si tu as une grande quatité de mots pouvant originer d'une base de données ou de RSS/XML?

Si c'est le cas, la fonction suivante t'inspirera peut-être, elle construit les mots uniques possibles d'un document HTML, son résultat peut-être utilisé directement dans la clause IN d'une requête SQL sur une base en ISO (latin1), car tout ce qui est posté ici est en ISO.

CODE
<?php

// La fonction

function extraire_mots($html) {

$balises=implode('|',array('head','script','h[1-6]','a')); // les exclusions

$regex='#<('.$balises.')[\s>].+?</\1>|</?[^>]+>|[^\w<]+#si';

$mots=preg_split($regex,$html,-1,PREG_SPLIT_NO_EMPTY);

return '\''.implode('\',\'',array_unique(array_map('strtolower',$mots))).'\'';

}

// Tester la fonction

$html = '<h1>le gros titre</h1>

<h2>le moyen titre</h2>

<p>Un paragraphe contenant des gros mots et aussi un lien <a href="protection.html">protection</a> qui ne doit pas être modifié</p>

<p>mais ce mot protection doit être converti</p>';

$mots=extraire_mots($html);

$query="SELECT id_mot,mot FROM mots WHERE mot IN ($mots);";

echo "<pre><b>Exemple de query:</b>\n$query</pre>";

?>

Si utilisée, alors une classe serait la bienvenue dans ton cas pour au moins uniformiser la définition des balises exclues.

Tisha

Link to post
Share on other sites

Merci beaucoup tisha_carpenter pour cette réponse,

J'ai cependant encore différents problèmes.

Je ne comprend pas pourquoi avec la fonction suivante, les accents des mots sont considérés comme inexistants.

// La fonction
function extraire_mots($html) {
$balises=implode('|',array('head','script','h[1-6]','a')); // les exclusions
$regex='#<('.$balises.')[\s>].+?</\1>|</?[^>]+>|[^\w<]+#si';
$mots=preg_split($regex,$html,-1,PREG_SPLIT_NO_EMPTY);
return '\''.implode('\',\'',array_unique(array_map('strtolower',$mots))).'\'';
}

De plus, j'ai dans ma base de donnée des mots avec des espaces genre "webmaster hub" ou encore des mots aves des tirets genre "tisha-carpenter" et parfois avec des apostrophes "l'apéro"tout ces mots ne ressortent pas avec la requête

WHERE mot IN ('webmaster','hub','un webmaster hub','des','gros','mots','et','aussi','lien','qui','ne','doit','pas','tre','modifi','mais','ce','mot','protection','converti').

je n'arrive pas non plus à utiliser la fonction ajouter_liens à mon cas.

Pour le moment mon code est

function parser($text) {
$sortie = array();
$mot = array();
$sortie = array();
$sqlquery=$xoopsDB->query("SELECT id, name from mabase ORDER LENGTH(name) DESC"); //la requête

while ($sqlfetch=$xoopsDB->fetchArray($sqlquery)) {
$id = $sqlfetch['id'];
$name = $myts->sanitizeForDisplay($sqlfetch['name']);

if (stristr($text,$name) ) { // si le mot se trouve dans le texte
if (stristr($name,' ') ) {// si le mot est un mot composé

$topictexturl = urlencode($name);
$nameb = str_replace(" ","-", $name); //j'ai été obligé de supprimer les espaces des mots composés car j'ai aussi bien des définitions pour "webmaster" que pour "webmaster hub" (c'est un exemple)

$mot[] = ('`\b(('.$name.')s?)\b`si');
$sortie[] = '<a href="id='.$id.'&mot='.$topictexturl.'">'.$nameb.'</a>';
} else {// si le mot n'est pas un mot composé

$topictexturl = urlencode($name);
$mot[] = ('`[^->abcdeéèfghijklmnopqrstuvwxyz](('.$name.')s?)\b`si');
$sortie[] = '<a href="id='.$id.'&mot='.$topictexturl.'">$1</a>';
}

}
}

$text = preg_replace($mot,$sortie,$text,2);
return $text;
}

Pour moi mon code ne fonctionne pas bien du tout car

* je suis obligé de tester tous les mots de ma base de donnée (plus de 2000) ça fait mal à ma base à chaque fois.

* obligé de rajouter des tirets pour tous les mots composés

* je ne sais pas évité de faire des changement dans les balises (un point très important)

* et surement encore d'autres problème que je ne vois pas.

Merci d'avance

Christophe

Link to post
Share on other sites
Je ne comprend pas pourquoi avec la fonction suivante, les accents des mots sont considérés comme inexistants.

Tu es dans un environnement configuré en autre chose que ISO-8859-(1 ou 15). PHP récupère les facilités du système d'exploitation sous le nom de locale et la fonction PHP setlocale() peut changer cette configuration le temps de l'exécution du script si la "locale" voulue est accessible par le système d'exploitation (et au moins PHP 4.3). Toutes les fonctions string de PHP sont soumises à cette configuration. Il y a toujours moyen de contourner ou de s'adapter si un changement de configuration est impossible et que setlocale() ne donne aucun résultat. Mais à la base, tu es en présence d'un problème de configuration. La ligne suivante devrait te dire dans quel encodage ton système est:

<?php
echo setlocale(LC_CTYPE,null);
?>

De plus, j'ai dans ma base de donnée des mots avec des espaces genre "webmaster hub" ou encore des mots aves des tirets genre "tisha-carpenter" et parfois avec des apostrophes "l'apéro"tout ces mots ne ressortent pas avec la requête

Ce que j'avais posté ici concernait le mot et non pas la phrase. La classe \b est à proscrire car \b est très très gourmant et refusera tout caractère non-mot à l'intérieur de l'énumération, \b doit être utilisé uniquement pour délimiter un mot car il donne un gain en temps d'exécution (cf.: test rapidement l'échec). Par contre dans la fonction ajouter_liens, la ligne:

$regex='#<('.$balises.')[\s>].+?</\1>|<[^>]+>|\b('.$mots.')\b#si';

peut-être remplacer par:

$regex='#<('.$balises.')[\s>].+?</\1>|<[^>]+>|(?<=\W)('.$mots.')(?=\W)#si';

Et ça devrait fonctionner aussi pour la phrase comme pour le mot sur un tri par longueur comme dans ton code.

Surtout pour array('la très belle'=>'tisha carpenter'); :)

* je suis obligé de tester tous les mots de ma base de donnée (plus de 2000) ça fait mal à ma base à chaque fois.

Je vois pas comment soulager ta base, sauf par une mise en cache de la requête. Car il devient impossible de savoir si un espace doit être conservé ou éliminé, à moins de faire aussi toutes les combinaisons de 2 mots par proximité, ce qui serait un tout autre genre de bestiaux.

* obligé de rajouter des tirets pour tous les mots composés

Ne devrait plus être nécessaire après la correction regex proposée un peu plus haut. Dans un bon encodage de caractère, bien entendu. Ta base est encodé en quoi? Tes documents HTML sont encodés en quoi?

* je ne sais pas évité de faire des changement dans les balises (un point très important)

OK. Je vais essayer de faire ma prof :) aucune garantie de satisfaction.

  • Le méta-caractère | permet d'énumérer des possibilités sous la forme de OU logique. L'ordre d'énumération est important puisqu'il doit aller du plus important au moins important.
  • J'utilise ce | pour énumérer 3 hypothèses
    1. Hypothèse des balises exclues <('.$balises.')[\s>].+?</\1>
      Revient à isoler de la balise ouvrante à la balise fermante inclusivement selon l'énumération contenu dans $balises. Par exemple, le HTML "<h1>une texte de titre</h1>" sera complètement capturer en un bloc parce que h1 fait partie de $balises.
    2. Hypothèse des attributs des autres balises <[^>]+>
      Permet d'isoler tous les attributs des autres balises car le mot pourrait être présent dans ces attributs.
    3. Hypothèse des phrases/mots recherchés (?<=\W)('.$mots.')(?=\W)
      Isolera une phase/mot de l'énumération contenu dans $mots, qui est précédé et suivi par un caractère \W, donc précédé ou suivi par un caractère non-mot tel que défini par locale (ref.: setlocale).

Voilà donc pourquoi $regex='#<('.$balises.')[\s>].+?</\1>|<[^>]+>|(?<=\W)('.$mots.')(?=\W)#si';

Le preg_replace_callback($regex,__FUNCTION__,$html); permet de valider quelle partie de la chaine est isolée, si l'expression régulière est satisfaite avant d'arriver à la seconde parenthèse qui énumère les phases/mots alors la capture numéro 2 sera indéfini... donc aucun changement. Par contre, si la capture de la seconde parenthèse est définie, alors un mot/phrase à remplacer a été trouvé. Le code PHP exécuter par ce callback est:

  // Traitement php de l'appel récursif sur preg_replace_callback
if(isset($html[2])) { // un mot a été trouvé pour remplacement
$mot_texte=$html[0];
$mot_url=strtolower($mot_texte);
if(count($mots_id)) { // S'il y a une relation id=>mot alors transposer
$mot_url=array_search($mot_url,$mots_id,true);
if(!$mot_url) return $html[0]; // Couvrir un échec (peu probable)
$mot_url=strtolower($mot_url);
}
return sprintf($remplacement,rawurlencode($mot_url),$mot_texte);
}
return $html[0]; // Cas où aucun remplacement

Qui est la logique expliquée plus haut :) Le tout en parcourant une seule fois le document HTML.

En espérant que mon message t'aidera à progresser vers ton objectif.

Tisha

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...