478

I am having a problem selecting nodes by attribute when the attributes contains more than one word. For example:

<div class="atag btag" />

This is my xpath expression:

//*[@class='atag']

The expression works with

<div class="atag" />

but not for the previous example. How can I select the <div>?

james.garriss
  • 12,959
  • 7
  • 83
  • 96
crazyrails
  • 4,864
  • 3
  • 16
  • 7
  • 9
    It's worth pointing out, I think, that "atag btag" is a single attribute, not two. You're trying to do substring matching in xpath. – skaffman Sep 07 '09 at 19:07
  • 4
    Yes you're right - thats what I want. – crazyrails Sep 07 '09 at 19:14
  • Related: http://stackoverflow.com/questions/8808921/selecting-a-css-class-with-xpath and http://stackoverflow.com/questions/1604471/how-can-i-find-an-element-by-css-class-with-xpath – Timo Huovinen Mar 31 '14 at 12:40
  • 2
    This is why you should use a CSS selector... `div.atag` or `div.btag`. Super simple, not string matching, and WAY faster (and better supported in browsers). XPath (against HTML) should be relegated to what it's useful for... finding elements by contained text and for DOM navigation. – JeffC Oct 21 '18 at 21:41

10 Answers10

545

Here's an example that finds div elements whose className contains atag:

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

Here's an example that finds div elements whose className contains atag and btag:

//div[contains(@class, 'atag') and contains(@class ,'btag')]

However, it will also find partial matches like class="catag bobtag".

If you don't want partial matches, see bobince's answer below.

Pikamander2
  • 7,332
  • 3
  • 48
  • 69
surupa123
  • 5,699
  • 1
  • 15
  • 4
  • 126
    @Redbeard: It's a literal answer but not usually what a class-matching solution should aim for. In particular it would match `
    `, which contains the target strings but is not a div with the given classes.
    – bobince Dec 02 '11 at 22:52
  • 3
    This will work for simple scenarios - but watch out if you want to use this answer in wider contexts with less or no control over the attribute values you are checking for. [The correct answer is bobince's.](http://stackoverflow.com/a/1390680/177710) – Oliver Feb 05 '13 at 09:11
  • 17
    Sorry, this does not match a class, it matches a substring – Timo Huovinen Mar 20 '14 at 14:18
  • 7
    it's plainly wrong as it finds also:
    which it should not.
    – Alexei Vinogradov Jul 20 '16 at 16:12
  • 7
    The question was "contains a certain string" not "matches a certain class" – Alsatian Dec 02 '16 at 15:36
322

mjv's answer is a good start but will fail if atag is not the first classname listed.

The usual approach is the rather unwieldy:

//*[contains(concat(' ', @class, ' '), ' atag ')]

this works as long as classes are separated by spaces only, and not other forms of whitespace. This is almost always the case. If it might not be, you have to make it more unwieldy still:

//*[contains(concat(' ', normalize-space(@class), ' '), ' atag ')]

(Selecting by classname-like space-separated strings is such a common case it's surprising there isn't a specific XPath function for it, like CSS3's '[class~="atag"]'.)

Jens Erat
  • 37,523
  • 16
  • 80
  • 96
bobince
  • 528,062
  • 107
  • 651
  • 834
  • 62
    bah, xpath needs some fixes – Randy L Sep 20 '10 at 23:10
  • The answer by surupa123 below is much better, used contains() just with the class you're interested in, no need for concats as far as I can see. – Redbeard Dec 02 '11 at 03:02
  • Couldn't `tokenize`, `exists`, and `index-of` be combined to solve this in a rather ugly yet very concise way? – cha0site Apr 19 '12 at 13:09
  • 13
    @Redbeard supra123's answer is problematic if there is a css class like "atagnumbertwo" that you don't want to select, though I'll admit this may not be likely (: – drevicko Jul 26 '12 at 09:08
  • 7
    @crazyrails: Could you please accept this answer as the correct answer? That will help future searchers identify the correct solution to the problem described by your question. Thank you! – Oliver Feb 05 '13 at 09:15
  • didn't try it yet. But wouldn't concat just append spaces on both sides of class attribute like `'atag btag'` would become `' atag btag '` or would it just append space before and after each class like `' atag btag '` – Muhammad Adeel Zahid Feb 25 '13 at 18:21
  • Is this still the best way to select an element with multiple classes, or have improvements been made? – Nate May 23 '14 at 19:32
  • 2
    @cha0site: Yes they could, in XPath 2.0 and following. This answer was written before XPath 2.0 became official. See http://stackoverflow.com/a/12165032/423105 or http://stackoverflow.com/a/12165195/423105 – LarsH May 27 '15 at 21:09
  • I want to select an element if it has all the classes in a "list", i.e. should have both atag and btag only. How do I do this ? – MasterJoe Oct 24 '16 at 17:06
  • @testerjoe2: with an `and` operator? eg for the XPath 1 version, `[contains(..., ' atag ') and contains(..., ' btag ')]` – bobince Oct 27 '16 at 21:06
  • 1
    Don't be like me and remove the spaces around the class you're looking for in this example; they're actually important. Otherwise it may look to work but defeats the purpose. – CTS_AE Sep 25 '17 at 23:13
  • The comments here saying that XPath "needs an improvement" for this don't really understand what XPath is. It's supposed to be able to operate on *any* XML. It doesn't know what HTML even is, so it doesn't know that HTML has a special attribute that's named `class` that contains a space-separated list of values that mean something special to HTML. Some other format might put, say, comma-separated values in a `blub` attribute that controls the `blub` setting in that language and XPath wouldn't know about that, either, because it's a generic XML query language. – Wayne Jun 12 '20 at 05:13
42

try this: //*[contains(@class, 'atag')]

SelenUser
  • 638
  • 5
  • 13
41

EDIT: see bobince's solution which uses contains rather than start-with, along with a trick to ensure the comparison is done at the level of a complete token (lest the 'atag' pattern be found as part of another 'tag').

"atag btag" is an odd value for the class attribute, but never the less, try:

//*[starts-with(@class,"atag")]
Jens Erat
  • 37,523
  • 16
  • 80
  • 96
mjv
  • 73,152
  • 14
  • 113
  • 156
32

A 2.0 XPath that works:

//*[tokenize(@class,'\s+')='atag']

or with a variable:

//*[tokenize(@class,'\s+')=$classname]
Jens Erat
  • 37,523
  • 16
  • 80
  • 96
Daniel Haley
  • 51,389
  • 6
  • 69
  • 95
  • How can this work if `@class` has more than one element? Because it is going to return a list of words and comparing that to a string fails with *wrong cardinality*. – Alexis Wilke Aug 10 '14 at 23:51
  • 3
    @AlexisWilke - From the spec (http://www.w3.org/TR/xpath20/#id-general-comparisons): *General comparisons are existentially quantified comparisons that may be applied to operand sequences of any length.* It's worked in every 2.0 processor that I've tried. – Daniel Haley Aug 11 '14 at 15:11
  • 2
    Note also, in XPath 3.1 this can be simplified to `//*[tokenize(@class)=$classname]` – Michael Kay Apr 18 '18 at 07:42
  • 2
    And for completeness, if you are fortunate enough to be using a schema-aware XPath processor, and if @class has a list-valued type, then you can simply write `//*[@class=$classname]` – Michael Kay Apr 18 '18 at 07:44
28

Be aware that bobince's answer might be overly complicated if you can assume that the class name you are interested in is not a substring of another possible class name. If this is true, you can simply use substring matching via the contains function. The following will match any element whose class contains the substring 'atag':

//*[contains(@class,'atag')]

If the assumption above does not hold, a substring match will match elements you don't intend. In this case, you have to find the word boundaries. By using the space delimiters to find the class name boundaries, bobince's second answer finds the exact matches:

//*[contains(concat(' ', normalize-space(@class), ' '), ' atag ')]

This will match atag and not matag.

Brent Atkinson
  • 379
  • 4
  • 3
  • This is the solution I was looking for. It clearly find 'test' in class='hello test world' and does not match 'hello test-test world'. Since I use only XPath 1.0 and have no RegEx this is only solution which works. – Jan Stanicek Nov 03 '16 at 14:05
  • How different is this from the answer by @bobince ? – Nakilon Jan 19 '22 at 22:16
  • @Nakilon the most complete solution is the second one I presented here, which is the same as bobince's second answer. However, the first solution is far simpler to understand and to read, but will only be correct if your class names can not be substrings of each other. The second is more general purpose, but the first is preferable if the assumptions are reasonable for your particular application. – Brent Atkinson Jan 21 '22 at 00:14
8

To add onto bobince's answer... If whatever tool/library you using uses Xpath 2.0, you can also do this:

//*[count(index-of(tokenize(@class, '\s+' ), $classname)) = 1]

count() is apparently needed because index-of() returns a sequence of each index it has a match at in the string.

Demi
  • 3,535
  • 5
  • 29
  • 45
armyofda12mnkeys
  • 3,214
  • 2
  • 32
  • 39
  • 1
    I suppose you meant to NOT put the `$classname` variable between quotes? Because as it is, that's a string. – Alexis Wilke Aug 10 '14 at 23:57
  • 1
    Finally, a correct (JavasScript compatible) implementation of getElementsByClassName...aside from the string literal `'$classname'` of course. – Joel Mellon Oct 19 '15 at 18:12
  • 1
    This is grossly over-complicated. See @DanielHaley's response for the correct XPath 2.0 answer. – Michael Kay Apr 18 '18 at 07:45
4

You can try the following

By.CssSelector("div.atag.btag")

Guilherme Franco
  • 1,465
  • 12
  • 19
Umesh Chhabra
  • 49
  • 1
  • 3
0

I came here searching solution for Ranorex Studio 9.0.1. There is no contains() there yet. Instead we can use regex like:

div[@class~'atag']
Jarno Argillander
  • 5,885
  • 2
  • 31
  • 33
-1

For the links which contains common url have to console in a variable. Then attempt it sequentially.

webelements allLinks=driver.findelements(By.xpath("//a[contains(@href,'http://122.11.38.214/dl/appdl/application/apk')]"));
int linkCount=allLinks.length();
for(int i=0; <linkCount;i++)
{
    driver.findelement(allLinks[i]).click();
}
Nathaniel Ford
  • 20,545
  • 20
  • 91
  • 102
user3906232
  • 63
  • 1
  • 2
  • 5