What this tool does
Paste a document, write an XPath expression, press Evaluate, and every node the expression selects is listed with its position in the document and a formatted, human readable snippet of what it contains. The document can be XML or HTML — a select next to the buttons decides which parser is used, because the two behave differently in ways the expression will feel.
It is a tester rather than a formatter: the point is to find out what an expression actually selects before you commit it to a scraper, an XSLT stylesheet, a test suite or a configuration file. Guessing at an XPath and then debugging it inside whatever runs it is slow; trying it against a real document takes seconds.
Each evaluation appears above the previous one, so two candidate expressions can be compared side by side without losing the first result. Cards can be removed one at a time or cleared all at once, and nothing survives a refresh — no cookie, no local storage, no upload.
The expression box accepts anything the browser XPath engine does, which is XPath 1.0. That covers the paths, predicates and functions almost every practical selection needs; the chapter on versions below says what falls outside it.
XPath in one page
An XPath expression is a path through a document, read left to right. /order/items/item walks from the root into the elements named at each step. A leading double slash, //item, means "anywhere in the document at any depth", which is the workhorse of quick selections.
A step can be filtered by a predicate in square brackets. //item[@qty > 1] keeps only the items whose qty attribute exceeds one; //item[1] takes the first item within each parent, not the first in the document, which is the single most common surprise in XPath. Predicates can be stacked and can themselves contain paths: //order[customer/@vip = "true"] selects orders by something nested inside them.
An @ selects an attribute rather than an element, so //item/@sku returns the attribute nodes themselves, which this page lists individually. text() selects text nodes, and * matches any element name.
Beyond the shorthand there are axes, which choose the direction of travel: parent::, ancestor::, following-sibling::, preceding-sibling::, descendant::. These are what let an expression climb rather than only descend — the classic case being "find the cell containing this label, then take its following sibling".
Finally there are functions. contains(), starts-with(), normalize-space(), count(), string-length(), last() and position() cover the great majority of real expressions. normalize-space() in particular earns its keep, since text in markup is rarely trimmed the way you expect.
Your document never leaves your browser
Nothing you paste here becomes a network request. The document you paste is parsed by your own browser, the expression is evaluated by the browser built-in XPath engine, and both exist only in memory — nothing is posted to a server, written to a cookie or saved in local storage. Refreshing clears every result, and closing the tab disposes of the data.
That is a deliberate design choice rather than a feature. The documents people test expressions against are exports, API responses, invoices and scraped pages from logged-in sessions, and they routinely contain names, addresses, order histories, account numbers and tokens that are still valid. A tester that uploads the document to evaluate the expression turns a two-minute task into a data transfer, and in a regulated environment that transfer is something you would have to be able to explain.
Check it rather than trusting it: open your network tab, paste a document and evaluate an expression. Beyond the analytics beacon this site sends on every page load — a URL and a page title — nothing is requested, whatever the document holds. Disconnect from the network entirely and the page keeps working, because there is nothing on the other end to talk to.
Namespaces: why a correct expression returns nothing
The most common reason an obviously correct expression matches nothing is namespaces. If the document declares one — <feed xmlns="http://www.w3.org/2005/Atom">, a SOAP envelope, an XSD, a sitemap, almost any standardised XML format — then its elements are not named feed and entry. They are named those things in that namespace, and an unprefixed expression looks only in no namespace at all. So //entry finds nothing, correctly and unhelpfully.
Evaluating with a namespace resolver is the formal fix, but there is a shorter one that works everywhere and needs no configuration: match on the local name. //*[local-name()="entry"] selects every element called entry regardless of namespace, and the same works for attributes with @*[local-name()="href"]. It is blunter than a resolver — it will also match a same-named element from a different namespace — but for testing and scraping that is nearly always what you wanted.
A prefix written in your expression, incidentally, has no relationship to the prefix in the document. Prefixes are local labels bound to URIs; the same document can be re-serialised with different prefixes and mean exactly the same thing. That is why //soap:Body fails here even when the document plainly contains that text.
Parsing the document as HTML sidesteps the whole problem, because the HTML parser puts everything in one namespace and ignores prefixes. If a document is not strictly XML anyway, switching the parser is often the fastest route to a working expression.
XML mode and HTML mode
The two parsers disagree in ways that change what your expression matches, so the select is not a formality.
XML mode is strict. The document must be well-formed — every tag closed, tags nested rather than overlapped, attribute values quoted, ampersands escaped — and if it is not, you get a parser message instead of a result. Element names are case-sensitive, so //Item and //item are different selections. Namespaces apply, with all of the above.
HTML mode is forgiving to a fault: the parser accepts anything and repairs what it must, so there is no such thing as a parse failure. It also restructures. A tr without a tbody gets one, which is why //table/tr selects nothing on a real page while //table/tbody/tr or simply //table//tr works. Tag names are normalised to lower case, so write them lower case in the expression regardless of how the source is written. Elements the parser considers stray are moved, sometimes out of the branch you expected them in.
The rule of thumb: use XML mode for anything that is genuinely XML and where you also want to know that it is well-formed, and HTML mode for pages, fragments and email bodies. If an expression works in one and not the other, the parser difference is nearly always the reason.
What comes back: node sets and scalars
Not every expression returns nodes. XPath has four result types, and this page renders each of them as what it is.
A node set is the usual case: //item, //item/@sku, //name/text(). Every node is listed with a path such as /order[1]/items[1]/item[2] — indexes included, which makes each line a working expression in its own right — and a beautified snippet of the node itself. Elements are printed with their attributes and children, attributes as name and value, text nodes as their content.
A number comes from an expression such as count(//item) or sum(//item/@qty). A boolean comes from a test such as //item[@qty > 5] wrapped in boolean(), or from a comparison. A string comes from string(//name), normalize-space(//name) or concat(). Each is shown as a single value with its type named in the card header.
One detail that trips people: when an expression that could return many nodes is coerced to a string, XPath 1.0 takes the first node only and silently discards the rest. string(//item) gives you one item, not all of them. If a scraper is quietly returning only the first row, that rule is usually why — and evaluating the same expression here, where the node set is shown in full, makes the discrepancy obvious.
A worked example
The sample document is a small order: a root order with an identifier, a customer with a name and an email, and two items each carrying a SKU, a quantity and a price as text.
The sample expression, //item[@qty > 1], returns one match — the item with a quantity of two — listed at /order[1]/items[1]/item[1] with its markup beside it. Change it to //item/@sku and you get two attribute nodes; to count(//item) and you get the number 2; to //customer/name/text() and you get the name as a text node; to //item[last()] and you get the final item within its parent. Each evaluation stacks on top of the last, so the differences stay on screen next to one another.
No. Nothing you paste is transmitted: parsing and evaluation happen in your browser, and the result exists only in the page in front of you. There is no cookie, no local storage and no logging of what you paste or of the expressions you try, which is also why a refresh empties the results.
XPath 1.0, which is what browsers implement. Paths, axes, predicates and the standard function library are all available. What is not: the 2.0 and 3.1 additions such as sequences, for and let expressions, regular-expression functions like matches() and replace(), and date arithmetic. Those need a dedicated processor such as Saxon.
Namespaces are the usual cause. If the document declares an xmlns, an unprefixed name matches nothing; use //*[local-name()="name"] instead, or parse the document as HTML. The second most common cause is an implicit tbody in an HTML table, which breaks //table/tr.
Because a predicate applies within each parent, not across the whole node set. //item[1] means "the first item inside each of its parents". To take the first node of the complete result, wrap the path in brackets first: (//item)[1].
Yes — copy the page source, paste it here, switch the parser to HTML and test the expression against it. Bear in mind that content injected by JavaScript is not in the source, so if the element you want is missing from the markup you saved, no expression will find it and the fix belongs upstream.
No. The document is parsed and queried; nothing is written back. The snippets shown are formatted for reading, which normalises whitespace between elements, so copy from your original if exact bytes matter.
Once the page has loaded, yes. The parser and the XPath engine are both built into your browser, so you can disconnect and keep testing — which is also the simplest proof that nothing is being uploaded.