Unser Interesse an dieser Thematik ist zwar gering, aber dennoch erwähnenswert: Wir sind Geonode und verkaufen Proxys an Personen, die Daten extrahieren, weshalb Selektoren in Support-Gesprächen ständig zur Sprache kommen. Bevor wir den Vergleich anstellen, sollte man jedoch erwähnen, dass keine der beiden Optionen Einfluss darauf hat, ob man blockiert wird. Eine Selektor-Frage und eine Netzwerk-Frage sehen auf den ersten Blick identisch aus – beide führen zu der Meldung „Mein Scraper liefert keine Daten mehr“ –, haben aber nichts miteinander zu tun. Wenn die Seite vollständig geladen wurde und Ihr Ausdruck mit nichts übereinstimmte, handelt es sich um ein Selektor-Problem, das durch keine infrastrukturelle Entscheidung behoben werden kann.
Die Kurzfassung
| CSS | XPath | |
|---|---|---|
| Auswahl nach Klasse, ID, Attribut | Ja, sauber | Ja, umständlich |
| Nachkommen- und Kinderbeziehungen | Ja | Ja |
| Auswahl nach Textinhalt | Nein | Ja |
| Navigation zum übergeordneten Element oder Vorfahren | Teilweise, über :has() | Ja, direkt |
| Navigation zum vorherigen Geschwisterelement | Teilweise, über :has() | Ja, direkt |
| Positionsauswahl | „:nth-child()“-Familie | „position()“, „last()“, Prädikate |
| Zeichenfolgenfunktionen | Nein | Ja |
| XML-Abfrage mit Namespaces | Nein | Ja |
| Lesbarkeit | Besser | Schlechter |
| Tool- und Browserunterstützung | Universell | Universell für 1.0 |
Zwei Zeilen sind dabei entscheidend. CSS kann überhaupt nicht nach Textinhalt selektieren, was es für das mit Abstand häufigste Extraktionsmuster ausschließt – nämlich das Auffinden eines Werts anhand der daneben stehenden Beschriftung. Und XPath ist schwerer zu lesen, was eine größere Rolle spielt, als man zugeben möchte, wenn ein Kollege Ihren Code ein Jahr später pflegen muss.
Was CSS besser macht
Die Auswahl von Klassen und Attributen – und das mit großem Abstand.
div.product-card
a[href^="https://"]
input[type="checkbox"]:checked
ul > li:first-child
section.content p:not(.footnote)
Die entsprechenden XPath-Ausdrücke sind länger und, was Klassen angeht, regelrecht unangenehm:
//div[contains(concat(' ', normalize-space(@class), ' '), ' product-card ')]
//a[starts-with(@href, 'https://')]
//ul/li[1]
Der erste Ausdruck ist das Idiom zur Klassenabgleichung, und es existiert, weil @class
eine durch ein einzelnes Leerzeichen getrennte Zeichenkette ist und XPath keine Token darin unterscheidet. Ein naiver „contains(@class, 'product-card')
“ passt auch auf „product-card-large
“ und „old-product-card
“, daher ist die mit Leerzeichen aufgefüllte Version die richtige. Sie ist zudem viermal so lang wie „div.product-card
“ und deutlich schwerer zu überfliegen.
Wenn Ihre Auswahlkriterien Klassen, IDs, Attribute und strukturelle Beziehungen sind, verwenden Sie CSS. Dies deckt einen Großteil der tatsächlichen Auswahlaufgaben ab, und die Wahl von XPath bedeutet in diesem Fall, dass Sie zugunsten von Funktionen, die Sie gar nicht nutzen, Einbußen bei der Lesbarkeit in Kauf nehmen.
CSS bietet zudem bessere Tooling-Möglichkeiten. Der Element-Inspector jedes Browsers generiert nativ CSS-Selektoren, die meisten Test-Frameworks verwenden diese standardmäßig, und document.querySelectorAll
ist überall ohne Hilfsmittel verfügbar.
Was XPath besser kann
Textabgleich, was CSS überhaupt nicht kann:
//button[normalize-space()='Continue']
//a[contains(., 'Download')]
//dt[normalize-space()='Price']/following-sibling::dd[1]
Das letzte Muster – das Label finden, den angrenzenden Wert übernehmen – ist das Arbeitspferd der Extraktion aus strukturierten Seiten, und dafür gibt es keinen CSS-Ausdruck, da CSS keinen Zugriff auf Textinhalte hat. Diese eine Fähigkeit ist der Grund, warum XPath weiterhin zum Scraping von Codebasen eingesetzt wird, die ansonsten durchgehend CSS verwenden.
Navigation zu Vorfahren:
//span[@class='price']/ancestor::div[contains(@class,'card')][1]
Von einem Wert aus nach oben zu dem Container navigieren, der ihn enthält. :has()
bietet CSS eine Form davon, deren Einschränkungen weiter unten erläutert werden.
Positionslogik relativ zum Inhalt:
//h2[normalize-space()='Specifications']/following-sibling::table[1]
Die erste Tabelle nach einer bestimmten Überschrift. :nth-child()
zählt Positionen unter Geschwistern; es kann nicht „nach dem Element, dessen Text X ist“ ausdrücken.
Zeichenfolgenfunktionen. normalize-space()
, substring-before()
, translate()
und die übrigen ermöglichen es Ihnen, innerhalb des Ausdrucks zu arbeiten. Insbesondere normalize-space()
ist nahezu unverzichtbar, da echtes HTML „pretty-printed“ ist und ein exakter Textvergleich mit "\n In stock\n"
fehlschlagen wird.
XML mit Namespaces. Wenn Sie XML statt HTML abfragen – eine Sitemap, einen RSS-Feed, eine SOAP-Antwort –, ist CSS nicht das richtige Werkzeug. XPath ist dafür konzipiert, und die Behandlung von Namespaces ist Teil des Designs.
Wie „:has()“ die Situation verändert hat
Die bedeutendste Entwicklung in diesem Vergleich – und viele Artikel sind bereits älter als diese.
Die MDN-Dokumentation beschreibt „:has()“ als Darstellung „eines Elements, wenn einer der als Argument übergebenen relativen Selektoren mindestens ein Element trifft, wenn er an diesem Element verankert ist“, und bietet damit „eine Möglichkeit, ein übergeordnetes Element oder ein vorhergehendes Geschwisterelement in Bezug auf ein Referenzelement auszuwählen“. Sein Baseline-Status lautet „weit verbreitet“; die Unterstützung durch verschiedene Browser besteht seit Dezember 2023.
Somit kann CSS nun Dinge ausdrücken, die zuvor nicht möglich waren:
div.card:has(span.sold-out) /* a card containing a sold-out marker */
h1:has(+ p) /* an h1 immediately followed by a p */
li:has(~ li.active) /* an li with a later active sibling */
tr:has(td.error) /* a row containing an error cell */
Dies deckt einen Großteil dessen ab, wofür man zuvor XPath benötigte – die Auswahl von übergeordneten Elementen und die Konditionierung auf ein Geschwisterelement.
Drei Einschränkungen sind dokumentiert und sollten beachtet werden. :has() „kann nicht innerhalb eines anderen :has() verschachtelt werden“. Pseudo-Elemente „sind keine gültigen Selektoren innerhalb von :has()“ und gelten nicht als gültige Anker dafür. Und seine Spezifität entspricht „der Spezifität des spezifischsten Selektors in seinen Argumenten“, was dem Verhalten von :is() und :not() entspricht.
Die für die Extraktion wichtigste Einschränkung ist nicht in dieser Liste enthalten: :has() kann nach wie vor keinen Text abgleichen. div:has(span) funktioniert; div:has(span:contains('Price')) existiert nicht, da :contains() kein standardmäßiger CSS-Selektor ist. Er wurde vorgeschlagen und wieder verworfen, und Browser implementieren ihn nicht. Somit bleibt das Label-Wert-Muster unabhängig von :has() weiterhin im Bereich von XPath.
Übersetzungen im Vergleich
Der schnellste Weg, ein Gespür für die Vor- und Nachteile zu entwickeln, besteht darin, dieselbe Absicht in beiden Formen zu betrachten.
| Absicht | CSS | XPath |
|---|---|---|
| Element mit einer Klasse | div.card | //div[contains(concat(' ',normalize-space(@class),' '),' card ')] |
| Element mit einer ID | #main | //*[@id='main'] |
| Attribut vorhanden | a[href] | //a[@href] |
| Attribut beginnt mit | a[href^="/docs"] | //a[starts-with(@href,'/docs')] |
| Attribut enthält | a[href*="pdf"] | //a[contains(@href,'pdf')] |
| Direktes Kind | ul > li | //ul/li |
| Beliebiger Nachkomme | div p | //div//p |
| Erstes Kind | li:first-child | //li[1] |
| Letztes Kind | li:last-child | //li[last()] |
| N-tes Kind | li:nth-child(3) | //li[3] |
| Nächstes Geschwisterelement | h2 + p | //h2/following-sibling::p[1] |
| Beliebiges späteres Geschwisterelement | h2 ~ p | //h2/following-sibling::p |
| Negation | p:not(.footnote) | //p[not(contains(@class,'footnote'))] |
| Elternelement eines Treffers | div:has(> span.price) | //span[contains(@class,'price')]/.. |
| Enthält Text | nicht möglich | //p[contains(., 'Price')] |
| Exakter Text | nicht möglich | //p[normalize-space()='Price'] |
| Wert neben einer Beschriftung | nicht möglich | //dt[normalize-space()='Price']/following-sibling::dd[1] |
Wenn man die Tabelle von oben nach unten durchliest, wird das Muster deutlich. Bei allen Einträgen oberhalb der übergeordneten Zeile ist CSS kürzer und übersichtlicher, und die Wahl von XPath bedeutet umsonst mehr Wortreichhaltigkeit. Bei den drei Zeilen ganz unten gibt es gar keine CSS-Spalte.
Zwei Übersetzungen verdienen einen zweiten Blick. Die „class“-Zeile bildet den stärksten Kontrast im gesamten Vergleich – neun Zeichen gegenüber gut siebzig – und die XPath-Version ist kein überflüssiger Ballast, da die Kurzform contains(@class,'card') tatsächlich mehr als nur die Elemente card-large und discard abdeckt. Und die Zeile „first child“ birgt eine Falle: „li:first-child“ und „//li[1]“ stimmen hier überein, aber „//li[1]“ ist ein Prädikat, das pro übergeordnetem Element angewendet wird, sodass es das erste „li“ unter jeder Liste im Dokument auswählt. Das CSS ist eindeutig; der XPath benötigt „(//li)[1]“, wenn nur eines gemeint ist.
Ein realistisches gemischtes Beispiel
So sieht das in der Praxis aus: das Extrahieren einer Produktseite.
# CSS for the structural work — clear and adequate
cards = tree.cssselect('div.product-grid > article.product-card')
for card in cards:
name = card.cssselect('h3.product-name')[0].text_content().strip()
image = card.cssselect('img.product-image')[0].get('src')
# XPath for the label-value pairs, which CSS cannot express
price = card.xpath(
".//dt[normalize-space()='Price']/following-sibling::dd[1]"
)[0].text_content().strip()
stock = card.xpath(
".//span[contains(., 'in stock') or contains(., 'out of stock')]"
)
Drei Dinge darin sind es wert, kopiert zu werden.
Das vorangestellte „.“ in jedem XPath-Ausdruck. „.//dt“ sucht innerhalb der aktuellen Karte; „//dt“ würde das gesamte Dokument ab der Wurzel durchsuchen und für jede Karte das erste übereinstimmende Label auf der Seite zurückgeben. Dies ist einer der häufigsten Fehler in gemischtem Extraktionscode und führt dazu, dass für jeden Datensatz derselbe Wert zurückgegeben wird – plausibel, einheitlich und falsch.
CSS, wo CSS ausreicht. Das Raster und die Kartenauswahl, die Überschrift, das Bild. Würde man diese in XPath schreiben, würde das den Code verlängern und die Übersichtlichkeit beeinträchtigen.
XPath nur dort, wo es nötig ist. Der Preis wird durch seine Bezeichnung identifiziert, der Lagerbestand durch seinen Text. Beides lässt sich nicht mit CSS ausdrücken, und beides ist der Grund, warum XPath überhaupt in der Datei enthalten ist.
Das Ergebnis ist ein Scraper, bei dem jeder Ausdruck so kurz wie möglich ist und der Leser auf einen Blick erkennen kann, welche Teile von der Struktur und welche vom Inhalt abhängen – was gleichzeitig eine Übersicht darüber liefert, was bei Änderungen an der Website nicht mehr funktionieren wird.
Leistung
Meist irrelevant, gelegentlich entscheidend.
CSS ist in Browsern im Allgemeinen schneller, da die Engines die Selektorauswertung stark optimieren – sie liegt auf dem kritischen Pfad für das Rendering. XPath durchläuft ein allgemeineres Auswertungsmodell.
Der Unterschied spielt bei dem Umfang, mit dem die meisten Menschen arbeiten, selten eine Rolle. Ein paar hundertmal auf einer einzelnen Seite eine Auswahl zu treffen, ist in beiden Fällen nicht messbar; die Netzwerkanfrage dominiert um Größenordnungen.
Wo es doch eine Rolle spielt:
Die Achsen „preceding“ und „following“ sind wirklich rechenintensiv. Sie durchsuchen das gesamte Dokument vor oder nach dem Kontextknoten. Innerhalb einer Schleife über viele Knoten wird dies quadratisch. Wenn ein XPath-Ausdruck merklich langsam ist, prüfen Sie, ob er eine dieser Achsen verwendet – „preceding-sibling“ und „following-sibling“ sind durch die Anzahl der Geschwisterknoten begrenzt und stellen kein Problem dar.
** „//“ am Anfang eines verschachtelten Ausdrucks führt eine erneute Suche ab der Wurzel durch.** „//div//span“ ist aufwendiger, als es den Anschein hat, und „.//span“ innerhalb einer Schleife über div-Elemente ist in der Regel das, was Sie gemeint haben.
** „:has()“ kann** in großen Dokumenten aufwendig sein, da die Engine den inneren Selektor für jeden Kandidaten auswerten muss. Für die Extraktion ist dies in Ordnung; bei einem Stylesheet, das auf eine große Seite angewendet wird, sollte man dies jedoch im Auge behalten.
Beim serverseitigen Parsen mit lxml oder ähnlichen Tools ist der Unterschied so gering, dass die Lesbarkeit ausschlaggebend sein sollte.
Fallstricke bei Verfügbarkeit und Versionen
Der Haken, der dazu führt, dass es „im Online-Tester funktioniert, in meinem Code aber nicht“.
Browser implementieren XPath 1.0 über document.evaluate, und Selenium nutzt die Engine des Browsers. Funktionen von XPath 2.0 und 3.1 – matches(), replace(), lower-case(), ends-with(), Sequenzen – sind dort nicht verfügbar. Ein Codeausschnitt, der eine dieser Funktionen verwendet, schlägt fehl.
lxml implementiert XPath 1.0 für seine allgemeine API sowie EXSLT-Erweiterungen, darunter „re:test()“ für reguläre Ausdrücke. Ein auf regulären Ausdrücken basierender Ausdruck, der in Python funktioniert, funktioniert daher nicht in Selenium.
Die CSS-Unterstützung in Parsing-Bibliotheken erfolgt in der Regel über eine Übersetzungsschicht, die CSS intern in XPath konvertiert. Das funktioniert gut für gängige Selektoren, bei neueren jedoch weniger gut – die Unterstützung von :has() in serverseitigen Parsern variiert erheblich, und ein Selektor, der in einem Browser funktioniert, funktioniert möglicherweise nicht in Ihrem Scraper.
Praktische Regel: Testen Sie Ihre Selektoren in der Umgebung, in der sie ausgeführt werden sollen, nicht in einer Browserkonsole oder einem Online-Tool. Die beiden häufigsten Überraschungen sind, dass XPath-2.0-Funktionen in Selenium fehlschlagen und dass „:has()“ in einem Python-Parser fehlschlägt.
Auswahl in der Praxis
Ein Entscheidungsprozess, der nahezu jeden Fall löst.
Beginnen Sie mit CSS. Es ist besser lesbar, verfügt über bessere Werkzeuge und eignet sich für die Auswahl anhand von Klassen, IDs, Attributen und Strukturen – was den Großteil der Auswahl ausmacht.
Wechseln Sie zu XPath, wenn Sie nach Text suchen müssen. Das ist der Hauptgrund und er ist entscheidend: Kein CSS-Konstrukt liest Textinhalte aus.
Wechseln Sie zu XPath für die Navigation zu Vorfahren, die über das hinausgeht, was „:has()“ abdeckt, insbesondere wenn Sie einen bestimmten Vorfahren mehrere Ebenen höher benötigen, anstatt eine bedingte Übereinstimmung mit einem bekannten übergeordneten Element.
Wechseln Sie zu XPath für XML. Namensräume und Operationen in Dokumentreihenfolge sind genau das, wofür es entwickelt wurde.
Verwende beides in einer Codebasis. Das ist normal und sinnvoll und kein Kompromiss. Ein Scraper, der CSS für die einfachen 90 % und XPath für die schwierigen 10 % nutzt, ist einfacher zu warten als einer, der sich auf eine der beiden Methoden festlegt.
Und eine Gewohnheit, die mehr wert ist als jede der beiden Entscheidungen: Prüfen Sie zunächst auf eingebettete strukturierte Daten, bevor Sie überhaupt einen Selektor schreiben. Viele Seiten enthalten JSON-LD in einem <script type="application/ld+json">-Block, da dies Suchfunktionen steuert, und das Parsen dieser Daten ist deutlich stabiler als das Parsen von gerendertem Markup. Es übersteht Neugestaltungen, die jeden Selektor auf der Seite unbrauchbar machen.
Was beides nicht löst
Beide sind auf dieselbe Weise anfällig, und genau das kostet wertvolle Zeit.
Strukturelle Selektoren versagen stillschweigend. Jemand fügt eine Spalte ein, und td[3] findet eine andere Zelle. Es wird kein Fehler ausgelöst; Ihre Daten sind einfach stillschweigend falsch. Verankern Sie sich nach Möglichkeit an Text oder Bezeichnern und überprüfen Sie die Struktur der extrahierten Daten – wenn ein Preis wie ein Preis aussehen soll, überprüfen Sie dies.
Keiner der beiden kommt mit clientseitig gerenderten Inhalten zurecht. Wenn das Markup nach dem Laden per JavaScript zusammengestellt wird, finden beide im ursprünglichen HTML nichts. Das ist ein Abrufproblem, das einen Browser oder die zugrunde liegende API erfordert, kein Selektorproblem.
Keines der beiden überlebt eine Neugestaltung der Seite von selbst. Abhilfe schafft es, Selektoren an einem Ort zu bündeln, sodass die Aktualisierung nach einer Änderung nur eine Stunde statt eines ganzen Tages dauert, und das geparste HTML zu speichern, damit Sie bei Fehlern Altes mit Neuem vergleichen können.
Und keines der beiden wird davon beeinflusst, wie die Seite abgerufen wurde. Und genau hier kommen wir wieder ins Spiel: Wenn das Dokument intakt ist und Ihr Ausdruck mit nichts übereinstimmt, ändert keine noch so umfangreiche Infrastruktur etwas an der Antwort.
Häufig gestellte Fragen
Ist XPath besser als CSS-Selektoren?
Generell ist keines von beiden besser. XPath ist leistungsfähiger – es kann Text abgleichen, zu übergeordneten Elementen navigieren und Zeichenfolgenfunktionen verwenden. CSS ist lesbarer und wird von Tools besser unterstützt. Verwenden Sie standardmäßig CSS und XPath für die speziellen Fälle, die mit CSS nicht ausgedrückt werden können.
Können CSS-Selektoren nach Text auswählen?
Nein. Es gibt keinen Standard-CSS-Selektor für Textinhalte – „:contains()“ wurde vorgeschlagen, aber nie übernommen, und Browser implementieren es nicht. Dies ist die größte Funktionslücke und der Hauptgrund, warum XPath bei der Datenextraktion weiterhin zum Einsatz kommt.
Ersetzt „:has()“ XPath?
Teilweise. Es ermöglicht die Auswahl von übergeordneten Elementen und das Abgleichen unter Berücksichtigung von Geschwistern, was zuvor nur mit XPath möglich war. Da es keinen Textabgleich hinzufügt, ist für das Label-Wert-Muster weiterhin XPath erforderlich. Außerdem lässt es sich nicht verschachteln, und die Unterstützung in serverseitigen Parsing-Bibliotheken ist unterschiedlich.
Was ist schneller, XPath oder CSS?
CSS ist in Browsern im Allgemeinen schneller, da die Selektorauswertung für die Darstellung optimiert ist. Der Unterschied ist im Vergleich zur Netzwerkzeit meist unerheblich. Wo XPath wirklich langsam wird, sind die Achsen „preceding“ und „following“, die das gesamte Dokument durchsuchen.
Kann ich XPath 2.0 in Selenium verwenden?
Nein. Browser implementieren XPath 1.0 über document.evaluate, und Selenium nutzt die Engine des Browsers. Funktionen wie matches(), lower-case() und ends-with() sind nicht verfügbar. Dies ist der übliche Grund, warum ein Snippet in einem Online-Tester funktioniert, in einer Testsuite jedoch fehlschlägt.
Wie wähle ich ein übergeordnetes Element aus?
In XPath: parent:: oder ... In CSS bietet :has() eine bedingte Form – div:has(> span.price) wählt das div anstelle des span aus. Für einen bestimmten Vorfahren, der mehrere Ebenen höher liegt, ist die ancestor::-Achse von XPath direkter.
Sollte ich für das Web-Scraping CSS oder XPath verwenden?
Beides, in derselben Codebasis. CSS für die Auswahl von Klassen, IDs und Attributen – das macht den Großteil aus. XPath, wenn Text abgeglichen werden muss – das Auffinden eines Werts anhand seiner Bezeichnung ist der typische Fall und hat kein CSS-Äquivalent.
Was ist stabiler, wenn sich eine Website ändert?
Von Natur aus keines von beiden – Stabilität hängt davon ab, worauf man sich bezieht, und nicht davon, welche Sprache man verwendet. Die Verankerung auf Text oder einem „data-*“-Attribut übersteht eine Neugestaltung; die Verankerung auf Position übersteht nichts. Beide Sprachen ermöglichen beides.
Fazit
Der Vergleich lässt sich auf zwei Asymmetrien reduzieren: CSS kann keinen Text lesen, und XPath ist schwerer zu lesen. Alles andere sind Details.
Daraus ergibt sich eine einfache Regel: Verwenden Sie standardmäßig CSS, da die meisten Auswahlen über Klassen, IDs oder Attribute erfolgen und CSS diese übersichtlich darstellt. Greifen Sie auf XPath zurück, wenn Sie etwas benötigen, das nur XPath bietet – vor allem Textabgleich, gefolgt von Vorfahren-Navigation und Zeichenfolgenfunktionen. Eine Mischung aus beidem ist normal, und ein Code, der beide Technologien dort einsetzt, wo sie ihre Stärken ausspielen, ist leichter zu warten als einer, der sich für eine Seite entschieden hat. „
:has()“ hat die Lücke tatsächlich verringert und lohnt sich dort, wo es passt: Auswahl von übergeordneten Elementen und Bedingungen für gleichrangige Elemente in reinem CSS, das seit Ende 2023 weit verbreitet ist. Beachten Sie jedoch, dass es keinen Text berücksichtigt und dass die Unterstützung durch serverseitige Parser weniger einheitlich ist als die Browserunterstützung.
Überprüfen Sie daher Ihre Umgebung, bevor Sie einem Ausdruck vertrauen. XPath 1.0 in Browsern und Selenium, EXSLT-Erweiterungen in lxml, Unterstützung für Variablen:has()en in Parsing-Bibliotheken – die häufigste Überraschung bei Selektoren ist kein Syntaxfehler, sondern eine Funktion, die an einer anderen Stelle vorhanden ist als dort, wo Sie den Ausdruck ausführen.
