Tutumumuzu ifade etmek çok basit: Biz Geonode olarak proxy satıyoruz ve bunun sınıf seçimi ile hiçbir ilgisi yoktur. Bu konuyla ilgili tek önemli gözlem, sınıf tabanlı seçicilerin en kırılgan tür olması ve bu kırılganlığın dışarıdan bakıldığında tam olarak bir ağ sorunu gibi görünmesidir — istek başarılı olur, sayfa gelir, ancak veri çıkarma işlemi hiçbir sonuç vermez ya da yanlış sonuç verir. Çalışmayı durduran bir kazıyıcıyı hata ayıklıyorsanız, sayfanın nasıl yüklendiğiyle ilgili herhangi bir şeyi kontrol etmeden önce sınıf adlarının değişip değişmediğini kontrol edin. Modern bir derleme boru hattı kullanan bir sitede, her dağıtımda bu adlar değişebilir.
Class Özniteliğiyle İlgili Sorun
class, boşluklarla ayrılmış bir belirteç listesi içerir; ancak XPath’te bu listeye ilişkin bir kavram yoktur. XPath bunu tek bir dize olarak algılar.
<div class="card featured large">...</div>
XPath için @class, "card featured large" dizesidir. "card, belirteçlerden biri mi?" diye sormak için yerleşik bir yol yoktur; oysa bu, tam da bir CSS seçicisinin .card ile doğal olarak yanıtladığı sorudur.
Bu eksiklik, iki hata durumuna yol açar.
Tam eşleme çok katıdır:
//div[@class='card']
Yalnızca class="card" ile eşleşir; başka hiçbir şeyle ve fazladan boşluk olmadan. class="card featured", class="featured card" ve class=" card " adreslerini gözden kaçırır. Gerçek bir sayfada, istediğiniz şeylerin çoğunu gözden kaçıracaktır.
Alt dize eşlemesi çok gevşektir:
//div[contains(@class, 'card')]
class="card" ile doğru eşleşir; ayrıca class="card-large", class="postcard", class="discard" ve class="card-footer" ile de eşleşir. İlgili isimlendirmelerin bulunduğu bir sayfada, istediğiniz sonuçların üst kümesini döndürür — ve kodunuz, hiçbir uyarı vermeden ilk sonucu alır.
İkinci hata daha tehlikelidir, çünkü sonuç üretir. Bir şey döndürülür, makul görünür, ancak yanlış öğedir.
Doğru Yöntem
Standart çözüm, yalnızca tam tokenlerin eşleşebilmesi için her iki tarafı da boşluklarla doldurur:
//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')]
Adım adım okuyun:
**normalize-space(@class)
** baş ve sonundaki boşlukları keser ve iç kısımdaki boşluk dizilerini tek bir boşluğa indirger. " card featured "
, "card featured"
haline gelir.
**concat(' ', ..., ' ')
** sonucu boşluklarla çevreler ve " card featured "
sonucunu verir. Artık her belirteç her iki tarafta da bir boşlukla sınırlandırılmıştır.
**contains(..., ' card ')
**, boşluklarla çevrili hedefi arar.
Bunu, basit sürümlerin başarısız olduğu durumlarla karşılaştırın:
| Sınıf özniteliği | Dolgulu dize | ' card '
içeriyor mu? |
|---|---|---|
| card
| " card "
| Evet |
| card featured
| " card featured "
| Evet |
| featured card
| " featured card "
| Evet |
| card-large
| " card-large "
| Hayır |
| discard
| " discard "
| Hayır |
| postcard footer
| " postcard footer "
| Hayır |
| card
| " card "
| Evet |
Her durumda doğrudur. normalize-space()
adımı sadece süs amaçlı değildir — bu adım olmadan, çift boşluk içeren class="card featured"
komutu " card featured "
sonucunu verecektir; bu sonuçta " card "
ifadesi yer alır ve tesadüfen çalışır, ancak satır sonu içeren class="card\nfeatured"
komutu çalışmaz.
Bu kod biraz uzun. Şöyle sarın:
def has_class(name):
return f"contains(concat(' ', normalize-space(@class), ' '), ' {name} ')"
tree.xpath(f"//div[{has_class('card')}]")
const hasClass = name =>
`contains(concat(' ', normalize-space(@class), ' '), ' ${name} ')`;
XPath ile ciddi düzeyde veri çıkarma işlemi yapan her kod tabanında, bu yardımcı fonksiyonun bir versiyonu mutlaka bulunur. Bunu bir kez yazmak, yirmi ifadeden birinde dolgu karakterlerini yanlış kullanmaktan daha iyidir.
Birden Fazla Sınıf
and
ile birleştirin:
//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')
and contains(concat(' ', normalize-space(@class), ' '), ' featured ')]
İşte bu noktada kodun uzunluğu gerçekten can sıkıcı hale geliyor — CSS’in div.card.featured
adresinde ifade ettiği şeyi anlatmak için 140 karakter gerekiyor.
“İki sınıftan herhangi biri” için or
adresini kullanın:
//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')
or contains(concat(' ', normalize-space(@class), ' '), ' tile ')]
“Bu sınıfa sahip ama diğerine sahip değil” için:
//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')
and not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]
İki veya daha fazla sınıf koşulu olduğunda, bir CSS seçicisinin işinizi görüp görmeyeceğini ciddi olarak değerlendirin. div.card.featured:not(.hidden)
, aynı mantığı karakter sayısının beşte biri kadar bir alanda ifade eder ve tüm yaygın ayrıştırma kütüphaneleri, XPath'in yanı sıra CSS seçicilerini de destekler. Dosyanın tamamı için tek bir dil seçmenizi gerektiren bir kural yoktur.
Oluşturulan ve Hash Değerine Dönüştürülen Sınıf Adları
Günümüzün karmaşıklığı; bu durum, önerileri de değiştiriyor.
CSS Modules, styled-components ve çeşitli CSS-in-JS kütüphaneleri gibi birçok ön uç derleme aracı, çakışmaları önlemek için kapsamlı sınıf adları oluşturur. Sonuç şöyle görünür:
<div class="ProductCard_container__3xK9p">...</div>
<div class="css-1x9dj2k">...</div>
Bitiş kısmı, bileşenin stilleri her değiştiğinde değişir; bu da pratikte birçok dağıtımda gerçekleşir. Tam ada bağlı bir seçici, uyarı vermeden çalışmaz hale gelir.
Bunu ele almanın üç yolu vardır; tercih sırasına göre:
Kararlı önek üzerine bağlanın; eğer araçlar bunu üretiyorsa:
//div[starts-with(@class, 'ProductCard_container__')]
CSS Modülleri geleneksel olarak ComponentName_elementName__hash
biçiminde üretilir; bu nedenle son çift alt çizgiden önceki kısım, tüm derlemelerde sabittir. Bu, uygulanabildiği durumlarda iyi sonuç verir.
**Bunun yerine bir data-*
özniteliği bulun.** Birçok uygulama, otomatik araçların sabit bir bağlantı noktasına sahip olması için data-testid
, data-test
veya benzer öznitelikler içerir:
//div[@data-testid='product-card']
Eğer varsa, onu kullanın. Bu, herhangi bir sınıf adından daha kararlıdır çünkü rastgele oluşturulmuş değil, kasıtlı olarak seçilmiştir.
Tamamen başka bir şeye dayanın. Bir başlığa, metin içeriğine veya bir öğe türüne göre yapılandırın. //h2[normalize-space()='Featured']/following-sibling::div[1]
, sınıfların ne adlandırıldığıyla ilgilenmez.
Ve tamamen belirsiz durumlarda — kararlı bir bileşeni olmayan css-1x9dj2k
— sınıfa dayalı seçim kesinlikle uygulanamaz ve aksini iddia etmek, her hafta bozulan bir veri toplayıcıya yol açar. Sayfada yapılandırılmış verileri veya sayfanın kendisinin çağırdığı API'yi arayın.
Sınıf Sırası ve Boşluk Karakterleri
İnsanların sık sık takıldığı ve üzerinde titizlikle durulması gereken iki konu.
Özellik içindeki sınıf sırası anlamsızdır. class="card featured"
ve class="featured card"
, tarayıcı ve CSS açısından eşdeğerdir. “Padded-concat” deyimi her ikisini de doğru şekilde işler; tam eşleştirme ise ikisini de güvenilir bir şekilde işleyemez. Eğer sıraya bağlı bir ifade yazarken kendinizi bulursanız, bu bir şeylerin yanlış olduğunun işaretidir.
Boşluklar herhangi bir şey olabilir. Sekme ve satır sonları, sınıf özniteliğinde geçerli ayırıcılardır ve elle biçimlendirilmiş HTML'de görünürler:
<div class="card
featured">
normalize-space()
hepsini birleştirir; bu yüzden bu ifade biçimi bunu içerir. concat(' ', @class, ' ')
'i normalleştirmeden kullanan bir ifade, bu işaretlemede hata verecektir ve bu hata, görüntülenen sayfada görünmez.
Büyük/küçük harf önemli. Sınıf adları, XHTML olarak ayrıştırılan HTML belgelerinde büyük/küçük harfe duyarlıdır ve CSS eşleşmesi için standart modda HTML'de büyük/küçük harfe duyarlı değildir — ancak XPath dize karşılaştırması her zaman büyük/küçük harfe duyarlıdır. Bir sayfada Card
ve card
karışık olarak kullanılıyorsa, boşluk eklenmiş ifade bunları farklı olarak değerlendirir. XPath 1.0’da lower-case()
işlevi bulunmadığından, geçici çözüm olarak translate()
kullanılarak alfabe karakterleri açıkça belirtilir; bu durumda ifade gerçekten okunamaz hale gelir ve CSS seçicisi açıkça daha iyi bir seçimdir.
Bir Sınıfın Atasını veya Torununu Bulma
Sınıf seçimi genellikle bir amaç değil, bir araçtır. Bu konunun büyük bir kısmı iki desenle ele alınabilir.
Bir sınıftan, o sınıfın içindeki bir öğeye:
//div[contains(concat(' ',normalize-space(@class),' '),' card ')]//span[@class='price']
Ya da, halihazırda card öğesini içeren kodda, göreceli bir ifade kullanın — burada baştaki nokta çok önemlidir:
for card in tree.xpath(f"//div[{has_class('card')}]"):
price = card.xpath(".//span[@class='price']/text()")
.//span
, card öğesinin içinde arama yapar. //span
, kökten başlayarak tüm belgeyi tarar; bu, her kart için sayfadaki ilk fiyatı döndürür. Bu, tutarlı, mantıklı görünen ancak yanlış bir çıktı üretir ve veri çıkarma kodlarındaki en yaygın hatalardan biridir.
Bir öğeden onun kapsayıcısına kadar:
//span[@class='price']/ancestor::div[contains(concat(' ',normalize-space(@class),' '),' card ')][1]
[1]
'i önemlidir: ancestor::
ters eksendir; dolayısıyla 1. konum, en dıştaki değil, en yakın eşleşen üst öğedir. Bu parametre olmadan her eşleşen üst öğe alınır ve iç içe geçmiş kapsayıcılarda bu, nadiren istenen sonuçtur.
Kütüphaneye Özgü Notlar
Kullanım biçimi her yerde aynıdır; ancak çevreleyen API farklıdır ve kütüphaneler arasındaki bazı farklılıklar, önlenebilir karışıklıklara yol açmaktadır.
lxml (Python). Her iki dili de destekler ve cssselect
sınıf çalışmaları için en pratik seçimdir:
from lxml import html
tree = html.fromstring(source)
tree.cssselect('div.card.featured') # clear
tree.xpath(f"//div[{has_class('card')}]") # when inside a larger expression
cssselect
, CSS'yi dahili olarak XPath'e çevirir; bu nedenle, desteklediği seçiciler açısından ikisi de eşdeğer yeteneklere sahiptir. Bunun lxml'den ayrı bir paket olduğunu ve kurulması gerektiğini unutmayın.
BeautifulSoup (Python). Hiçbir şekilde XPath desteği sunmaz — bu durum, diğer ekosistemlerden gelen kullanıcıları şaşırtmaktadır. CSS seçiciler için select()
ve kendi find_all(class_='card')
API'sini sunar; bu API, token eşleştirmesini yerel olarak doğru bir şekilde gerçekleştirir. Özellikle XPath'e ihtiyacınız varsa, lxml'i kullanmanız gerekir.
Selenium. Hem By.XPATH
hem de By.CSS_SELECTOR
biçimlerini kabul eder ve her biri için tarayıcının motorunu kullanır. Bu, yalnızca XPath 1.0'ı desteklediği anlamına gelir; ayrıca CSS desteği, :has()
dahil olmak üzere tarayıcının desteklediği her şeyle uyumludur.
Playwright. Konum belirleyici türünü dizeden otomatik olarak algılar; bu sayede page.locator('div.card')
ve page.locator('//div[@id="x"]')
önek olmadan çalışır. Ayrıca, metin tabanlı konum belirleyiciler de sunar — page.getByText()
, page.getByRole()
— bunlar, kullanıcıların daha önce XPath'in metin eşleştirme özelliğine ihtiyaç duydukları durumların büyük bir kısmını karşılar ve her iki dilden de daha okunaklıdır.
Scrapy. Seçiciler üzerinde .css()
ve .xpath()
sağlar ve bunlar zincirlenebilir, bu da gerçekten kullanışlıdır:
for card in response.css('div.card'):
price = card.xpath(".//dt[normalize-space()='Price']/following-sibling::dd[1]/text()").get()
Aynı ifade zincirinde, yapısal geçiş için CSS, etiket araması için XPath. XPath'in başındaki .
kısmına dikkat edin — Scrapy'nin zincirlenmiş seçicileri, diğer her şeyde olduğu gibi kök ve göreceli adres tuzağına sahiptir.
Tarayıcı konsolu. $x('//div[@class="card"]')
, Chrome ve Firefox geliştirici araçlarında XPath'i değerlendirir; document.querySelectorAll('div.card')
ise CSS'yi işler. İfadeyi koda eklemeden önce burada test etmek on saniyenizi harcamaya değer; ancak şunu unutmayın: tarayıcının DOM'u JavaScript'ten sonra oluşturulur, oysa ayrıştırıcınızınki öyle değildir — konsolda çalışan bir ifade, ham HTML'de hiçbir şey bulamayabilir.
Ne Zaman CSS Kullanılmalı?
Basitçe söylemek gerekirse, bu özel iş için cevap genellikle “evet”tir.
Kriterleriniz sınıflarsa CSS kullanın. div.card, div.card.featured, div.card:not(.hidden) — hepsi daha net, daha kısa ve varsayılan olarak doğrudur. CSS, sınıf özniteliğini bir belirteç listesi olarak yorumlar; bu da tam olarak XPath’te eksik olan şeydir.
Sınıf ikincil öneme sahipse ve asıl kriter CSS ile ifade edilemeyen bir şeyse — özellikle metin içeriği — XPath kullanın. //div[contains(@class,'card')][.//span[contains(., 'Sold out')]] adresindeki örnekte metin koşulu için XPath gerekir ve sınıf kısmı da buna eşlik eder.
İkisini karıştırın. Her ana akım ayrıştırma kütüphanesi her ikisini de destekler. lxml'de cssselect ile birlikte xpath vardır; Selenium her iki konum belirleme stratejisini de kabul eder; Playwright da her ikisini kabul eder. Yapısal seçim için CSS'yi, metin koşulları için XPath'i kullanmak bir uzlaşma değildir; bu, en kısa ve okunabilir kodu üreten düzenlemedir.
XPath sınıf seçiminin tek kullanım alanı, daha büyük bir XPath ifadesinin içinde buna ihtiyaç duyduğunuz ve ifade ortasında dil değiştirmenin mümkün olmadığı durumlardır. Bu gerçek bir kısıtlamadır ve bu deyimin var olmasının nedeni de budur — ancak bu durum, pratikte karşılaşacağınız XPath sınıf eşleşmelerinin sayısından daha sınırlıdır.
Sık Sorulan Sorular
XPath'te bir öğeyi sınıfına göre nasıl seçebilirim?
//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')] ifadesini kullanın. Dolgu karakteri, yalnızca tam sınıf belirteçlerinin eşleşmesini sağlar. Basit @class='card' ifadesi, ek sınıflara sahip öğeleri gözden kaçırır; contains(@class,'card') ifadesi ise card-large ve discard gibi adreslerle de eşleşir.
contains(@class, 'name') neden yanlış öğelerle eşleşiyor?
Çünkü bu, kelime sınırları kavramını içermeyen basit bir alt dize testidir. class özniteliği, boşluklarla ayrılmış bir listedir; ancak XPath bunu tek bir dize olarak görür, bu nedenle contains(@class,'card'), bu dört karakteri herhangi bir yerde içeren tüm sınıf adlarıyla eşleşir.
XPath'te iki sınıfa sahip bir öğeyi nasıl seçerim?
İki "padded-concat" koşulunu and ile birleştirin. Bu yöntem işe yarar ve yaklaşık 140 karakter uzunluğundadır. İki veya daha fazla sınıf koşulu söz konusu olduğunda, bir CSS seçici — div.card.featured — çok daha anlaşılırdır ve çoğu ayrıştırma kütüphanesi bunu kullanmanıza izin verir.
XPath’te sınıf sırası önemli mi?
Özellik içinde nerede geçerse geçsin bir belirteçle eşleşen “padded-concat” deyiminde önemli değildir. Ancak tam eşleme söz konusu olduğunda önemlidir; bu, @class='card featured'’in kötü bir seçim olmasının birkaç nedeninden biridir. HTML’de sınıf sırası anlamsızdır, bu nedenle buna bağlı bir seçici, her an ortaya çıkabilecek bir hatadır.
Rastgele oluşturulan sınıf adlarını nasıl ele almalıyım?
Araçlar tarafından üretilen durumlarda starts-with() ile sabit önek üzerine sabitleyin, uygulama sağlıyorsa data-testid özniteliğini kullanın ya da metin veya yapı gibi tamamen başka bir şeye sabitleyin. Sabit bir kısmı olmayan, tamamen opak karma adlar için sınıfa dayalı seçim uygulanamaz.
Sınıf bazında seçim yapmak için XPath mi, CSS mi daha iyidir?
Kesinlikle CSS. CSS, sınıf özniteliğini bir belirteç listesi olarak ele alır; ki aslında öyledir; bu nedenle div.card hem daha kısadır hem de doğrudur. XPath’in aynı şeyi ifade etmek için yetmiş karakterlik bir deyime ihtiyacı vardır. Metin eşleştirme gibi CSS’in yapamadığı bir şeye de ihtiyacınız olduğunda XPath’i kullanın.
XPath’te sınıfa göre bir üst öğeyi nasıl bulurum?
//span[@class='price']/ancestor::div[contains(concat(' ',normalize-space(@class),' '),' card ')][1]. [1], en yakın eşleşen atayı seçer; çünkü ancestor::, konum 1’in en dıştaki değil en yakın olanı ifade ettiği ters bir eksendir.
Nispi XPath'im neden her öğe için aynı değeri döndürüyor?
Çevrimin içinde .// yerine // kullandığınız için. Başında // bulunan ifade, bağlamdan bağımsız olarak belge kökünden arama yapar; bu nedenle her yinelemede sayfanın tamamında ilk eşleşme bulunur. .// ise mevcut öğe içinde arama yapar; bu da sizin kastettiğiniz şeydir.
Sonuç
Sınıf seçimi, XPath’in eskiliğinin ortaya çıktığı noktadır. Sınıf özniteliği bir belirteç listesidir ve XPath’te belirteç işlemleri bulunmadığından, “bu sınıfa sahip” ifadesini oluşturmak için dizeye boşluk eklemek ve bu şekilde genişletilmiş belirteç aramak gerekir — CSS’in dokuz karakterle ifade ettiği şeyi, yetmiş karakterlik bir deyimle ifade etmek.
Bu ifade biçimini öğrenin, çünkü daha büyük XPath ifadelerinin içinde buna ihtiyacınız olacak; ayrıca, yirmi kez yazmak yerine tek seferde yazabilmek için bunu bir yardımcı fonksiyona sarın. normalize-space() adımı isteğe bağlı değildir: gerçek işaretlemelerde sınıf özniteliklerinde satır sonları ve sekmeler bulunur ve normalleştirilmemiş bir ifade, bunlar yüzünden görünmez bir şekilde hata verir.
Ancak daha yararlı olan sonuç, bu deyimi kullanmaktan kaçınmaktır. Seçim kriterleriniz sınıflar ve başka hiçbir şey değilse, bir CSS seçici yazın. Her ana akım kütüphane her ikisini de destekler; bunları tek bir dosyada karıştırmak normaldir ve ifade başına doğru aracı seçmek, bir iş arkadaşınızın okuyabileceği daha kısa kod üretir.
Ve sınıf adlarını mevcut en az kararlı bağlantı noktası olarak değerlendirin. Oluşturulan ve karma işlenmiş adlar dağıtım sırasında değişir; elle yazılmış olanlar bile birisi bir bileşenin stilini değiştirdiğinde değişir. Bir data-testid, bir başlığın metni veya bir öğenin tanımlanabilir bir şeyle olan ilişkisi, sınıf adından daha uzun ömürlü olacaktır — ve bir sınıfın ortadan kaybolması durumunda ortaya çıkan hata modu bir hata değildir; sessiz, biçimsel olarak doğru, boş bir çıktıdır.
