Aller au contenu

Manque REGEXP pour preg_replace


FrenchKiss

Sujets conseillés

Hello à tous

J'utilise sur mon site une fonction très sympa, qui cherche les mots contenus dans un array dans un texte donné, et qui les remplace par un lien vers leur définition.

En gros, dans le texte, MOT devient <a href="lexique/mot">MOT</a>.

Et ça fonctionne bien.

C'est juste que la fonction que j'utilise est pas super au point niveau regexp.

$pattern = "#(?!<.*?)(%s)(?![^<>]*?>)#"; 
$pattern .= 'i';

$needle = (array) $needle;
foreach ($needle as $needle_s) {
$needle_s = preg_quote($needle_s);

// Escape needle with optional whole word check
$needle_s = '\b' . $needle_s . '\b';


$regex = sprintf($pattern, $needle_s);
$text = preg_replace($regex, $highlight, $text,1);

Bon alors voila.

Là, si je cherche à "highlighter" le mot AI, ça va donner ça :

"J'aime les bananes, j'ai toujours aimé ça"

-> le "j'aime" est ignoré (tant mieux), mais pas le "j'ai" ... et ça c'est embêtant.

Je veux qu'il ne traite que les mots isolés genre

"J'ai écrit la définition de ce qu'est un AI, vous devriez la lire"

Je voudrais que le masque de ma regexp ( $pattern ) :

- traite les mots n'étant pas compris entre < et > (ne pas traiter les mots dans les balises HTML)

- traite les mots précédés de " " ou "'" ou " ou " ou "(" et pas les autres,

- et suivis de " " ou "'" ou " ou " ou ")"

En tatônnant un peu, je pensais pouvoir y'arriver MAIS

la structure de ce masque m'est totalement hermétique, je ne comprends rien à ce qu'elle veut dire !!

$pattern = "#(?!<.*?)(%s)(?![^<>]*?>)#";

- J'ai pigé que les # étaient les délimiteurs de la regexp (j'ai aussi compris que n'importe quel caractère pouvait être employé à la place du #)

- J'ai pigé que le (%s), c'est le $neddle qui est placé là par le sprintif dans

 $regex = sprintf($pattern, $needle_s);

Par contre (?!<.*?) et (?![^<>]*?>) ??? :wacko:

Je suspecte vaguement (?! et ?) d'être des délimiteurs de classe, mais ça ne m'aide pas beaucoup plus, cette syntaxe demeure obscure à mes yeux de profane.

Si quelqu'un pouvait m'aider à réécrire ce $pattern pour qu'il traite les mots dans les conditions que j'ai spécifiées, je lui en serais grandement reconnaissant.

Lien vers le commentaire
Partager sur d’autres sites

Bonjour,

Oui, cela peut sembler être du charabia...mais tout s'explique ;)

Dans (?!<.*?)

( et ) délimitent le sous-masque, comme d'habitude.

?! est une assertion négative (d'où le !) qui travaille en-deçà de la position courante.

< correspond à un chevron ouvrant (tel qu'on en utilise pour un code HTML)

.*? signifie, n'importe quel caractère zéro, une ou plusieurs fois. Et le point d'interrogation suivant le * signifie inverser le mode courant de PCRE pour l'option PCRE_UNGREEDY (si tu as besoin d'aide sur ce point, ce sujet éplinglé devrait d'aider)

Le raisonnement pour (?![^<>]*?>) est identique, simplement ce sous-masque prend tous les caractères qui ne sont pas un chevron, jusqu'au prochain chevron fermant.

Concernant ton exemple avec "AI", il vient du fait que l'assertion simple \b (qui signifie word boundary, soit délimiteur de mot) considère que l'apostrophe est un délimiteur de motet donc que "ai" dans l'expression "j'ai" est un mot, il le remplace donc pas ce que tu désires...

Si tu veux changer ce comportement, je ne vois pas d'autres solution que de redéfinir une classe pour limiter tes mots et remplacer cette assertion simple \b par cette classe... le seul inconvénient c'est que pour être sur que l'apostrophe est utilisée comme dans ton exemple il faut encore vérifier le caractère la précédent...si c'est un espace, il se peut qu'en fait l'apostrophe ait été utilisée pour une 'citation' (ça arrive)...cela complique grandement la chose...

Et essayer de ne pas remplacer les expressions types (j'a*, j'e*, j'i*, j'o*, j'u, j'y, d'a, ...) va vite devenir ingérable: la liste est longue et variable... tu as par exemple le cas des "h aspirés"...lorsquils ne le sont pas il y aura une apostrophe...avoir une liste exhaustive des mots en h, cest te dire comme cest ingérable efficacement !

Bonne chance.

Lien vers le commentaire
Partager sur d’autres sites

Posté (modifié)

Hello, merci pour ta réponse.

?! est une assertion négative (d'où le !) qui travaille en-deçà de la position courante.

< correspond à un chevron ouvrant (tel qu'on en utilise pour un code HTML)

.*? signifie, n'importe quel caractère zéro, une ou plusieurs fois. Et le point d'interrogation suivant le * signifie inverser le mode courant de PCRE pour l'option PCRE_UNGREEDY (si tu as besoin d'aide sur ce point, ce sujet éplinglé devrait d'aider)

Le raisonnement pour (?![^<>]*?>) est identique, simplement ce sous-masque prend tous les caractères qui ne sont pas un chevron, jusqu'au prochain chevron fermant

Si je suis bien :

Le premier sous masque veut dire "non précédé de "<..." ...

Et le second veut dire non suivi de "...>"

Donc ce masque voudrait dire

"chopper tous les mots non compris entre <... et ...>" ce qui exclue

<a href="ai">

mais pas <strong>ai</strong> (dans ce cas là, il renvoie quoi ? "<strong>ai</strong>" ou JUSTE "ai" ?)

c'est bien ça ?

A ce moment là, comment puis-je implémenter le masque pour qu'il exclue les mots :

- PRECEDES ET / OU SUIVIS de "\w", ou "-" ou "'"

et / ou

- COMPRIS ENTRE "<..." et ...>" et / ou "html://|ftp://" et suivis de ".php|.html|.htm"

... ce qui me permettrait de restreindre la selection aux mots distincts (wholewords) non attributs html, ne faisant pas partie d'une URL ... et ainsi de régler mon problème.

Je crains ne pas avoir l'expérience suffisante pour réussir à écrire ce masque sans un petit coup de main :/

Modifié par FrenchKiss
Lien vers le commentaire
Partager sur d’autres sites

Bonsoir,

Je n'ai pas le temps de tester en profondeur l'expression régulière que je vais te proposer, mais les 2-3 cas que tu as cités semblent fonctionner, à toi de tester complètement.

<?php
/**
* Perform a simple text replace
* This should be used when the string does not contain HTML
* (off by default)
*/
define('STR_HIGHLIGHT_SIMPLE', 1);

/**
* Only match whole words in the string
* (off by default)
*/
define('STR_HIGHLIGHT_WHOLEWD', 2);

/**
* Case sensitive matching
* (off by default)
*/
define('STR_HIGHLIGHT_CASESENS', 4);

/**
* Overwrite links if matched
* This should be used when the replacement string is a link
* (off by default)
*/
define('STR_HIGHLIGHT_STRIPLINKS', 8);

/**
* Highlight a string in text without corrupting HTML tags
*
* @author Aidan Lister <aidan_AT_php.net>
* @version 3.1.1
* @link [url="http://aidanlister.com/repos/v/function.str_highlight.php"]http://aidanlister.com/repos/v/function.str_highlight.php[/url]
* @param string $text Haystack - The text to search
* @param array|string $needle Needle - The string to highlight
* @param bool $options Bitwise set of options
* @param array $highlight Replacement string
* @return Text with needle highlighted
*/
function str_highlight($text, $needle, $options = null, $highlight = null)
{
// Default highlighting
if ($highlight === null) {
$highlight = '<strong>\1</strong>';
}

// Select pattern to use
if ($options & STR_HIGHLIGHT_SIMPLE) {
$pattern = '#(%s)#';
$sl_pattern = '#(%s)#';
} else {
$pattern = '#(?!<.*?)(%s)(?![^<>]*?>)#';
$sl_pattern = '#<a\s(?:.*?)>(%s)</a>#';
}

// Case sensitivity
if (!($options & STR_HIGHLIGHT_CASESENS)) {
$pattern .= 'i';
$sl_pattern .= 'i';
}
$pattern .= 'm';
$sl_pattern .= 'm';


$needle = (array) $needle;
foreach ($needle as $needle_s) {
$needle_s = preg_quote($needle_s);

// Escape needle with optional whole word check
if ($options & STR_HIGHLIGHT_WHOLEWD) {
$needle_s = '([^\'-]|\s|^)(' . $needle_s . ')([^\'-]|\s|$)';
}

// Strip links
if ($options & STR_HIGHLIGHT_STRIPLINKS) {
$sl_regex = sprintf($sl_pattern, $needle_s);
$text = preg_replace($sl_regex, '\1', $text);
}

$regex = sprintf($pattern, $needle_s);
$text = preg_replace($regex, $highlight, $text);
}

return $text;
}

?>

Cette fonction est une version modifiée de celle-ci (je suppose que c'est de celle-là dont tu t'es inspiré, à en voir les noms de variables).

Je te laisse lire les options possible, le cas que tu veux traiter requière les options STR_HIGHLIGHT_WHOLEWD "et" STR_HIGHLIGHT_STRIPLINKS, donc l'appel devrait ressembler à quelque chose comme :

echo str_highlight($text,$needle,STR_HIGHLIGHT_WHOLEWD|STR_HIGHLIGHT_STRIPLINKS,'\2<a href="lexique/\3">\3</a>\4');

L'usage des références arrières \2 et \4 servent à replacer les espaces ou autres caractères...car en fait la méthode que j'ai utilisé est la suivante. Au lieu de "\b" pour définir les limites du mot, j'ai redéfini une classe qui correspond à tes besoins.

([^\'-]|\s|^) <- Tout ce qui n'est pas une apostrophe ou tiret ou un espace ou un début de chaîne (c'est la référence arrière \2)

([^\'-]|\s|$) <- Tout ce qui n'est pas une apostrophe ou tiret ou un espace ou une fin de chaîne (c'est la référence arrière \3)

Le début est fin de chaîne dans ces deux dernier sous-masques sont utilisable dans le contexte d'un $text multi-lignes grâce à l'option :

 		$pattern .= 'm';
$sl_pattern .= 'm';

m représente PCRE_MULTILINE et i représente PCRE_CASELESS (des options de masque)

J'espère que cela correspond à ce dont tu as besoin...tu peux toujours faire varier ces deux sous-classes.

Lien vers le commentaire
Partager sur d’autres sites

Posté (modifié)

Excellent !

Ta fonction ne marchait pas tout à fait (elle surlignait TOUTES les occurences du mot recherché, qu'il soit ou non dans un mot, précédé de ', ou autre), mais j'ai réussi en la modifiant un peu, à lui faire faire exactement ce que je veux :

define('STR_HIGHLIGHT_WHOLEWD', 2);
define('STR_HIGHLIGHT_CASESENS', 4);
define('STR_HIGHLIGHT_STRIPLINKS', 8);

function str_highlight($text, $needle, $options = null, $highlight = null)
{
// Default highlighting
$highlight = '\2<u>\3</u>\4';

// Select pattern to use
if ($options & STR_HIGHLIGHT_SIMPLE) {
$pattern = '#(%s)#';
$sl_pattern = '#(%s)#';
} else {
$pattern = '#(?!<.*?)(%s)(?![^<>]*?>)#';
$sl_pattern = '#<a\s(?:.*?)>(%s)</a>#';
}

// Case sensitivity
$pattern .= 'i';
$sl_pattern .= 'i';

$pattern .= 'm';
$sl_pattern .= 'm';


$needle = (array) $needle;
foreach ($needle as $needle_s) {
$needle_s = preg_quote($needle_s);

// Escape needle with optional whole word check
$needle_s = '([\.,"(]|\s)(' . $needle_s . ')([\.,")]|\s)';

// Strip links
if ($options & STR_HIGHLIGHT_STRIPLINKS) {
$sl_regex = sprintf($sl_pattern, $needle_s);
$text = preg_replace($sl_regex, '\1', $text);
}

$regex = sprintf($pattern, $needle_s);
$text = preg_replace($regex, $highlight, $text);
}

return $text;
}

A présent, ça fonctionne parfaitement.

Comme tu peux le voir, j'ai systématisé quelques options, étant donné que je sais dans quel cas j'utilise cette fonction - qui, à la base, était bien le str_highlight, bien vu :)

Un grand merci à toi, je n'aurais pas réussi sans ton aide, tout cela fait appel à des notions bien trop complexes pour mon niveau intermédiaire.

Si tu veux voir dans quelle utilisation je fais de cette fonction, fais le moi savoir et je te communiquerai l'url de mon site par MP.

Merci encore.

Modifié par FrenchKiss
Lien vers le commentaire
Partager sur d’autres sites

De rien.

Oui, effectivement il y avait une erreur dans mon expression rationnelle...je t'avoue (et je t'ai avoué) que je ne l'avais pas testée dans tous les sens ;)

Par contre tu auras un problème avec celle que tu as trouvée, si la chaîne que tu recherches est le premier mot ou le dernier mot d'une ligne il ne sera pas remplacé.

			$needle_s = '([\.,"(]|\s|^)(' . $needle_s . ')([\.,")]|\s|$)';

Cela devrait faire l'affaire... j'ai juste ajouté : "^" pour le sous-masque avant le mot et "$" pour celui après dans la liste d'alternatives. Sauf erreur on aurait pu faire de même avec \A et \Z (qui sont des assertions signifiant respectivement début de chaîne et fin de ligne et/ou de chaîne).

Lien vers le commentaire
Partager sur d’autres sites

Veuillez vous connecter pour commenter

Vous pourrez laisser un commentaire après vous êtes connecté.



Connectez-vous maintenant
×
×
  • Créer...