Geonode logo
Geonode Team

Geonode Team

Mis à jour : 7 octobre 2026

Publié : 2 septembre 2026

XPath contains() : un guide complet avec des exemples

`contains()` C'est la fonction XPath la plus utilisée, mais aussi la moins bien comprise. Elle ressemble à un test de sous-chaîne et ne se comporte comme tel que lorsque vous lui transmettez une chaîne de caractères. Lorsqu'on lui fournit un ensemble de nœuds — ce que produisent la plupart des expressions —, elle n'utilise, sans le signaler, que le premier nœud et ignore les autres. Ce guide aborde ce piège, l’idiome de correspondance de classes que personne ne maîtrise du premier coup, ainsi que la gestion des espaces et de la casse, que XPath 1.0 rend plus difficile qu’elle ne devrait l’être.

Une petite précision sur la raison pour laquelle nous écrivons cet article. Nous sommes Geonode ; nous vendons des proxys à des personnes qui extraient des données, ce qui fait que nous recevons constamment des questions sur les sélecteurs. La mise en garde à ce sujet est simple : les bugs liés aux sélecteurs et les problèmes de proxy produisent des symptômes complètement différents, et les confondre fait perdre des heures. Une requête bloquée renvoie une page de vérification ou un statut d’erreur. Une contains() défectueuse renvoie une page parfaitement valide et un résultat vide. Si le code HTML est bien présent et que votre expression ne trouve rien, cet article est la bonne référence et le réseau fonctionne correctement.

La fonction elle-même

La spécification XPath 1.0 est concise :

La fonction contains renvoie « true » si la chaîne de caractères du premier argument contient celle du deuxième argument, et « false » dans le cas contraire.

Les deux arguments sont des chaînes de caractères. Deux arguments, un résultat booléen, sensible à la casse, pas de caractères génériques, pas d’expressions régulières.

//a[contains(@href, 'download')]
//div[contains(@class, 'product')]
//p[contains(text(), 'Price')]

Ce qui est intéressant, c’est ce qui se passe lorsque l’argument fourni n’est pas une chaîne de caractères, ce qui est le cas la plupart du temps.

Le piège du « node-set » : « contains()

» ne prend en compte que le premier nœud C'est ce bug qui explique pourquoi « mon XPath fonctionne sur certaines pages et pas sur d'autres », et la spécification l'explique précisément :

Un ensemble de nœuds est converti en chaîne de caractères en renvoyant la valeur de chaîne du nœud de l'ensemble de nœuds qui est le premier dans l'ordre du document. Si l'ensemble de nœuds est vide, une chaîne vide est renvoyée.

Ainsi, lorsque vous écrivez un code qui génère un ensemble de nœuds et que vous le transmettez à contains() , XPath ignore silencieusement tout sauf le premier nœud.

<div>
  <p>Introduction</p>
  <p>Price: £42</p>
  <p>Availability</p>
</div>
contains(//p, 'Price')      → false

Faux, car //p est un ensemble de trois nœuds ; la conversion en chaîne de caractères ne retient que le premier — « Introduction » — qui ne contient pas « Price ». Les deux autres paragraphes n’ont jamais été pris en compte.

La solution consiste à appliquer le prédicat à chaque nœud plutôt que de convertir un ensemble :

//p[contains(., 'Price')]   → the second paragraph

Ici, contains() est évalué une fois pour chaque p , . correspondant à ce nœud individuel. C’est la différence entre demander « l’ensemble contient-il ceci ? » et « quels éléments de l’ensemble contiennent ceci ? », et c’est généralement la seconde question que l’on entend.

Le même piège apparaît avec text() , qui est également un ensemble de nœuds :

//div[contains(text(), 'Price')]

text() renvoie tous les enfants directs de type nœud de texte, et la conversion en chaîne de caractères ne retient que le premier. Si l’élément contient du texte réparti sur plusieurs nœuds — ce qui se produit dès qu’il y a un balisage imbriqué —, vous ne testez que le premier fragment.

« .

» contre « text()

» : l’autre facette d’un même problème La spécification définit la valeur de chaîne d’un élément comme suit :

la concaténation des valeurs de chaîne de tous les nœuds de texte descendants du nœud élément, dans l’ordre du document

C’est là la différence cruciale entre les deux formes.

<p>Total: <strong>£42.00</strong> including VAT</p>
contains(., '£42.00')          → true   (all descendant text, concatenated)
contains(text(), '£42.00')     → false  (first direct text node only: "Total: ")

.

explore les éléments imbriqués. text()

ne prend en compte que les enfants directs, et uniquement le premier d’entre eux.

**Utilisez .

par défaut.** Cela correspond à ce qu’un lecteur considérerait comme « le texte de cet élément », et cela résiste aux modifications de balisage, par exemple lorsqu’une valeur est placée entre des balises <span>

.

**Utilisez délibérément « text()

»** lorsque vous souhaitez spécifiquement exclure le contenu imbriqué — par exemple, pour faire correspondre une étiquette sans faire correspondre le texte situé à l’intérieur d’un badge ou d’une infobulle enfant.

Pour « l’un des nœuds de texte contient ceci », la forme correcte applique le prédicat aux nœuds de texte eux-mêmes :

//div[text()[contains(., 'Price')]]

Verbeux, mais correct.

Espaces : « normalize-space() » n’est pas facultatif

Le véritable code HTML est formaté de manière lisible, et les espaces sont inclus dans la valeur de la chaîne.

<td>
    In stock
</td>

La valeur de la chaîne de cette cellule est « "\n In stock\n" », ce qui fait qu’une comparaison exacte avec « 'In stock' » échoue.

La spécification définit la solution :

La fonction normalize-space renvoie la chaîne d’arguments dont les espaces ont été normalisés en supprimant les espaces de début et de fin et en remplaçant les séquences de caractères d’espacement par un seul espace.

//td[normalize-space() = 'In stock']
//td[contains(normalize-space(), 'In stock')]

Notez que la fonction normalize-space(), sans argument, agit sur le nœud de contexte, ce qui correspond à ce que vous recherchez à l’intérieur d’un prédicat.

Dans le cas spécifique de contains(), les espaces ont moins d’importance aux extrémités et beaucoup plus au milieu. Une recherche de 'In stock' échoue par rapport à "In stock" à moins de procéder d’abord à une normalisation. Si vous ne devez retenir qu’une seule habitude dans vos sélecteurs, veillez à toujours encadrer vos comparaisons de texte dans normalize-space().

Utilisation correcte des classes de correspondance

L'erreur la plus courante dans l'utilisation de ``contains()`

`, et celle qui risque le plus de passer inaperçue.

//div[contains(@class, 'btn')]

Cela correspond à class="btn"

. Cela correspond également à class="btn-primary"

, class="unbtn"

et class="sidebar-btn-group"

. Comme @class

est une chaîne unique séparée par des espaces et que contains()

est un simple test de sous-chaîne, il ne tient pas compte des limites entre les mots.

La syntaxe correcte consiste à ajouter des espaces autour de l’attribut et de la cible afin que seuls les tokens entiers correspondent :

//div[contains(concat(' ', normalize-space(@class), ' '), ' btn ')]

Interprétez-le ainsi : prenez l’attribut « class », normalisez ses espaces, encadrez-le d’espaces afin que chaque token soit délimité par des espaces des deux côtés, puis recherchez la cible entourée d’espaces. « class="btn-primary"

» devient « " btn-primary "

», ce qui ne contient pas « " btn "

». « class="icon btn large"

» devient « " icon btn large "

», ce qui le contient.

C’est peu élégant. C’est pourtant correct, et c’est ce à quoi aboutit toute base de code de scraping mature. Enveloppez-le dans une fonction d’aide :

def has_class(name):
    return (f"contains(concat(' ', normalize-space(@class), ' '), ' {name} ')")

Lorsque vous avez besoin de deux classes :

//div[contains(concat(' ', normalize-space(@class), ' '), ' btn ')
      and contains(concat(' ', normalize-space(@class), ' '), ' primary ')]

À ce stade, un sélecteur CSS — div.btn.primary

