Simon CHABROL

Écriture et recherche indépendante (FR/EN)

Technicien de support IT

Dans cet article, nous allons apprendre à traiter des fichiers HTML et XML dans le but d’analyser leur syntaxe et extraire de l’information. Bien que les formats HTML et le XML soient différents de par leurs objectifs, ils possèdent toutefois un point commun majeur : l’information est structurée et hiérarchisée à l’aide de balises. Ces particularités, comme vous allez le voir, obligent à mettre en œuvre des techniques spécifiques. Nous ferons donc d’abord un rappel de ce que sont le XML et le HTML, comment ces syntaxes à l’aide de balises fonctionnent, puis nous passerons à la pratique en développant un outil de traitement compatible avec ces deux syntaxes.

Table des matières :

  1. Qu’est-ce que le XML et le HTML ?
  2. Construction d’un outil de traitement en Javascript
  3. Conclusions

Qu’est-ce que le XML et le HTML ?

Avant de commencer à construire un outil de traitement du XML et du HTML, il est important de rappeler ce que sont ces deux formats. Il s’agit donc de deux formats de données où l’information est organisée à l’aide de balises. Le XML (pour “Extensible Markup Language”) désigne un format d’échange de données qui fait appel à des balises pour identifier des éléments et sous-éléments. Voici un exemple de fichier XML :

<mail>
<to>Simon</to>
<from>Gerard</from>
<titre>Hello</titre>
<body>Hello</body>
</mail>

Il s’agit ici d’un email au format XML. Nous avons donc un grand ensemble “mail” qui est désigné par les balises “mail” (pour le début) et “/mail” (pour la fin). Ce grand ensemble contient plusieurs sous-ensembles. Le premier est “to” (délimité par les balises “to” et “/to”) dont le contenu est “Simon”. Le second est “from” (délimité par les balises “from” et “/from”) dont le contenu est “Gérard”. Et ainsi de suite. Chaque élément est donc identifié par une balise qui désigne un nom d’objet (par exemple “from”) avec une valeur associée (par exemple “Gérard”). Le format XML est couramment utilisé pour transférer des données, au même titre que le format JSON par exemple. On le retrouve notamment comme format d’import et d’export des données de WordPress. Le format HTML (pour “HyperText Markup Language”) désigne quant à lui un langage utilisé pour générer des pages internet. Ce dernier fait également appel à des balises pour désigner les éléments à afficher sur une page, en tenant compte également d’une certaine hiérarchie. Exemple d’un fichier HTML :

<html>
<body>
<h1>
Titre
</h1>
<p>
Paragraphe
</p>
</body>
</html>

Il s’agit d’une simple page qui comporte un titre et un paragraphe. Nous avons donc ici un grand élément “html” qui représente l’ensemble de la page à afficher. Cet élément contient un sous-ensemble dénommé “body” qui contient le corps de la page internet. Ce sous-ensemble étant lui-même composé de deux sous-ensembles : un titre “h1” et un paragraphe “p”. Le format HTML est indispensable au fonctionnement d’internet. Pour un certain nombre de raisons, on peut avoir besoin de réaliser des traitements sur des fichiers HTML si on souhaite par exemple extraire des informations de façon automatique. Comme vous pouvez le voir, la hiérarchisation est assez nette. Ces fichiers fonctionnent comme des poupées russes, où les éléments s’emboîtent les uns dans les autres. En anglais, on parle souvent de “parent” pour une racine, et de “children” pour les dépendances. Pour comprendre comme nous allons devoir procéder, partons de l’exemple de la page HTML, que j’affiche à nouveau ici :

<html>
<body>
<h1>
Titre
</h1>
<p>
Paragraphe
</p>
</body>
</html>

On va donc d’abord partir du premier élément, à savoir la balise “html”. Comme il s’agit de la première ligne (et donc de la première balise) on peut supposer qu’il s’agit de la racine. On constate d’ailleurs qu’elle est suivie d’une autre balise que l’on va dire “ouverte” (au sens où elle est marque une forme d’emboîtement, représenté par l’indentation). Cette balise fonctionne également comme un “parent” qui va nous permettre d’identifier les “enfants” (ou dépendances plus précisément) qui y sont rattachées. On peut donc stocker cette information de cette façon par exemple :

