Introduction

Il y a quelques semaines, elle me parle de Thierry qui a des soucis avec son blog hébergé sur le site de libération. En effet la plupart des liens sont morts et/ou redirigent ailleurs, et il n'a plus accès à ses anciens articles. Avec un peu de bidouille, elle avait déjà réussi à reconstituer une liste d'url vers la totalité des articles du blog.

Bref, le défi - reconstituer le blog - semblait être intéressant.

Wget

La première étape fut de prendre en entrée la liste d'URL, et de tout scraper comme un bourrin avec Wget, mixant un peu les options entre mode offline, spider et d'autres. J'ai perdu la commande utilisée malheureusement.

En revanche, j'avais à peu près le corpus des articles, mais perdu dans un mélange de HTML, de Javascript et de JSON peu exploitables.

Parsing

J'ai mis un exemple de page "brute" récupérée dans ce dossier (cf sample-page.html). Parfois il faut supprimer la popup à la main dans la console développeur pour arriver à voir quelquechose, parfois il faut carrément désactiver Javascript.

Mais bref, dans chaque fichier index.html récupéré, il y a quelquechose qui semble contenir de quoi reconstituer les posts de blog. Mais pour le moment, ouvrir les fichiers html à la main donne d'une part un résultat assez aléatoire, et d'autre part, c'est assez peu exploitable comme sauvegarde.

Fusion ?

En creusant un peu leur structure, on se rend assez vite compte que les pages sont construites à partir d'objets Javascript. Elles en sont d'ailleurs truffées de scripts divers, mais un motif revient régulièrement dans l'une des balises <script> de la page.

cf ci-après pour un exemple, que je me suis permis de réindenter:

window.Fusion=window.Fusion||{}; Fusion.arcSite="liberation"; Fusion.contextPath="/pf"; Fusion.deployment="19"; Fusion.globalContent={...gigantesque objet JSON }

Fusion semble donc être le nom du moteur Javascript permettant de rendre les blog posts.

La première étape consiste donc à utiliser python-beautifulsoup pour extraire ce premier payload.

JSON or not JSON ?

L'étape suivante a été de tenter d'évaluer le payload précédent en le passant dans une machine virtuelle Javascript pour Python. Malheureusement, l'objet Fusion.globalContent n'est pas un objet JSON valide.

En effet, dans les éléments formant les posts de blog, typiquement les objets de type "text" (qui correspondent à des paragraphes, on y reviendra par la suite), à chaque fois qu'une url est référencée dans par exemple une balise html <a>, le lien est de la forme suivante:

<a href="<url du blogpost courant>&quot;<la véritable url>&quot;">

Evidemment, le parser Javascript utilisé lève une erreur de syntaxe, car la chaine de caractère se termine brutalement par l'arrivée de la double-quote.

J'ai donc commencé par écrire une méthode permettant de faire en sorte que le javascript récupéré puisse être valide, en utilisant de simples appels à string.replace().

Une fois les 140 posts de blog validés par l'approche (je me suis permis d'en modifier un, car les émojis utilisés dedans faisaient également planter le parseur), nous étions en possession de cette variable Fusion.globalContent qui nous permet de reconstituer le blogpost.

globalContent

Le parseur Javascript utilisé m'a permis de récupérer sous forme d'objet python le payload Javascript précédent. On peut donc effectuer la démarche inverse, et reserialiser l'objet en JSON dans un fichier.

J'ai ensuite utilisé l'utilitaire jq pour voir à quoi cela ressemblait. J'ai retrouvé un certain motif sur les objets des différents blogposts, qui m'ont permis d'écrire un template Jinja2 (cf annexes ci-dessous) assez basique:

* titre premier niveau: <objet_json.label.basic.text> soit ici "Ma lumière rouge" * titre second niveau: <objet_json.headlines.basic>, le titre du post * titre 3eme niveau: <objet.json.subheadlines.basic>, un sous-titre pour le post * publié le <date de publication>, écrite dans un format "francais", pas ISO8601 * si le blog possède un <promo_items>, ainsi qu'un <promo_items.basic.additional_properties>, alors on a une image d'illustration que l'on inclue. * si le blog ne possède juste un <promo_items>, alors on affiche le <promo_items.basic.caption>. * On itère ensuite sur tous les éléments du tableau <content_elements> * si l'élément est de type text, on met <element.conent> entre deux balises <p> * s'il est de type image, on ajoute l'image * de type quote, on le met dans une div avec une couleur lègerement grisée * de type "oembed_response" on le met dans une div et on va chercher <item.raw_oembed.html>

J'ai rajouté un peu de twitter-bootstrap pour le coup de peinture CSS.

Après divers essais et erreurs sur les différents types d'objets constituant les posts, j'ai fini par avoir un rendu à peu près acceptable.

Relogeage des ressources

Nous avons vu précédemment que dans un certain nombre de cas, les urls vers les ressources étaient broyées, volontairement ou non. Je n'arrive pas à imaginer la raison qui a poussée les développeurs chez Libération à faire cela, et pour l'instant nous avons juste fait en sorte de pouvoir produire un JSON valide de nos données.

En tout état de cause, une fois l'objet JSON valide, il nous est posible de récupérer l'URL du blog courant sauvée dans une propriété de l'objet, de repasser sur l'objet Python généré afin de supprimer les morceaux d'url en trop, ainsi que les caractères encodés html """ mentionnés plus haut, sur les différents items du post.

Après cette étape, nous nous retrouvons avec des liens dans les articles fonctionnels, et qui ne pointent plus n'importe où.

Mais un certain nombre de ressources sont encore chargées sur des serveurs distants.

Sauvegarde des ressources

Nous avons dorénavant des articles de blog reconstitués, certes, mais certaines ressources, notamment les images sont encore situées sur des serveurs en ligne.

On peut alors repasser à nouveau sur l'objet python, récupérer les URLs des images, les télécharger dans un sous-répertoire images/ et corriger les liens, et cela avant de passer par l'étape de templating Jinja.

Et après cela, on est à peu près bon:

  • les posts de blog sont reconstitués "à peu près"
  • les images sont en local

Améliorations possibles

  • Certains objets dans les blogs sont de type <twitter-widget>. Je n'ai pas trouvé chez Twitter de bibliothèque JS permettant de rendre automatiquement ces objets dans la page.

  • Un certain nombre d'articles référencent des articles plus anciens, mais toujours avec les liens qui pointent chez Libération. On pourrait les réécrire pour pointer chez nous, connaissant dorénavant comment le site est construit.

Epilogue

J'ai l'impression d'avoir fait le tour, mais j'avais envie de vérifier quelquechose avant de tourner la page: tester si cela fonctionne avec n'importe quel blog Libération.

J'ai donc testé en ouvrant la page suivante: https://www.liberation.fr/debats/2020/12/16/une-petite-histoire-du-banjo_1815578/

J'ai sauvegardé la page, passé le fichier html en argument de mon script python. Et ca a fonctionné !

J'ai également testé sur un article du journal, et j'ai obtenu à peu près le rendu, d'origine, sans les publicités. Bien entendu l'article était réservé aux abonnés, mais le rendu fournissait les mêmes infos que sur la page originale.

Annexes