Variantes d'écriture d'une requête SQL simple


28 octobre 2007

Présentation

Pendant la lecture du chapitre 6, pages 126 et 127, du livre de Joe Celko "Sql Programming Style", Elsevier, 2005, j'ai eu envie de vérifier un schéma et une requête qu'il exécute sur un certain type de données.

Les données sont des prêts pour lesquels on stocke des paiements successifs qui peuvent avoir chacun un status (S, U ou F) qui détermine, si on veut, un paiement a échéance, en retard ou envoyé (la signification de ce status n'a pas d'importance). Il cherche, étant donné ce schéma, la liste des prêts dont TOUS les paiements ont le status 'F'.

Simple ? Oui, mais on peut diverger quand à la construction des requêtes possibles pour trouver ce résultat.

Le schéma et la solution de Celko

Son schéma, que je reprend, est le suivant :

CREATE TABLE loans (
        loanId int not null,
        paymentId int not null,
        paymentStatus = ENUM('F', 'U', 'S'),
        primary key (loanId, paymentId)
    )

Sa solution est élégante et rapide (cf ci-dessous) et je voulais savoir si on pouvais faire mieux. Tout d'abord présentons sa solution :

select loanId, count(*) as nbPayments
        FROM loans
        GROUP BY loanId
        HAVING MAX(paymentStatus)='F' and MIN(paymentStatus)='F';

On parcourt donc la totalité de la table et on extrait bien les prêts dont les paiements n'ont pas d'autres statuts que 'F'.

Les alternatives

J'ai deux solutions alternatives. La première utilise deux sous-requêtes, une qui donne pour chaque prêts le nombre de paiements ayant le statuts 'F' et une autre qui donne le nombre de paiements total. Une clause WHERE est présente pour ne garder que les prêts pour lesquels ces décomptes sont égaux.

select GlobalStats.loanId, GlobalStats.nbPayments
        from
        (select loanId, count(*) as nbPayments from loans where paymentStatus='F' group by loanId) as FStats,
        (select loanId, count(*) as nbPayments from loans group by loanId) as GlobalStats
        where FStats.loanId=GlobalStats.loanId and FStats.nbPayments=GlobalStats.nbPayments;

La deuxième solution calcule directement pour chaque prêt le nombre de paiements et le nombre de paiements ayant le statut 'F', en utilisant la somme d'une valeur issue d'un IF.

select loanId, nbPayments
        from (
            select loanId, count(loanId) as nbPayments, sum(if(paymentStatus='F', 1, 0)) as nbFPayments from loans group by loanId
        ) as inter
        where inter.nbPayments=inter.nbFPayments;

Conclusion

Le résultat ? Déprimant ! Mes deux solutions peuvent être plus simples à comprendre au premier coup d'oeil mais sont jusqu'à 30% plus lente à l'exécution, alors que la solution de Celko qui peut surprendre au premier regard, est en fait très simple à comprendre, plus rapide et plus constante dans son temps d'exécution.

Pour tester ça j'ai utilisé un petit script PHP qui initialise la table, la remplit avec des milliers de données, règle certains prêts de manière à ce qu'ils apparaissent dans le résultat des requêtes de cette page et compare les temps d'exécution des trois variantes.

C'est moi ou Joe Celko ressemble drôlement à Anton Lavey ?

SQL adapté au classement sportifs (comprenez Foot)


28 octobre 2007

Présentation

Pas la peine de crier, pensez ce que vous voulez du foot, ça m'amuse de compiler des données là-dessus et d'établir la meilleure structure de données possible afin d'obtenir non seulement des résultats le plus simplement du monde (comprenez une seule requête SQL) et d'extraire des informations d'un amas plutôt brut de résultats de matchs (ex : Juninho à 75% de chance de marquer un but s'il rentre avant la 23ème minute d'un match. Cherchez pas je viens de l'inventer mais on peut calculer ça !).

L'objectif : le classement du championnat de France