{ Name: '<html>', Value: '<html>' }

On conserve le nom de l’élément et sa valeur. La deuxième ligne comporte une balise “body” (qui désigne le corps de la page). On peut donc supposer que cette balise se rattache à la balise “html” dans la mesure où nous n’avons pas rencontré une balise de clôture (désignée par un slash à l’intérieur). On suppose donc que son “parent” est la balise “html” :

{ Name: '<body>', Value: '', Parent: '<html>' }

La troisième balise “h1” (qui désigne un titre) qui se manifeste à la suite doit dépendre de la balise “body” (qui elle-même dépend de la balise “html”). On note par contre que la valeur suivante n’est pas une balise. Il s’agit au contraire d’un texte qui correspond au titre. On peut donc consigner les informations suivantes :

{ Name: '<h1>', Value: 'Titre', Parent: '<body>' }

La quatrième balise “p” (pour un paragraphe) se manifeste après la balise terminale pour “h1” (avec un slash comme ceci : “/h1”). On peut donc supposer que cette balise ne se rattache pas au précédent élément. Par contre, elle doit se rattacher à l’élément “body”. On peut donc consigner ces informations :

{ Name: '<p>', Value: 'Paragraphe', Parent: '<body>' }

Avec toutes ces informations, on peut alors reconstruire l’arborescence de la page au format JSON :

{
"Name":"<body>","Value":"","Parent":"<html>",
"Dependance_1":{
"Name":"<p>","Value":"Paragraphe","Parent":"<body>"
},
"Dependance_2":{
"Name":"<h1>","Value":"Titre","Parent":"<body>"
}
}

Il est ensuite possible de naviguer beaucoup plus facilement dans le contenu d’un fichier XML ou HTML avec un tel format. Si on récapitule, on doit donc construire un outil capable d’extraire de l’information située entre des balises, et de conserver la structure hiérarchique de cette même information pour faire apparaître les dépendances. Maintenant que nous avons fait le rappel de ce que sont les formats XML et HTML, et vu comment les analyser, je vous propose de passer à la pratique en Javascript.

Construction d’un outil de traitement en Javascript

Pour réaliser notre implémentation d’un outil de traitement de fichiers XML et HTML en Javascript, nous allons nous appuyer sur l’exemple suivant :

<html>
<body>
<h1>TITRE</h1>
<p>
This is a paragraph
<div>
<h2>TITRE 2</h2>
</div>
</p>
</body>
</html>

Je précise tout de suite que si nous pouvons traiter du HTML, nous pouvons aussi faire du XML. Nous ne traiterons donc pas de façon séparée le traitement du HTML et du XML puisqu’ils obéissent à la même contrainte. Le fichier que je vais utiliser en exemple est relativement simpliste, mais il va nous permettre de couvrir la plupart des problématiques liées à l’analyse de la syntaxe de langages/formats qui utilisent des balises : à savoir que nous avons une page avec des dépendances principales qui peuvent également posséder des sous-dépendances (comme c’est le cas avec le paragraphe). Commençons donc par déclarer une variable “Data” qui va contenir cette page HTML :

var Data = '<html><body><h1>TITRE</h1><p>This is a paragraph<div><h2>TITRE 2</h2></div></p></body></html>'

On suppose que certains traitements sont réalisés, comme la suppression de certains espaces superflus entre les balises par exemple. Pour ce faire, partons du fichier suivant “index.html” :

<html>
<body>
<h1>TITRE</h1>
<p>
This is a paragraph
<div>
<h2>TITRE 2</h2>
</div>
</p>
</body>
</html>

On peut l’ouvrir en supprimant l’ensemble des retours à la ligne :

var Data = fs.readFileSync('index.html').toString().replace(/(\r\n|\r|\n)/g,'')

Ensuite, on peut utiliser une série d’expressions régulières pour supprimer les différents espaces possibles :

var regex_1 = />\s{1,}</g
var regex_2 = />\s{1,}/g
var regex_3 = /\s{1,}</g

Data = Data.replace(regex_1,'><')
console.log(Data)

/*
<html><body><h1>TITRE</h1><p> This is a paragraph <div><h2>TITRE 2</h2></div></p></body></html>
*/

Data = Data.replace(regex_2,'>')
console.log(Data)

/*
<html><body><h1>TITRE</h1><p>This is a paragraph <div><h2>TITRE 2</h2></div></p></body></html>
*/

Data = Data.replace(regex_3,'<')
console.log(Data)

/*
<html><body><h1>TITRE</h1><p>This is a paragraph<div><h2>TITRE 2</h2></div></p></body></html>
*/

La première opération à réaliser (une fois le document mis en forme) est de préparer le document afin de faciliter sa découpe en plusieurs morceaux, et si possible, en détachant bien les balises du texte. On commence donc par ajouter des virgules avant les signes “>” et “<” comme ceci :

Data = Data.replace(/>/gi, '>,')
Data = Data.replace(/</gi, ',<')

On peut ensuite réaliser un découpage à l’aide des virgules (qui vont nous servir de séparateurs) :

Data = Data.split(',')

Si vous affichez le contenu de la variable “Data” dans la console, vous aurez alors ce résultat :

[
'<html>', '<body>',
'<h1>', 'TITRE',
'</h1>', '<p>',
'This is a paragraph', '<div>',
'<h2>', 'TITRE 2',
'</h2>', '</div>',
'</p>', '</body>',
'</html>'
]

Même si nous n’en avons pas besoin ici, il pourrait être utile de réaliser un traitement supplémentaire pour supprimer les éléments vides de ce tableau ou faire d’autres modifications. Par exemple, on pourrait vouloir rassembler des éléments qui ne sont pas situés entre des balises mais qui apparaissent séparés (comme des lignes de textes découpées après l’utilisation de la virgule comme séparateur). On peut ensuite déclarer une variable “List” pour stocker les éléments analysés dans le fichier, et une variable “Previous” qui va nous être utile pour identifier les “parents” des différents éléments, et reconstituer correctement les dépendances entre les éléments. Il nous faut également prévoir une expression régulière pour confirmer que certains éléments sont bien des balises (celle-ci est stockée dans la variable “RegEx”) :

var List = []
var Previous
var RegEx = /<(.*)>/

On va ensuite déclarer une boucle qui itère sur la longueur de la variable “Data” et une variable “Json” pour stocker les résultats :

for (var i = 0; i < Data.length; i++) {
var Json = {}

Le premier cas de figure se manifeste lorsque la variable “Previous” est indéfinie et si l’élément sélectionné dans “Data” concorde avec l’expression régulière (il doit donc s’agir d’une balise). On génère alors un JSON qui comprend le nom de cette balise et sa valeur (qui sera le nom de la balise). On assigne également à la variable “Previous” le nom de cette balise puis on stocke le JSON dans la variable “List” :

  if (Data[i].match(RegEx) !== null && Previous === undefined) {
Previous = Data[i]
Json = {'Name':Data[i],'Value':Data[i]}
List.push(Json)
}

Le deuxième cas de figure se manifeste quand l’élément sélectionné concorde avec l’expression régulière, si la variable “Previous” n’est pas indéfinie et si l’élément sélectionné n’est pas une balise terminale (que l’on repère avec la présence d’un slash) :

  else if (Data[i].match(RegEx) !== null && Previous !== undefined && Data[i].includes('</') === false) {

On prévoit ensuite deux sous conditions. La première sous-condition se manifeste si l’élément suivant ne concorde pas avec l’expression régulière (il ne s’agit donc pas d’une balise). Il faut alors mettre en place une boucle qui va itérer sur la longueur de la variable “Data”. Si on rencontre un élément qui concorde avec l’expression régulière et si il correspond à une balise terminale, on peut alors envoyer l’élément sélectionné par la précédente boucle, ainsi que sa valeur (qui correspond à l’élément suivant, toujours par rapport à celui sélectionné dans la première boucle) et son “parent”. Même chose si il ne s’agit pas d’une balise terminale. Par contre, on modifie dans ce cas la valeur de la variable “Previous” pour retenir l’élément sélectionné par la première boucle. Dans les deux cas, la boucle est ensuite interrompue :

  if (Data[i+1].match(RegEx) === null) {
for (var j = i+1; j < Data.length; j++) {
if (Data[j].match(RegEx) !== null && Data[j].includes('</') === true) {
Json = {'Name':Data[i],'Value':Data[i+1], 'Parent': Previous}
break
}
if (Data[j].match(RegEx) !== null && Data[j].includes('</') === false) {
Json = {'Name':Data[i],'Value':Data[i+1], 'Parent': Previous}
Previous = Data[i]
break
}
}
}

La deuxième sous-condition se manifeste si l’élément suivant concorde avec l’expression régulière. Auquel cas on sauvegarde son nom, sa valeur et son parent. Par contre, on doit changer la valeur de “Previous” pour retenir l’élément sélectionné. On peut ensuite stocker le JSON dans la variable “List” :

   else if (Data[i+1].match(RegEx) !== null) {
Json = {'Name':Data[i],'Value':Data[i], 'Parent': Previous}
Previous = Data[i]
}
List.push(Json)
}

Enfin, on veut pouvoir être sûr que l’on travaille toujours avec le bon “parent”, notamment parce que cette information nous permet de bien restituer les dépendances. On ajoute donc une boucle qui va nous servir à repartir en arrière et vérifier que nous utilisons le bon “parent”. Ce que ni se manifeste que si nous avons face à nous une balise terminale. La valeur de “Previous” est alors modifiée pour récupérer la valeur de “Parent” dans l’élément identifié. On peut ensuite afficher les résultats :

  if (Data[i].includes('</') === true) {
var FindParent = Data[i].replace('/','')
for (var j = List.length - 1; j >= 0; j--) {
if (List[j]['Name'] === FindParent) {
Previous = List[j]['Parent']
break
}
}
}
}
console.log(List)

Et voici un exemple de résultat :

[
{ Name: '<html>', Value: '<html>' },
{ Name: '<body>', Value: '', Parent: '<html>' },
{ Name: '<h1>', Value: 'TITRE', Parent: '<body>' },
{ Name: '<p>', Value: 'This is a paragraph', Parent: '<body>' },
{ Name: '<div>', Value: '', Parent: '<p>' },
{ Name: '<h2>', Value: 'TITRE 2', Parent: '<div>' }
]

On peut ensuite réorganiser ce résultat en tenant compte de la logique “d’enfant” et de “parents”, et ainsi pouvoir visualiser l’arborescence au format JSON. On va donc travailler de la fin vers le début de la variable “Data”. On fusionne à chaque fois les lignes pour lesquelles la valeur de “Parent” est égale au “Name” :

var Child = 0
for (var j = List.length - 1; j >= 0; j--) {
for (var k = j+1; k >= 0; k - ) {
if (k < List.length - 1) {
if (List[j]['Parent'] === List[k]['Name'] && List[j] !== List[k]) {
List[k]['Children_'+Child] = List[j]
Child+=1
break
}
}
}
}

console.log(JSON.stringify(List[0]))

Et voici un exemple de résultat :

{
"Name":"<html>","Value":"<html>","Parent":"<html>",
"Children_4":{
"Name":"<body>","Value":"","Parent":"<html>",
"Children_2":{
"Name":"<p>","Value":"This is a paragraph","Parent":"<body>",
"Children_1":{
"Name":"<div>","Value":"","Parent":"<p>",
"Children_0":{
"Name":"<h2>","Value":"TITRE 2","Parent":"<div>"
}
}
},
"Children_3":{"Name":"<h1>","Value":"TITRE","Parent":"<body>"}
}
}

Conclusions

Nous avons donc appris ici à manipuler des fichiers XML et HTML dans le but de pouvoir analyser leur syntaxe et ainsi extraire de l’information. Comme vous avez pu le constater, les manipulations à mettre en place sont complexes pour deux raisons : l’usage de balises et les notions de hiérarchisation de l’information. Il existe bien entendu des librairies spécialisées qui permettent de réaliser ce travail sans accomplir le travail que nous venons de mettre en œuvre. Le plus important était de pouvoir comprendre comment s’organisent ces formats de données et comment les traiter pour extraire de l’information. Vous pouvez librement améliorer l’outil que nous venons de créer, comme par exemple en nommant les sous-objets par leurs vrais noms.

Laisser un commentaire