CSS Selector ve XPath: Web Scraping İçin Hangisi?

Yayın tarihi:

12 dk okuma

Acar Diveroli
Yazar: Acar Diveroli
Solda .item yazılı CSS küpü, ortada DOM ağacı kartı, sağda //a yazılı XPath küpü bulunan karşılaştırma çizimi

Bir sayfayı indirdikten sonra scraping'in asıl işi başlar: yüzlerce etiketin arasından ürün adını, fiyatı ve bağlantıyı bulup çıkarmak. Bunun için sayfanın hangi öğesini istediğinizi bir seçici ile tarif edersiniz. İki yaygın dil vardır: web geliştiricilerin stil dosyalarından tanıdığı CSS selector ve XML dünyasından gelen XPath. Aynı öğeyi çoğu zaman ikisiyle de seçebilirsiniz, ama biri kısa ve okunaklıyken diğeri CSS'in hiç yapamadığı bazı işleri yapar.

Bu yazıda iki dilin ne olduğunu, söz dizimlerini yan yana, XPath'in CSS'e göre fazladan yapabildiklerini ve performans farkının gerçekte önemli olup olmadığını anlatıyoruz. Ardından sayfa tasarımı değiştiğinde bozulmayan seçicilerin nasıl yazılacağını ve seçicilerin tarayıcıda nasıl test edileceğini gösteriyoruz. Yazının ortasında yer imlerinize ekleyebileceğiniz 20 satırlık bir hızlı başvuru tablosu var; tablodaki her CSS ve XPath çiftinin aynı öğeleri seçtiğini bir test sayfasında doğruladık.

CSS selector nedir?

CSS selector, CSS stil dosyalarında hangi öğeye stil uygulanacağını belirtmek için kullanılan desen dilidir. Tarayıcılar aynı dili JavaScript'te document.querySelector() ve querySelectorAll() fonksiyonlarıyla da destekler. Dilin güncel tanımı W3C'nin Selectors Level 4 belgesidir.

CSS selector bir öğeyi şu bilgilere göre seçer:

  • Etiket: div, a, span
  • Sınıf: .urun
  • Kimlik: #liste
  • Öznitelik: a[href], img[src$=".webp"]
  • Hiyerarşi: ul > li (doğrudan alt öğe), div span (herhangi bir derinlikteki alt öğe)
  • Kardeşlik: h2 + p (hemen sonraki), h2 ~ span (sonraki bütün kardeşler)
  • Konum: li:first-of-type, li:nth-of-type(3)

CSS'in temel yönü aşağı ve ileri doğrudur: bir öğeden alt öğelerine ve sonraki kardeşlerine gidebilirsiniz. Selectors Level 4 ile gelen :has() sözde sınıfı bu kuralı kısmen esnetir: div.urun:has(> span.indirim) içinde indirim etiketi bulunan ürün kartlarını seçer. Ancak :has() bir öğeden yukarı "çıkmaz"; içindekilere bakarak dıştaki öğeyi seçer.

XPath nedir?

XPath (XML Path Language), XML ve HTML belgelerinde düğümleri bir yol ifadesiyle seçen sorgu dilidir. Belgeyi bir ağaç olarak görür ve bu ağaçta her yöne hareket edebilir: aşağı, yukarı, önceki ve sonraki kardeşlere. W3C'nin güncel tanımı XPath 3.1 sürümüdür.

Pratikte önemli bir ayrıntı var: tarayıcılar ve Python'daki lxml kütüphanesi XPath 1.0 destekler. Sonraki sürümlerde gelen ends-with(), düzenli ifade fonksiyonları ve daha zengin tür sistemi bu ortamlarda kullanılamaz. Scraping için yazdığınız XPath ifadelerini 1.0 sınırları içinde tutun.

XPath'in temel parçaları:

  • // belgedeki herhangi bir derinlik, / doğrudan alt düğüm: //ul/li
  • [...] koşul (predicate): //a[@href], //li[3]
  • @ öznitelik: //img/@src
  • text() metin düğümü: //h2/text()
  • Eksenler (axis): parent::, ancestor::, following-sibling::, preceding-sibling::
  • Fonksiyonlar: contains(), starts-with(), normalize-space(), last(), string-length()

Tarayıcıda XPath, JavaScript'in document.evaluate() fonksiyonuyla çalıştırılır; kullanımı MDN belgesinde anlatılır.

Söz dizimi karşılaştırması

ÖlçütCSS selectorXPath
OkunabilirlikKısa, web geliştiricilere tanıdıkUzun, öğrenme eğrisi daha dik
YönAşağı ve ileri (:has() ile kısmen dışa)Her yöne: üst öğe, önceki kardeş, ata
Metne göre seçimStandartta yokVar: text(), contains()
Öznitelik değeri döndürmeYok (kütüphane uzantısıyla)Var: /@href
Metin düğümü döndürmeYok (kütüphane uzantısıyla)Var: /text()
Sınıf eşleştirme.urun kesin ve kısacontains(@class, ...) dikkat ister
Tarayıcı desteğiquerySelectorAlldocument.evaluate (XPath 1.0)
Python desteğiBeautifulSoup, lxml (cssselect), parsellxml, parsel; BeautifulSoup desteklemez
Headless tarayıcıPlaywright, Puppeteer, SeleniumPlaywright, Puppeteer, Selenium
Tipik kullanımSınıf ve özniteliğe göre liste çıkarmaEtiket-değer tabloları, metne bağlı seçim

Hızlı başvuru tablosu

Aşağıdaki tabloda her satır aynı seçimin iki dildeki karşılığıdır. Karşılığı olmayan satırlarda XPath'in fazladan yapabildiği işler gösterilir.

#Ne seçilir?CSS selectorXPath
1Bütün div öğeleridiv//div
2Kimliğe göre#liste//*[@id="liste"]
3Sınıfa göre.urun//*[contains(concat(" ", normalize-space(@class), " "), " urun ")]
4Doğrudan alt öğeul > li//ul/li
5Herhangi bir derinlikteki alt öğediv span//div//span
6Özniteliği olana[href]//a[@href]
7Öznitelik değeri eşitinput[name="q"]//input[@name="q"]
8Öznitelik şununla başlara[href^="https"]//a[starts-with(@href, "https")]
9Öznitelik şunu içerira[href*="urun"]//a[contains(@href, "urun")]
10Öznitelik şununla biterimg[src$=".webp"]//img[substring(@src, string-length(@src) - 4) = ".webp"]
11İlk öğeul > li:first-of-type//ul/li[1]
12Son öğeul > li:last-of-type//ul/li[last()]
13Üçüncü öğeul > li:nth-of-type(3)//ul/li[3]
14Hemen sonraki kardeşh2 + p//h2/following-sibling::*[1][self::p]
15Sonraki bütün kardeşlerh2 ~ span//h2/following-sibling::span
16Birden fazla seçimh1, h2//h1 | //h2
17İçinde belirli öğe olan kartdiv.urun:has(> span.indirim)//span[@class="indirim"]/..
18Metni tam eşit olan butonStandartta yok//button[text()="Sepete ekle"]
19Bir öğenin belirli atasıStandartta yok//span[@class="fiyat"]/ancestor::div[@data-sku][1]
20Etiketin yanındaki değerStandartta yok//th[normalize-space()="Stok"]/following-sibling::td[1]

Tablodaki birkaç ayrıntı sık hataya yol açar:

  • Satır 3: //*[contains(@class, "urun")] daha kısa görünür ama urunler veya eski-urun sınıflarını da seçer. Tablodaki uzun ifade yalnızca tam urun sınıfını eşleştirir.
  • Satır 10: XPath 1.0'da ends-with() yoktur; substring ile son karakterler karşılaştırılır. Sayı, aranan uzantının uzunluğundan bir eksiktir (.webp beş karakter, - 4).
  • Satır 11: li:first-child ile li:first-of-type farklıdır. İlki, ebeveyninin ilk çocuğu olan li'yi seçer; ilk çocuk başka bir etiketse hiçbir şey seçmez.
  • Satır 14: //h2/following-sibling::p[1] "sonraki ilk p" demektir ve arada başka öğeler olsa da eşleşir. CSS'teki h2 + p ise yalnızca h2'nin hemen ardından gelen öğe p ise eşleşir.

Öznitelik değeri ve metin düğümü döndürmek için XPath'te //a/@href ve //h2/text() kullanılır. Python'daki parsel kütüphanesi CSS'e bu işler için standart dışı a::attr(href) ve h2::text uzantılarını ekler.

XPath'in yapabildiği, CSS'in yapamadığı

Metne göre seçim

Bir e-ticaret sayfasında "Sepete ekle" butonu ile "Stokta yok" butonu aynı sınıfı taşıyabilir. İkisini ayıran tek şey metinleridir:

text
//button[text()="Sepete ekle"]
//span[contains(normalize-space(.), "İndirim")]

text() yalnızca öğenin doğrudan metin düğümüne bakar; metin iç içe etiketlere bölünmüşse eşleşmez. normalize-space(.) ise öğenin bütün metnini birleştirir ve baştaki, sondaki boşlukları temizler; bu yüzden genellikle daha güvenilirdir.

Standart CSS'te metne göre seçim yoktur. Bazı araçlar kendi uzantılarını sunar: Playwright'ta has_text seçeneği ve :has-text(), BeautifulSoup'ta :-soup-contains(). Bu uzantılar yalnızca o araçta çalışır.

Üst öğeye ve önceki kardeşe çıkma

Bir ürün kartında yalnızca fiyat alanı kararlı bir sınıfa sahipse, fiyattan başlayıp kartın tamamına ulaşmak gerekir:

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

.. doğrudan ebeveyni, ancestor:: herhangi bir üst düzeydeki atayı seçer. [1] en yakın atayı alır.

Etiket-değer yapıları

Ürün özellik tabloları ve tanım listeleri scraping'in en sık karşılaştığı yapılardandır. Hangi satırın hangi sırada geleceği ürüne göre değişir; sabit olan, etiketin metnidir:

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

Bu ifadeler "Stok" başlığı tablonun kaçıncı satırında olursa olsun yanındaki değeri bulur. CSS ile aynı işi yapmak için bütün satırları çekip Python'da döngüyle aramanız gerekir.

Python'da CSS ve XPath kullanımı

Python'da üç yaygın kütüphane vardır ve seçici desteği birbirinden farklıdır.

BeautifulSoup yalnızca CSS selector destekler (select ve select_one); XPath desteği yoktur:

python
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")
urunler = []
for kart in soup.select("div.urun"):
    urunler.append({
        "sku": kart.get("data-sku"),
        "ad": kart.select_one("h2").get_text(strip=True),
        "fiyat": kart.select_one(".fiyat").get_text(strip=True),
        "link": kart.select_one("a[href]")["href"],
    })

lxml XPath 1.0'ı doğrudan çalıştırır ve string() gibi fonksiyonlarla sonucu metin olarak döndürebilir:

python
from lxml import html as lxml_html

agac = lxml_html.fromstring(html)
stok = agac.xpath('string(//th[normalize-space()="Stok"]/following-sibling::td[1])')
indirimli_sku = agac.xpath('//span[@class="indirim"]/ancestor::div[@data-sku][1]/@data-sku')

parsel (Scrapy'nin seçici kütüphanesi) iki dili aynı nesnede, birbirine zincirlenebilir biçimde sunar. CSS ile kartları bulup kartın içinde XPath kullanmak mümkündür:

python
from parsel import Selector

sayfa = Selector(text=html)
for kart in sayfa.css("div.urun"):
    ad = kart.css("h2::text").get()
    link = kart.css("a::attr(href)").get()
    satilabilir = kart.xpath('.//button[text()="Sepete ekle"]').get() is not None
    print(ad, link, satilabilir)

Zincirlemede dikkat edilecek nokta, kartın içinde çalışan XPath ifadesinin .// ile başlamasıdır. //button yazarsanız arama kartın içinde değil, bütün belgede yapılır ve her kart için ilk butonu bulursunuz.

Hangi dilde ve hangi kütüphaneyle çalışacağınıza karar vermek için Web Kazıma: JavaScript mi Python mu? yazımıza bakabilirsiniz.

Headless tarayıcılarda seçiciler

İçeriği JavaScript ile yüklenen sayfalarda seçiciler bir tarayıcı otomasyon aracının içinde kullanılır. Playwright iki dili de destekler ve // ile başlayan ifadeyi otomatik olarak XPath kabul eder:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    tarayici = p.chromium.launch()
    sayfa = tarayici.new_page()
    sayfa.goto("https://ornek.com/kampanyalar")

    fiyatlar = sayfa.locator("div.urun .fiyat").all_inner_texts()
    stok = sayfa.locator('xpath=//th[normalize-space()="Stok"]/following-sibling::td[1]').inner_text()
    sepete_eklenebilir = sayfa.locator("div.urun", has_text="Sepete ekle")

    print(fiyatlar, stok, sepete_eklenebilir.count())
    tarayici.close()

Selenium'da aynı işler By.CSS_SELECTOR ve By.XPATH ile yapılır; kurulumu ve proxy ayarını Selenium ile Proxy Kullanımı ve SeleniumBase ile Proxy yazılarımızda anlattık. Bir sayfanın gerçekten tarayıcı gerektirip gerektirmediğini anlamak için Statik ve Dinamik Sayfalar yazımıza bakabilirsiniz.

Performans farkı gerçekten önemli mi?

İnternette "CSS selector XPath'ten hızlıdır" ifadesi sık geçer. Durum ortama göre değişir:

  • Tarayıcıda querySelectorAll doğrudan tarayıcı motorunun optimize edilmiş seçici eşleştiricisini kullanır; document.evaluate ile çalışan XPath genellikle daha yavaştır. Fark, çok büyük sayfalarda ve binlerce kez tekrarlanan sorgularda hissedilir.
  • Python'da lxml ve parsel ile CSS selector'lar çoğunlukla arka planda XPath'e çevrilip çalıştırılır. Yani iki dil aynı motoru kullanır ve fark büyük ölçüde ortadan kalkar.
  • BeautifulSoup'ta seçim hızını seçici dilinden çok kullanılan ayrıştırıcı (html.parser veya lxml) belirler.

Asıl belirleyici olan ise seçicinin nasıl yazıldığıdır. Belgenin tamamını tarayan //*[contains(@class, "fiyat")], önce kartı sonra kartın içindeki fiyatı arayan bir ifadeden çok daha fazla iş yapar. Ve bütün bunlar, sayfayı ağdan indirmek ya da bir headless tarayıcıda yüklemek için geçen sürenin yanında genellikle küçük kalır. Bir scraper'ın gerçek darboğazını Concurrency ve Parallelism yazımızda ele aldık.

Kırılgan seçici nasıl önlenir?

Scraper'ların en sık bozulma nedeni engellenme değil, sitenin tasarımının değişmesidir. Seçicinin sayfadaki küçük değişikliklere dayanıklı olması için:

  • Otomatik üretilmiş sınıf adlarından kaçının. css-1x9k2ab veya sc-bdVaJa gibi sınıflar her sürümde değişebilir.
  • Mutlak yol yazmayın. Tarayıcıdan kopyalanan /html/body/div[3]/div[2]/ul/li[4]/span ifadesi, sayfaya tek bir banner eklendiğinde bozulur.
  • Konuma değil anlama bağlanın. "Üçüncü span" yerine "fiyat sınıflı span" ya da "Stok başlığının yanındaki hücre".
  • Kararlı öznitelikleri tercih edin. data-sku, data-testid, itemprop, aria-label gibi öznitelikler görsel tasarımdan bağımsız olduğu için daha az değişir.
  • Önce yapılandırılmış veriye bakın. Birçok ürün sayfası <script type="application/ld+json"> içinde ürün adı, fiyat ve stok bilgisini schema.org biçiminde taşır. Bu veriyi okumak, görsel HTML'i ayrıştırmaktan çok daha dayanıklıdır.
  • Önce kapsayıcıyı, sonra alanı seçin. Kartı bir kez bulup alanları kartın içinde aramak, alanlar arasında karışıklığı önler.
  • Boş sonucu hata olarak ele alın. Seçici hiçbir şey bulamadığında sessizce boş değer yazmak yerine kaydedip uyarı verin; tasarım değişikliğini ilk gün fark edersiniz.

Seçicilerin boş dönmesi her zaman tasarım değişikliği değildir; bazen bot korumasının döndürdüğü farklı bir sayfa, bazen de JavaScript ile sonradan yüklenen içerik yüzündendir. Teşhis için Web Scraping'de Engellenmeden Veri Toplama Yöntemleri yazımızdaki listeye bakabilirsiniz.

Tarayıcıda seçici nasıl test edilir?

Seçiciyi koda yazmadan önce tarayıcıda denemek zaman kazandırır. Chrome, Edge ve Firefox'un geliştirici araçlarında (F12):

  • Konsolda $$("div.urun .fiyat") CSS selector ile eşleşen bütün öğeleri dizi olarak döndürür.
  • Konsolda $x('//th[normalize-space()="Stok"]/following-sibling::td[1]') XPath ifadesini çalıştırır.
  • Elements panelinde Ctrl + F arama kutusu hem düz metin hem CSS selector hem XPath kabul eder ve eşleşen öğeleri tek tek vurgular.

İki uyarı: tarayıcıda gördüğünüz sayfa JavaScript çalıştıktan sonraki hâlidir. Scraper'ınız bir HTTP istemcisiyle yalnızca sunucunun döndürdüğü HTML'i alıyorsa, tarayıcıda çalışan seçici kodda boş dönebilir; kaynağı görüntüleyerek (Ctrl + U) kontrol edin. İkincisi, tarayıcının "XPath'i kopyala" özelliği genellikle mutlak ve kırılgan yollar üretir; bunları başlangıç noktası olarak kullanıp sadeleştirin.

Kullanım senaryoları

  • Fiyat takibi: Ürün kartları CSS ile, "Stok" gibi etiket-değer alanları XPath ile. Kurgusu fiyat takibi çözümü sayfamızda.
  • Katalog toplama: Önce JSON-LD, yoksa kararlı data-* öznitelikleriyle CSS selector. Genel veri toplama kurgusu veri kazıma çözümü sayfamızda.
  • Karşılaştırma siteleri: Farklı sitelerin farklı yapıları için site başına seçici tanım dosyası; metne bağlı alanlar için XPath.
  • Test otomasyonu: Kendi uygulamanızda data-testid öznitelikleriyle CSS selector; tasarım değişikliklerinden etkilenmeyen testler.
  • Çok sayıda sayfada düzenli çekim: Seçiciler sabit kaldığı sürece iş, istek dağıtımına dönüşür; tek adres üzerinden farklı çıkış IP'leri için Rotating Proxy kullanılabilir.

Karar rehberi

İhtiyacınızÖneri
Sınıf, kimlik veya özniteliğe göre seçimCSS selector
BeautifulSoup kullanıyorsunuzCSS selector (XPath desteği yok)
Öğeyi metnine göre bulmakXPath
Bir öğeden üst öğeye veya önceki kardeşe gitmekXPath
Etiket-değer tabloları ve tanım listeleriXPath
Öznitelik değerini doğrudan döndürmekXPath /@attr veya parsel ::attr()
Aynı projede ikisi birdenparsel
Headless tarayıcıPlaywright locator, CSS öncelikli
Tasarım değişikliğine dayanıklılıkdata-* öznitelikleri veya JSON-LD

Sık sorulan sorular

CSS selector mı XPath mi öğrenmeliyim?

Önce CSS selector öğrenin; daha kısa, daha yaygın ve web geliştirmede de işinize yarar. Scraping'de metne göre seçim ve üst öğeye çıkma ihtiyacı doğduğunda XPath'in eksenlerini ve fonksiyonlarını öğrenmek yeterlidir. İkisini birlikte kullanan parsel gibi kütüphanelerle geçiş kolaydır.

BeautifulSoup XPath destekliyor mu?

Hayır. BeautifulSoup yalnızca CSS selector destekler. XPath gerekiyorsa aynı HTML'i lxml veya parsel ile ayrıştırabilirsiniz.

CSS'te metne göre seçim yapılabilir mi?

Standart CSS'te yapılamaz. Bazı araçlar kendi uzantılarını sunar: Playwright'ta has_text ve :has-text(), BeautifulSoup'ta :-soup-contains(). Bu uzantılar yalnızca ilgili araçta çalışır, taşınabilir değildir.

Tarayıcıdan kopyalanan XPath neden çalışmıyor?

Tarayıcının ürettiği yol genellikle mutlaktır ve JavaScript çalıştıktan sonraki sayfaya göre oluşturulur. Sunucunun ilk döndürdüğü HTML'de o yapı olmayabilir ya da küçük bir tasarım değişikliği yolu bozabilir. Yolu kararlı bir sınıf, kimlik veya öznitelikten başlayacak şekilde sadeleştirin.

text() ile normalize-space() arasındaki fark nedir?

text() öğenin doğrudan metin düğümlerini döndürür ve boşlukları olduğu gibi bırakır. normalize-space(.) öğenin iç içe etiketler dahil bütün metnini birleştirir, baştaki ve sondaki boşlukları siler, aradaki boşlukları teke indirir. Metin karşılaştırmalarında genellikle normalize-space() daha güvenilirdir.

Seçicilerim neden bazen boş dönüyor?

En sık üç neden vardır: sayfa tasarımı değişmiştir, içerik JavaScript ile sonradan yüklenmektedir ya da site size beklediğiniz sayfa yerine bir doğrulama veya hata sayfası döndürmektedir. Yanıtın HTML'ini bir dosyaya kaydedip seçiciyi o dosya üzerinde denemek, üç nedeni birbirinden ayırmanın en hızlı yoludur.

Özetle

CSS selector kısa, okunaklı ve her araçta desteklenir; etiket, sınıf, kimlik ve özniteliğe göre seçimde varsayılan tercih olmalıdır. XPath ise metne göre seçim, üst öğeye ve önceki kardeşe çıkma ve etiket-değer yapıları gibi CSS'in yapamadığı işler için gerekir; tarayıcılarda ve lxml'de 1.0 sürümüyle sınırlıdır. Performans farkı çoğu scraping işinde ağ süresinin yanında önemsizdir; asıl önemli olan, tasarım değişikliklerine dayanıklı seçici yazmaktır. Veri toplama işleriniz için uygun proxy türlerini proxy hizmetlerimizde bulabilirsiniz.

ChatGPT'ye sorClaude'a sor