Temps de lecture : 7 minutes

Bonjour,

Pour inaugurer mon nouveau petit blog, qui n’a pas la prétention de devenir grand, je vous propose d’étudier la mise en place de la diffusion de titres principaux de la presse française, à l’aide du langage de programmation PHP, qui permet de produire des pages web dynamiques.
C’est en effet un exercice que j’ai mené ces derniers jours, avec un certain succès qui me satisfait amplement 🙂

Introduction

Le but initial était de remplacer le flux RSS de Google News, dont la structure a changé et qui ne me permet plus d’accéder aux titres aussi simplement qu’auparavant. Pour faire simple, je me suis dit que c’était une bonne occasion de changer de crèmerie !
Donc la première étape était de reconstituer un nouveau flux RSS sur le même principe que Google News, à savoir proposant une sélection d’articles issus de divers titres de la presse française.
Et puis, puisque je maîtrise aussi un peut peu la publication automatisée de posts sur le réseau social Mastodon, je me suis dit : « après tout, pourquoi pas en profiter pour faire un petit robot de diffusion des mêmes titres d’actualité sur Mastodon, via un compte robot dédié ? » Mais nous verrons çà dans un deuxième temps…

La recherche d’une nouvelle source

Première étape, la recherche d’une Interface de Programmation (API) qui va me permettre de récupérer les articles. En impératif : son usage doit être gratuit pour mes modestes besoins à usage non lucratif (ne générant aucun revenu), tout en proposant des articles dès leur publication, sans délai.

Mon choix c’est donc porté sur l’API de https://gnews.io (aucun lien de parenté avec Google News), qui coche toutes les cases, et est gratuit pour un usage allant jusqu’à 100 requêtes par jour (ce qui, nous allons le voir, est bien suffisant pour mon usage).

Et le point de départ consiste à récupérer les titres d’actualité (et leur résumé) depuis l’API. Ceci se fait avec cette simple requête :

$content = @file_get_contents("https://gnews.io/api/v4/top-headlines?apikey=xxxxxxxxxxxxxxxxxxxxxxxxxxxx&country=fr");

En utilisant sa propre clé d’API que le service fournit à la souscription du service.

Génération du flux RSS

D’abord, pour les personnes qui voudraient en savoir plus sur les flux RSS,  leur utilité et leur fabrication, çà se passe ici. J’insiste sur la partie fabrication, dont le procédé doit être maîtrisé pour construire son propre flux. Nous allons donc construire un fichier XML avec quelques balises spécifiques aux flux RSS.

Sans rentrer dans le détail du code, ce fichier XML va donc être construit avec un objet PHP standard qui va bien faciliter la tâche, SimpleXMLElement. Celui-ci permet de créer des balises simplement, sans avoir à écrire toute la syntaxe associée.
Ainsi, la génération du fichier débute ainsi :

//Create the RSS feed
$xmlFeed = new SimpleXMLElement('<!--?xml version="1.0" encoding="UTF-8"?--><rss version="2.0"></rss>');
$xmlFeed->addChild("channel");

L’idée est ensuite de parcourir chaque élément (article) fourni par l’API et d’ajouter toutes les informations pertinentes dans la structure du RSS :

