Dans le cadre du chantier éditorial visant à faire migrer la “Revue Internationale de photolittérature”, actuellement publiée sur le CMS Wordpress, vers une solution de publication plus pérenne et davantage conforme aux standards de l’édition savante, une première étape consiste à récupérer les données à travers l’élaboration d’une chaîne de rétroconversion (HTML to MD/YAML).

En complément d’un précédent billet consacré aux enjeux de la déplateformisation des revue, ce nouveau billet présente la méthode adoptée pour mener le chantier de rétroconversion, tout en présentant les difficultés et les limites de l’exercice.

Méthodologie du chantier de rétroconversion

Le travail de rétroconversion mené avec la Revue Internationale de photolittérature s’est concentré sur la transformation du HTML de Wordpress vers les formats MD (corps de l’article) et YAML (métadonnées). Nous avons fait le choix d’abandonner la rétroconversion des contenus bibliographiques en BibTex faute de temps (des convertisseurs existent, comme celui-ci, mais le résultat n’est pas optimal et nécessite un important travail de retouche manuel). Si le HTML Worpress a servi de fichier source, c’est que les archives de la revue (qui nous auraient par exemple permis de repartir des fichiers words fournis par les auteurs) étaient incomplètes et que, surtout, des modifications mineures avaient pu être apportées directement sur le CMS en fin de processus éditorial (le fameux “dernier kilomètre” avant la publication, dont nous aurons sans doute l’occasion de reparler). Worpress propose un export XML qui aurait pu également constituer une bonne solution, mais le fichier obtenu était insuffisamment structuré. Le HTML s’est révélé plus facile à manipuler avec un outil de scrapping.

La méthodologie adoptée est la suivante :

(1) Automatisation de la récupération et de la conversion des articles (cf. ci-dessous le modèle de notebook documenté pour la conversion des articles par numéro) :

  • scrapping des articles par numéro à l’aide de la librairie Beautiful Soup
  • transformation en MD via pandoc

(2) Dépôt des articles sur l’outil Stylo

Je vais présenter pas à pas cette méthode, avant de revenir sur les difficultés et la limites de l’exercice.

Notebook documenté

Phlit-n1

Notebook pour récupérer le sommaire du numéro 1 et moissonner transformer en md l’ensemble des articles du numéro. On commence par récupérer toutes les url des articles du numéro deux, pour les stocker dans un dossier appelé “phlit1”. On affiche également la liste dans le notebook, afin de l’avoir à disposition.

import requests, re
import os.path
import pypandoc as ppandoc


# this is a utility method that fetches URL and stores them locally for next time
def fetch_url(url):
    numero = "phlit1"
    escaped = re.sub(r'\W+',"_",url)
    filename = os.path.join(numero, escaped)
    if os.path.exists(filename):
        with open(filename, 'r') as file:
            return file.read()
    else:
        if not os.path.exists(numero):
            os.makedirs(numero)
        request = requests.get(url)
        with open(filename, 'a') as file:
            file.write(request.text)
            return request.text
from bs4 import BeautifulSoup
html = fetch_url("http://phlit.org/press/?post_type=numerorevue&p=2786")
soup = BeautifulSoup(html, 'lxml')

def scrap_link(sommaire):
    malist = []
    for article in sommaire:
           malist.append(article.a['href'])
    return malist


sommaire_dossier1 = soup.find_all('div', attrs={'class': 'article_details'})
uri_dossier1 = scrap_link(sommaire_dossier2)
print(uri_dossier1)
['http://phlit.org/press/?post_type=articlerevue&p=2870', 'http://phlit.org/press/?post_type=articlerevue&p=2872', 'http://phlit.org/press/?post_type=articlerevue&p=2868', 'http://phlit.org/press/?post_type=articlerevue&p=2851', 'http://phlit.org/press/?post_type=articlerevue&p=2841', 'http://phlit.org/press/?post_type=articlerevue&p=2835', 'http://phlit.org/press/?post_type=articlerevue&p=2827', 'http://phlit.org/press/?post_type=articlerevue&p=2823', 'http://phlit.org/press/?post_type=articlerevue&p=2819', 'http://phlit.org/press/?post_type=articlerevue&p=2877', 'http://phlit.org/press/?post_type=articlerevue&p=2817']

On définit une fonction qui va récupérer la page entière de chaque article, url par url, et la stocker dans un dossier “articles”. Pour le moment, on ne transforme rien : le fichier récupéré est en HTML, on a “tout” pris (échelle de la page). On change simplement le titre du fichier : numéro de dossier + identifiant. Les identifiants sont ceux qui ont été générés par le wordpress Phlit, de manière à garder une traçabilité.