— est nettement plus lisible et fait exactement ce qu’il faut. Si vous effectuez une correspondance sur des classes et rien d’autre, utilisez le CSS. L’XPath prend tout son sens lorsque vous avez besoin d’une correspondance de texte ou d’une navigation en amont, et non pour les tâches que le CSS maîtrise déjà parfaitement. Nous avons comparé les deux dans XPath preceding-sibling.

Sensibilité à la casse et solution de contournement « translate() »

La méthode « contains() » est sensible à la casse, et XPath 1.0 ne dispose pas de fonction « lower-case() ». Les navigateurs et Selenium implémentent XPath 1.0 ; cette limitation s'applique donc là où elle est la plus critique.

La solution de contournement utilise la fonction « translate() », définie comme renvoyant « la chaîne du premier argument dans laquelle les occurrences des caractères de la chaîne du deuxième argument sont remplacées par le caractère situé à la position correspondante dans la chaîne du troisième argument » :

//p[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
                          'abcdefghijklmnopqrstuvwxyz'), 'price')]

Translittération caractère par caractère, ASCII uniquement. Elle ne mettra pas en minuscules les caractères accentués, à moins que vous n’étendiez les deux chaînes pour les couvrir, ce qui devient rapidement fastidieux.

Deux meilleures options sont disponibles :

Rechercher une sous-chaîne insensible à la casse. Si la page affiche « Price » ou « PRICE » mais jamais « price », la recherche de « 'rice' » est peu élégante mais fonctionne. C'est souvent la solution la plus pragmatique.

Utiliser une bibliothèque proposant un XPath plus riche. Les analyseurs côté serveur tels que lxml prennent en charge les extensions EXSLT, notamment re:test() pour les expressions régulières, qui gèrent la casse et bien d'autres aspects. Ce n'est pas le cas des navigateurs ; ainsi, un extrait de code que vous avez trouvé pour lxml peut échouer dans Selenium précisément pour cette raison.

Si vous vous retrouvez à écrire une longue expression de type «translate()», c’est le signe que vous avez dépassé les capacités d’XPath 1.0 pour cette tâche.

Les fonctions de chaîne associées : `

contains()

` fait partie d’une petite famille, et les autres sont souvent plus précises.

** ``starts-with()`

** — « renvoie true si la chaîne du premier argument commence par la chaîne du deuxième argument ». Plus spécifique que ``contains()

` et, par conséquent, moins susceptible de renvoyer des résultats superflus :

//a[starts-with(@href, 'https://')]

Il n’existe pas de fonction ``ends-with()`

dans XPath 1.0. La solution de contournement utilisesubstring()`

et string-length()

, mais elle est suffisamment peu pratique pour qu’une autre approche soit généralement préférable.

**substring-before()

et substring-after()

** — la première « renvoie la sous-chaîne de la chaîne du premier argument qui précède la première occurrence de la chaîne du deuxième argument… ou la chaîne vide si la chaîne du premier argument ne contient pas la seconde ». Utile pour scinder une valeur à l’intérieur de l’expression :