foreach ($jf['articles'] as $item) {
 $newItem = $xmlFeed->channel->addChild('item');

if (isset($item['title'])) $newItem->addChild('title', $item['title']);
if (isset($item['source'])) $newItem->addChild('author', $item['source']['name']);
if (isset($item['content'])) $newItem->addChild('description', $item['content']);
if (isset($item['publishedAt'])) $newItem->addChild('pubDate', $item['publishedAt']);
if (isset($item['url'])) {
    $newItem->addChild('link', $item['url']);
    $newItem->addChild('guid', $item['url']);
}

Il y a là des contrôles d’existence mais ces informations sont toujours présentes dans les résultats fournis l’API.
Notez l’utilisation de la balise GUID : cet identifiant unique permet au lecteur RSS d’identifier un article, lors de l’analyse du flux, si un article précédemment enregistré revient plusieurs fois entre plusieurs requêtes, ce GUID permet au lecteur RSS de déterminer qu’il a déjà été stocké et qu’il n’est pas nécessaire de l’enregistrer une seconde fois.

Enfin, quelques commandes PHP sont nécessaires pour retourner des données XML propres :

$dom = new DOMDocument("1.0");
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($xmlFeed->asXML());
return $dom->saveXML();

Et voilà ! Il ne reste plus qu’à appeler la fonction de construction du RSS en lui passant en entrée la réponse de l’API contenant les articles de presse à traiter ($content, voir le premier extrait de code de l’article), et à afficher directement le résultat, ce dernier étant le XML constituant le flux RSS :

echo convert_jsonfeed_to_rss($content)."\n";
D’une pierre deux coups !

Et le traitement aurait pu s’arrêter là… mais puisque nous avons récupéré des informations de l’API de GNews.io, pourquoi pas en profiter pour les exploiter une 2è fois, sans nécessiter de refaire un (coûteux) rappel d’API.
En résumé, nous avons :
– les titres d’actualité renvoyés par l’API sous forme de données JSON
– un flux RSS construit à partir de ces données
Et donc, toujours ces données à dispositions, stockées dans la variable $content. Du coup, c’est parti pour un 2è traitement !

Nouvelle destination, nouveau format

En plus, c’est plus simple, du moins avec une petite librairie PHP qui va faire le gros du boulot. Exit le formalisme du XML utilisé par les flux RSS, ici il va suffire de réunir quelques informations et de les envoyer à mon instance Mastodon pour publication.

Et ca commence par quelques petite initialisations simples :

// Mastodon configuration
$token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // Token of your Mastodon bot account
$baseURL = 'https://your.instance'; // URL of your instance (Do not include '/' at the end.)
$privacy = 'public'; // "Direct" means sending message as a private message. The four tiers of privacy for toots are public , unlisted, private, and direct
$language = 'fr'; // en for English, zh for Chinese, de for German etc.
$mastodon = new MastodonAPI($token, $baseURL);

Rien de bien compliqué, à partir du moment où on a compris la notion d’instance Mastodon et de token, un jeton qui permet de communiquer avec un compte précis, sans utiliser la combinaison classique nom d’utilisateur/mot de passe.
Et la dernière ligne fait référence à librairie PHP que j’ai utilisé pour communiquer avec Mastodon, elle se trouve ici (ce n’est sans doute pas la plus évoluée, mais elle suffit à notre besoin).

Et on repart, comme pour le RSS, à une analyse et extraction de données, article par article :

foreach ($jf['articles'] as $item) {
    $itemUrl = $item['url'];
    $itemTitle = $item['title'];
    $itemSource = $item['source']['name'];
    $source_lower_nospace = strtolower(str_replace(' ', '', $itemSource));        // Enlève d'éventuels espaces dans le nom de la source, et la passe en minuscules, pour la citer en hashtag
    $source_hashtag = '#' . $source_lower_nospace;                    // Ajoute le dièse du hashtag
    $itemDatePub = date('d/m H:i', strtotime($item['publishedAt']));
    $itemContent = $item['content'];
    $statusText = '...';    // Mise en forme libre du contenu du toot
    $statusData = [
        'status'      => $statusText,
        'privacy'     => $privacy,
        'language'    => $language,
    ];
}

Et ca s’arrête là s’il n’y a pas d’image à intégrer. Il va falloir quelques lignes en plus pour intégrer une image d’illustration de l’article. Il faut déjà la récupérer, puis l’envoyer sur Mastodon (attention, le $statusData est spécifique à l’envoi d’image, il ne complète pas celui créé sans image) :

if(isset($item['image'])) {
    $imgname = basename($stripped_url);
    copy($item['image'], $imgname);
    $curl_file = curl_file_create($imgname, 'image/jpg', $imgname);
    $body = [
        'file' => $curl_file,
        'description' => 'Image d\'illustration de l\'article',
    ];
    $response = $mastodon->uploadMedia($body);
    unlink($imgname);
    $file_id = $response['id'];
    $statusData = [
        'status'      => $statusText,
        'privacy'     => $privacy,
        'language'    => $language,
        'media_ids[]' => $file_id,
    ];
}

Après quoi il ne reste plus qu’à envoyer le toot préparé, ce qui se fait en une ligne avec notre librairie dédiée :

$mastodon->postStatus($status)

Petite subtilité supplémentaire, le code ci-dessus s’applique pour un seul toot, qui lui même ne référence qu’un article. Il faut donc répéter cette préparation et l’envoi vers Mastodon pour chaque article à publier, dans une boucle.

Une fois la fonction terminée, un simple appel dans le corps principal du script suffit, avec en paramètre la même variable $content que celle utilisée lors de la génération du RSS, contenant donc les mêmes données :

$result = send_json_feed_to_mastodon($content);

Et voilà pour les grands principes. Mais attention, ce que l’on gagne en formalisme, on le perd en contrôle de ce qui a été précédemment publié. Ce contrôle de publication est automatiquement géré par le client RSS, via la balise GUID. Ici pas de fonctionnalité similaire, nous devons donc coder nous même ce contrôle.

Gestion du cache local

Pour ma part, je fais ca de manière très simple à l’aide d’un fichier cache unique contenant l’URL de chaque article publié sur Mastodon, ainsi que l’horodatage associé à cette publication.
Ainsi, lorsque le traitement se fait, pour déterminer le besoin de publication d’un article, le traitement va vérifier si l’URL de l’article se trouve dans le fichier cache. Si c’est le cas, l’article sera ignoré pour ne pas être publié une seconde fois.

Ainsi, le contrôle suivant est réalisé, pour chaque article à publier :

$cached = 0;
// Gestion du cache : vérifie si l'article a déjà été publié, le traitement ne continue que si ce n'est pas le cas
$cache_file = fopen($cache_file_path, "r") or die("Unable to open cache file!");
while (($line = fgetcsv($cache_file,NULL,";")) !== FALSE) {
    $url_cache = $line[0];
    if ($itemUrl == $url_cache) {
        $cached = 1;    // Article déjà publié
        break;
    }    
}
fclose($cache_file);
        
if ($cached == 0) {    // Article pas encore publié
     ........ (préparation du toot à publier)
}

Et ce n’est pas fini. Si on ne veut pas que le cache grossisse à l’infini, il faut régulièrement le purger des articles les plus anciens, pour lesquels on sait que l’API ne renverra plus la référence (l’API ne retourne que des articles récents).

Voici donc les quelques lignes de code que j’utilise pour gérer ce nettoyage (en créant un second fichier cache purgé des articles à supprimer, puis en remplaçant le contenu de l’ancien cache par le nouveau) , sachant que le cache a la structure d’un fichier CSV, chaque ligne du cache contient l’URL de l’article, un séparateur et sa date de mise en cache :

// Gestion du cache : supprime les entrées obsolètes (>24h)
$now = date_create(date('Y-m-d', strtotime("now")));
$cache_file = fopen($cache_file_path, "r") or die("Unable to open cache file!");
$new_cache_file = fopen($new_cache_file_path, "w") or die("Unable to open temp cache file!");
while (($line = @fgetcsv($cache_file,NULL,";")) !== FALSE) {    // Lecture de chaque ligne du cache
    $date_cache = $line[1];
    $date_article = date_create(date('Y-m-d', $date_cache));
    $diff = $date_article->diff($now);
    $daydiff = $diff->format('%a');
    if ($daydiff == 0) {    // Si l'article a moins de 24h, on le conserve dans le cache (réécriture dans un cache temporaire)
        fwrite($new_cache_file, $line[0].';'.$line[1]."\n");
    }    
}
fclose($new_cache_file);
fclose($cache_file);
rename($new_cache_file_path, $cache_file_path);      // Transforme par renommage du fichier le cache temporaire en cache définitif, purgé des entrées obsolètes.

Et voilà, ainsi s’achève cet article. J’espère qu’il vous aura été utile, dans le cas où vous auriez un besoin similaire.

Un commentaire sur “PHP : Comment faire un robot de diffusion d’actualités issues de la presse française”

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *