Pour effectuer certaines tâches, disposer d’un corpus textuel est très utile, voire indispensable : faire des essais pour un projet dont le corpus de travail est en cours de constitution, tester des scripts et traitements, prendre en main un nouvel outil et/ou le comparer à d’autres outils similaires…
Ce sont des usages qui relèvent principalement des phases préparatoires d’un projet, et pour lesquels il est surtout important de pouvoir construire un corpus rapidement et sans trop d’effort. Ce n’est pas grave si sa constitution n’est pas particulièrement précise puisqu’elle ne nécessite qu’un investissement limité et que les résultats obtenus n’ont pas vocation à être exploités en conditions réelles.
Je vous présente ici un script Python de web scraping très simple, qui vous permettra d’obtenir un corpus textuel au format XML tout à fait adapté à ce type d’utilisation.
Le script ne sera pas reproduit intégralement dans cet article. Il est librement téléchargeable sur Github et Gitlab. Un exemple de fichier de sortie est également disponible (Github et Gitlab).
Le corpus XML
Le but sera ici d’obtenir un corpus au format XML regroupant le contenu textuel d’une sélection de pages web. Pour plus de facilité lors de l’utilisation d’outils de traitement de corpus, un identifiant sera ajouté, et l’URL sera elle aussi reportée dans le corpus.
La structure XML est aussi simple que possible. Elle comporte :
- Un élément racine appelé
CORPUS
. - Pour chaque page web scrapée, un élément
Page
porteur d’un attributid
. - Pour chaque élément
Page
, un sous-élémentURL
reprenant l’URL de la page web correspondante, et un sous-élémentText
contenant l’ensemble du texte extrait de la page.
<?xml version="1.0" encoding="utf-8"?> <CORPUS> <Page id="1"> <URL> <!-- URL de la page web --> </URL> <Text> <!-- Contenu textuel de la page web --> </Text> </Page> </CORPUS>
Prérequis
Le point de départ du traitement est une liste d’URL : il vous faudra simplement un peu de prospection pour sélectionner les pages dont vous voulez récupérer le contenu textuel pour constituer votre corpus. Cette liste correspond à la variable url_list
dans le script téléchargeable.
Pour l’exemple, j’ai associé à url_list
les URL de quatre pages Wikipédia.
url_list = ["https://ja.wikipedia.org/wiki/YOSAKOI", "https://ja.wikipedia.org/wiki/YOSAKOI%E3%82%BD%E3%83%BC%E3%83%A9%E3%83%B3%E7%A5%AD%E3%82%8A", "https://ja.wikipedia.org/wiki/%E9%B3%B4%E5%AD%90_(%E9%9F%B3%E5%85%B7)", "https://ja.wikipedia.org/wiki/%E3%82%88%E3%81%95%E3%81%93%E3%81%84%E7%A5%AD%E3%82%8A"]
Ces pages, relatives au thème du yosakoi1, sont en japonais. La langue du texte n’a pas d’incidence sur le fonctionnement du script.
Concernant l’installation Python 3, il faudra installer et importer :
- La librairie
requests
(documentation). BeautifulSoup
de la librairiebs4
(documentation).xml.etree.ElementTree
, qui fait partie de la librairie standard (documentation).
Le script Python
Comme annoncé dans le titre de cet article, une fois les librairies importées et votre liste d’URL déclarée, le traitement à proprement parler ne nécessite qu’une quinzaine de lignes de code.
Créer l’arborescence XML
Recréer la structure XML montrée précédemment constitue la majeure partie du traitement effectué par le script. L’élément racine, arbitrairement appelé CORPUS
, est à déclarer en premier avec une instance d’ET.Element()
2. Il est nécessaire de l’associer à une variable, ce qui permettra ensuite de créer des sous-éléments :
root = ET.Element("CORPUS")
Si comme dans l’exemple, vous voulez utiliser un identifiant chiffré pour chaque page, c’est le moment de le déclarer et lui attribuer sa valeur initiale.
On peut maintenant alimenter le corpus, en parcourant la liste url_list
avec une boucle for
pour créer un élément Page
pour chaque URL. Cet élément Page
est un sous-élément de l’élément racine CORPUS
(= variable root
), à déclarer avec ET.SubElement()
:
page = ET.SubElement(root, "Page")
L’attribut id
est ajouté avec la fonction set()
. Attention, comme ici sa valeur (stockée dans la variable id
) est un entier, il faut la convertir en chaîne de caractères3.
page.set("id", f"{id}")
Il suffit maintenant de répéter l’opération pour créer les sous-éléments URL
et Text
, en prenant soin d’indiquer l’élément parent approprié, soit page
(correspondant à l’élément Page
dans la structure XML). Ces sous-éléments n’ont pas d’attribut mais comportent du texte : l’URL de la page pour URL
, le contenu textuel extrait de la page pour Text
. Ce texte peut être ajouté en attribuant une valeur à l’attribut .text
de l’élément, soit par exemple pour URL
:
page_url = ET.SubElement(page, "URL")
page_url.text = url
Le sous-élément Text
est également déclaré via une instance de .SubElement()
4. Il doit recevoir le contenu textuel de la page web, qu’il va maintenant falloir extraire.
Extraire le contenu textuel de la page web
La récupération du texte intégral de la page web courante, débarrassé de son balisage HTML, s’effectue en ajoutant la ligne suivante :
page_text.text = soup(requests.get(url).text, "html.parser").text
Comme pour l’URL, le contenu est assigné à l’élément en associant une valeur à son attribut .text
, ici page_text.text
. Voici comment fonctionne cette commande qui permet de récupérer le texte d’une page web :
- La fonction
requests.get(url)
envoie une requête à l’adresse associée à la variableurl
, et renvoie la réponse reçue. En d’autres termes, elle permet d’accéder à l’URL. requests.get(url).text
semble correspondre au texte de la page. C’est le cas, dans une certaine mesure : il s’agit du texte avec tout son balisage HTML. Ce n’est pas exactement ce qu’on cherche à récupérer ici, il va falloir supprimer les balises HTML.- C’est une fonction de la librairie
bs4
qui va retirer le balisage, mais elle ne peut pas traiter directementrequests.get(url).text
qui est une chaîne de caractères. Cette fonction étant en fait un attribut de la classeBeautifulSoup
, il faut convertirrequests.get(url)
en instance d’objetBeautifulSoup
, ce qui est fait avecsoup()
dans le script téléchargeable. Concrètement,bs4
parse la structure HTML de la chaîne de caractères, il faut alors désigner également quel parser utiliser. Icisoup(requests.get(url).text, "html.parser")
5 contient toujours le texte intégral de la page avec son balisage, sauf qu’il ne s’agit plus d’une chaîne de caractères (str
) mais bien d’un objetBeautifulSoup
. On peut maintenant appeler l’attribut correspondant au texte sans balises… qui lui aussi s’appelle.text
. Le texte brut d’une page web s’obtient donc avecsoup(requests.get(url).text, "html.parser").text
.
Cette commande est à inclure dans la boucle traitant chaque URL de la liste, puisque qu’elle est à appliquer à chaque page web qui constituera le corpus.
Voici la boucle complète :
for url in url_list:
page = ET.SubElement(root, "Page")
page.set("id", f"{id}")
id += 1
page_url = ET.SubElement(page, "URL")
page_url.text = url
page_text = ET.SubElement(page, "Text")
page_text.text = soup(requests.get(url).text, "html.parser").text
Écrire le fichier de sortie
La structure XML et son contenu sont maintenant prêts et stockés dans la variable root
, correspondant à la racine de l’arborescence. Il n’est toutefois pas possible de l’enregistrer dans un fichier, car son type ne le permet pas. La valeur retournée par print(type(root))
est la suivante :
<class 'xml.etree.ElementTree.Element'>
Afin d’être écrit dans un fichier, cet élément doit être converti en chaîne de caractères (classe str
). C’est ce que permet de faire la fonction tostring()
de xml.etree.ElementTree
. Celle-ci prend en argument l’élément racine de l’arbre XML (root
dans le cas présent).
xml_output = ET.tostring(root)
Avec la liste d’URL fournie dans l’exemple, ET.tostring(root)
6 renvoie quelque chose qui ressemble à une très longue chaîne de caractères en une seule ligne. Sa longueur varie selon le nombre d’URL que comporte votre liste et de la quantité de texte sur chaque page. Il ne sera pas possible de l’écrire dans un fichier en l’état, car il ne s’agit pas vraiment d’une chaîne de caractères (str
) mais d’une séquence d’octets appartenant donc à la classe bytes
7.
Ajouter l’argument optionnel encoding="unicode"
suffit à corriger le problème. Le XML a bien été converti en chaîne de caractères.
xml_output = ET.tostring(root, encoding="unicode")
Un autre argument optionnel qui peut être ajouté est method
; dans le script téléchargeable, la valeur "xml"
lui a été attribuée. Sauf erreur de ma part, il s’agit de sa valeur par défaut et en l’état il n’est pas indispensable de le spécifier. Si, pour une raison ou une autre, vous voulez obtenir une version de votre corpus en texte brut, qui comportera le même contenu que l’arborescence XML mais sans aucune balise, vous pouvez lui attribuer la valeur "text"
.
Avant d’enregistrer le corpus dans un fichier, je vous conseille de lui appliquer la fonction prettify()
de bs4
. Encore une fois, il faut convertir la chaîne de caractères en instance d’objet BeautifulSoup
, en indiquant cette fois "xml"
pour le parser :
pretty_xml = soup(xml_output, "xml").prettify()
Cette étape de traitement supplémentaire vous permettra d’obtenir un fichier de sortie beaucoup plus lisible pour un œil humain, avec l’ajout de sauts de ligne et d’indentation suivant la structure. La déclaration XML sera aussi ajoutée automatiquement, ce qui est très appréciable.
Le résultat de prettify()
est une chaîne de caractères (str
), qui peut être enregistrée dans un fichier avec write()
.
Si vous avez utilisé ou recréé le script mis à disposition sans modifier la liste d’URL, vous devriez avoir obtenu un fichier XML semblable à l’exemple de fichier de sortie téléchargeable sur Github et Gitlab.
Obtenir un “meilleur” corpus
Le script présenté ici a l’avantage d’être très simple et de permettre de produire très facilement un corpus textuel issu du web. Le résultat obtenu est vraiment très basique : voici quelques suggestions d’amélioration ainsi que des pistes pour les mettre en place.
Prendre en entrée une (très) longue liste d’URL | Si vous avez un grand nombre d’URL à traiter, il peut être plus confortable de les stocker dans un fichier séparé. Vous pouvez par exemple les stocker dans un fichier en texte brut avec une URL par ligne qui sera ensuite lu ligne par ligne pour reconstituer la liste. Ou encore, constituer un fichier CSV et exploiter le module csv pour en lire le contenu (cet article pourrait vous aider). |
Suivre la progression du traitement | Avec un petit nombre d’URL le traitement est très rapide, mais avec une liste plus longue c’est assez pratique d’en connaître l’avancée. Le plus simple est certainement d’ajouter une instruction print(url) dans la boucle for pour afficher l’URL courante. |
Supprimer les éléments de navigation | Il existe des librairies Python consacrées au retrait de boilerplate, comme jusText ou boilerpy3. Vous pouvez aussi exploiter les fonctions de bs4 pour filtrer les éléments HTML en fonction de leur tag. |
Supprimer les lignes vides | Une solution possible est de mettre en place une boucle sur soup(requests.get(url).text, "html.parser").text.split('\n') pour filtrer les lignes sans texte avant d’associer une valeur à page_text.text . Si vous ne conservez que certains éléments HTML (cf ci-dessus), le filtrage des éléments sans texte peut aussi être effectué à ce moment-là. |
Ajouter des métadonnées | Rien n’empêche de créer une structure XML plus complexe en ajoutant des (sous-)éléments avec .SubElement() et/ou des attributs avec .set() pour ajouter des métadonnées telles que la source, l’auteur, la date, la langue, etc. Si ces métadonnées ont été recueillies en amont, elles peuvent être lues à partir d’un fichier d’entrée (par exemple si vous utilisez un fichier CSV comme source de la liste d’URL, des colonnes peuvent être consacrées aux métadonnées). |
Rendre les identifiants plus explicites | Un identifiant sous forme de chiffres est facile à mettre en place mais pas particulièrement explicite. En fonction de votre projet, il sera peut-être nécessaire de trouver une autre forme d’identifiant. Une solution pourrait être de déclarer la liste d’URL sous forme d’un dictionnaire pour produire des identifiants de type wikipedia_yosakoi_1 , wikipedia_yosakoi_2 (etc.), par exemple : url_list = {"wikipedia_yosakoi_": ["url1", "url2", "url3"]} |
Vérifier l’encodage des caractères | Le fichier de sortie est en UTF-8. Si votre corpus XML comporte des caractères mal reconnus, vérifier l’encodage d’origine peut aider à identifier d’où vient le problème. Vous pouvez récupérer l’encodage d’une page web avec requests.get(url).encoding . |
Ajouter un étiquetage morphosyntaxique | Le corpus construit ici comporte du texte non étiqueté, mais il est tout à fait possible d’exploiter les résultats d’un étiqueteur automatique pour mettre en place une structure XML qui intègre l’étiquetage du texte. La structure XML d’un token étiqueté pourrait être la suivante : <tagged_token> Vous trouverez sur ce carnet des explications pour utiliser certains étiqueteurs automatiques en Python : SudachiPy et spaCy pour le japonais ; PyNLPIR, Jieba et spaCy pour le chinois. |
- Le texte des pages Wikipedia est disponible sous licence CC BY-SA 3.0. Pour davantage d’informations, en français, sur le yosakoi (よさこい) : site web, page Facebook et chaîne Youtube de l’association Yosakoi Paris Hinodemai, page de la Fédération de Yosakoi de France (FYF). [↩]
ET
désignexml.etree.ElementTree
, qui a été importé comme suit :import xml.etree.ElementTree as ET
. [↩]- Conserver un entier ne poserait pas de problème à cette étape mais ferait planter le script lors de la préparation du XML pour l’écriture du fichier de sortie. [↩]
page_text = ET.SubElement(page, "Text")
[↩]- L’objet est instancié avec
soup
parce que l’import a été fait de cette façon :from bs4 import BeautifulSoup as soup
. Si on utiliseimport bs4
l’équivalent seraitbs4.BeautifulSoup(requests.get(url), "html.parser")
. [↩] - Comme avec soup, c’est la façon dont l’import a été fait qui permet d’utiliser
ET
pourxml.etree.ElementTree
. Cette commande est équivalente àxml.etree.ElementTree.tostring(root)
. [↩] - D’ailleurs, si vous l’affichez dans la console elle sera précédée de
b
, indiquant bien le typebytes
. De même,print( type(xml_output) )
renverra<class 'bytes'>
. [↩]
OpenEdition vous propose de citer ce billet de la manière suivante :
Alexander Delaporte (4 mai 2021). Produire un corpus web format XML en 15 lignes de code. Tekipaki. Consulté le 10 décembre 2024 à l’adresse https://doi.org/10.58079/up2k