substring-after(//span[@class='price'], '£')

**normalize-space()

** — déjà abordé plus haut, et celui que vous devriez utiliser le plus souvent.

**translate()

** — conversion de casse, et suppression de caractères en les remplaçant par rien :

translate(., ',', '')

**string-length()

** — filtrage des valeurs vides ou tronquées :

//td[string-length(normalize-space()) > 0]

C’est en les combinant que l’on tire le meilleur parti de ces fonctions :

//tr[contains(normalize-space(td[1]), 'Weight')]/td[2]

La deuxième cellule de toute ligne dont la première cellule mentionne « Weight », sans distinction entre majuscules et minuscules.

Modèles récurrents

Les expressions ci-dessous couvrent la plupart des tâches d'extraction réelles et méritent d'être conservées à portée de main.

Rechercher la valeur située à côté d'une étiquette. C'est l'exigence la plus courante lors du scraping de pages structurées :

//dt[contains(normalize-space(), 'Price')]/following-sibling::dd[1]
//th[contains(normalize-space(), 'Weight')]/following-sibling::td[1]

Notez l’[1]

— sans elle, following-sibling::td

renvoie toutes les cellules suivantes de la ligne, et votre code prend silencieusement la première alors que vous supposez qu’il s’agit de la seule.

Rechercher un lien par son texte visible plutôt que par son href :

//a[contains(normalize-space(), 'Download')]

Plus robuste que la correspondance d’URL lorsque celle-ci est un identifiant haché, et plus fragile lorsque le site est traduit. Choisissez en fonction de ce qui change le plus souvent.

Rechercher un conteneur à partir d’un élément qu’il contient :

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')][.//span[contains(., 'Sold out')]]

Deux prédicats en séquence : une carte contenant un élément span mentionnant « Épuisé ». Cela se compose bien et se lit mieux que d’essayer de l’exprimer en une seule condition.

Exclure plutôt qu’inclure. C’est souvent la formulation la plus claire :

//tr[not(contains(@class, 'header'))]
//li[not(contains(normalize-space(), 'Advertisement'))]

**Identifier un bouton, qu’il s’agisse d’un button

ou d’un a

:**

//*[self::button or self::a][contains(normalize-space(), 'Continue')]

Trouver la ligne contenant une valeur et extraire une autre colonne :

//tr[td[contains(normalize-space(), 'SKU-1234')]]/td[3]

Lire de l’extérieur vers l’intérieur : les lignes comportant une cellule mentionnant la référence (SKU), puis la troisième cellule de cette ligne. Il s’agit du modèle de recherche dans une table, qui résiste bien mieux au réordonnancement des colonnes qu’un index absolu sur l’ensemble de la table.

Méfiez-vous des correspondances vides. Un prédicat qui filtre les cellules vides ne coûte rien et évite toute confusion en aval :

//td[string-length(normalize-space()) > 0][contains(., 'Ltd')]

Quand « contains() » n’est pas l’outil adapté

Lorsque vous recherchez l’égalité exacte. « contains(., 'Price') » correspond également à « Historic Price » et à « Price excluding VAT ». Si vous souhaitez trouver exactement cette étiquette, utilisez « normalize-space() = 'Price' ». Les correspondances excédentaires ne sont pas signalées : votre code prend le premier résultat et ne sait jamais qu’il y en avait trois.

Lorsque vous effectuez une correspondance de classes et rien d’autre. Le CSS le fait correctement et de manière lisible. Voir ci-dessus.

Lorsque vous avez besoin d’une expression régulière. XPath 1.0 n’en propose aucune. Extrayez les données avec contains() si nécessaire, puis appliquez une expression régulière dans votre langage hôte, où vous pourrez également voir ce qui a été trouvé.

Lorsqu’il existe un identifiant utilisable. Un identifiant (id), un attribut « data » ou du JSON-LD intégré est plus stable que n’importe quelle correspondance textuelle. Le texte est du contenu, et le contenu change : une refonte, une traduction ou une révision éditoriale rendra inopérant un sélecteur basé sur une correspondance textuelle, sans qu’aucun avertissement ne vous en informe.

Lorsque la chaîne provient d’une saisie utilisateur. L’interpolation de texte non fiable dans une expression XPath constitue une injection XPath. Utilisez la liaison de variables de votre bibliothèque lorsqu’elle existe, et échappez correctement les caractères lorsque ce n’est pas le cas — en particulier les guillemets, car XPath 1.0 ne dispose d’aucune séquence d’échappement pour un guillemet à l’intérieur d’une chaîne littérale et vous devez utiliser concat() pour en créer une.

Questions fréquentes

À quoi sert la fonction contains() en XPath ?

Elle renvoie « true » lorsque la première chaîne de caractères fournie contient la seconde en tant que sous-chaîne. Les deux arguments sont des chaînes de caractères, la comparaison est sensible à la casse et il n'y a ni caractères génériques ni expressions régulières. Elle sert généralement, lors de l'extraction, à faire correspondre un élément à une partie de son texte ou de la valeur de son attribut.

Pourquoi ma fonction contains() en XPath ne trouve-t-elle rien ?

Le plus souvent, c’est parce que vous lui avez transmis un ensemble de nœuds. XPath convertit un ensemble de nœuds en chaîne de caractères en prenant le premier nœud dans l’ordre du document et en ignorant le reste ; ainsi, contains(//p, 'x') n’examine jamais que le premier paragraphe. Appliquez plutôt le prédicat à chaque nœud : //p[contains(., 'x')].

Quelle est la différence entre contains(.) et contains(text()) ? `

.`` utilise la valeur de chaîne de l’élément, que la spécification définit comme la concaténation de tous les nœuds de texte descendants — il accède donc au balisage imbriqué. ``text()`` renvoie les enfants directs de type nœud de texte, et la conversion en chaîne ne prend en compte que le premier. Utilisez .` sauf si vous souhaitez spécifiquement exclure le contenu imbriqué.

Comment faire correspondre une classe avec XPath ?

Utilisez contains(concat(' ', normalize-space(@class), ' '), ' name '), qui ajoute des caractères de remplissage à l’attribut afin que seules les chaînes complètes correspondent. Un simple contains(@class, 'btn') correspond également à btn-primary et unbtn. Si vous effectuez une correspondance uniquement sur des classes, un sélecteur CSS est plus clair et correct par défaut.

La fonction contains() d’XPath est-elle sensible à la casse ?

Oui, et XPath 1.0 ne dispose pas d’une fonction «lower-case()». La solution standard consiste à utiliser translate() en précisant explicitement les lettres majuscules et minuscules, ce qui ne prend en charge que les caractères ASCII. Les bibliothèques côté serveur telles que lxml prennent en charge les expressions régulières EXSLT ; ce n’est pas le cas des navigateurs ni de Selenium.

Comment utiliser la fonction contains() avec plusieurs conditions ?

Combinez les prédicats à l’aide de and et or : //div[contains(@class, 'card') and contains(., 'In stock')]. Chaque contains() correspond à un test booléen distinct évalué par rapport au même nœud de contexte.

XPath dispose-t-il d’une fonction « commence par » ou « se termine par » ?

La fonction starts-with() existe et est à privilégier par rapport à contains() lorsque cela est possible, car elle génère moins de correspondances superflues. Il n’existe pas de fonction ends-with() dans XPath 1.0 — la solution de contournement consiste à utiliser substring() avec string-length(), mais elle est suffisamment peu pratique pour qu’une autre approche soit généralement préférable.

Pourquoi la fonction contains() correspond-elle à plus d’éléments que prévu ?

Parce qu’il s’agit d’un test de sous-chaîne sans notion de limites de mots. contains(., 'Price') correspond également à « Historic Price » et « Price excluding VAT ». Utilisez normalize-space() = 'Price' pour l’égalité, ou l’idiome « padded-concat » pour les jetons de classe.

Conclusion :

contains() est simple en théorie mais regorge de pièges à l'usage, et presque tous proviennent d'une seule et même source : XPath convertit un ensemble de nœuds en chaîne de caractères en ne conservant que le premier nœud et en ignorant les autres. Cette règle unique explique pourquoi contains(//p, 'x') donne une réponse erronée en toute confiance, pourquoi contains(text(), 'x') ne détecte pas le texte réparti sur plusieurs nœuds, et pourquoi la même expression fonctionne sur une page et échoue sur la suivante.

Les astuces pour éviter cela sont peu nombreuses. Appliquez contains() à l’intérieur d’un prédicat afin qu’il s’évalue nœud par nœud. Utilisez . plutôt que text(), sauf si vous avez une bonne raison de faire autrement. Encadrez les comparaisons de texte dans des balises normalize-space(), car le véritable HTML est formaté de manière lisible. Et pour la correspondance de classes, utilisez soit l’idiome « padded-concat », soit — mieux encore — un sélecteur CSS, qui a été conçu précisément pour cette tâche.

Réservez XPath à ce qu’il sait faire de manière unique : la correspondance sur du texte et la navigation en arrière. Ce sont là de véritables capacités qui n’ont pas d’équivalent en CSS, et elles justifient l’utilisation de cette syntaxe. L’utiliser pour sélectionner div.card revient à en payer le prix sans en tirer aucun avantage.