Why match on text at all
Sometimes the most natural way to find an element is by what it says. A "Submit" button, an "Accept all" prompt, a "Next page" link or a row containing a specific product name are all defined more clearly by their words than by any ID or class. Text matching mirrors how a human reads the page, which makes scripts easy to follow and resilient when developers reshuffle markup but keep labels the same. This handbook covers the XPath patterns that make text matching dependable in Selenium, and the recurring traps that cause an otherwise correct locator to return nothing.
The basic exact-match pattern
Selenium has no dedicated "by visible text" strategy for arbitrary elements, so text matching is done with XPath. The simplest form matches a node whose text equals a value:
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("https://example.com")
button = driver.find_element(By.XPATH, "//button[text()='Submit']")
This works when the label is clean and lives directly inside the element. The trouble is that real pages rarely keep their text that tidy, which is where the more robust patterns earn their place.
Why normalize-space is almost always better
Designers and templating engines add stray spaces, line breaks and indentation around text. An exact text() match fails the moment a hidden newline sneaks in. Wrapping the comparison in normalize-space() trims leading and trailing whitespace and collapses internal runs to single spaces, which removes a whole class of frustrating misses:
button = driver.find_element(
By.XPATH, "//button[normalize-space()='Submit']"
)
As a habit, prefer normalize-space() over bare text() for any label that a human typed, because the whitespace you cannot see is the whitespace that breaks your script.
If a text locator works locally but fails in a headless or production run, suspect whitespace or nesting first. normalize-space() and contains(., ...) resolve the large majority of these failures.
Partial matching with contains
When the visible text includes dynamic fragments such as a count, a date or a price, an exact match is too brittle. The contains function matches when your value appears anywhere within the node's text:
# Matches "3 items in cart", "12 items in cart", etc.
element = driver.find_element(
By.XPATH, "//span[contains(., 'items in cart')]"
)
Using the dot inside contains(., ...) rather than contains(text(), ...) matters: the dot evaluates the element's full string value including nested children, which is what you usually want.
Text split across child elements
A frequent cause of failure is text broken up by inline tags, such as a button that wraps part of its label in a <span> or an icon. In that case text() only sees the first text node and your exact match fails. Matching against the element's whole string value with contains(., ...) or normalize-space() on the parent node handles the split correctly, because both consider the combined text of the element and its descendants.
Case-insensitive matching
Selenium relies on XPath 1.0, which has no built-in lower-case or case-insensitive comparison. The standard workaround is translate() to fold both sides to the same case before comparing:
xpath = (
"//button[translate(normalize-space(), "
"'ABCDEFGHIJKLMNOPQRSTUVWXYZ', "
"'abcdefghijklmnopqrstuvwxyz')='submit']"
)
button = driver.find_element(By.XPATH, xpath)
It is verbose, so for many cases it is simpler to match a stable substring with contains and accept a looser comparison rather than fight case sensitivity head-on.
Link text strategies for anchors
Anchor elements get dedicated locators that are cleaner than XPath. By.LINK_TEXT matches the full visible link label, and By.PARTIAL_LINK_TEXT matches a fragment of it. These are ideal for navigation, pagination and "read more" links. They only apply to <a> elements, so for buttons, spans and other tags you still fall back to XPath text matching.
Waiting for text to appear
As with any locator, text that is injected after load needs an explicit wait. Selenium even offers a condition that waits for specific text to be present in an element, which is handy when you are confirming an asynchronous result rather than locating a static label:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
WebDriverWait(driver, 10).until(
EC.text_to_be_present_in_element((By.ID, "status"), "Complete")
)
A checklist for reliable text locators
- Default to
normalize-space()instead of baretext()for human-entered labels. - Use
contains(., ...)when the text includes variable fragments. - Match against the element's whole string value when labels are split across child tags.
- Reach for
By.LINK_TEXTorBy.PARTIAL_LINK_TEXTfor anchors. - Handle case with
translate()or a substring match rather than assuming case matches. - Wrap async text in an explicit wait so you do not search too early.
Whitespace and invisible characters
Beyond ordinary spaces, pages sometimes contain non-breaking spaces, zero-width characters or trailing tabs that look identical to a normal space on screen but break an exact match. When a locator stubbornly fails on text you can clearly read, copy the value from the live DOM rather than the rendered page, and lean on contains with a short, unambiguous fragment to sidestep the hidden characters entirely.
When text matching is the wrong choice
Text locators shine for stable, meaningful labels, but they are a poor fit for content that changes language, gets A/B tested, or is fully dynamic. If your scraper must run across localised versions of a site, matching the English word "Submit" will fail everywhere else. In those situations a structural locator such as a data attribute, a stable class or position within a form is more durable than the words on screen.
Where proxies fit into text-based scraping
A text locator only works on the text you actually receive. Many sites localise their interface by region, run experiments that change labels, or return a challenge page with completely different wording when they suspect automation. Any of these will make your carefully written text XPath miss. For projects that span regions or run at volume, routing through reliable residential, ISP or mobile proxies helps you receive the intended version of the page, so the labels you are matching are the ones that actually render.
Which proxy types suit this work
For automation that should resemble normal browsing, residential and ISP proxies blend in best because they sit on consumer networks and are less likely to trigger a different, defensive layout. Datacenter proxies are faster and cheaper for permissive targets. Mobile proxies suit the most defensive sites at a higher cost. If you specifically need a given country's localisation, geo-targeted proxies in that market ensure you see the labels real users there see.
Common mistakes to avoid
- Using exact
text()when whitespace or nested tags make the match brittle. - Matching
contains(text(), ...)instead ofcontains(., ...)and missing nested content. - Hard-coding a single language label on a site that localises by region.
- Forgetting that A/B tests can change the very words you are matching.
- Searching before async content has rendered the text you expect.
How text matching compares with other locators
Text matching is the most human-readable locator and the most resilient to markup reshuffles that keep labels intact, but it is the most fragile against language changes, experiments and dynamic strings. ID and CSS selectors are faster and more stable when reliable hooks exist. A balanced scraper uses text locators where labels are stable and meaningful, and structural locators where the words are likely to shift.
Recommended proxy providers for scraping projects
When text-based scraping spans regions or runs at scale, a dependable proxy provider helps ensure each session loads the page version whose labels you are matching. As an independent ranking site we suggest the value pick first, then compare fairly.
- Cheapest Proxies — our Featured Value Pick, worth considering first for affordable residential, ISP and datacenter access on text-based scraping and regional testing.
- Smartproxy — often a balanced choice for mid-sized projects wanting residential and datacenter pools with approachable tooling.
- Bright Data — an enterprise-grade network that may suit large jobs needing wide geographic coverage.
- IPRoyal — frequently a flexible option for smaller budgets that still want a mix of proxy types.
Getting started checklist
Begin with a single normalize-space() match against a stable label, confirm it works, then add contains where text varies and explicit waits where text loads late. Once the core matching is solid, extend to your full target set and add proxies if you encounter localisation or block-page differences. Building in this order keeps each new behaviour testable against a foundation you have already proven.
Key takeaways
Find elements by text with XPath, defaulting to normalize-space() for clean exact matches and contains(., ...) for partial or variable text. Watch for the recurring traps of whitespace, nested tags, case and async loading, and use dedicated link-text strategies for anchors. Remember that the words on a page can change with localisation, experiments and defensive responses, so for regional or high-volume scraping a reliable proxy ensures the text you are matching is the text that actually renders.
Related proxy guides
Frequently asked questions
Questions or a correction? Email info@proxyranked.com. Always confirm a provider's exact package, proxy type and locations before ordering.