def fetch_one_url(url):
    data = "articles-numero1"
    escaped = re.sub(r'\W+',"_",url)
    numero = "phlit1"
    filename = numero + "-" + url[-4:] + ".html"
    filename = os.path.join(data, filename)
    if os.path.exists(filename):
        with open(filename, 'r') as file:
            return file.read()
    else:
        if not os.path.exists(data):
            os.makedirs(data)
        request = requests.get(url)
        with open(filename, 'a') as file:
            file.write(request.text)
            return request.text

La page récupérée ci-dessus comprend de nombreuses données qui ne sont pas pertinentes et concerne l’environnement du site (headers, footers, menus, etc.) Dans cette nouvelle fonction get_article, il s’agit donc de récupérer l’article (il restera encore des données à nettoyer, mais on opère ainsi un premier ménage).

#fonction pour récupérer les éléments html pertinents (article)

def get_article(urlarticle):
    html = fetch_one_url(urlarticle)
    soup = BeautifulSoup(html, 'lxml')

    ArticleBlop = soup.find("div", {"class": "wp-article"})
    return ArticleBlop

Le texte obtenu à présent est plus précis, mais il comprend encore de nombreuses donneés hétérogènes. Parmi celles-ci, on note la présence de métadonnées qui sont précieuses. La fonction ci-dessous va les récupérer et les insérer dans un yaml (le format de métadonnées que prend Stylo).

# métadonnées

def get_metadata(articleblop, uri):
    metadata = articleblop.find_all("div", {"class": "article_content"})
    resume = metadata[0].text # le résumé brut
    resume = " ".join(resume.split()) # on enlève les espaces en trop
    resume = resume.replace('Résumé : ', '', 1) # on enlève l'entête

    motscle = metadata[1].text
    motscle = " ".join(motscle.split())
    motscle = motscle.replace('mots-clés : ', '', 1)

    auteurs = metadata[2].find_all("a", {"class": "popup_info"})

    yaml = "---" + "\n- abstract: >-\n  " + resume + "\n- keywords: " + motscle

    for auteur in auteurs:
        # print("---", auteur.text, " : ", auteur['data-info'])
        sauteur = auteur.text
        sauteur = " ".join(sauteur.split())
        yaml = yaml + "\n- author: " + sauteur + "\n  bio: >-\n    " + auteur['data-info']

    yaml = yaml + "\n---"
    
    return yaml

Reste un dernier nettoyage à faire, dans cette fonction clean_article, qui va ôter les derniers éléments parasitaires.

#nettoyage

def clean_article(articleblop):
    menu = articleblop.find("div", {"class": "menu-menu-revue-container"}) 
    image = articleblop.find("img", {"class": "image-full"})
    titreAuteur = articleblop.find_all("h1")[0]
    borderBottom = articleblop.find("div", {"class": "border_bottom"})
    abbr = articleblop.find("abbr", {"class": "unapi-id"})

    menu.decompose()
    image.decompose()
    titreAuteur.decompose()
    borderBottom.decompose()
    abbr.decompose()

    for metadata in articleblop.find_all("div", {"class": "article_content"}):
        metadata.decompose()
        
    return articleblop

On définit ensuite une fonction qui va permettre de convertir les HTML en markdown (le format pivot de Stylo), à l’aide de pypandoc.

On ajoute plusieurs regex afin de traiter le cas particulier des notes de bas de pages, qui sont souvent mal formées et n’ont pas été directement converties par pypandoc.


def convert_md(html):
    
    pdoc_args = ['--wrap=none',
                '--atx-headers']
    output = ppandoc.convert_text(html, 'md', format='html', 
  extra_args=pdoc_args)
    
    output = re.sub(r'applewebdata://[0-9A-Z-]*', r'', output) # pour une variante des nbsp écrites en ref
    output = re.sub(r'\[\\\[([0-9]{1,2})\\\]\]\(#_ftn\d*\)\{#_ftnref\d*\}', r'[^\1]', output) # pour une variante des nbsp écrites en ref
    output = re.sub(r'\[\\\[([0-9]{1,2})\\\]\]\(#_ftnref\d*\)\{#_ftn\d*\}', r'[^\1]:', output) # idem 
    output = re.sub(r'\[\\\[([0-9]{1,2})\\\]\]\(#_edn\d*\)\{#_ednref\d*\}', r'[^\1]', output)
    output = re.sub(r'\[\\\[([0-9]{1,2})\\\]\]\(#_ednref\d*\)\{#_edn\d*\}', r'[^\1]:', output)
    output = re.sub(r'\n\n\\', '', output) # supprime les backslash et espace qui trainent
    output = re.sub(':::.*', '', output) # supprime les divs (on doit pouvoir le faire avec pandoc directement

    return output

