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/@srctext()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çüt | CSS selector | XPath |
|---|---|---|
| Okunabilirlik | Kısa, web geliştiricilere tanıdık | Uzun, öğrenme eğrisi daha dik |
| Yön | Aşağı ve ileri (:has() ile kısmen dışa) | Her yöne: üst öğe, önceki kardeş, ata |
| Metne göre seçim | Standartta yok | Var: text(), contains() |
| Öznitelik değeri döndürme | Yok (kütüphane uzantısıyla) | Var: /@href |
| Metin düğümü döndürme | Yok (kütüphane uzantısıyla) | Var: /text() |
| Sınıf eşleştirme | .urun kesin ve kısa | contains(@class, ...) dikkat ister |
| Tarayıcı desteği | querySelectorAll | document.evaluate (XPath 1.0) |
| Python desteği | BeautifulSoup, lxml (cssselect), parsel | lxml, parsel; BeautifulSoup desteklemez |
| Headless tarayıcı | Playwright, Puppeteer, Selenium | Playwright, Puppeteer, Selenium |
| Tipik kullanım | Sınıf ve özniteliğe göre liste çıkarma | Etiket-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 selector | XPath |
|---|---|---|---|
| 1 | Bütün div öğeleri | div | //div |
| 2 | Kimliğe göre | #liste | //*[@id="liste"] |
| 3 | Sınıfa göre | .urun | //*[contains(concat(" ", normalize-space(@class), " "), " urun ")] |
| 4 | Doğrudan alt öğe | ul > li | //ul/li |
| 5 | Herhangi bir derinlikteki alt öğe | div span | //div//span |
| 6 | Özniteliği olan | a[href] | //a[@href] |
| 7 | Öznitelik değeri eşit | input[name="q"] | //input[@name="q"] |
| 8 | Öznitelik şununla başlar | a[href^="https"] | //a[starts-with(@href, "https")] |
| 9 | Öznitelik şunu içerir | a[href*="urun"] | //a[contains(@href, "urun")] |
| 10 | Öznitelik şununla biter | img[src$=".webp"] | //img[substring(@src, string-length(@src) - 4) = ".webp"] |
| 11 | İlk öğe | ul > li:first-of-type | //ul/li[1] |
| 12 | Son öğe | ul > li:last-of-type | //ul/li[last()] |
| 13 | Üçüncü öğe | ul > li:nth-of-type(3) | //ul/li[3] |
| 14 | Hemen sonraki kardeş | h2 + p | //h2/following-sibling::*[1][self::p] |
| 15 | Sonraki bütün kardeşler | h2 ~ span | //h2/following-sibling::span |
| 16 | Birden fazla seçim | h1, h2 | //h1 | //h2 |
| 17 | İçinde belirli öğe olan kart | div.urun:has(> span.indirim) | //span[@class="indirim"]/.. |
| 18 | Metni tam eşit olan buton | Standartta yok | //button[text()="Sepete ekle"] |
| 19 | Bir öğenin belirli atası | Standartta yok | //span[@class="fiyat"]/ancestor::div[@data-sku][1] |
| 20 | Etiketin yanındaki değer | Standartta 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 amaurunlerveyaeski-urunsınıflarını da seçer. Tablodaki uzun ifade yalnızca tamurunsınıfını eşleştirir. - Satır 10: XPath 1.0'da
ends-with()yoktur;substringile son karakterler karşılaştırılır. Sayı, aranan uzantının uzunluğundan bir eksiktir (.webpbeş karakter,- 4). - Satır 11:
li:first-childileli:first-of-typefarklıdır. İlki, ebeveyninin ilk çocuğu olanli'yi seçer; ilk çocuk başka bir etiketse hiçbir şey seçmez. - Satır 14:
//h2/following-sibling::p[1]"sonraki ilkp" demektir ve arada başka öğeler olsa da eşleşir. CSS'tekih2 + pise yalnızcah2'nin hemen ardından gelen öğepise 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:
//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:
//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:
//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:
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:
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:
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:
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
querySelectorAlldoğrudan tarayıcı motorunun optimize edilmiş seçici eşleştiricisini kullanır;document.evaluateile ç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.parserveyalxml) 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-1x9k2abveyasc-bdVaJagibi 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]/spanifadesi, sayfaya tek bir banner eklendiğinde bozulur. - Konuma değil anlama bağlanın. "Üçüncü
span" yerine "fiyatsınıflıspan" ya da "Stok başlığının yanındaki hücre". - Kararlı öznitelikleri tercih edin.
data-sku,data-testid,itemprop,aria-labelgibi ö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 + Farama 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çim | CSS selector |
| BeautifulSoup kullanıyorsunuz | CSS selector (XPath desteği yok) |
| Öğeyi metnine göre bulmak | XPath |
| Bir öğeden üst öğeye veya önceki kardeşe gitmek | XPath |
| Etiket-değer tabloları ve tanım listeleri | XPath |
| Öznitelik değerini doğrudan döndürmek | XPath /@attr veya parsel ::attr() |
| Aynı projede ikisi birden | parsel |
| Headless tarayıcı | Playwright locator, CSS öncelikli |
| Tasarım değişikliğine dayanıklılık | data-* ö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.




