CSS-Selektor oder XPath: Was passt zum Web Scraping?

Veröffentlicht:

13 Min. Lesezeit

Acar Diveroli
Autor: Acar Diveroli
Links ein CSS-Würfel mit .item, in der Mitte eine DOM-Baumkarte und rechts ein XPath-Würfel mit //a

Nach dem Herunterladen einer Seite beginnt die eigentliche Arbeit beim Scraping: Produktname, Preis und Link unter Hunderten Tags finden und extrahieren. Welches Element Sie wollen, beschreiben Sie mit einem Selektor. Zwei Sprachen sind verbreitet: CSS-Selektoren, die Webentwickler aus Stylesheets kennen, und XPath aus der XML-Welt. Oft wählen Sie dasselbe Element mit beiden aus, doch die eine ist kurz und lesbar, während die andere manches kann, was CSS gar nicht beherrscht.

In diesem Artikel erklären wir, was die beiden Sprachen sind, stellen ihre Syntax gegenüber, zeigen, was XPath über CSS hinaus kann, und prüfen, ob der Leistungsunterschied wirklich zählt. Danach zeigen wir, wie Sie Selektoren schreiben, die bei Designänderungen nicht brechen, und wie Sie Selektoren im Browser testen. In der Mitte des Artikels finden Sie eine Kurzreferenz mit 20 Zeilen zum Speichern; auf einer Testseite haben wir geprüft, dass jedes CSS- und XPath-Paar der Tabelle dieselben Elemente auswählt.

Was ist ein CSS-Selektor?

Ein CSS-Selektor ist die Mustersprache, mit der in CSS-Stylesheets festgelegt wird, für welche Elemente ein Stil gilt. Browser unterstützen dieselbe Sprache in JavaScript über document.querySelector() und querySelectorAll(). Die aktuelle Definition ist Selectors Level 4 des W3C.

Ein CSS-Selektor wählt ein Element aus nach:

  • Tag: div, a, span
  • Klasse: .product
  • ID: #list
  • Attribut: a[href], img[src$=".webp"]
  • Hierarchie: ul > li (direktes Kind), div span (Nachfahre in beliebiger Tiefe)
  • Geschwister: h2 + p (unmittelbar folgend), h2 ~ span (alle folgenden Geschwister)
  • Position: li:first-of-type, li:nth-of-type(3)

Die Grundrichtung von CSS ist nach unten und nach vorne: Sie gelangen von einem Element zu seinen Nachfahren und folgenden Geschwistern. Die in Selectors Level 4 eingeführte Pseudoklasse :has() lockert diese Regel teilweise: div.product:has(> span.discount) wählt Produktkarten, die ein Rabattetikett enthalten. :has() „klettert" aber nicht von einem Element nach oben; es wählt das äußere Element anhand seines Inhalts aus.

Was ist XPath?

XPath (XML Path Language) ist eine Abfragesprache, die Knoten in XML- und HTML-Dokumenten mit einem Pfadausdruck auswählt. Sie sieht das Dokument als Baum und kann sich darin in jede Richtung bewegen: nach unten, nach oben und zu vorherigen und folgenden Geschwistern. Die aktuelle Definition des W3C ist XPath 3.1.

Ein wichtiges praktisches Detail: Browser und die Python-Bibliothek lxml unterstützen XPath 1.0. In späteren Versionen hinzugekommene Funktionen wie ends-with(), Funktionen für reguläre Ausdrücke und ein reichhaltigeres Typsystem gibt es in diesen Umgebungen nicht. Halten Sie XPath-Ausdrücke für Scraping innerhalb der Grenzen von 1.0.

Die Grundbausteine von XPath:

  • // beliebige Tiefe im Dokument, / direktes Kind: //ul/li
  • [...] eine Bedingung (Prädikat): //a[@href], //li[3]
  • @ Attribut: //img/@src
  • text() Textknoten: //h2/text()
  • Achsen: parent::, ancestor::, following-sibling::, preceding-sibling::
  • Funktionen: contains(), starts-with(), normalize-space(), last(), string-length()

Im Browser läuft XPath über die JavaScript-Funktion document.evaluate(); ihre Nutzung beschreibt die MDN-Dokumentation.

Syntaxvergleich

KriteriumCSS-SelektorXPath
LesbarkeitKurz, Webentwicklern vertrautLang, steilere Lernkurve
RichtungNach unten und vorne (mit :has() teils nach außen)Jede Richtung: Elternelement, vorheriges Geschwister, Vorfahre
Auswahl nach TextNicht im StandardJa: text(), contains()
Attributwerte zurückgebenNein (mit Bibliothekserweiterungen)Ja: /@href
Textknoten zurückgebenNein (mit Bibliothekserweiterungen)Ja: /text()
Klassenabgleich.product exakt und kurzcontains(@class, ...) erfordert Sorgfalt
BrowserunterstützungquerySelectorAlldocument.evaluate (XPath 1.0)
Python-UnterstützungBeautifulSoup, lxml (cssselect), parsellxml, parsel; BeautifulSoup unterstützt es nicht
Headless-BrowserPlaywright, Puppeteer, SeleniumPlaywright, Puppeteer, Selenium
Typischer EinsatzListen nach Klasse und Attribut extrahierenLabel-Wert-Tabellen, textbasierte Auswahl

Kurzreferenz

Jede Zeile der folgenden Tabelle ist dieselbe Auswahl in beiden Sprachen. In Zeilen ohne CSS-Entsprechung sehen Sie, was XPath zusätzlich kann.

#Was wird ausgewählt?CSS-SelektorXPath
1Alle div-Elementediv//div
2Nach ID#list//*[@id="list"]
3Nach Klasse.product//*[contains(concat(" ", normalize-space(@class), " "), " product ")]
4Direktes Kindul > li//ul/li
5Nachfahre in beliebiger Tiefediv span//div//span
6Hat das Attributa[href]//a[@href]
7Attributwert ist gleichinput[name="q"]//input[@name="q"]
8Attribut beginnt mita[href^="https"]//a[starts-with(@href, "https")]
9Attribut enthälta[href*="product"]//a[contains(@href, "product")]
10Attribut endet mitimg[src$=".webp"]//img[substring(@src, string-length(@src) - 4) = ".webp"]
11Erstes Elementul > li:first-of-type//ul/li[1]
12Letztes Elementul > li:last-of-type//ul/li[last()]
13Drittes Elementul > li:nth-of-type(3)//ul/li[3]
14Unmittelbar folgendes Geschwisterh2 + p//h2/following-sibling::*[1][self::p]
15Alle folgenden Geschwisterh2 ~ span//h2/following-sibling::span
16Mehrere Auswahlenh1, h2//h1 | //h2
17Karte mit einem bestimmten Elementdiv.product:has(> span.discount)//span[@class="discount"]/..
18Schaltfläche mit exaktem TextNicht im Standard//button[text()="In den Warenkorb"]
19Ein bestimmter Vorfahre eines ElementsNicht im Standard//span[@class="price"]/ancestor::div[@data-sku][1]
20Wert neben einem LabelNicht im Standard//th[normalize-space()="Bestand"]/following-sibling::td[1]

Einige Details der Tabelle führen oft zu Fehlern:

  • Zeile 3: //*[contains(@class, "product")] wirkt kürzer, wählt aber auch Klassen wie products oder old-product. Der lange Ausdruck in der Tabelle passt nur auf die exakte Klasse product.
  • Zeile 10: XPath 1.0 hat kein ends-with(); die letzten Zeichen werden mit substring verglichen. Die Zahl ist um eins kleiner als die Länge der gesuchten Endung (.webp hat fünf Zeichen, - 4).
  • Zeile 11: li:first-child und li:first-of-type sind verschieden. Ersteres wählt ein li, das erstes Kind seines Elternelements ist; ist das erste Kind ein anderes Tag, wählt es nichts.
  • Zeile 14: //h2/following-sibling::p[1] bedeutet „das erste folgende p" und passt auch, wenn andere Elemente dazwischenliegen. h2 + p in CSS passt nur, wenn das Element direkt nach h2 ein p ist.

Um Attributwerte und Textknoten zurückzugeben, nutzt XPath //a/@href und //h2/text(). Die Python-Bibliothek parsel ergänzt CSS dafür um die nicht standardisierten Erweiterungen a::attr(href) und h2::text.

Was XPath kann und CSS nicht

Auswahl nach Text

Auf einer E-Commerce-Seite können die Schaltflächen „In den Warenkorb" und „Nicht vorrätig" dieselbe Klasse tragen. Nur ihr Text unterscheidet sie:

text
//button[text()="In den Warenkorb"]
//span[contains(normalize-space(.), "Rabatt")]

text() betrachtet nur den direkten Textknoten des Elements; ist der Text auf verschachtelte Tags verteilt, passt es nicht. normalize-space(.) fügt den gesamten Text des Elements zusammen und entfernt Leerzeichen am Anfang und Ende, daher ist es meist zuverlässiger.

Standard-CSS kennt keine Auswahl nach Text. Manche Werkzeuge bieten eigene Erweiterungen: die Option has_text und :has-text() in Playwright sowie :-soup-contains() in BeautifulSoup. Diese Erweiterungen funktionieren nur im jeweiligen Werkzeug.

Zum Elternelement und vorherigen Geschwister klettern

Hat auf einer Produktkarte nur das Preisfeld eine stabile Klasse, müssen Sie vom Preis ausgehen, um die ganze Karte zu erreichen:

text
//span[@class="price"]/..
//span[@class="price"]/ancestor::div[@data-sku][1]

.. wählt das direkte Elternelement, ancestor:: einen Vorfahren auf beliebiger Ebene darüber. [1] nimmt den nächstgelegenen Vorfahren.

Label-Wert-Strukturen

Produktspezifikationstabellen und Definitionslisten gehören zu den Strukturen, auf die Scraping am häufigsten stößt. Welche Zeile in welcher Reihenfolge kommt, variiert je Produkt; fest bleibt der Text des Labels:

text
//th[normalize-space()="Bestand"]/following-sibling::td[1]
//dt[normalize-space()="Garantie"]/following-sibling::dd[1]

Diese Ausdrücke finden den Wert neben „Bestand", egal in welcher Tabellenzeile er steht. Mit CSS müssten Sie dafür jede Zeile holen und in Python durchlaufen.

CSS und XPath in Python

Python hat drei verbreitete Bibliotheken, und ihre Selektorunterstützung unterscheidet sich.

BeautifulSoup unterstützt nur CSS-Selektoren (select und select_one); XPath gibt es nicht:

python
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")
products = []
for card in soup.select("div.product"):
    products.append({
        "sku": card.get("data-sku"),
        "name": card.select_one("h2").get_text(strip=True),
        "price": card.select_one(".price").get_text(strip=True),
        "link": card.select_one("a[href]")["href"],
    })

lxml führt XPath 1.0 direkt aus und kann das Ergebnis mit Funktionen wie string() als Text zurückgeben:

python
from lxml import html as lxml_html

tree = lxml_html.fromstring(html)
stock = tree.xpath('string(//th[normalize-space()="Bestand"]/following-sibling::td[1])')
discounted_sku = tree.xpath('//span[@class="discount"]/ancestor::div[@data-sku][1]/@data-sku')

parsel (die Selektorbibliothek von Scrapy) bietet beide Sprachen auf demselben Objekt, miteinander verkettbar. Sie finden Karten mit CSS und nutzen XPath innerhalb einer Karte:

python
from parsel import Selector

page = Selector(text=html)
for card in page.css("div.product"):
    name = card.css("h2::text").get()
    link = card.css("a::attr(href)").get()
    available = card.xpath('.//button[text()="In den Warenkorb"]').get() is not None
    print(name, link, available)

Beim Verketten ist darauf zu achten, dass ein XPath-Ausdruck innerhalb einer Karte mit .// beginnt. Schreiben Sie //button, läuft die Suche über das gesamte Dokument statt innerhalb der Karte, und Sie finden für jede Karte die erste Schaltfläche der Seite.

Welche Sprache und Bibliothek zu Ihnen passt, klärt Web Scraping: JavaScript oder Python?.

Selektoren in Headless-Browsern

Auf Seiten, deren Inhalt per JavaScript lädt, werden Selektoren in einem Werkzeug zur Browserautomatisierung genutzt. Playwright unterstützt beide Sprachen und behandelt einen Ausdruck, der mit // beginnt, automatisch als XPath:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com/deals")

    prices = page.locator("div.product .price").all_inner_texts()
    stock = page.locator('xpath=//th[normalize-space()="Bestand"]/following-sibling::td[1]').inner_text()
    addable = page.locator("div.product", has_text="In den Warenkorb")

    print(prices, stock, addable.count())
    browser.close()

In Selenium erledigen By.CSS_SELECTOR und By.XPATH dieselben Aufgaben; Einrichtung und Proxy-Konfiguration erklären wir in Proxys mit Selenium verwenden und Proxys mit SeleniumBase verwenden. Ob eine Seite wirklich einen Browser braucht, klärt Statische und dynamische Seiten.

Zählt der Leistungsunterschied wirklich?

Die Behauptung „CSS-Selektoren sind schneller als XPath" liest man online oft. Das hängt von der Umgebung ab:

  • Im Browser nutzt querySelectorAll direkt den optimierten Selektorabgleich der Browser-Engine; XPath über document.evaluate ist meist langsamer. Spürbar wird der Unterschied bei sehr großen Seiten und tausendfach wiederholten Abfragen.
  • In Python mit lxml und parsel werden CSS-Selektoren im Hintergrund meist in XPath übersetzt und als XPath ausgeführt. Beide Sprachen nutzen also dieselbe Engine, und der Unterschied verschwindet weitgehend.
  • In BeautifulSoup hängt die Auswahlgeschwindigkeit eher vom genutzten Parser (html.parser oder lxml) ab als von der Selektorsprache.

Entscheidend ist, wie der Selektor geschrieben ist. //*[contains(@class, "price")], das das ganze Dokument durchsucht, leistet weit mehr Arbeit als ein Ausdruck, der zuerst die Karte und dann den Preis darin findet. Und all das ist meist gering neben der Zeit für das Herunterladen der Seite oder das Laden im Headless-Browser. Den eigentlichen Engpass eines Scrapers behandeln wir in Concurrency und Parallelism.

Wie vermeiden Sie anfällige Selektoren?

Der häufigste Grund, warum Scraper brechen, ist nicht eine Sperre, sondern eine Designänderung der Website. Damit ein Selektor kleine Änderungen übersteht:

  • Meiden Sie automatisch erzeugte Klassennamen. Klassen wie css-1x9k2ab oder sc-bdVaJa können sich mit jedem Release ändern.
  • Schreiben Sie keine absoluten Pfade. Ein aus dem Browser kopierter Ausdruck wie /html/body/div[3]/div[2]/ul/li[4]/span bricht, sobald ein einziges Banner hinzukommt.
  • Binden Sie sich an Bedeutung, nicht an Position. Nicht „das dritte span", sondern „das span mit der Klasse price" oder „die Zelle neben der Überschrift Bestand".
  • Bevorzugen Sie stabile Attribute. Attribute wie data-sku, data-testid, itemprop und aria-label sind unabhängig vom visuellen Design und ändern sich seltener.
  • Sehen Sie zuerst nach strukturierten Daten. Viele Produktseiten tragen Produktname, Preis und Bestand im schema.org-Format in <script type="application/ld+json">. Diese Daten zu lesen, ist viel robuster als visuelles HTML zu parsen.
  • Wählen Sie zuerst den Container, dann das Feld. Eine Karte einmal zu finden und Felder darin zu suchen, verhindert, dass Felder vertauscht werden.
  • Behandeln Sie ein leeres Ergebnis als Fehler. Statt stillschweigend einen leeren Wert zu schreiben, wenn ein Selektor nichts findet, protokollieren Sie es und lösen einen Alarm aus; so bemerken Sie die Designänderung am ersten Tag.

Leere Selektorergebnisse sind nicht immer eine Designänderung; manchmal liefert der Bot-Schutz eine andere Seite, manchmal lädt der Inhalt später per JavaScript. Zur Diagnose sehen Sie sich die Liste in Web Scraping ohne Sperren an.

Wie testen Sie einen Selektor im Browser?

Einen Selektor im Browser auszuprobieren, bevor er in den Code kommt, spart Zeit. In den Entwicklertools (F12) von Chrome, Edge und Firefox:

  • $$("div.product .price") in der Konsole gibt alle Elemente, die auf den CSS-Selektor passen, als Array zurück.
  • $x('//th[normalize-space()="Bestand"]/following-sibling::td[1]') in der Konsole führt einen XPath-Ausdruck aus.
  • Strg + F im Elements-Panel öffnet ein Suchfeld, das Klartext, CSS-Selektoren und XPath akzeptiert und passende Elemente nacheinander hervorhebt.

Zwei Warnungen: Die Seite im Browser zeigt den Zustand nach der Ausführung von JavaScript. Erhält Ihr Scraper über einen HTTP-Client nur das HTML des Servers, kann ein im Browser funktionierender Selektor im Code leer bleiben; prüfen Sie das mit dem Quelltext (Strg + U). Zweitens erzeugt die Browserfunktion „Copy XPath" meist absolute, anfällige Pfade; nutzen Sie sie als Ausgangspunkt und vereinfachen Sie sie.

Anwendungsfälle

  • Preisbeobachtung: Produktkarten mit CSS, Label-Wert-Felder wie „Bestand" mit XPath. Den Aufbau beschreibt unsere Seite zur Preisbeobachtung.
  • Katalogerfassung: zuerst JSON-LD; fehlt es, CSS-Selektoren mit stabilen data-*-Attributen. Den allgemeinen Aufbau zeigt unsere Seite zur Datenerfassung.
  • Vergleichsportale: eine Selektordefinitionsdatei pro Website für unterschiedliche Strukturen; XPath für textbasierte Felder.
  • Testautomatisierung: CSS-Selektoren mit data-testid-Attributen in Ihrer eigenen Anwendung; Tests, die Designänderungen nicht betreffen.
  • Regelmäßige Erfassung vieler Seiten: Solange Selektoren stabil bleiben, geht es nur noch um die Verteilung der Anfragen; ein Rotierender Proxy liefert über eine Adresse verschiedene Ausgangs-IPs.

Entscheidungshilfe

Ihr BedarfEmpfehlung
Auswahl nach Klasse, ID oder AttributCSS-Selektor
Sie nutzen BeautifulSoupCSS-Selektor (keine XPath-Unterstützung)
Ein Element anhand seines Textes findenXPath
Vom Element zum Elternelement oder vorherigen GeschwisterXPath
Label-Wert-Tabellen und DefinitionslistenXPath
Einen Attributwert direkt zurückgebenXPath /@attr oder parsel ::attr()
Beides im selben Projektparsel
Headless-BrowserPlaywright-Locators, zuerst CSS
Robustheit gegenüber Designänderungendata-*-Attribute oder JSON-LD

Häufig gestellte Fragen

Sollte ich CSS-Selektoren oder XPath lernen?

Lernen Sie zuerst CSS-Selektoren; sie sind kürzer, verbreiteter und auch in der Webentwicklung nützlich. Brauchen Sie beim Scraping Auswahl nach Text und das Klettern zu Elternelementen, genügt es, die Achsen und Funktionen von XPath zu lernen. Mit Bibliotheken wie parsel, die beides nutzen, fällt der Wechsel leicht.

Unterstützt BeautifulSoup XPath?

Nein. BeautifulSoup unterstützt nur CSS-Selektoren. Brauchen Sie XPath, parsen Sie dasselbe HTML mit lxml oder parsel.

Kann CSS nach Text auswählen?

In Standard-CSS nicht. Manche Werkzeuge bieten eigene Erweiterungen: has_text und :has-text() in Playwright sowie :-soup-contains() in BeautifulSoup. Diese Erweiterungen funktionieren nur im jeweiligen Werkzeug und sind nicht übertragbar.

Warum funktioniert der aus dem Browser kopierte XPath nicht?

Der vom Browser erzeugte Pfad ist meist absolut und beruht auf der Seite nach der Ausführung von JavaScript. Diese Struktur gibt es im ersten HTML des Servers womöglich nicht, oder eine kleine Designänderung bricht den Pfad. Vereinfachen Sie den Pfad so, dass er bei einer stabilen Klasse, ID oder einem Attribut beginnt.

Was ist der Unterschied zwischen text() und normalize-space()?

text() gibt die direkten Textknoten des Elements zurück und lässt Leerraum unverändert. normalize-space(.) fügt den gesamten Text des Elements einschließlich verschachtelter Tags zusammen, entfernt Leerzeichen am Anfang und Ende und fasst innere Leerzeichen zu einem zusammen. Für Textvergleiche ist normalize-space() meist zuverlässiger.

Warum liefern meine Selektoren manchmal nichts?

Drei Gründe sind verbreitet: Das Seitendesign hat sich geändert, der Inhalt lädt später per JavaScript, oder die Website liefert statt der erwarteten Seite eine Prüf- oder Fehlerseite. Das HTML der Antwort in eine Datei zu speichern und den Selektor auf dieser Datei auszuprobieren, trennt die drei am schnellsten.

Fazit

CSS-Selektoren sind kurz, lesbar und werden von jedem Werkzeug unterstützt; für die Auswahl nach Tag, Klasse, ID und Attribut sollten sie die Standardwahl sein. XPath braucht es für das, was CSS nicht kann, etwa Auswahl nach Text, Klettern zu Elternelementen und vorherigen Geschwistern sowie Label-Wert-Strukturen; in Browsern und lxml ist es auf Version 1.0 beschränkt. In den meisten Scraping-Aufgaben ist der Leistungsunterschied neben der Netzwerkzeit vernachlässigbar; wirklich zählt, Selektoren zu schreiben, die Designänderungen überstehen. Proxy-Typen für Ihre Datenerfassung finden Sie in unseren Proxy-Diensten.

ChatGPT fragenClaude fragen