Page 1 sur 2

YAML et la gestion des quotes

Posté : 25 avr. 2007, 16:19
par Hywan
Bonjour tout le monde :)

J'aurais une question de méthodologie aujourd'hui.
Alors, pour mon Framework, j'ai construit un package YAML. Il supporte tout le langage sans problème, sauf la gestion des quotes dans un cas particulier. Je précise que je vais parler du parseur, et pas du dumper.

Un rapide résumé de YAML.
On appelle mapping tout ce qui est de la forme : key: value.
On appelle sequence tout ce qui est de la forme : - sequence.
YAML définit un mapping comme un tableau, et une sequence comme une liste.
On a aussi des mappings et des sequences sous forme serialisée, telles que : {a: b, c: d, e: f, ..., key: value} est un groupe de mapping, et [a, b, c, d, e, ..., sequence] soit une sequence.
On peut avoir des formes imbriquées, telles que : [{a: b}, c, d, {e: f}, g] ou encore [[a, b, c, d], e, [[f], g], h] etc etc. Après tous les délires sont possibles.

Bien.

Maintenant, on doit s'amuser à gérer les quotes.
En ce qui concerne les mappings et les sequences, je gère sans problème. En revanche pour les formes sérialisées j'ai plus de soucis.

Si on a : {And Bob says: "I'm a ..., hmm a Bobby !"} je devrais avoir : Array { 'And Bob says' => 'I\'m a ..., hmm a Bobby !' } nous sommes bien d'accord.
Mais avec mon programme, je fais un explode par la virgule-espace (', '), et donc il me coupe mon value en 2 parties. Ce que je ne veux pas.

Et je cherche à éviter ça. Et je n'ai aucune piste ^^
A vrai dire, faire un parser YAML est lourd, et compliqué. Ca demande pas mal de réflexion, et après 8 jours none-stop, jsuis au bout. J'arrive plus à avoir de réflexion, alors je viens demander un peu d'aide :P
Vous trouverez le code sur le Wall de PHPFrance : Il faut regarder la fonction unseralizedMappingSequence de la class YAML, et c'est là-dedans que j'ai mon problème.


Aussi je me rends bien compte qu'il faille se mettre dans le code pour le comprendre, j'espère que c'est "clair".
Donc sinon, une petite piste, ou si quelqu'un aurait rencontrer un problème similaire, qu'il me le fasse savoir.


Merci :)

PS : une idée me vient pendant ma relecture ; on pourrait faire un preg_split, mais là, il faudrait écrire une nouvelle expression régulière qui va encore tous nous faire rire ...

Posté : 27 avr. 2007, 18:42
par Hywan
Bonjour.

Je reformule ma question.

Imaginons que nous avons la chaîne :

Code : Tout sélectionner

aaaa, "bbbb", "cc\"cc", "dd, dd", "e\"e, ee"
On veut l'éclater pour avoir :

Code : Tout sélectionner

0 => aaaa 1 => "bbbb" 2 => "cc\"cc" 3 => "dd, dd" 4 => "e\e, ee"
Vous comprendrez qu'un simple explode() ne suffit pas.

Comment feriez vous ? Je ne trouve pas, help :P


Merci.

Posté : 27 avr. 2007, 19:15
par momox
Il y a des fonctions de parser xml qui peuvent être utilisées pour cela je crois, mais j'ai un doute d'un coup :?

