Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use //*[contains(., '_')] to find elements whose complete string-value contains an underscore. The dot includes text in descendant elements. Use //*[contains(text(), '_')] only when the underscore must be in a direct text child, and use equality such as //*[. = '_ready_'] for an exact complete value.

The correct XPath for most cases

The general-purpose locator is:

//*[contains(., '_')]

contains() performs a substring test. The first argument, ., is the context element’s string-value: the text contributed by that element and its descendants. The underscore is an ordinary character inside the quoted XPath string, so it needs no special escaping.

For example, this markup is matched by the expression above:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<button>file_<strong>name</strong></button>

The button’s complete string-value is file_name, even though the word is split by a nested <strong> element.

Choose the expression that matches your requirement

Requirement XPath What it examines
Underscore anywhere in the element’s complete text //*[contains(., '_')] The element string-value, including descendant text
Underscore in a direct text child //*[contains(text(), '_')] Text-node children immediately under the element
Exact complete text //*[. = '_ready_'] The whole element string-value must equal _ready_
Underscore in an attribute //*[@data-label and contains(@data-label, '_')] The value of the data-label attribute

Substring matching versus exact matching

Find any occurrence

//*[contains(., '_')] matches user_name, _leading, trailing_, and __. It also matches an ancestor whose descendant contains an underscore, because that ancestor’s string-value includes descendant text.

Require one exact value

Use equality when the complete string must be one value:

//*[. = '_ready_']

This does not mean “contains the phrase.” An element with string-value status: _ready_ will not match. Equality also evaluates the element’s complete string-value, so nested markup is still included.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Match a known element type

Replace the wildcard with a name when the page has many matching nodes:

Rank #2
XPath 2.0 Programmer's Reference
  • Used Book in Good Condition
//button[contains(., '_')]

For namespaces or a larger document, add an identifying attribute or another structural predicate so that the result set describes the target, not every ancestor containing the same character.

Why text() can miss an underscore

text() selects direct text-node children. Consider:

<button>file_<strong>name</strong></button>

The button has a direct text node containing file_, so it happens to match. But if the underscore appears only inside the nested element, as in:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<button>file<strong>_name</strong></button>

contains(text(), '_') checks the button’s direct text child (file) and can miss the underscore. contains(., '_') sees the combined descendant text and is the safer default when visual text may be wrapped in spans, emphasis tags, or other inline markup.

There is another XPath 1.0 subtlety: when a node-set such as text() is converted to a string, the conversion uses the first node in document order. An element with several direct text nodes therefore should not be treated as though text() automatically tests every one of them. Use the dot form for the complete string-value, or make the structure explicit if direct-child matching is intentional.

Text, attributes, and element boundaries

When the underscore is in an attribute

Element-text predicates do not inspect attributes. For a label stored in data-label, use:

//*[@data-label and contains(@data-label, '_')]

The first condition excludes elements without that attribute; the second tests the attribute value. Change @data-label to the actual attribute name, such as @id or @aria-label, when that is where the underscore lives.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When ancestors are unwanted

A broad descendant-aware predicate can return both the target and its parents. Scope by element name, class, or another relationship:

//section//button[contains(., '_')]

If a parent must not match merely because a child does, target the expected node type (for example, //button) or add a structural condition that identifies the leaf or control you need. The right scope depends on the page’s markup.

Using the locator in automation code

Pass the XPath as a string to your framework; XPath quoting and host-language quoting are separate layers. The following examples show the same locator in common environments.

Python with Selenium

from selenium.webdriver.common.by import By

matches = driver.find_elements(By.XPATH, "//*[contains(., '_')]
")
for element in matches:
    print(element.text)

If your Python string uses single quotes outside, use double quotes around the underscore in XPath, or escape the outer quote. Keep the XPath valid first, then satisfy the programming language’s string rules.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

JavaScript DOM evaluation

const result = document.evaluate(
  "//*[contains(., '_')]",
  document,
  null,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  null
);

for (let i = 0; i < result.snapshotLength; i++) {
  console.log(result.snapshotItem(i));
}

In browser automation libraries that accept a locator string directly, provide //*[contains(., '_')] to the library’s XPath locator method rather than evaluating it as CSS.

Version, case, and collation

XPath 3.1 defines contains() as a substring test that takes collations into account. XPath 3.1 expressions are case-sensitive by default. Case is irrelevant when the only character of interest is the underscore, but it matters if the same predicate also tests surrounding letters.

Do not assume that every browser, XML processor, or automation framework implements the same XPath version or exposes configurable collations. Check the host application’s XPath support before relying on version-specific behavior. The basic underscore expressions above use long-established syntax, while collation-sensitive or case-insensitive requirements may need a host-specific solution.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Practical troubleshooting

  • No result when text is visibly present: inspect the markup. If the underscore is inside a descendant element, replace text() with ..
  • Too many results: remember that a parent can match because a descendant contains the character. Replace * with the expected element name or add an identifying predicate.
  • The whole value does not match: contains() is substring matching. Use [. = 'value'] for exact equality.
  • The underscore is not in text: select the attribute explicitly, for example contains(@data-label, '_').
  • Only some direct text nodes are considered: XPath 1.0 string conversion of a node-set uses the first node in document order. Use the element dot form when you need the combined string-value.
  • Invalid expression in code: separate XPath quoting from host-language quoting. A quote that is valid in XPath may still terminate a Python, JavaScript, Java, or shell string.
  • Unexpected case behavior: verify the XPath version and collation supplied by the host. Do not assume a browser’s evaluator supports every XPath 3.1 feature.
  • Matches differ between tools: confirm the context node. An expression beginning with // searches descendants of the context, and a framework may set that context to a document, frame, or component root.

A repeatable selection workflow

  1. Inspect whether the underscore is in element text or an attribute.
  2. Start with //*[contains(., '_')] for complete descendant-aware text.
  3. If only direct text is valid, test contains(text(), '_') and verify that multiple text nodes are not being overlooked.
  4. Switch to equality when the entire string must match.
  5. Reduce false positives by naming the element or adding a structural condition.
  6. Run the expression in the same frame, document, and XPath implementation used by your automation code.

Or skip the browser setup

If your next step is to obtain a clean visual capture of the page rather than inspect nodes interactively, ScreenshotNeo returns a screenshot or PDF from one GET request. It is separate from XPath selection, so use the expressions above when you need element locators; use this API when you need the rendered page asset.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Its API documentation is at https://screenshotneo.com/docs/. A cURL request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in Python:

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)

And Node.js:

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 and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers report the page verdict and whether it was billed.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
  • The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan.

Create a free ScreenshotNeo account to try the 1,000 monthly screenshots without a card.

Quick decision guide

Question Use
Can the underscore appear inside nested markup? contains(., '_')
Must it be a direct text child? contains(text(), '_'), with the text-node caveat
Must the entire value be one exact string? . = 'your_value'
Is the value stored in an attribute? contains(@attribute, '_')
Are ancestors being returned accidentally? Scope the path to the expected element or structure

For ordinary element-text searches, begin with //*[contains(., '_')], then narrow the path only when the page structure requires it.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.