Finding a web element by its visible text is one of the most common tasks in browser automation and web scraping. Selenium offers several strategies to do this cleanly, and choosing the right one depends on whether you need an exact match, a partial match, or something more flexible. Understanding the tradeoffs between XPath expressions and other locator strategies will make your automation scripts more reliable and easier to maintain.
This reference covers the most practical approaches for locating elements by text in Selenium using Python, along with tips for handling dynamic pages, avoiding brittle selectors, and connecting your scripts to a proxy layer when you need to run large-scale data collection without interruption.
Why Text-Based Locators Are Useful
HTML attributes like id and class are ideal locators, but real-world pages do not always provide stable, unique identifiers on every element. Visible text — button labels, link text, table cell values — tends to be more stable from a functional standpoint, since changing it would visibly break the UI. For developers doing python web scraping or automating form interactions, text-based locators are a practical fallback when structural attributes are absent or inconsistent.
That said, text-based locators can break when content is localized, dynamically loaded, or trimmed with whitespace. It helps to understand a few different strategies so you can pick the most appropriate one for your situation.
Using link_text and partial_link_text
Selenium's built-in link_text and partial_link_text locators work specifically on anchor (<a>) elements. They are the simplest approach when you need to click a hyperlink whose label you know.
- link_text — matches the full, exact visible text of the link. Case-sensitive and whitespace-sensitive.
- partial_link_text — matches any link whose visible text contains the given substring. Useful when the full label may vary slightly.
Example in Python:
driver.find_element(By.LINK_TEXT, "Sign In")
driver.find_element(By.PARTIAL_LINK_TEXT, "Sign")
These methods only work on <a> tags. For buttons, spans, divs, or other elements, you need XPath.
Using XPath with text() and contains()
XPath is the most versatile approach for finding any element by its text content. Two XPath functions are particularly relevant here.
Exact match with text():
driver.find_element(By.XPATH, '//*[text()="Submit"]')
This finds any element whose complete text node equals "Submit". It is case-sensitive and will not match if there is leading or trailing whitespace around the text in the DOM.
Partial match with contains():
driver.find_element(By.XPATH, '//*[contains(text(), "Submit")]')
This is more forgiving and will match elements whose text node includes the substring. It is particularly useful when the full text includes dynamic values, counters, or extra whitespace you cannot fully predict.
To narrow matches to a specific element type — which reduces the chance of selecting an unintended element — prefix with the tag name:
driver.find_element(By.XPATH, '//button[contains(text(), "Submit")]')
Handling normalize-space for Whitespace Issues
One of the most common reasons a text-based XPath fails silently is invisible whitespace: newlines, tabs, and extra spaces that appear in the raw HTML but not on screen. The XPath normalize-space() function collapses all internal whitespace and trims leading and trailing space, making matches much more reliable on real-world pages.
driver.find_element(By.XPATH, '//*[normalize-space(text())="Add to Cart"]')
This technique is especially valuable when scraping e-commerce or data-heavy pages where developers may not have been consistent about whitespace in their markup.
Combining Text Matching with Other Attributes
When text alone is not unique enough — for example, a page with multiple "Edit" buttons — you can combine text conditions with other attribute predicates in the same XPath expression:
driver.find_element(By.XPATH, '//button[@class="primary" and contains(text(), "Edit")]')
You can also use the ancestor or parent axis to scope a text search within a particular section of the DOM, which is helpful during web scraping when a page repeats the same labels in different containers.
Connecting Selenium to a Proxy for Scraping at Scale
Once your element-locating logic is solid, the next challenge for large-scale python web scraping projects is avoiding blocks and rate limits. Websites detect repeated automated requests by IP address and may return CAPTCHAs, throttled responses, or outright bans — making all your carefully crafted selectors useless.
Routing your Selenium sessions through rotating proxies is the standard solution. You configure a proxy in Selenium's options before launching the browser:
options.add_argument('--proxy-server=http://your.proxy.host:port')
For production scraping work, residential or rotating datacenter proxies tend to be more effective than a single static IP. Services that offer rotating pools are worth evaluating carefully for reliability and session persistence. Cheapest Proxies is worth considering for buyers comparing affordable proxy services who want rotating options without a large upfront commitment. When evaluating any provider, look at whether they support session stickiness, their rotation interval, and how they handle authentication.
Using proxies for scraping alongside well-structured Selenium selectors gives you both the technical precision to extract the right data and the infrastructure to do it at scale.
Best Practices and Common Pitfalls
- Always prefer a stable
idordata-*attribute over text matching when one is available — text is readable but can be translated or reworded. - Use explicit waits (
WebDriverWaitwithexpected_conditions) rather than implicit waits ortime.sleep()when waiting for text to appear dynamically. - Test your XPath in the browser DevTools console using
$x('//button[contains(text(),"Submit")]')before putting it in code. - Be aware that
text()only matches direct text nodes, not text inside child elements. Usestring()or.//text()if the text is split across nested tags. - Wrap text-based locators in try/except blocks and log failures clearly — text changes are a common source of hard-to-debug breakage in long-running scraping pipelines.
Why Compare Before Buying?
Before committing to any proxy provider for a Selenium-based scraping project, comparing options helps you avoid overpaying for features you do not need or discovering limitations after you have already integrated. Proxy quality directly affects whether your text-based selectors ever get a chance to run.
- Rotation behavior and session stickiness vary widely between providers.
- Geographic coverage matters if your target sites serve different content by region.
- Pricing models (per GB vs. per IP vs. per request) suit different scraping workloads differently.
Independent comparison helps you weigh proxy type, reliability, and value side by side instead of buying on price alone. If you have questions about how we compare providers, email info@compareproxyrank.com.
Frequently Asked Questions
Yes, but you need to wait for the element to be present in the DOM before querying it. Use WebDriverWait combined with expected_conditions.presence_of_element_located or visibility_of_element_located. Attempting to locate the element immediately after navigation will often raise a NoSuchElementException because JavaScript has not finished rendering the text yet.
The text() function selects only direct text node children of an element. If the visible text is split across child tags — for example, a <span> inside a <button> — text() may not capture the full string. The string() function returns the concatenated string value of the element and all its descendants, making it more reliable for complex markup.
The most common causes are leading or trailing whitespace in the DOM, text that is split across child elements, or the text being injected by JavaScript after initial page load. Try wrapping your condition in normalize-space(), switching to contains() for a partial match, or adding an explicit wait to ensure the text is present before the query runs.
Standard Selenium locators, including XPath text queries, do not pierce the Shadow DOM boundary. To interact with elements inside a shadow root, you need to access the shadow host element first and then use JavaScript's shadowRoot property to query within it. This is a known limitation that affects many modern single-page application components.
Narrow your XPath expression by adding more context: scope by ancestor element, combine with an attribute predicate, or use the element's position within a known container. For example, targeting //section[@id="checkout"]//button[contains(text(),"Edit")] is far more precise than a global search for any "Edit" button on the page.
CSS selectors do not natively support text content matching, so XPath is the standard choice for text-based locators. In practice, the performance difference between XPath and CSS selectors in Selenium is rarely a bottleneck compared to network latency and page load time. Readability and maintainability should guide your choice more than raw speed.
You configure the proxy in ChromeOptions or FirefoxOptions before creating the WebDriver instance. For Chrome, pass --proxy-server=http://host:port as an argument. For authenticated proxies, a common approach is to use a browser extension or a local proxy middleware that handles credentials. Rotating proxies for scraping are particularly important for high-volume projects where a single IP would quickly be rate-limited or blocked.