Enfin, on fait tourner un script à partir de toutes ces fonctions, qui va chercher tous les articles un par un pour effectuer ces transformations et conversions.

# fonction get et clean dossier

for uri in uri_dossier1:
    article = get_article(uri)
    yaml = get_metadata(article, uri)
    cleanarticle = clean_article(article)
    mdArticle = convert_md(cleanarticle)
    # on ecrit le md dans un fichier
    data = "articles-numero1"
    escaped = re.sub(r'\W+',"_", uri)
    numero = "phlit1"
    filename = numero + "-" + escaped[-4:] + ".md"
    filename = os.path.join(data, filename)
    # Ouvre le fichier en écriture (pour écraser)
    f = open(filename, "w")
    f.write(yaml) # on ajoute le yaml
    f = open(filename, "a") # ouvre le fichier en "append" (pour ajouter à la suite)
    f.write(mdArticle) # on ajoute le markdown
    #f = open("2870.md", "r") # ouvre le fichier en lecture seule
    #print(f.read()) 
    f.close()

Une révision manuelle indispensable

À noter : le résultat obtenu après moissonnage et conversion des HTML est plus ou moins satisfaisant. Globalement, les fichiers markdown obtenus étaient propres. Mais en raison de la très faible structuration des articles sources, il est nécessaire de reprendre à la main un certain nombre d’éléments, notamment dans les métadonnées qui n’ont pas pu être récupérées dans leur ensemble. Par ailleurs, la conversion des notes n’a pas toujours bien fonctionné, comme on peut le voir ici :

Exemple d'erreur dans la conversion des notes.

Le notebook a pourtant intégré de très nombreuses regex afin de prendre en compte les différentes particularités des appels de note (qui pouvaient prendre différentes formes au sein d’un seul et même article). Notons que parmi ces multiples cas particuliers, certains ont gardé l’empreinte de leur inscription technique initiale (où l’on peut repérer différences mises à jour du WordPress, mais également les différents formats et/ou systèmes informatiques). Apple, en particulier, laisse des traces en plusieurs endroits, en laissant des caractères spéciaux en plusieurs endroits :

Imgur.

Enfin, sur des articles anciens, il a fallu nettoyer de nombreuses traces d’un balisage graphique pas toujours très heureux :

Reliquat de HTML dans les fichiers md.

Dans un second temps, les articles ont été déposés sur Stylo, un outil d’édition développé par la CRC sur les écritures numériques en collaboration avec Huma-Num. D’aucuns pourraient objecter que ce choix conduit inévitablement à une re-plateformisation. Stylo n’impose cependant aucun workflow particulier et ne se pense pas comme une plateforme de publication, mais plutôt comme un assistant pour l’édition dans des formats plain/text. Le petit éditeur YAML inclus dans le logiciel permet par exemple de structurer correctement ce format très sensible à l’indentation (une source d’erreur récurrente). Stylo ouvre par ailleurs la porte à de nombreuses solutions de publication en HTML, PDF, XML… Un export vers Lodel a même été prévu par les concepteurs. Dans le cas de la Revue Internationale de photolittérature, nous souhaitons cela dit expérimenter d’autres solutions : en particulier, une publication sous forme de site statique, mais également un site généré à partir de l’API de Stylo. Ce sera l’objet de prochains posts.

Bilan

En conclusion, le chantier de rétroconversion s’est avéré un peu plus complexe que prévu, en raison tout d’abord de la très faible structuration des fichiers sources, qui n’a pas permis l’autonomisation complète du processus de transformation des articles. Une révision manuelle des articles s’est révélée indispensable. Cette révision a par ailleurs permis de repérer des erreurs d’édition ou des lacunes dans les métadonnées. Celles-ci s’expliquent en partie par la jeunesse de la revue (4 numéros + une base de 45 articles publiés ou republiés avant la création officielle de la revue), ainsi que par sa réalisation entièrement assurée par des chercheurs, sans l’assistance d’éditeurs professionnels. Par ailleurs, ce chantier laisse en suspens la question des illustrations des articles – un sujet qui fera l’objet d’un prochain post également. Ajoutons, pour terminer, que ce travail de rétroconversion a permis d’opérer une plongée passionnante dans les archives de la revue et, plus largement, de la communauté des chercheurs en photolittérature, en faisant émerger des récurrences entre les textes. De quoi alimenter la construction de la nouvelle revue.