Cette page fait suite à
ProduireDesFilsRss. Elle a pour objectif d'arriver à produire un fil des derniers changements RSS dont les éléments contiendraient un diff depuis la dernière révision, lorsque celle-ci existe (pages modifiées). Pour les nouvelles pages, le contenu serait simplement celui de la page.
NB.:
MediaWiki produit ce genre de fils RSS, mais avec beaucoup d'autres informations supplémentaires comme les modifications de permissions etc.
Je vais présenter ici ma solution (partielle) et les observations que j'ai faites pour y aboutir. --
LordFarquaad
Observations
- les fils produits par MediaWiki:
- le diff généré est affiché sur deux colonnes, ce qui est plutôt pratique pour voir les modifications
- le contenu des éléments (<item>) est placé dans les balises <description> et </description>
- pour afficher sur deux colonnes, il faut du HTML...
- les balises <description> ne peuvent contenir que du texte brut
- MediaWiki htmlencode() donc le diff pour le mettre entre ces balises, la plupart des lecteurs de flux restituent cela correctement mais ce n'est pas standardisé
- le texte htmlencode()é prend un peu de poid car pour faire un diff sur deux colonnes, il faut beaucoup de balises, et donc beaucoup de carractères à échapper...
- le texte htmlencode()é est très difficile à lire et donc à débugger...
- le résultat est pourtant très léger ! Si on consulte le fil RSS de la version anglophone de WikiPedia, qui contient pourtant 50 items, il fait moins de 200ko !
- (je pense que ceci est dû au fait que ce fil contient pas que des diff et des nouvelles pages, mais ça reste tout de même peu)
- produire un diff, c'est pas facile !
- WikiNi propose un diff simple: trop simple... on ne sait pas dire quelle partie est réellement modifiée, et on ne voit pas les parties qui ont bougé (inversion de paragraphes etc.). Quelques fois ce diff indique aucune différence, et pourtant il y en a...
- WikiNi propose un diff "avancé": trop avancé à mon goût: toute la page est affichée et on ne voit pas bien où sont les modiffications. De plus le diff se fait au mot à mot ce qui perturbe le formatter. Sans formattage, on n'y verrait pourtant rien...
- Ces deux diff font partie du handler diff, il faudrait les sortir...
- j'ai essayé un truc à ma sauce, mais c'est peut-être un peu compliqué... sans doute pas très performant d'ailleurs.
- Un algo de diff qui semble assez bon: http://www.holomind.de/phpnet/diff.src.php et il est sous GPL ! Je l'ai donc modifié pour le corriger, l'améliorer et l'adapter à mes besoins. (voir plus bas)
- un flux XML, ça gonfle vite
- comme dit plus haut, MediaWiki arrive à produire des fils de moins de 200ko
- moi, je grimpe vite au dessus d'1mo, voir presque 2mo pour 50 items (en prenant comme base de test la base de WikiNiPointNet à une époque où elle avait été fort spammée => les diff sont lourds)
- on peut alléger le flux en n'htmlencode()ant pas contenu dans la balise <description> mais en utilisant plutôt <content:encoded><![CDATA[ et ]]></content:encoded> avec le xmlns quivabien. Je n'ai pas mesuré les gains mais je pense que c'est déjà ça.
- le mieux serait sans doute d'utiliser un fichier de mise en cache En comparant la date du fichier avec celle de la dernière page modifiée/crée, on allégerait considérablement le travail du serveur.
- sans compter la mémoire qui lache ! Au début j'avais quelques bugs, et j'ai réussi à faire atteindre une utilisation de 200mo de mémoire par mon script. Heureusement ceci m'a permis de me rendre compte que la limite que j'avais placée était un peu haute (:-j), et je l'ai redescendu à 8mo ce qui est, il me semble, une valeur courante dans les serveurs en production non ? Eh bien pour plus de 65 éléments, ça lache toujours...
- si on propose des diff, il faut les donner tous
- si on a le diff, il y a des chances qu'on ne consulte pas le wiki tant qu'on ne veut pas y participer (on regarde le diff depuis son aggrégateur, c'est tout)
- actuellement, des DerniersChangementsRSS ne fournissent pas les dernierers changements mais les dernières pages changées et il y a une grosse nuance: si une page est modifiée deux fois entre deux consultations du flux, même par des utilisateurs différents, l'aggrégateur ne verra que la dernière modification...
- Si on se fie au diff, on risque de louper certaines modifications. Le pire serait dans le cas d'un spammer qui éditerait deux fois la page et ne changerait pratiquement rien lors de la deuxième édition...
- il faut donc fournir dans le flux toutes les modifications, c'est à dire utiliser une fonciton LoadRecentChanges (inexistante) à la place de LoadRecentlyChanged.
- pour faciliter le diff LoadRecentChanges peut fournir l'id de la version précédente. Combiné à une mise en cache par id, on peut y gagner sur les éditions multiples... (moins de requêtes MySQL)
Implémentation
Cette implémentation n'est pas encore tout à fait au point puisque, comme dit plus haut, il arrive encore que 8mo de mémoire ne soient pas suffisants pour générer le flux (pour plus 65
items), même si celui-ci est de l'ordre de 2-3mo. Il doit y avoir une fuite de mémoire quelque part mais je n'arrive pas à trouver où... Le problème c'est que chez moi php n'affiche même pas d'erreur dans ce cas là, alors qu'au début il le faisait. Et dans mon log apache, j'ai l'erreur mais sans indiquer de n° de ligne ni même de fichier... (je devrais peut-être configurer php pour sauver son log dans un fichier mais bon...)
L'implémentation se décompose en plusieurs parties.
- les fonctions de difff, placées dans un fichier séparé (diff.inc.php) pour plus de clarté:
<?php
/**
diff.inc.php - diff functions
Diff implemented in pure php, written from scratch.
found at http://www.holomind.de/phpnet/diff.src.php
Copyright (C) 2003 Daniel Unterberger <diff.phpnet@holomind.de>
Copyright (C) 2005 Didier Loiseau
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
http://www.gnu.org/licenses/gpl.html
About:
I searched a function to compare arrays and the array_diff()
was not specific enough. It ignores the order of the array-values.
So I reimplemented the diff-function which is found on unix-systems
but this you can use directly in your code and adopt for your needs.
Simply adopt the formatline-function. with the third-parameter of arr_diff()
you can hide matching lines. Hope someone has use for this.
Contact: d.u.diff@holomind.de <daniel unterberger>
**/
if (!defined('WIKINI_VERSION'))
{
exit;
}
if (defined('WIKINI_DIFF_FUNCTIONS'))
{
return;
}
define('WIKINI_DIFF_FUNCTIONS', true);
/**
* Computes a diff between two arrays of strings
* @param array $f1 An array of strings (supposed to be the older text)
* @param array $f2 An array of strings (supposed to be the newer text)
* @param boolean $show_equal If true, the result will also contain the common lines
* (default false)
* @return array An array of arrays describing the differencies between $f1 and $2.
* Each array describes one line and is structured like this:
* array {
* 'type' => value, # the type of diffence
* 0 => 'a line', # a line from $f1 or $f2
* 1 => 'another line' # if usefull, a line from $f2
* }
* The type describes the difference and the other values depend on the type:
* - for a removed line: 0 is the removed line, 1 is not set
* + for an added line: 0 is the added line, 1 is not set
* = for an unchanged line (only if $show_equal): 0 is the common line, 1 is not set
* c for a changed line: 0 is the old line, 1 is the new one
*/
function arr_diff( $f1 , $f2 , $show_equal = false )
{
$c1 = 0 ; # current line of left
$c2 = 0 ; # current line of right
$max1 = count( $f1 ) ; # maximal lines of left
$max2 = count( $f2 ) ; # maximal lines of right
$hit1 = "" ; # hit in left
$hit2 = "" ; # hit in right
$stop = 0; # stop flag
$out = array(); # output buffer
$trimf1 = array();
$trimf2 = array();
foreach ($f1 as $key => $value)
{
$trimf1[$key] = trim($value);
}
foreach ($f2 as $key => $value)
{
$trimf2[$key] = trim($value);
}
while (
$c1 < $max1 # have next line in left
&&
$c2 < $max2 # have next line in right
&&
$stop++ < 1000 # don't have more than 1000 ( loop-stopper )
)
{
/*
* ignore empty lines
*
if (empty($trimf1[$c1]))
{
$c1++;
}
elseif (empty($trimf2[$c2]))
{
$c2++;
}*/
/*
* is the trimmed line of the current left and current right line
* the same ? then this is a hit (no difference)
* /
else */ if ( $trimf1[$c1] == $trimf2[$c2] )
{
/*
* Add this line to output if "show_equal" is enabled.
* This is more for demonstration purpose
*/
if ( $show_equal )
{
$out[] = array('type' => '=', &$f1[ $c1 ]);
}
/**
* move the current-pointer in the left and right side
*/
$c1 ++;
$c2 ++;
}
/*
* the current lines are different so we search in parallel
* on each side for the next matching pair, we walk on both
* sided at the same time comparing with the current-lines
* this should be most probable to find the next matching pair
* we only search in a distance of 10 lines, because then it
* is in the same paragraph most of the time. other algos
* would be very complicated, to detect 'real' block movements.
*/
else
{
$b = array() ;
$s1 = 0 ; # search on left
$s2 = 0 ; # search on right
$b1 = array() ;
$b2 = array() ;
$fstop = 0 ; # distance of maximum search
#fast search in on both sides for next match.
while (
$c1 + $s1 < $max1 # we are inside of the left lines
&&
$c2 + $s2 < $max2 # and we are inside of the right lines
&&
$fstop++ < 10 # and the distance is lower than 10 lines
)
{
/**
* test the left side for a hit
*
* comparing current line with the searching line on the left
* b1 is a buffer, which collects the line which not match, to
* show the differences later, if one line hits, this buffer will
* be used, else it will be discarded later
*/
#hit
if (!empty($trimf1[$c1+$s1]) && $trimf1[$c1+$s1] == $trimf2[$c2] )
{
$c1 += $s1 - 1 ; # move forward the current left, so next loop hits
$c2-- ; # move back the current right, so next loop hits
$b = $b1 ; # set b=output (b)uffer
break ; # stop search
}
#no hit: move on
else
{
/**
* add current search-line to diffence-buffer
*/
$b1[] = array( 'type' => '-', &$f1[ $c1+$s1 ] );
}
/**
* test the right side for a hit
*
* comparing current line with the searching line on the right
*/
if (!empty($trimf2[$c2+$s2]) && $trimf1[$c1] == $trimf2[$c2+$s2] )
{
$c2 += $s2 - 1 ; # move forward the current right line, so next loop hits
$c1-- ; # move current left line back, so next loop hits
$b = $b2 ; # get the buffered difference
break;
}
else
{
/**
* add current searchline to buffer
*/
$b2[] = array( 'type' => '+', &$f2[ $c2+$s2 ] );
}
/**
* search in bigger distance
*
* increase the search-pointers (satelites) and try again
*/
$s1++ ; # increase left search-pointer
$s2++ ; # increase right search-pointer
}
/**
* add line as different on both arrays (no match found)
*/
if ( !$b )
{
$out[] = array('type' => 'c', &$f1[$c1], &$f2[$c2]);
}
/**
* add current buffer to outputstring
*/
else
{
$out = array_merge($out, $b);
}
$c1++ ; # move current line forward
$c2++ ; # move current line forward
/**
* comment the lines are tested quite fast, because
* the current line always moves forward
*/
} /* endif */
}/* endwhile */
// lines might juste have been removed at the end...
if ($c1 < $max1)
{
for ($i = $c1; $i < $max1; $i++)
{
$out[] = array('type' => '-', &$f1[$i]);
}
}
// ... or added
elseif ($c2 < $max2)
{
for ($i = $c2; $i < $max2; $i++)
{
$out[] = array('type' => '+', &$f2[$i]);
}
}
return $out;
}/* end func */
/**
* Computes the diff between two texts, line by line.
* The lines are supposed to be separated by a NL ("\n")
* @param string $textA The old text
* @param string $textB The new text
* @param boolean $show_equal If true, the result will also contain the common lines
* (default false)
* @return array An array of arrays describing the diff
* @see arr_diff for the return value
*/
function text_diff_by_lines($textA, $textB, $show_equal = false)
{
return arr_diff(explode("\n", $textA), explode("\n", $textB), $show_equal);
}
?>
- la fonction LoadRecentChanges à ajouter à la ClasseWiki (balises php ajoutées pour permettre la coloration syntaxique):
<?php
function LoadRecentChanges($limit = 50)
{
$limit = (int) $limit;
$prefix = $this->GetConfigValue('table_prefix');
$sql = 'SELECT a.*, max(b.id) previous_version '
. 'FROM ' . $prefix . 'pages a '
. 'LEFT JOIN ' . $prefix . 'pages b ON a.tag = b.tag AND a.id > b.id '
. 'WHERE a.comment_on = "" '
. 'GROUP BY a.id '
. 'ORDER BY a.time DESC '
. 'LIMIT ' . $limit;
if ($pages = $this->LoadAll($sql))
{
foreach ($pages as $page)
{
$this->CachePage($page);
}
}
return $pages;
}
?>
(notez que la partie de mise en cache n'est utile que si elle est combinée à une cache par id de version, et qu'elle risque de poser problème si le système de cache actuel n'est pas révisé. Je passerai au cvs ma version de la mise en cache des pages)
- l'action recentchangesrss.php:
<?php
/*
recentchangesrss.php
Copyright 2003 David DELON
Copyright 2005 Didier LOISEAU
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
if ($user = $this->GetUser())
{
$max = $user["changescount"];
}
else
{
$max = 50;
}
if ($pages = $this->LoadRecentChanges($max))
{
if (!($link = $this->GetParameter("link"))) $link=$this->config["root_page"];
echo "<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?>\n";
$output = '<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">' . "\n";
$output .= "<channel>\n";
$output .= "<title>Derniers changements sur ". $this->config["wakka_name"] . "</title>\n";
$output .= "<link>" . $this->Href(false, $link) . "</link>\n";
$output .= "<description>Derniers changements sur " . $this->config["wakka_name"] . " </description>\n";
$output .= "<language>fr</language>\n";
$output .= '<generator>WikiNi ' . WIKINI_VERSION . "</generator>\n";
foreach ($pages as $i => $page)
{
$output .= "<item>\n";
$output .= "<title>" . $page["tag"] . "</title>\n";
$output .= '<dc:creator>' . $page["user"] . "</dc:creator>\n";
$output .= '<pubDate>' . gmdate('D, d M Y H:i:s \G\M\T', strtotime($page['time'])) . "</pubDate>\n";
$output .= '<link>' . $this->Href(false, $page['tag'], 'time=' . rawurlencode($page['time'])) . "</link>\n";
$output .= '<description>' . ($page['previous_version'] ? 'Modification' : 'Création');
$output .= ' de ' . $page['tag'] . ' par ' .$page['user'] . ' le ' . $page['time'] . "</description>\n";
$output .= "<content:encoded><![CDATA[ ";
if ($page['previous_version'])
{
require_once 'diff.inc.php';
$previous = $this->LoadPageById($page['previous_version']);
$diff = text_diff_by_lines($previous['body'], $page['body'], false);
$htmlDiff = "<table style=\"width: 100%;\">";
$htmlDiff .= '<tr><th colspan="2" style="text-align: center;">Version du ' . $previous['time']
. '</th><th colspan="2" style="text-align: center;">Version du ' . $page['time'] . "</th></tr>";
foreach ($diff as $line)
{
$htmlDiff .= "<tr><td" . ($line['type'] == '+' ? ' colspan="2"> ' : '>- </td><td>' . htmlspecialchars($line[0])) . "</td>";
$htmlDiff .= '<td' . ($line['type'] == '-' ? ' colspan="2"> ' : '>+ </td><td>' . htmlspecialchars($line[(int) ($line['type'] == 'c')])) . "</td></tr>";
}
$htmlDiff .= "</table>";
unset($diff);
}
else
{
$htmlDiff = "<h2>Page créée le " . $page['time'] . ":</h2>";
$htmlDiff .= nl2br(htmlspecialchars($page['body']));
}
$output .= $htmlDiff . "]]></content:encoded>\n";
$output .= "</item>\n";
unset($pages[$i]);
}
$output .= "</channel>\n";
$output .= "</rss>\n";
echo $output ;
}
?>