Page 1 sur 1

Detection + remplacement de mots, conseil d'optimisation

Posté : 31 mars 2009, 23:22
par supercanard
Bonsoir,

Je suis en train de créer une classe qui permet de détecter des mots dans une chaine pour les remplacer par un lien HTML.
Çà va me servir à faire un glossaire, et à pouvoir générer des liens automatique vers les définitions à la volée sur des textes.
L'idée va être de stocker dans un fichier texte les mots à détecter.
Ensuite j'ai un explode de ma chaine et une reconstruction de celle-ci avec comparaison des valeurs avec les mots de mon glossaire, avant d'y coller un lien, pour ensuite afficher le tout... ouf rien que ça !

Seulement je pense que je vais avoir un gros hic lorsque j'aurais des textes importants et un gros fichier de mots. Avec pas moins de 3 foreach au total dans ma classe, je pense qu'il y a du travail sur l'optimisation.

Si vous avez des idées :D :
<?php
class detecterMotGlossaire{
	public $inputString;
	public $outputString;
	private $listeMotGlossaire;
	private $motDetecte;
	public function __construct($inputString=null){
		$this->inputString = $inputString;
		$this->outputString = null;
		$this->listeMotGlossaire = array('article', 'un');
		$this->motDetecte = array();
	}
	public function generer($url){
		$temp = $this->detecter();
		return $this->output($temp, $url);
	}
	private function detecter(){
		$temp = explode(' ', $this->inputString);
		foreach($temp as $k=>$mot){
			foreach($this->listeMotGlossaire as $motGlosaire){
				if($mot == $motGlosaire){ // cas simple
					$this->motDetecte[$k] = $mot;
				}
				elseif(preg_match('#'.$motGlosaire.'(,|.|;|:){1}$#', $mot, $matches)){ // cas : mot,
					$this->motDetecte[$k] = str_replace($matches[1], null, $mot);
				}
				elseif(preg_match('#((.*){1}(\'))'.$motGlosaire.'#', $mot, $matches)){ // cas : mot,
					$this->motDetecte[$k] = str_replace($matches[1], null, $mot);
				}
			}
		}
		return $temp;
	}
	private function output($temp, $url){
		foreach($temp as $k=>$mot){
			if(array_key_exists($k, $this->motDetecte)){
				$finalString.= '<a href="'.$url.$this->convertWordForUrl($this->motDetecte[$k]).'">'.$mot.'</a>';
			}
			else{
				$finalString.= $mot;
			}
			$finalString.= ' ';
		}
		return $finalString;
	}
	private function convertWordForUrl($string)
	{
		$replace = array(
			' '=>'-',
			'ŕ'=>'a',
			'á'=>'a',
			'â'=>'a',
			'ă'=>'a',
			'ä'=>'a',
			'ĺ'=>'a',
			'ň'=>'o',
			'ó'=>'o',
			'ô'=>'o',
			'ő'=>'o',
			'ö'=>'o',
			'č'=>'e',
			'é'=>'e',
			'ę'=>'e',
			'ë'=>'e',
			'ě'=>'i',
			'í'=>'i',
			'î'=>'i',
			'ď'=>'i',
			'ů'=>'u',
			'ú'=>'u',
			'ű'=>'u',
			'ü'=>'u',
			'˙'=>'y',
			'ń'=>'n',
			'ç'=>'c',
			'ř'=>'0'
		);
		return strtr(strtolower($string), $replace); 
	}
}
$chaine = new detecterMotGlossaire("Dans cet article, nous allons voir comment configurer le host pour développer un site en local. Notre but est de transférer un site d'un serveur local sur un serveur distant (votre hébergeur par exemple) sans avoir ŕ...");
echo($chaine->generer('glossaire.php?mot='));
?>

Posté : 01 avr. 2009, 07:39
par julian
Normalement, en t'arrachant un peu les cheveux sur une expression régulière et un preg_replace, tu n'aurais même pas besoin de foreach... Sachant que tu as une liste de mot, tu as des outils pour écrire des motifs qui permettent d'avoir du multi-choix. Ensuite, il te suffit de remplacer par un lien.

Posté : 01 avr. 2009, 11:26
par supercanard
Oui ça c'est clair... mais je suis pas super fort en regex et pour tout dire j'avais envi de faire travailler mes méninges avec des manips sur les tableaux, des boucles, etc...

Déjà je sais qu'avec un condition dans un regex je pourrais rassembler les deux en un seul :
elseif(preg_match('#'.$motGlosaire.'(,|.|;|:){1}$#', $mot, $matches)){ // cas : mot,
                    $this->motDetecte[$k] = str_replace($matches[1], null, $mot);
                }
                elseif(preg_match('#((.*){1}(\'))'.$motGlosaire.'#', $mot, $matches)){ // cas : mot,
                    $this->motDetecte[$k] = str_replace($matches[1], null, $mot);
                } 
Mais je me pose quand même cette question.

Ce n'est pas plus rapide d'avoir :
Une condition ( qui va correspondre au 3/4 des cas ) => un "petit" regex
Une condition => un "petit" regex

Au lieu de :
Une seule condition => un gros regex

?

Posté : 07 avr. 2009, 14:21
par Topper
J'ai déjà fait quelque chose comme ça.

Moi j'ai fais un tableau dans lequel je place mes mots.
$tableau_lien_articles=array();
$index=0;
// Recupere la liste des articles
...
// Tant qu'il y a des resultats
while(...){
	// Actualise le tableau
	$tableau_lien_articles[$index]["taille"]=strlen($titre);
	$tableau_lien_articles[$index]["titre"]=strtolower($titre);
	$tableau_lien_articles[$index]["lien"]="<a href=\"".$lien.".html\" title=\"titre\">";
	// Incremente l'index
	$index++;
}
Maintenant t'as tous tes mots a contrôler avec leur taille et le lien qui va bien (du moins la balise ouvrante.

Ensuite tu tries ton tableau en fonction de la taille du mot. Cela permet de traiter en premier les mots plus long et que les plus petits n'entrent pas en conflit avec les premiers.
// Attribue les colonnes
foreach($tableau_lien_articles as $key => $row) {
	$taille[$key]=$row['taille'];
}
// Trie le tableau selon le nombre de caracteres
array_multisort($taille,SORT_DESC,$tableau_lien_articles);
Pour terminer, tu utilises une fonction qui va faire tout le boulot et retrouver chaque mot et y inclure le lien.
function ajout_liens_articles($chaine,$tableau_lien_articles){
	// Tant qu'il y a des articles
	for($index=0;$index<count($tableau_lien_articles);$index++){
		// Assigne les variables
		$offset=$dernier_ajout;
		// Mot cible
		$mot=$tableau_lien_articles[$index]["titre"];
		$mot_longueur=strlen($mot);
		// Balises
		$balise_debut=$tableau_lien_articles[$index]["lien"];
		$balise_fin="</a>";
		$balises_longueur=strlen($balise_debut.$balise_fin);
		// Recherche le mot
		$position=stripos(suppression_accents($chaine),$mot);
		// Si le mot est trouve
		if($position!==false){
			// Decompose le texte
			$chaine_precedente=substr($chaine,0,$position);
			$chaine_suivante=substr($chaine,($position+$mot_longueur));
			// Recherche un lien dans la chaine precedente
			$position_lien=stripos(suppression_accents($chaine_precedente),"<a");
			// Si aucun lien est trouve
			if($position_lien===false){
				// Integre les balises dans le texte
				$chaine=$chaine_precedente.$balise_debut.substr($chaine,$position,$mot_longueur).$balise_fin.$chaine_suivante;
			}
		}
	}
	return $chaine;
}
Je fais appel à une autre fonction pour supprimer les accents que voici (mais si tu en as un cela devrait faire l'affaire) :
// Fonction de suppression d'accents dans les chaines
function suppression_accents($chaine){
	// Supprime les accents
	$recherche=array('/[àâäåãáÂÄÀÅÃÁæÆ]/','/[ß]/','/[çÇ]/','/[Ð]/','/[éèêëÉÊËÈ]/','/[ïîìíÏÎÌÍ]/','/[ñÑ]/','/[öôóòõÓÔÖÒÕ]/','/[Šš]/','/[ùûüúÜÛÙÚ]/','/[¥ŸÝŸýÿ]/','/[Žž]/');
	$remplacement=array('a', 'b', 'c', 'd', 'e', 'i', 'n', 'o', 's', 'u', 'y', 'z');
	// Renvoie la chaine modifiee
	return preg_replace($recherche,$remplacement,$chaine);
}
Ce code tourne depuis au moins 3 mois et je n'ai pas décelé de gros problème dessus.

Il peut toutefois être amélioré de la manière suivante si quelqu'un veut mettre le nez dedans :

Pour la chaine suivante : "Les expressions régulières j'ai l'impression que c'est la pression et pressions"
Si mon mot recherché est : "pression"
La fonction va mettre un lien de la manière suivante : "Les ex<a ...>pression</a>s régulières j'ai l'im<a ...>pression</a> que c'est la <a ...>pression</a> et <a ...>pression</a>s"

Dans l'idéal cela devrai exclure les mots qui correspondent pas du tout et conserver quand même ceux au pluriel (avec un s) de la manière suivante :

"Les expressions régulières j'ai l'impression que c'est la <a ...>pression</a> et <a ...>pressions</a>"

En tous cas pour une telle mise à jour je suis preneur. :wink:

Posté : 07 avr. 2009, 14:49
par supercanard
Merci pour ces explications :D
Je vais regarder ton code dès que j'aurais un peu de temps.

Entre temps j'ai modifié le miens et j'ai essayé de faire une version avec des regex qui marche plus ou moins bien.

On voit sur ce lien deux test, le deuxième étant la version regex. Il y a des mots qui passent un peu à la trappe : http://supercanard.phpnet.org/test/dete ... v/test.php

De plus lorque j'ai des mots composés ex base de données, il y a conflit avec base par exemple
<?php
/*
regex : \.
casse : ignoré
*/
class detectionMot{

	public $inputString; // Chaine d'entrée
	private $dictionaire; // contient les mots du fichiers texte chargé
	private $motDetecte; // contient les mots présent à la fois dans $arrayOfInputString et $dictionaire
	private $arrayOfInputString; // contient les mots de la chaine $inputString
	
	public function __construct($file){
		$this->inputString = null;
		$this->dictionaire = $this->load($file);
		$this->motDetecte = array();
		$this->arrayOfInputString = array();
	}
	
	public function genererLien($url, $inputString){ // param : url des liens ( glossaire.php?mot= ), chaine à tester
		$this->inputString = $inputString;
		$this->detecter();
		foreach($this->arrayOfInputString as $k=>$mot){
			if(array_key_exists($k, $this->motDetecte)){
				$finalString.= '<a href="'.$url.$this->simplifierMot($this->motDetecte[$k]).'">'.$mot.'</a>';
			}
			else{
				$finalString.= $mot;
			}
			$finalString.= ' ';
		}
		return $finalString;
	}
	public function detecterSpam($tauxMax, $inputString){ // param : seuil max avant considération de la chaine comme spam, chaine à tester
		$this->inputString = $inputString;
		$this->detecter();
		if($this->calculerTauxPresence() > $tauxMax){
			return true;
		}
		else{
			return false;
		}
	}
	
	private function load($file){
		$temp = file_get_contents($file);
		return(explode("\n", $temp));
	}
	private function calculerTauxPresence(){
		// Calcul le pourcentage de mots trouvés dans $arrayOfInputString étant présents dans $dictionaire
		return round(count($this->motDetecte)*100/count($this->arrayOfInputString));
	}
	private function detecter(){
		// Construit $arrayOfInputString, cherche ses valeurs présentes dans $dictionaire, construit $motDetecte
		$this->arrayOfInputString = explode(' ', $this->inputString);
		foreach($this->arrayOfInputString as $k=>$mot){
			foreach($this->dictionaire as $motGlosaire){
				if(strtolower($mot) == strtolower($motGlosaire)){ // cas simple
					$this->motDetecte[$k] = $mot;
				}
				elseif(preg_match('#'.strtolower($motGlosaire).'(,|\.|;|:|!){1}$#', strtolower($mot), $matches)){ // cas : mot,
					$this->motDetecte[$k] = str_replace($matches[1], null, $mot);
				}
				elseif(preg_match('#((.*){1}(\'))'.strtolower($motGlosaire).'#', strtolower($mot), $matches)){ // cas : l'mot
					$this->motDetecte[$k] = str_replace($matches[1], null, $mot);
				}
			}
		}
	}
	public function test($url, $inputString){
		foreach($this->dictionaire as $motGlosaire){
			$this->inputString = $inputString;
			//$output = $this->inputString;
			preg_match_all('#( ){1}('.$motGlosaire.')(,|\.|;|:|!| ){1}#ui', $this->inputString, $matches);
			//print_r($matches);
			foreach($matches[0] as $motTrouve){
				//echo $motTrouve;
				$pattern[] = '#'.$motTrouve.'#';
				$replace[] = '<a href="'.$url.$this->simplifierMot($motGlosaire).'">'.$motTrouve.'</a>';
				//$output = preg_replace('#'.$motTrouve.'#', 'TOTO', $output);
				//echo $output;
			}
		}
		return preg_replace($pattern, $replace, $this->inputString);
	}
	private function simplifierMot($string)
	{
		$replace = array(
			' '=>'-',
			'ŕ'=>'a',
			'á'=>'a',
			'â'=>'a',
			'ă'=>'a',
			'ä'=>'a',
			'ĺ'=>'a',
			'ň'=>'o',
			'ó'=>'o',
			'ô'=>'o',
			'ő'=>'o',
			'ö'=>'o',
			'č'=>'e',
			'é'=>'e',
			'ę'=>'e',
			'ë'=>'e',
			'ě'=>'i',
			'í'=>'i',
			'î'=>'i',
			'ď'=>'i',
			'ů'=>'u',
			'ú'=>'u',
			'ű'=>'u',
			'ü'=>'u',
			'˙'=>'y',
			'ń'=>'n',
			'ç'=>'c',
			'ř'=>'0'
		);
		return strtr(strtolower($string), $replace); 
	}
}
?>

Posté : 07 avr. 2009, 15:00
par Topper
Pour ton problème de conflit c'est pour ça que j'ai utilisé un tableau que je trie en fonction de la taille du mot.

Par contre ma méthode diffère du fait qu'elle ne prend en compte qu'une occurrence sur tout le texte. Si le mot se présente 4 fois, seul le premier va être lié, sinon ça fait un peu "lourding" selon moi mais tout dépend des besoins. ;)

Posté : 07 avr. 2009, 15:12
par supercanard
Exact...

En fait je me dit que sur la deuxième version il suffirait que je rajoute à mon masque une condition pour que le remplacement ne s'applique que si le mot n'est pas entouré de <a href=""> </a>

Mais alors les conditions dans un regex ça devient un peu tordu et compliqué...

Actuellement voilà ce qui se passe :

Code : Tout sélectionner

<a href="http://fr.wikipedia.org/wiki/base-de-donnees"></a><a href="http://fr.wikipedia.org/wiki/base"> base </a>de données

Posté : 12 avr. 2009, 13:03
par supercanard
En reprenant l'idée de topper j'ai réussi à pondre une méthode dans ma classe glossaire que j'utilise sur un site.
Donc voilà j'ai réussi à faire le lien entre les textes du site et la table du glossaire. Je suis super content, tout marche impec : Combinaison de mots ou un seul, tout est bien détecté sans bug pour le moment.

Voici la méthode, qui n'a plus rien a voir avec le code que j'ai mis au début, puisqu'elle fait partie d'une classe spécifique :

Code : Tout sélectionner

public function autoLink($inputString){ $liste = $this->constructAll(); foreach($liste->objets as $k=>$motGlosaire){ $temp[$k]['taille'] = mb_strlen($motGlosaire->getLibelle()); $temp[$k]['libelle'] = $motGlosaire->getLibelle(); $temp[$k]['id'] = $motGlosaire->getId(); } foreach($temp as $k=>$mot) { $taille[$k] = $mot; } array_multisort($taille, SORT_DESC, $temp); $i = 0; foreach($temp as $motGlosaire){ if(preg_match('#( ){1}('.$motGlosaire['libelle'].')(,|\.|;|:|!| ){1}#ui', $inputString, $matches)){ $motTrouve = $matches[0]; $pattern[$i] = '#'.$motTrouve.'#'; $replace[$i] = '<a href="index.php?p=lexique&definition='.$this->simplifierMot($motGlosaire['libelle']).'&idDefinition='.$motGlosaire['id'].'">'.$motTrouve.'</a>'; $code[$i] = '--'.$i.'--'; $inputString = preg_replace($pattern[$i], $code[$i], $inputString); } $i++; } return str_replace($code, $replace, $inputString); } private function simplifierMot($chaine) { $chaine = mb_strtolower($chaine, 'UTF-8'); $chaine = str_replace( array( 'à', 'â', 'ä', 'á', 'ã', 'å', 'î', 'ï', 'ì', 'í', 'ô', 'ö', 'ò', 'ó', 'õ', 'ø', 'ù', 'û', 'ü', 'ú', 'é', 'è', 'ê', 'ë', 'ç', 'ÿ', 'ñ', '\'', ' ' ), array( 'a', 'a', 'a', 'a', 'a', 'a', 'i', 'i', 'i', 'i', 'o', 'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u', 'e', 'e', 'e', 'e', 'c', 'y', 'n', '-', '-', ), $chaine ); return trim($chaine); }

Posté : 12 avr. 2009, 13:07
par Topper
Tant mieux si j'ai pu t'aider alors. :wink: