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.
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.
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.
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.
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>"<la véritable url>"">
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.
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.
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.
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:
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.
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.
pip freeze du virtualenv utilisé