Wat deze tool doet
Plak een document, schrijf een XPath-expressie, klik op Evalueren, en elke node die de expressie selecteert wordt weergegeven met zijn positie in het document en een opgemaakt, leesbaar fragment van de inhoud. Het document kan XML of HTML zijn — een keuzelijst naast de knoppen bepaalt welke parser wordt gebruikt, want de twee gedragen zich op manieren die je in de expressie zult merken.
Het is eerder een tester dan een opmaker: het doel is uit te zoeken wat een expressie werkelijk selecteert voordat je hem vastlegt in een scraper, een XSLT-stylesheet, een testsuite of een configuratiebestand. Gokken naar een XPath en die vervolgens debuggen in wat hem uiteindelijk uitvoert is traag; hem uitproberen op een echt document kost seconden.
Elke evaluatie verschijnt boven de vorige, zodat twee kandidaat-expressies naast elkaar vergeleken kunnen worden zonder het eerste resultaat kwijt te raken. Kaarten kunnen één voor één worden verwijderd of in één keer worden gewist, en niets overleeft een herlaad — geen cookie, geen lokale opslag, geen upload.
Het invoerveld voor de expressie accepteert alles wat de XPath-engine van de browser ondersteunt, en dat is XPath 1.0. Dat dekt vrijwel elke praktische selectie met paden, predicaten en functies; het hoofdstuk over versies verderop legt uit wat daarbuiten valt.
XPath op één pagina
Een XPath-expressie is een pad door een document, van links naar rechts gelezen. /order/items/item loopt vanaf de root naar de elementen die bij elke stap genoemd worden. Een dubbele schuine streep vooraan, //item, betekent "overal in het document, op elke diepte", en is het werkpaard van snelle selecties.
Een stap kan worden gefilterd met een predicaat tussen rechte haken. //item[@qty > 1] houdt alleen de items over waarvan het attribuut qty groter is dan één; //item[1] neemt het eerste item binnen elke ouder, niet het eerste in het hele document — en dat is de meest voorkomende verrassing in XPath. Predicaten kunnen worden gestapeld en zelf paden bevatten: //order[customer/@vip = "true"] selecteert orders op basis van iets dat erin genest zit.
Een @ selecteert een attribuut in plaats van een element, dus //item/@sku geeft de attribuutnodes zelf terug, die deze pagina individueel opsomt. text() selecteert tekstnodes, en * komt overeen met elke elementnaam.
Naast de afkortingen bestaan er assen, die de richting van de beweging bepalen: parent::, ancestor::, following-sibling::, preceding-sibling::, descendant::. Dit zijn de middelen waarmee een expressie kan klimmen in plaats van alleen af te dalen — het klassieke voorbeeld is "zoek de cel met dit label, en neem dan de daaropvolgende cel".
Tot slot zijn er functies. contains(), starts-with(), normalize-space(), count(), string-length(), last() en position() dekken de overgrote meerderheid van echte expressies. normalize-space() verdient in het bijzonder zijn plek, want tekst in markup is zelden zo netjes bijgesneden als je zou verwachten.
Je document verlaat je browser nooit
Niets wat je hier plakt, wordt een netwerkverzoek. Het document dat je plakt wordt geparst door je eigen browser, de expressie wordt geëvalueerd door de ingebouwde XPath-engine van de browser, en beide bestaan alleen in het geheugen — niets wordt naar een server gepost, in een cookie geschreven of in lokale opslag bewaard. Herladen wist elk resultaat, en het sluiten van het tabblad ruimt de gegevens op.
Dat is een bewuste ontwerpkeuze, geen bijkomstigheid. De documenten waarop mensen expressies testen zijn exports, API-responses, facturen en gescrapete pagina's uit ingelogde sessies, en ze bevatten regelmatig namen, adressen, bestelgeschiedenissen, rekeningnummers en nog geldige tokens. Een tester die het document uploadt om de expressie te evalueren, maakt van een taak van twee minuten een gegevensoverdracht — en in een gereguleerde omgeving is dat een overdracht die je moet kunnen verantwoorden.
Controleer het liever dan het te vertrouwen: open je netwerktabblad, plak een document en evalueer een expressie. Afgezien van de analytics-beacon die deze site bij elke paginalading verstuurt — een URL en een paginatitel — wordt er niets aangevraagd, wat het document ook bevat. Verbreek de netwerkverbinding volledig en de pagina blijft werken, want er is aan de andere kant niets om mee te praten.
Namespaces: waarom een correcte expressie niets teruggeeft
De meest voorkomende reden waarom een ogenschijnlijk correcte expressie niets matcht, zijn namespaces. Als het document er een declareert — <feed xmlns="http://www.w3.org/2005/Atom">, een SOAP-envelope, een XSD, een sitemap, vrijwel elk gestandaardiseerd XML-formaat — dan heten de elementen niet feed en entry. Ze heten zo binnen die namespace, en een expressie zonder voorvoegsel zoekt alleen in helemaal geen namespace. Dus //entry vindt niets, correct en onhandig tegelijk.
Evalueren met een namespace-resolver is de formele oplossing, maar er is een kortere die overal werkt en geen configuratie nodig heeft: matchen op de lokale naam. //*[local-name()="entry"] selecteert elk element genaamd entry, ongeacht de namespace, en hetzelfde werkt voor attributen met @*[local-name()="href"]. Het is botter dan een resolver — het matcht ook een gelijknamig element uit een andere namespace — maar voor testen en scrapen is dat vrijwel altijd precies wat je bedoelde.
Een voorvoegsel dat je in je expressie schrijft, heeft overigens geen relatie met het voorvoegsel in het document. Voorvoegsels zijn lokale labels die aan URI's gekoppeld zijn; hetzelfde document kan opnieuw geserialiseerd worden met andere voorvoegsels en toch precies hetzelfde betekenen. Daarom faalt //soap:Body hier zelfs als het document die tekst overduidelijk bevat.
Het document als HTML parsen omzeilt het hele probleem, want de HTML-parser plaatst alles in één namespace en negeert voorvoegsels. Als een document toch niet strikt XML is, is het wisselen van parser vaak de snelste weg naar een werkende expressie.
XML-modus en HTML-modus
De twee parsers zijn het op manieren oneens die veranderen wat je expressie matcht, dus de keuzelijst is geen formaliteit.
XML-modus is streng. Het document moet welgevormd zijn — elke tag gesloten, tags genest in plaats van overlappend, attribuutwaarden tussen aanhalingstekens, ampersands ge-escaped — en zo niet, dan krijg je een parserbericht in plaats van een resultaat. Elementnamen zijn hoofdlettergevoelig, dus //Item en //item zijn verschillende selecties. Namespaces gelden, met alles wat daarboven beschreven staat.
HTML-modus is tot in het extreme toegeeflijk: de parser accepteert alles en herstelt wat nodig is, dus een parseerfout bestaat niet. Hij herstructureert ook. Een tr zonder tbody krijgt er een, en daarom selecteert //table/tr niets op een echte pagina, terwijl //table/tbody/tr of simpelweg //table//tr wel werkt. Tagnamen worden genormaliseerd naar kleine letters, dus schrijf ze in de expressie in kleine letters, ongeacht hoe de bron ze schrijft. Elementen die de parser als afgedwaald beschouwt, worden verplaatst, soms buiten de tak waar je ze verwachtte.
De vuistregel: gebruik XML-modus voor alles wat werkelijk XML is en waarbij je ook wilt weten of het welgevormd is, en HTML-modus voor pagina's, fragmenten en e-mailberichten. Als een expressie in de ene modus werkt en in de andere niet, is het parserverschil bijna altijd de reden.
Wat je terugkrijgt: nodesets en scalars
Niet elke expressie geeft nodes terug. XPath kent vier resultaattypes, en deze pagina toont elk daarvan zoals het is.
Een nodeset is het gebruikelijke geval: //item, //item/@sku, //name/text(). Elke node wordt weergegeven met een pad zoals /order[1]/items[1]/item[2] — inclusief indexen, wat elke regel tot een werkende expressie op zichzelf maakt — en een opgemaakt fragment van de node zelf. Elementen worden getoond met hun attributen en kinderen, attributen als naam en waarde, tekstnodes als hun inhoud.
Een getal komt van een expressie zoals count(//item) of sum(//item/@qty). Een booleaanse waarde komt van een test zoals //item[@qty > 5] verpakt in boolean(), of van een vergelijking. Een tekst komt van string(//name), normalize-space(//name) of concat(). Elk wordt getoond als één waarde, met het type genoemd in de kop van de kaart.
Eén detail waar mensen over struikelen: wanneer een expressie die meerdere nodes zou kunnen teruggeven wordt omgezet naar tekst, neemt XPath 1.0 alleen de eerste node en gooit de rest stilzwijgend weg. string(//item) geeft je één item, niet allemaal. Als een scraper stilletjes maar de eerste rij teruggeeft, is deze regel meestal de reden — en dezelfde expressie hier evalueren, waar de volledige nodeset getoond wordt, maakt de discrepantie meteen duidelijk.
Een uitgewerkt voorbeeld
Het voorbeelddocument is een kleine bestelling: een root order met een identifier, een klant met naam en e-mailadres, en twee items die elk een SKU, een aantal en een prijs als tekst dragen.
De voorbeeldexpressie, //item[@qty > 1], geeft één match terug — het item met een aantal van twee — weergegeven op /order[1]/items[1]/item[1] met de bijbehorende markup. Verander het naar //item/@sku en je krijgt twee attribuutnodes; naar count(//item) en je krijgt het getal 2; naar //customer/name/text() en je krijgt de naam als tekstnode; naar //item[last()] en je krijgt het laatste item binnen zijn ouder. Elke evaluatie stapelt zich boven de vorige, zodat de verschillen naast elkaar op het scherm blijven staan.
Nee. Niets van wat je plakt wordt verzonden: parsen en evalueren gebeuren in je browser, en het resultaat bestaat alleen in de pagina voor je. Er is geen cookie, geen lokale opslag en geen logging van wat je plakt of van de expressies die je probeert, en dat is ook waarom een herlaad de resultaten leegt.
XPath 1.0, wat browsers implementeren. Paden, assen, predicaten en de standaard functiebibliotheek zijn allemaal beschikbaar. Wat niet beschikbaar is: de toevoegingen uit 2.0 en 3.1, zoals sequences, for- en let-expressies, functies voor reguliere expressies zoals matches() en replace(), en datumberekeningen. Daarvoor is een specifieke processor nodig, zoals Saxon.
Namespaces zijn meestal de oorzaak. Als het document een xmlns declareert, matcht een naam zonder voorvoegsel niets; gebruik in plaats daarvan //*[local-name()="naam"], of parse het document als HTML. De op één na meest voorkomende oorzaak is een impliciete tbody in een HTML-tabel, waardoor //table/tr niet werkt.
Omdat een predicaat geldt binnen elke ouder, niet over de hele nodeset. //item[1] betekent "het eerste item binnen elk van zijn ouders". Om de eerste node van het volledige resultaat te nemen, zet het pad eerst tussen haakjes: (//item)[1].
Ja — kopieer de paginabron, plak hem hier, zet de parser op HTML en test de expressie erop. Houd er rekening mee dat inhoud die door JavaScript is ingevoegd niet in de bron staat, dus als het element dat je zoekt ontbreekt in de opgeslagen markup, vindt geen enkele expressie het en ligt de oplossing stroomopwaarts.
Nee. Het document wordt geparst en bevraagd; er wordt niets teruggeschreven. De getoonde fragmenten zijn opgemaakt om te lezen, wat de witruimte tussen elementen normaliseert — kopieer dus uit je origineel als exacte bytes ertoe doen.
Zodra de pagina geladen is, ja. De parser en de XPath-engine zitten beide ingebouwd in je browser, dus je kunt de verbinding verbreken en blijven testen — wat ook meteen het eenvoudigste bewijs is dat er niets wordt geüpload.