How to Find Elements With Underscores in Their Text Using XPath
Use XPath contains() to find underscores reliably, handle nested text, exact matches, attributes, quoting, and common locator mistakes.
Use //*[contains(., '_')] to find elements whose complete text value contains an underscore. The dot represents the current element’s string-value, including text inside descendant elements.
//*[contains(., '_')]
Use contains(text(), '_') only when the underscore must appear in a direct text child:
//*[contains(text(), '_')]
For a whole-value match, use equality instead of substring matching:
//*[. = '_ready_']
XPath’s contains() function returns true when its first string argument contains its second as a substring. The XPath 3.1 specification describes the function and its collation behavior in the W3C function reference.
1. Choose the expression that matches your requirement
| Need | XPath | What it checks |
|---|---|---|
| Underscore anywhere in complete element text | //*[contains(., '_')] |
The element string-value, including descendant text |
| Underscore in a direct text child | //*[contains(text(), '_')] |
Direct text-node children |
| Exact complete text | //*[. = '_ready_'] |
The full element string-value equals _ready_ |
| Underscore in an attribute | //*[@data-label and contains(@data-label, '_')] |
An attribute value, not element text |
2. Understand . versus text()
Consider this markup:
<button>file_<strong>name</strong></button>
<button>plain_name</button>
<button data-label="item_ready">Ready</button>
The button’s complete string-value is the concatenation of its descendant text. Therefore:
//button[contains(., '_')]
can match both text buttons, including file_<strong>name</strong>. By contrast, text() selects direct text-node children. A predicate such as:
//button[contains(text(), '_')]
can miss an underscore that exists only inside a nested descendant. In XPath 1.0, a node-set passed to a string function is converted using its first node in document order, so contains(text(), ...) is not a general test across every direct text node.
3. Scope the search to the element you actually need
The broad form is useful for discovery, but it may match ancestors as well as the intended control. Add an element name, class, role, or structural condition:
//button[contains(., '_')]
//a[contains(., '_')]
//*[@role='button' and contains(., '_')]
//section[@id='results']//*[contains(., '_')]
A parent can match because a descendant contains the character. Narrowing the context reduces accidental matches and usually makes locator evaluation cheaper.
4. Match exact text instead of a substring
contains() succeeds for any occurrence, so it matches _ready_, already_ready, and name_1. If the complete string must be exactly _ready_, use:
//*[. = '_ready_']
Use an element name or other identifying condition when several nodes can have the same string-value:
//button[. = '_ready_']
5. Match an underscore in an attribute
Element text and attributes are different values. If the underscore is in data-label, inspect the attribute directly:
//*[@data-label and contains(@data-label, '_')]
For a specific attribute:
//input[contains(@name, '_')]
//*[@aria-label and contains(@aria-label, '_')]
6. The underscore needs no XPath escaping
Inside a quoted XPath string, underscore is an ordinary character:
//*[contains(., '_')]
If the expression is inside JavaScript, Python, Java, or another host-language string, escape the host-language quotes according to that language. XPath quoting and host-language quoting are separate layers.
7. XPath version, case, and collation
XPath 3.1 expressions are case-sensitive. The specification defines contains() as collation-aware, so the active collation can affect matching. For the underscore character itself, letter case is irrelevant. If the surrounding text must match without regard to case, check the XPath version and collation supported by the browser, XML processor, or automation tool; do not assume every environment supports the same XPath features.
8. Runnable examples
The following document gives you concrete nodes to inspect in any XPath-capable tool:
<!doctype html>
<html>
<body>
<button id="nested">file_<strong>name</strong></button>
<button id="direct">plain_name</button>
<button id="exact">_ready_</button>
<button id="clean" data-label="item_ready">Ready</button>
</body>
</html>
Useful expressions for this markup:
//button[contains(., '_')] # nested and direct text
//button[contains(text(), '_')] # direct text-node check
//button[. = '_ready_'] # exact text
//*[@data-label and contains(@data-label, '_')] # attribute value
//button[@id='nested' and contains(., '_')] # tightly scoped match
When your tool returns multiple nodes, inspect the matched element’s tag, attributes, and full string-value before interacting with it.
9. Troubleshooting checklist
It finds no element, but I can see an underscore
- Try
contains(., '_')instead ofcontains(text(), '_'); nested markup may contain the underscore. - Confirm the underscore is in element text rather than an attribute. Use
contains(@attribute, '_')for attributes. - Check whether the node is in the context or document being queried.
It matches too many elements
- Replace
//*with a specific element such as//buttonor//a. - Add an identifying attribute, role, ancestor, or container condition.
- Remember that a parent can match because descendant text contains an underscore.
I need the whole value, not a substring
Use equality: //button[. = '_ready_']. contains() intentionally matches any substring.
The expression works in one tool but not another
Compare the XPath version, context node, and collation support. Browser automation commonly exposes XPath 1.0 behavior, while other processors may support later versions.
My host-language string is rejected
Keep the XPath quotes valid, then escape the outer programming-language string. For example, choose a host-language quote style that lets the XPath’s '_' remain intact, or escape that quote according to the host language.
10. Performance and reliability notes
- Scope searches to a known container and element type when possible. This reduces unnecessary node checks and makes the result more deterministic.
- Prefer an identifying attribute when the page provides one; text can change while attributes may remain stable.
- Use
.when nested markup is expected. Usetext()only when direct text-node semantics are required. - For exact labels, equality avoids accidental substring matches.
- Re-check the final matched node when content is dynamic; the XPath expression itself does not guarantee that a page has finished rendering.
11. Or skip the browser setup
If your goal is to capture a page after locating or inspecting its elements, ScreenshotNeo provides a website screenshot API and MCP server. Its capture options include CSS element selection, custom JavaScript and CSS, waits, hidden selectors, device settings, and full-page shots. Read the ScreenshotNeo API documentation for the available parameters.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients take screenshots. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
12. FAQ
Does underscore have a special meaning in XPath?
No. In a quoted string such as '_', it is an ordinary character and needs no XPath escaping.
Should I use contains(., '_') or contains(text(), '_')?
Use the first for complete descendant text, and the second only for direct text children.
How do I select an exact underscore-only label?
Use equality, for example //*[. = '_'].
How do I search an attribute instead of visible text?
Reference the attribute explicitly, such as //*[@data-label and contains(@data-label, '_')].
Is matching case-sensitive?
XPath 3.1 expressions are case-sensitive, and collation support can vary by XPath implementation.