Chaque journée (groupe de N matchs entre les 2N équipes de ligue 1 ou 2 du championnat) permet, avec les journées précédentes, d'établir un classement de ces 2N équipes en fonction d'un barème simple et précis : une victoire = 3 points, un match nul = 1 point et une défaite = aucun point. On passera pour l'instant sur les tris utilisés quand ces scores sont égaux et qui permettent de départager les ex-aequo.

Les données

On va créer une table extrèmement simpl(ist)e pour stocker les matches :

CREATE TABLE `matches` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `home` varchar(100) collate utf8_unicode_ci NOT NULL,
  `visitor` varchar(100) collate utf8_unicode_ci NOT NULL,
  `nbGoalsHome` int(10) default NULL,
  `nbGoalsVisitor` int(10) default NULL,
  `dayIndex` int(11) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='Matches de foot';

La plupart des champs sont évident, à l'exception de dayIndex qui est l'indice de la "journée" à laquelle appartient le match. Ceci posé on va la remplir avec les résultats des 8 premières journées du championnat dont je dispose à cette date, ce qui nous fait 80 matches car il y a 20 équipes en lice.

Premier essai : mix-up de PHP et SQL

La version du code qui vient immédiatement consiste à procéder à des requêtes simples et à traiter les résultats de ces requêtes en programmation classique et enfin a afficher le résultat. D'où :

//On détermine les noms des différentes équipes évoluant dans le championnat
    $Result = mysql_query("SELECT home as name from matches group by home;");
    while ($Item = mysql_fetch_array($Result))      $Buckets[$Item["name"]] = 0;

    //Pour chaque équipe on calcule les points issus des ses rencontres jouées à domicile
    $Result = mysql_query("SELECT
        home, nbGoalsHome, sum(if (nbGoalsHome>nbGoalsVisitor,3,if(nbGoalsHome=nbGoalsVisitor,1,0))) as homeScore,
        visitor, nbGoalsVisitor, sum(if (nbGoalsHome<nbGoalsVisitor,3,if(nbGoalsHome=nbGoalsVisitor,1,0))) as visitorScore
        FROM `matches` group by visitor;");
    while ($Item = mysql_fetch_array($Result))      $Buckets[$Item["visitor"]] += $Item["visitorScore"];

    //Comme précédemment mais pour les rencontres jouées à l'extérieur
    $Result = mysql_query("SELECT
        home, nbGoalsHome, sum(if (nbGoalsHome>nbGoalsVisitor,3,if(nbGoalsHome=nbGoalsVisitor,1,0))) as homeScore,
        visitor, nbGoalsVisitor, sum(if (nbGoalsHome<nbGoalsVisitor,3,if(nbGoalsHome=nbGoalsVisitor,1,0))) as visitorScore
        FROM `matches` group by home;");
    while ($Item = mysql_fetch_array($Result))      $Buckets[$Item["home"]] += $Item["homeScore"];

    //Le tri final et l'affichage
    arsort($Buckets);
    foreach($Buckets as $TeamName=>$TeamScore)      echo "$TeamName = $TeamScore<br/>";

Ce qui renvoie :

Nancy = 16
Rennes = 15
Lyon = 15
Bordeaux = 15
Valenciennes = 14
Monaco = 13
Le mans = 13
Lorient = 12
Strasbourg = 12
Paris-SG = 11
Saint-Etienne = 11
Nice = 11
Toulouse = 10
Lille = 9
Marseille = 7
Auxerre = 6
Lens = 5
Caen = 4
Sochaux = 4
Metz = 2

On a ici une première version du classement attendu mais avec quelques petites différences dûes aux ex-aequo qui ne sont pas bien placés du fait de la présence d'un seul tri (sur le score global) qui ne suffit pas.

Note : j'ai fait un tri simple (la ligne du arsort) parce que trier au sens de la LFP comme évoqué plus loin m'ennuie profondément en PHP.

Deuxième essai : une seule requête SQL

Le chemin vers une requête SQL unique plutôt qu'un mélange de deux langages est assez direct :

  • on doit cumuler les points de chaque équipe qu'elle joue à domicile ou à l'extérieur
  • ayant cumulé les points engrangés à chaque match avec une requête on les cumule avec une deuxième qui va l'encapsuler et procéder à des additions (grossièrement)
  • on va trier ces cumuls pour refléter le classement correct au sens de la LFP

Calcul des points par match

Le premier point nous oblige à doubler les matchs contenus dans notre table, car sinon les cumuls vont être impossibles à réaliser en une seule requête. Je m'explique : les points d'une équipe à domicile ne pourront être cumulés à ceux qu'elle a rapportés de ses voyage à l'extérieur, les champs (home et visitor) sont tout simplement différents ! On va donc leurrer le code qui réalise les cumuls en ajoutant pour chaque match son match miroir, ainsi Toulouse-Marseille est l'équivalent de Marseille-Toulouse et les cumuls réalisés sur chaque équipe sont valides. On va faire tout ceci en utilisant un UNION :

select
        t1.id, t1.home, t1.nbGoalsHome, t1.visitor, t1.nbGoalsVisitor
    from matches as t1
UNION ALL
    select
        t2.id, t2.visitor, t2.nbGoalsVisitor, t2.home, t2.nbGoalsHome
    from
        matches as t2
order by id

Cette requête renvoie 180 lignes, deux fois plus de lignes que de matchs, comme suit, du moins pour les 10 premières lignes :

idhomenbGoalsHomevisitornbGoalsVisitor
1Toulouse2Marseille1
1Marseille1Toulouse2
2Lens1Nancy0
2Nancy0Lens1
3Valenciennes0Le mans2
3Le mans2Valenciennes0
4Bordeaux1Lille1
4Lille1Bordeaux1
5Lyon5Metz1
5Metz1Lyon5

On voit clairement les lignes doublées, elles ont la même id mais avec les équipes inversées.

On peut lire ce résultat comme : "je vais traiter les résultats de chaque équipe de chaque match peu importe qu'elle joue à l'extérieur ou à domicile".

Pour calculer le nombre de points engrangés par chaque équipe on doit utiliser une clause IF pour chaque SELECT de l'opération UNION, placé juste après le nom de l'équipe qui apparaît en premier sur chaque ligne :

select
    t1.id, t1.home,
    IF(t1.nbGoalsHome>t1.nbGoalsVisitor, 3, IF(t1.nbGoalsHome=t1.nbGoalsVisitor, 1, 0)) as score,
    t1.nbGoalsHome, t1.visitor, t1.nbGoalsVisitor
from matches as t1

UNION ALL

select
    t2.id, t2.visitor,
    IF(t2.nbGoalsHome<t2.nbGoalsVisitor, 3, IF(t2.nbGoalsHome=t2.nbGoalsVisitor, 1, 0)),
    t2.nbGoalsVisitor, t2.home, t2.nbGoalsHome
from matches as t2

order by id

Ce qui renvoie :

idhomescorenbGoalsHomevisitornbGoalsVisitor
1Marseille01Toulouse2
1Toulouse32Marseille1
2Lens31Nancy0
2Nancy00Lens1
3Le mans32Valenciennes0
3Valenciennes00Le mans2
4Lille11Bordeaux1
4Bordeaux11Lille1
5Metz01Lyon5
5Lyon35Metz1

Ce qui se lit en français "chaque équipe remporte 3, 1 ou zéro points suivant le résultat du match qu'elle a joué à domicile ou à l'extérieur".

Les cumuls des points

Tout ceci est bien beau mais il faut accumuler les points calculés précédemment pour obtenir des totaux nous permettant d'ordonner les équipes entre elles, ce qui est le but initial... Pour ça, on peut transformer la requête précédente en sous-requête d'une requête principale de cumuls :

select nomEquipe, sum(score) as totalScore
    from (sous-requête) as liste
group by nomEquipe
order by totalScore desc

si on remplace la mention sous-requête par celle qu'on a écrit précédemment. Ce qui renvoie :

nomEquipetotalScore
Nancy16
Rennes15
Bordeaux15
Lyon15
Valenciennes14
Le mans13
Monaco13
Strasbourg12
Lorient12
Paris-SG11
Saint-Etienne11
Nice11
Toulouse10
Lille9
Marseille7
Auxerre6
Lens5
Caen4
Sochaux4
Metz2

et qui correspond presque au classement officiel de cette 8ème journée, si ce n'est que quelques équipes à égalité de points sont mal placées et qui nous fait arriver au dernier point.

Les tris officiels

On peut consulter les modes de calculs et de tri des résultats dans les règles du championnat, article 307 éditées par la Ligue de Football Professionnel. Je cite :

En cas dâ??égalité de points, le classement des clubs ex-aequo est déter- miné par la différence entre les buts marqués et les buts concédés par chacun dâ??eux au cours des matches joués pour lâ??ensemble de la divi- sion. En cas de nouvelle égalité, avantage sera donné au club ayant marqué le plus grand nombre de buts.

On va donc transposer ça en mettant bout à bout des tris au sein d'une clause ORDER BY, en considérant que totalScore est le nombre de points d'une équipe, Diff est la différence de buts cumulée et BP est le nombre de buts marqués cumulé :

order by totalScore desc, Diff desc, BP desc

Le résultat final

Voici donc, en utilisant tout ce qui précède, la requête finale à exécuter pour obtenir en quelques millièmes de secondes le classement des équipes jouant le championnat de France :

select
    nomEquipe, sum(score) as totalScore, sum(J) as J, sum(G) as G, sum(N) as N, sum(P) as P, sum(bp) as BP, sum(bc) as BC, sum(diff) as Diff
    from (
        SELECT
            t1.id, t1.home as nomEquipe,
            IF (t1.nbGoalsHome > t1.nbGoalsVisitor, 3, IF (t1.nbGoalsHome = t1.nbGoalsVisitor, 1, 0)) as score,
            if(t1.nbGoalsHome is null, 0, 1) as J,
            if(t1.nbGoalsHome>t1.nbGoalsVisitor, 1, 0) as G, if(t1.nbGoalsHome=t1.nbGoalsVisitor, 1, 0) as N,
            if(t1.nbGoalsHome<t1.nbGoalsVisitor, 1, 0) as P,
            t1.nbGoalsHome as bp, t1.nbGoalsVisitor as bc,
            t1.nbGoalsHome-t1.nbGoalsVisitor as diff
        FROM matches AS t1
        UNION ALL
        SELECT
            t2.id, t2.visitor as nomEquipe,
            IF (t2.nbGoalsVisitor > t2.nbGoalsHome, 3, IF (t2.nbGoalsVisitor = t2.nbGoalsHome, 1, 0)),
            if(t2.nbGoalsVisitor is null, 0, 1),
            if(t2.nbGoalsVisitor>t2.nbGoalsHome, 1, 0),
            if(t2.nbGoalsHome=t2.nbGoalsVisitor, 1, 0),
            if(t2.nbGoalsVisitor<t2.nbGoalsHome, 1, 0),
            t2.nbGoalsVisitor, t2.nbGoalsHome,
            t2.nbGoalsVisitor-t2.nbGoalsHome as diff
        FROM matches AS t2
    ) as resultat
    group by nomEquipe
    order by totalScore desc, Diff desc, BP desc

Cette requête renvoie ce qu'on attend d'elle, à savoir :

nomEquipetotalScoreJGNPBPBCDiff
1Nancy1675111358
2Lyon1575021477
3Bordeaux1584311064
4Rennes158431954
5Valenciennes1484221293
6Monaco1384131385
7Le mans13841312111
8Strasbourg128332853
9Lorient1283321091
10Saint-Etienne118323963
11Paris-SG118251761
12Nice118323770
13Toulouse106312880
14Lille98161880
15Marseille78143710-3
16Auxerre68206415-11
17Lens5612324-2
18Caen4611449-5
19Sochaux48044614-8
20Metz28026213-11

Vous pouvez vérifier que ce classement est correct en vous rendant sur la page officielle de cette 8ème journée 2007/2008 de la LFP.

Dernier arrangement

On voit tout de suite que les deux SELECT que l'on mélange avec le UNION de la requête précédente sont optimisables : il suffit de déplacer les deux listes de champs et de les mélanger avec les sommes qu'exécutent la requête supérieure. �a marche très bien, la requête est plus courte, plus claire et elle s'exécute 20% plus rapidement sur plusieurs installations de MySQL :

ELECT
        nomEquipe,
        sum(IF(nbGoals1 > nbGoals2, 3, IF (nbGoals1 = nbGoals2, 1, 0))) as totalScore,
        sum(if(nbGoals1 is null, 0, 1)) as J,
        sum(if(nbGoals1>nbGoals2, 1, 0)) as G,
        sum(if(nbGoals1=nbGoals2, 1, 0)) as N,
        sum(if(nbGoals1<nbGoals1, 1, 0)) as P,
        sum(nbGoals1) as BP,
        sum(nbGoals2) as BC,
        sum(nbGoals1-nbGoals2) as Diff
    FROM (
            SELECT
                t1.home as nomEquipe, t1.nbGoalsHome as nbGoals1, t1.nbGoalsVisitor as nbGoals2
            FROM matches as t1
        UNION ALL
            SELECT
                t2.visitor, t2.nbGoalsVisitor, t2.nbGoalsHome
            FROM matches as t2
    ) as mList
    GROUP BY nomEquipe
    ORDER BY totalScore desc, Diff desc, BP desc

Fonctionnalités supplémentaires

En ajoutant une clause WHERE très simple, WHERE dayIndex<=X, dans la grosse requête précédente, on obtient le classement après une journée quelconque, X, et plus sur l'ensemble des journées écoulées. En répètant cette requête pour toutes les valeurs de X partant de 1 jusqu'à la dernière journée jouée, on a les classements successifs journées après journées et on peut établir, par exemple, la progression d'une équipe dans ce classement.

//Je n'ai pas pu résister à l'envie de faire un petit graphique regroupant les positions successives des équipes renvoyées par cette nouvelle requête : //<IMAGE>

On pourra ajouter beaucoup de clauses WHERE dans cette requête histoire de gérer un design plus poussé mais ceci est l'objet d'un autre article plus complet. Je voulais ici me concentrer sur le point précis de l'obtention de ce classement et ce qu'il supposait du point de vue SQL.

Dernier point

Il y a une chose étrange : après avoir ajouté cet indice de la journée de championnat qui correspond au match j'ai cru constater un ralentissement de l'exécution de la requête quand ce nombre était déclaré comme INDEX alors que je croyais que cette déclaration allait optimiser mon WHERE... A suivre donc.

AJAX dans Konqueror / Safari


22 mai 2007

Du XML dans Konqueror

Pour charger un document XML dans Konqueror et le manipuler avec le DOM, il faut au préalable créer le document et charger le texte XML. Comme suit dans cet exemple JS :

    var Doc = "<"."?xml version='1.0' encoding='utf-8'?".">\
    <root2>\
        <child1>Contenu1</child1>\
        <child2>Contenu2\
            <child21>Contenu du 21</child21>\
        Fin du contenu 2\
        </child2>\
        <child3>Contenu3</child3>\
        <child4>Contenu4</child4>\
    </root2>"
    //
    var XMLDoc = document.implementation.createDocument();
    XMLDoc.loadXML(Doc);
    var Root = XMLDoc.documentElement;</code>

            <p>A partir de là on a un objet JS (Root) qui représente la version DOM de notre document XML. Si vous ne me croyez pas, essayez d'utiliser la fonction suivante qui parse le DOM en question :</p>
            <code legend='A appeler comme suit : getNodeHierarchy(Root, 0)'>function    getNodeHierarchy(node, level)
{
    var Output = typeof node+"-"+level+" : "+node.nodeName+", "+NodeTypesDisplay[node.nodeType]+", "+node.nodeValue+"\n";
    if (!node.childNodes.length)        return Output
    node = node.firstChild
    do { Output += getNodeHierarchy(node, level+1) } while (node = node.nextSibling);
    return Output
}

La fonction précédente avec notre XML d'exemple affichera :

object-0 : root, Node, null
object-1 : #text, Texte,

object-1 : child1, Node, null
object-2 : #text, Texte, On
object-1 : #text, Texte,

object-1 : child2, Node, null
object-2 : #text, Texte, Contenu2

object-2 : child21, Node, null
object-3 : #text, Texte, Contenu du 21
object-2 : #text, Texte,
Fin du contenu 2

object-1 : #text, Texte,

object-1 : child3, Node, null
object-2 : #text, Texte, Con
object-2 : b, Node, null
object-3 : #text, Texte, tenu3
object-1 : #text, Texte,

object-1 : child4, Node, null
object-2 : #text, Texte, Contenu4
object-1 : #text, Texte,

Jusque là tout va bien, mais comment nourrir un script avec un XML dynamique, i.e. que l'on ne peut pas hardcoder et que l'on peut même recharger plusieurs fois au cours du déroulement de l'application ? C'est très simple : on intègre une iframe dans la page web, on règle l'attribut src de cette iframe à une URL correspondant au fichier et en utilisant l'évènement onLoad de l'iframe on peut recopier peu ou prou les trois lignes qui crée le document XML et qui charge les données. Seul problème, si on essaye, Konqueror va transformer le XML en document HTML pour le rendre (c'est une IFrame après tout) alors plutôt que d'essayer tout de suite de faire des XSL pour pallier à ça, on construit une petite passerelle PHP qui va servir un fichier en transformant les < en &lt; et les > en &gt;. J'ai presque honte de faire ça mais ça fonctionne très bien, vous allez voir. Voici déjà la passerelle PHP qui sert les fichiers XML transformés :

<?
    if (!isset($_GET["file"]))      die("Pas de paramètre qui indique un fichier à servir");
    $FilePath = $_GET["file"];
    while (true)        {
        if (strpos("/", \$FilePath) !== false)      die("Interdit");
        if (file_exists($FilePath))     break;
        $Temp = rawurldecode($FilePath);
        if ($Temp == $FilePath)     die("Fichier introuvable");
    }
    $Contents = file_get_contents($FilePath);
    echo str_replace("<", "&lt;", str_replace(">", "&gt;", $Contents));
?"."></code>
            <p>Ainsi, en utilisant cette passerelle et en interceptant l'évènement onLoad de l'iframe avec la fonction JS suivante on obtient le même résultat que précédemment mais sans avoir hardcodé le XML :</p>
            <code>function ReceiveDatas()
{
    var Input = document.getElementById("inputXML")
    if (!Input.src.length)      return
    var XMLContent = Input.contentDocument.body.innerHTML
    XMLContent = XMLContent.replace(/&amp;lt;/g, "&lt;").replace(/&amp;gt;/g, "&gt;")
    //
    var XMLDoc = document.implementation.createDocument();
    XMLDoc.loadXML(XMLContent)
    var Root = XMLDoc.documentElement
    alert(getNodeHierarchy(Root, 0))
}?>

Et ça y est, Konqueror peut charger et manipuler du XML comme un grand. Maintenant qu'on a trouvé une méthode pour charger un XML "à la main", pour les navigateurs équipés passons à...

L'utilisation des xmlHttpRequest

Convertisseur HTML vers PDF


8 avril 2007

Convertisseur de fichier HTML en PDF

Objectif : réaliser une combinaison logicielle permettant, quelque soit la page HTML, d'en déduire un fichier PDF le plus fidèle possible. Piste : l'ensemble contiendra une application C++ (aka binaire) rapide qu'on nourrira de la page HTML qui, utilisant PDFLib, générera le fichier PDF. Note : il ne faut pas oublier la gestion des fontes à insérer dans les PDFs produits. Gabarit :

//On récupère le flux d'entrée
    HTMLFile = stdin();
    //On vérifie que l'entrée suit bien les specs XHTML
    if (!(ParsedDatas = parseXML(HTMLFile)))        return EXIT_FAILURE;
    //On crée le document PDF
    Doc = new PDFDocument();
    if (!Doc->generatePDF(ParsedDatas))     return EXIT_FAILURE;
    //On envoie le PDF produit sur la sortie standard
    Doc->echo();
    return EXIT_SUCCESS;
        

Bien sûr les fonctions/méthodes utilisées (stdin(), parseXML() etc) n'existent pas encore, mais le but de ce document est d'y remédier. Le choix est de recevoir des données depuis stdin et d'émettre le produit sur stdout pour rendre l'usage de l'application multiple : aujourd'hui, nous avons besoin de transformer des pages web depuis une application serveur PHP, mais demain les spécifications peuvent évoluer (Java, transformation PDF en batch etc) il faut donc qu'elles puissent s'adapter à ce genre de situations. Je veux pouvoir être capable d'écrire les lignes de scripts suivantes :

    cat test.html | pdfconverter > test.pdf
    wget "http://new.google.com/" | pdfconverter > new.google.com.pdf
    wget "http://new.google.com/" | pdfconverter > test.pdf && echo "Texte du mail" | mutt -a test.pdf -t "Titre" 42@flubb.net
    pdfconverter --input-file test.html > test.pdf
    pdfconverter --input-file test.html --output-file test.pdf', "Insertion du convertisseur de HTML en PDF dans n'importe quelle chaîne de production

Il faut à présent détailler toutes ces étapes : 1- interception du flux d'entrée depuis l'entrée standard 2- parsing XML utilisant libxml2 3- implémentation d'une classe PDFDocument 4- émission sur la sortie standard Le point 3 est le plus problématique mais les trois autres ne sont pas à négliger : elles peuvent en effet poser problèmes plus tard si elles ne sont pas bien affutées maintenant. On va donc les traiter dans le désordre :

1- interception du flux d'entrée depuis l'entrée standard

�a peut être fait de façon simpl(e|iste) mais si on veut essayer de minimiser les new (en gros n'en faire qu'un seul), la façon ci-dessous correspond :

void    getSTDIn(char** ptr, int& nbLines, int& totalLength)
{
    int     LineLength = 1000;
    nbLines = 0;
    totalLength = 0;
    char*   Line = new char[LineLength];
    char*   FGetsStatus = NULL;
    FILE*   STDInCopy = tmpfile();
    //
    do  if (FGetsStatus = fgets(Line, LineLength, stdin))       {
        int Ln = strlen(Line);
        fwrite(Line, Ln, 1, STDInCopy);     //###CHECK
        nbLines++;
        totalLength += Ln;
    } while(FGetsStatus != NULL);
    delete []Line;
    //
    rewind(STDInCopy);
    //
    *ptr = new char[totalLength+1];
    (*ptr)[totalLength] = 0;
    fread((*ptr), totalLength, 1, STDInCopy);       //###CHECK
    fclose(STDInCopy);      //On efface le fichier temporaire
        }
Lire stdin en deux passes pour minimiser les new

2- parsing XML utilisant libxml2

void    displayNode(xmlNodePtr node, int level)
{
    if (!node)      return;
    while (node)        {
        int i;
        for(i=0; i<level; i++)      printf(\"  \");
        printf(\"%s, type %d, Contenu : %s\\n\", node->name, node->type, node->content);
        if (node->children)     displayNode(node->children, level+1);
        node = node->next;
    }
}

//Affiche l'arbre de parsing de libXML2
void showXMLTree(xmlDocPtr doc)
{
    xmlNodePtr  HTMLNode;
    printf(\"Document : %s\\n\", doc->name);
    xmlNodePtr  CurNode = doc->children;
    displayNode(CurNode, 1);
}


int main(int argc, char *argv[])
{
    // Buffer contient le texte à parser
    xmlDocPtr   HTMLDoc;
    HTMLDoc = xmlParseDoc((xmlChar*)Buffer);
    showXMLTree(HTMLDoc);
    //...
        }

Ce qui produit :

Pour l'entrée HTML suivante on obtient l'output au-dessous :

<html>
    <head>
        <title>Titre de ma page</title>
    </head>
    <body>
        <p>Premier paragraphe</p>
    </body>
</html>

-------------------------------------

Document : (null)
    html, type 1, Contenu : (null)
        text, type 3, Contenu :

        head, type 1, Contenu : (null)
            text, type 3, Contenu :

            title, type 1, Contenu : (null)
                text, type 3, Contenu : Titre de ma page
            text, type 3, Contenu :

        text, type 3, Contenu :

        body, type 1, Contenu : (null)
            text, type 3, Contenu :

            p, type 1, Contenu : (null)
                text, type 3, Contenu : Premier paragraphe
            text, type 3, Contenu :

   text, type 3, Contenu :

4- émission sur la sortie standard

On utilise simplement la fonction puts pour envoyer un contenu sur le terminal (ou même un - encore plus simple - printf). On peut donc passer aux choses sérieuses...

3- implémentation d'une classe PDFDocument

On reprend le code de PDFDocument.php et html2pdf.php pour commencer le travail. Question de debug : comment débugguer un programme que l'on veut nourrir par un pipe ? Je ne vois pas comment faire ça sous KDevelop. C'est ça qui m'empêchait de debugguer le fgets qui était pending (les io étaient bloquantes). Deux choses : essayer stdio sans lock et voir comment envoyer des données à un programme qui tourne (un pipe est activé une fois que l'applicatino est lancée donc).

Le programme de développement

Pour la version 0.1 : - générer un PDF simple pour tester les bindings de PDFLib6. Pour la version 0.2 : - Gérer plusieurs formats de pages (a4, a3 etc). - Récupérer le code du texteur PHP pour afficher du texte multi-ligne. Pour la version 0.3 : - faire que les scripts du tableau 2 fonctionnent. - gérer les éléments HTML suivants : p, b, i, u, a, div. Pour la version 0.4 : - gérer les styles suivants : font-style, color, background-color Pour la version 0.5 : - gérer les images (essentiel mais très utile également pour le débug des tableaux de la phase suivante). Pour la version 0.6 : - gérer les tableaux Pour la version 0.7 : - gérer la pagination - gérer les headers et les footers - ajouter la gestion d'autres styles. Pour la version 0.8 : - afficher correctement la fiche contact de Linda - ajouter la gestion d'autres styles. Pour la version 0.9 : - afficher correctement un rapport NCA de Linda - ajouter la gestion d'autres styles. Pour la version 1.0 : - afficher correctement deux synthereports de GED-Pro : hopital-couple-enfant et chu-rennes. - finir la gestion des styles.

Les expressions régulières


7 avril 2007

Expressions régulières

Une compilation de conseils et d'exemples sur les expressions régulières. Histoire de ne pas avoir de multiples sources de renseignements pour des choses simples et qui reviennent souvent. �videmment, ces regex d'exemples et ces conseils sont initialement tirés de ce site pour montrer qu'ils sont utilisables directement.

Regarder si l'expension des tabs pourrait se faire avec une simple regex.
global  $TabWidth, $Tabs, $TabLengths;
    $Tabs = array();    $TabLengths = array();
    for($j=0; $j<=$TabWidth; $j++)      $TabLengths[$j] = strlen($Tabs[$j] = $j ? ($Tabs[$j-1])."&nbsp;" : "");
    $Ln = strlen($snip);
    $Computed = 0;
    foreach(array(true, false) as $ComputeWidth)        {
        $PosInLine = 0;
        $i = 0;
        while ($i<$Ln)      {
            $c = $snip[$i++];
            if ($c == "\n")         $PosInLine = -1;
            if ($c != "\t")     {   $PosInLine++;   continue;   }
            //On étend les tabs
            $RealWidth = $TabWidth - $PosInLine%$TabWidth;
            $Offset = $TabLengths[$RealWidth] - 1;
            if (!$ComputeWidth)     {
                $snip = substr($snip, 0, $i-1).$Tabs[$RealWidth].substr($snip, $i);
                $Ln += $Offset;         $i += $Offset;
            }   else    $Computed += $Offset;
            $PosInLine += $RealWidth;
            if ($ComputeWidth)      $Computed++;
        }
    }

Boucle qui étend les tabs d'une chaîne, $snip, en 2 passes : calcul de la taille finale et transformation.

Une expression régulière tirée d'un commentaire de la doc PHP de preg_grep.

/((?:(?!BADWORD).)*)/s
Regexp pour éliminer BADWORD de l'entrée
Accueil1 2 3 4 5 6 7