Posté : 27 avr. 2007, 19:22
par naholyr
Le plus simple serait de faire un parsing caractère par caractère, typiquement un parsing dans ce genre
$contexte = "";
$prec = null;
$car = null;
$ouverture_chaine = null;
$echappements = 0;
$len = strlen($chaine);
$elements = array();
$souschaine = "";
$ajouter_element = false;
for ($i=0; $i<$len; ++$i) {
    $car = $chaine{$i};
    // on est dans une chaine (contexte = "chaine")
    // on rencontre le caractère qui a ouvert cette chaine
    // on a un nombre pair de caractères d'échappement qui précèdent
    if ($car == $ouverture_chaine && $contexte = "chaine" && $echappement %2 == 0) {
        $contexte = ""; // on sort du contexte
        $souschaine = substr($souschaine,1); // on zappe le guillemet ouvrant
        $ajouter_element = true;
        $echappements = 0;
    }
    // on est dans une chaine (contexte = "chaine")
    // on rencontre un caractère d'échappement
    elseif ($contexte == "chaine" && $car == "\\") {
        $echappements++;
    }
    // on est hors-contexte (contexte = "")
    // on rencontre un caractère d'ouverture de chaine
    elseif ($contexte == "" && ($car == '"' || $car == "'")) {
        $contexte = "chaine";
        $ouverture_chaine = $car;
        $car = "";
    }
    // on est hors-contexte (contexte = "")
    // on rencontre un caractère différent de l'ouverture de chaine ou de l'espace
    elseif ($contexte == "" && $car != '"' && $car != "'" && $car != ' ' && $car != "\t") {
        $contexte = "expression";
    }
    // on est dans un contexte d'expression
    // on rencontre une virgule
    elseif (contexte == "expression" && $var == ",") {
        $contexte = ""; // on repasse hors-contexte
        $ajouter_element = true;
    }
    if ($ajouter_element) {
        $elements[] = $souschaine;
        $souschaine = "";
        $ajouter_element = false;
    }
    $souschaine .= $car;
    $prec = $car;
}
Bien sûr celui-ci n'est qu'un jet rapide et est donc incomplet, mais c'est pour que tu aies une idée.

Par contre je pense qu'il est tout-à-fait possible de jouer avec les expressions régulières (surtout en voyant du côté du callback).

Posté : 27 avr. 2007, 19:41
par Hywan
Justement, j'ai déjà quelques choses avec les callbacks.

Alors voici ce que j'ai déjà fais :

Code : Tout sélectionner

Méthode pour échapper les quotes. Par exemple si on a : "aa\"aa" on aura: aa"aa Si on a : 'bb\'b"b' on aura : bb'b"b
Voici le code :
/**
 * unquote
 * Unquote a string.
 *
 * @access  public
 * @param   value   string    Value.
 * @return  bool
 */
function unquote ( &$value ) {

	if(preg_match('#(?:(?<!\\\)("|\'))(.*)?(?:(?(1)(?<!\\\)\1|))#U', $value, $matches)) {
		$value = str_replace('\\'.$matches[1], $matches[1], $matches[2]);
		return true;
	}

	return false;
}
La regex est un peu compliquée mais ça va encore.

On pourrait peut être s'en servir (en la modifiant) pour arriver à nos fins nan ?


Ta solution Naholyr est pas mal, mais très longue :s
Imaginons qu'on aurait un fichier YAML de 500Ko, je me vois mal parser caractère par caractère :P


Sinon, si quelqu'un comprend le comportement de preg_split, ce serait utile.

Posté : 27 avr. 2007, 22:30
par Hubert Roksor
L'un dans l'autre, le faire caractère par caractère a ses avantages. Tu peux généralement grandement accélérer les choses avec strcspn(), strpos() et strspn(). Soit $pos la position de ton curseur, tu peux sauter jusqu'à la prochaine occurence de ' ou " avec
$len = strcspsn($str, '\'"', $pos);
$pos += $len;
Une fois "dans" la chaîne (il faut incrémenter $pos pour rentrer dedans) tu sautes jusqu'à la prochaine occurence de \ ou " (le caractère qui enveloppe ta chaîne, donc " ou ' selon les cas). Si le caractère sur lequel tu viens de "sauter" est " alors c'est la fin de la chaîne, sinon ça veut dire que tu es en train d'échapper un caractère spécial. Là tu utilises
$len = strspn($str, '\\', $pos);
pour compter le nombre de backslashes, si $len est pair alors on continue comme si de rien n'était, sinon on avance le curseur de 1 de plus pour sauter le caractère échappé. Voilà, je sais pas si c'est super clair mais bon :)

À part ça, n'as-tu pas trouvé de classe qui gérait le format correctement ? Si mes souvenirs sont bons c'est le format des fichiers config de Symfony donc logiquement tu devrais avoir accès à une classe GPL et qui fonctionne non ?

Posté : 27 avr. 2007, 22:47
par naholyr
il y a déjà spyc, premier résultat google de php+yaml.

Edit : en l'occurrence il s'agit de la librairie utilisée par symfony ;)

Posté : 27 avr. 2007, 23:25
par Hywan
Je sais bien et je compte le concurrencer :lol:

Etant donné que je fais un Framework (et peut être un CMS à l'avenir), je me dois d'avoir un paquetage YAML :) Le support du framework (site) est en construction. Je vous ferai savoir quand il sortira hehe :wink:


Merci pour vos solutions. Je test tout ça demain, et je vous tiens au jus :)

Posté : 28 avr. 2007, 00:00
par Xenon_54

Posté : 28 avr. 2007, 02:47
par Hubert Roksor
[sarcasme]
C'est rigolo, je me demandais justement ce qu'il manquait à PHP : un framework... :lol:
[/sarcasme]

Posté : 28 avr. 2007, 09:35
par naholyr
YAML et CSV n'ont rien à voir :?

Posté : 28 avr. 2007, 12:37
par Hywan
Hehe Hubert :)
Je ne compte pas faire concurrence à Symfony et compagnie.

C'est juste un parcours personnel, histoire d'améliorer mon niveau et mon cadre de développement.
Car, mine de rien, en faisant un Framework, qu'est-ce qu'on apprend !

Et accessoirement, s'il est apprécié, alors bon, c'est un bonus :)

Bon c'est parti alors, je vais encore me battre avec ce problème. Tayo.

Posté : 28 avr. 2007, 12:50
par Hywan
Je souhaite encore persisté un petit peu sur les regex. Si jamais je n'y arrive pas, je passerai alors au traitement caractère par caractère.

Voilà où j'en suis (ça évolue un peu).
Comme le comportement de preg_split est très étrange, et qu'il accèpte très bizarrement les PCRE, j'ai décidé d'utiliser un preg_match_all, avec un tag U, on arrive presque au même comportement.

Voilà le code :
$a = '"Is", "Get, t\"ing", ';

preg_match_all('#(?:(?<!\\\)("|\'))(.*)(?:(?(1)(?<!\\\)\1|)), #U', $a, $m);

print_r($m);
On aura :

Code : Tout sélectionner

Array ( [0] => Array ( [0] => "Is", [1] => "Get, t\"ing", ) [1] => Array ( [0] => " [1] => " ) [2] => Array ( [0] => Is [1] => Get, t\"ing ) )
En revanche, si on a : Is, et pas "Is", ça ne fonctionne plus. Mais on s'en rapproche :P

Posté : 28 avr. 2007, 13:01
par Hywan
Bonne nouvelle :]

Voici l'expression régulière qui règle le problème :

Code : Tout sélectionner

#(?:(?<!\\\)("|\'|))(.*)(?:(?(1)(?<!\\\)\1|)), #U
Il suffisait de capturer les quotes " et ', et aussi rien. Et dans ce cas, tout fonctionne bien :)

Mais enlever moi d'un doute : un expression régulière est plus rapide qu'un traitement caractère par caractère n'est-ce pas ?

Merci pour votre aide et pour vos idées :) J'ai découvert strcspn(), et strspn() que je ne connaissais pas. Merci :)

Bonne journée (bientôt, la publication du paquetage, pour les personnes intéressées).


PS : Elle ne résoud pas entièrement le problème, mais déjà une grosse partie. Je vais pouvoir faire le reste avec vos propositions :)

Posté : 28 avr. 2007, 14:01
par naholyr
Mais enlever moi d'un doute : un expression régulière est plus rapide qu'un traitement caractère par caractère n'est-ce pas ?
En général oui, mais c'est à tester tout de même. La piste donnée par Hubert permet d'optimiser drastiquement le parsing.