ProxynetProxynet

انتخابگر CSS یا XPath: کدام برای اسکرپینگ؟

تاریخ انتشار:

14 دقیقه مطالعه

Acar Diveroli
نویسنده: Acar Diveroli
مکعب CSS با برچسب .item در چپ، کارت درخت DOM در میانه و مکعب XPath با برچسب //a در راست

پس از دانلود صفحه، کار اصلی اسکرپینگ آغاز می‌شود: یافتن و استخراج نام محصول، قیمت و پیوند از میان صدها تگ. اینکه کدام عنصر را می‌خواهید با یک انتخابگر توصیف می‌کنید. دو زبان رایج وجود دارد: انتخابگرهای CSS که توسعه‌دهندگان وب از برگه‌های سبک می‌شناسند، و XPath که از دنیای XML آمده است. اغلب می‌توانید یک عنصر را با هر دو انتخاب کنید، اما یکی کوتاه و خواناست و دیگری کارهایی می‌کند که CSS اصلاً نمی‌تواند.

در این نوشته توضیح می‌دهیم این دو زبان چیستند، نحو آن‌ها را کنار هم می‌گذاریم، بررسی می‌کنیم XPath فراتر از CSS چه می‌کند و آیا تفاوت کارایی واقعاً اهمیت دارد. سپس نشان می‌دهیم چگونه انتخابگرهایی بنویسید که با تغییر طراحی صفحه نشکنند و چگونه انتخابگر را در مرورگر بیازمایید. در میانه نوشته جدول مرجع سریع 20 سطری آمده که می‌توانید نشانه‌گذاری کنید؛ روی صفحه آزمون بررسی کردیم که هر جفت CSS و XPath جدول همان عنصرها را انتخاب می‌کند.

انتخابگر CSS چیست؟

انتخابگر CSS زبان الگویی است که در برگه‌های سبک CSS برای تعیین عنصرهایی که سبک بر آن‌ها اعمال می‌شود به کار می‌رود. مرورگرها همین زبان را در جاوااسکریپت از طریق document.querySelector() و querySelectorAll() هم پشتیبانی می‌کنند. تعریف فعلی آن ⁦Selectors Level 4⁩ از W3C است.

انتخابگر CSS عنصر را بر اساس این‌ها انتخاب می‌کند:

  • تگ: div، a، span
  • کلاس: .product
  • id: #list
  • ویژگی: a[href]، img[src$=".webp"]
  • سلسله‌مراتب: ul > li (فرزند مستقیم)، div span (نواده در هر عمقی)
  • هم‌نیاها: h2 + p (بلافاصله پس از آن)، h2 ~ span (همه هم‌نیاهای بعدی)
  • جایگاه: li:first-of-type، li:nth-of-type(3)

جهت پایه CSS رو به پایین و رو به جلو است: از یک عنصر به نوادگان و هم‌نیاهای بعدی آن می‌روید. شبه‌کلاس :has() که در ⁦Selectors Level 4⁩ آمد تا حدی این قاعده را نرم می‌کند: div.product:has(> span.discount) کارت‌های محصولی را انتخاب می‌کند که برچسب تخفیف دارند. اما :has() از یک عنصر «بالا نمی‌رود»؛ عنصر بیرونی را با نگاه به درون آن انتخاب می‌کند.

XPath چیست؟

XPath (XML Path Language) زبان پرس‌وجویی است که گره‌ها را در سندهای XML و HTML با یک عبارت مسیر انتخاب می‌کند. سند را درخت می‌بیند و در آن به هر جهتی می‌تواند حرکت کند: رو به پایین، رو به بالا و به سوی هم‌نیاهای پیشین و بعدی. تعریف فعلی W3C ⁦XPath 3.1⁩ است.

نکته عملی مهمی وجود دارد: مرورگرها و کتابخانه lxml پایتون از XPath 1.0 پشتیبانی می‌کنند. قابلیت‌هایی که در نسخه‌های بعدی افزوده شد، مانند ends-with()، توابع عبارت منظم و سامانه نوع غنی‌تر، در این محیط‌ها در دسترس نیستند. عبارت‌های XPath برای اسکرپینگ را در مرزهای نسخه 1.0 نگه دارید.

اجزای پایه XPath:

  • // هر عمقی از سند و / فرزند مستقیم: //ul/li
  • [...] شرط (predicate): //a[@href]، //li[3]
  • @ ویژگی: //img/@src
  • text() گره متنی: //h2/text()
  • محورها: parent::، ancestor::، following-sibling::، preceding-sibling::
  • توابع: contains()، starts-with()، normalize-space()، last()، string-length()

در مرورگر، XPath با تابع جاوااسکریپت document.evaluate() اجرا می‌شود؛ شیوه کاربرد آن در مستندات MDN آمده است.

مقایسه نحو

معیارانتخابگر CSSXPath
خواناییکوتاه، آشنا برای توسعه‌دهندگان وببلند، منحنی یادگیری تندتر
جهترو به پایین و جلو (با :has() تا حدی رو به بیرون)هر جهت: والد، هم‌نیای پیشین، نیا
انتخاب بر اساس متندر استاندارد نیستدارد: text()، contains()
برگرداندن مقدار ویژگینه (با افزونه کتابخانه‌ها)دارد: /@href
برگرداندن گره متنینه (با افزونه کتابخانه‌ها)دارد: /text()
تطبیق کلاس.product دقیق و کوتاهcontains(@class, ...) دقت لازم دارد
پشتیبانی مرورگرquerySelectorAlldocument.evaluate با XPath 1.0
پشتیبانی پایتونBeautifulSoup، lxml با cssselect، parsellxml، parsel؛ BeautifulSoup پشتیبانی نمی‌کند
مرورگر headlessPlaywright، Puppeteer، SeleniumPlaywright، Puppeteer، Selenium
کاربرد رایجاستخراج فهرست بر اساس کلاس و ویژگیجدول‌های برچسب و مقدار، انتخاب متنی

جدول مرجع سریع

هر سطر جدول زیر یک انتخاب یکسان در دو زبان است. در سطرهایی که معادل CSS ندارند، کارهای اضافه XPath آمده است.

#چه انتخاب می‌شود؟انتخابگر CSSXPath
1همه عنصرهای divdiv//div
2بر اساس id#list//*[@id="list"]
3بر اساس کلاس.product//*[contains(concat(" ", normalize-space(@class), " "), " product ")]
4فرزند مستقیمul > li//ul/li
5نواده در هر عمقیdiv span//div//span
6دارای ویژگیa[href]//a[@href]
7مقدار ویژگی برابر استinput[name="q"]//input[@name="q"]
8ویژگی با این آغاز می‌شودa[href^="https"]//a[starts-with(@href, "https")]
9ویژگی شامل این استa[href*="product"]//a[contains(@href, "product")]
10ویژگی با این پایان می‌یابدimg[src$=".webp"]//img[substring(@src, string-length(@src) - 4) = ".webp"]
11نخستین عضوul > li:first-of-type//ul/li[1]
12آخرین عضوul > li:last-of-type//ul/li[last()]
13سومین عضوul > li:nth-of-type(3)//ul/li[3]
14هم‌نیای بلافاصله بعدیh2 + p//h2/following-sibling::*[1][self::p]
15همه هم‌نیاهای بعدیh2 ~ span//h2/following-sibling::span
16چند انتخابh1, h2//h1 | //h2
17کارتی که عنصر مشخصی داردdiv.product:has(> span.discount)//span[@class="discount"]/..
18دکمه با متن دقیقدر استاندارد نیست//button[text()="Add to cart"]
19نیای مشخص یک عنصردر استاندارد نیست//span[@class="price"]/ancestor::div[@data-sku][1]
20مقدار کنار یک برچسبدر استاندارد نیست//th[normalize-space()="Stock"]/following-sibling::td[1]

چند جزئیات جدول اشتباه رایج می‌سازد:

  • سطر 3: عبارت //*[contains(@class, "product")] کوتاه‌تر به نظر می‌رسد، اما کلاس‌های products یا old-product را هم انتخاب می‌کند. عبارت بلند جدول فقط با کلاس دقیق product تطبیق دارد.
  • سطر 10: در XPath 1.0 تابع ends-with() وجود ندارد؛ نویسه‌های پایانی با substring مقایسه می‌شوند. عدد، یکی کمتر از طول پسوند مورد جست‌وجوست (.webp پنج نویسه دارد، پس - 4).
  • سطر 11: li:first-child و li:first-of-type متفاوت‌اند. اولی li را انتخاب می‌کند که فرزند نخست والدش باشد؛ اگر فرزند نخست تگ دیگری باشد، چیزی انتخاب نمی‌کند.
  • سطر 14: عبارت //h2/following-sibling::p[1] یعنی «نخستین p بعدی» و حتی با وجود عنصرهای دیگر در میانه تطبیق دارد. h2 + p در CSS فقط وقتی تطبیق دارد که عنصر بلافاصله پس از h2 یک p باشد.

برای برگرداندن مقدار ویژگی و گره متنی، XPath از //a/@href و //h2/text() استفاده می‌کند. کتابخانه parsel پایتون برای این کارها افزونه‌های غیراستاندارد a::attr(href) و h2::text را به CSS می‌افزاید.

کارهایی که XPath می‌کند و CSS نه

انتخاب بر اساس متن

در صفحه یک فروشگاه اینترنتی، دکمه افزودن به سبد و دکمه ناموجود ممکن است کلاس یکسانی داشته باشند. تنها چیزی که آن‌ها را از هم جدا می‌کند متنشان است:

text
//button[text()="Add to cart"]
//span[contains(normalize-space(.), "Discount")]

text() فقط به گره متنی مستقیم عنصر نگاه می‌کند؛ اگر متن میان تگ‌های تودرتو پخش شده باشد تطبیق ندارد. normalize-space(.) همه متن عنصر را به هم می‌پیوندد و فاصله‌های آغاز و پایان را حذف می‌کند، پس معمولاً قابل‌اعتمادتر است.

CSS استاندارد انتخاب بر اساس متن ندارد. برخی ابزارها افزونه‌های خودشان را دارند: گزینه has_text و :has-text() در Playwright و :-soup-contains() در BeautifulSoup. این افزونه‌ها فقط در همان ابزار کار می‌کنند.

رفتن به والد و هم‌نیای پیشین

اگر در کارت محصول فقط فیلد قیمت کلاس پایداری دارد، باید از قیمت آغاز کنید و به کل کارت برسید:

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

.. والد مستقیم و ancestor:: نیایی در هر سطح بالاتر را انتخاب می‌کند. [1] نزدیک‌ترین نیا را برمی‌دارد.

ساختارهای برچسب و مقدار

جدول‌های مشخصات محصول و فهرست‌های تعریف از رایج‌ترین ساختارهایی‌اند که اسکرپینگ با آن‌ها روبه‌رو می‌شود. اینکه کدام سطر با چه ترتیبی بیاید از محصولی به محصول دیگر فرق می‌کند؛ آنچه ثابت می‌ماند متن برچسب است:

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

این عبارت‌ها مقدار کنار «Stock» را در هر سطری از جدول باشد پیدا می‌کنند. برای همین کار با CSS باید همه سطرها را بگیرید و در پایتون روی آن‌ها حلقه بزنید.

CSS و XPath در پایتون

پایتون سه کتابخانه رایج دارد و پشتیبانی آن‌ها از انتخابگرها متفاوت است.

BeautifulSoup فقط انتخابگر CSS را پشتیبانی می‌کند (select و select_one) و از XPath پشتیبانی ندارد:

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 مستقیم XPath 1.0 را اجرا می‌کند و با توابعی مانند string() نتیجه را به‌صورت متن برمی‌گرداند:

python
from lxml import html as lxml_html

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

parsel (کتابخانه انتخابگر Scrapy) هر دو زبان را روی یک شیء و قابل زنجیره شدن با یکدیگر می‌دهد. می‌توانید کارت‌ها را با CSS پیدا کنید و درون کارت از XPath استفاده کنید:

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()="Add to cart"]').get() is not None
    print(name, link, available)

نکته‌ای که در زنجیره باید رعایت شود این است که عبارت XPath درون کارت با .// آغاز شود. اگر //button بنویسید، جست‌وجو به‌جای درون کارت روی کل سند انجام می‌شود و برای هر کارت نخستین دکمه صفحه را پیدا می‌کنید.

انتخابگرها در مرورگر headless

در صفحه‌هایی که محتوا با جاوااسکریپت بارگذاری می‌شود، انتخابگرها درون ابزار خودکارسازی مرورگر به کار می‌روند. Playwright از هر دو زبان پشتیبانی می‌کند و عبارتی را که با // آغاز شود خودکار 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()="Stock"]/following-sibling::td[1]').inner_text()
    addable = page.locator("div.product", has_text="Add to cart")

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

در Selenium همین کارها با By.CSS_SELECTOR و By.XPATH انجام می‌شود؛ نصب و تنظیم پروکسی را در استفاده از پروکسی با Selenium و استفاده از پروکسی با SeleniumBase توضیح داده‌ایم. برای اینکه بفهمید صفحه واقعاً مرورگر لازم دارد یا نه، صفحه‌های ایستا و پویا را ببینید.

آیا تفاوت کارایی واقعاً اهمیت دارد؟

ادعای «انتخابگر CSS از XPath سریع‌تر است» در اینترنت زیاد تکرار می‌شود. پاسخ به محیط بستگی دارد:

  • در مرورگر، querySelectorAll مستقیم از موتور بهینه‌شده انتخابگر مرورگر استفاده می‌کند و XPath که با document.evaluate اجرا می‌شود معمولاً کندتر است. تفاوت در صفحه‌های بسیار بزرگ و پرس‌وجوهایی که هزاران بار تکرار می‌شوند محسوس می‌شود.
  • در پایتون با lxml و parsel، انتخابگرهای CSS بیشتر پشت صحنه به XPath ترجمه و مانند XPath اجرا می‌شوند. پس هر دو زبان از یک موتور استفاده می‌کنند و تفاوت تا حد زیادی از میان می‌رود.
  • در BeautifulSoup سرعت انتخاب بیش از زبان انتخابگر به تحلیلگر به‌کاررفته (html.parser یا lxml) بستگی دارد.

آنچه واقعاً تعیین‌کننده است شیوه نوشتن انتخابگر است. عبارت //*[contains(@class, "price")] که کل سند را می‌پیماید، بسیار بیشتر از عبارتی که نخست کارت و سپس قیمت درون آن را پیدا می‌کند کار انجام می‌دهد. و همه این‌ها کنار زمان دانلود صفحه از شبکه یا بارگذاری آن در مرورگر headless معمولاً ناچیز است. گلوگاه واقعی اسکرپر را در همروندی و موازی‌سازی بررسی کرده‌ایم.

چگونه از انتخابگرهای شکننده پرهیز کنیم؟

رایج‌ترین دلیل شکستن اسکرپرها مسدود شدن نیست، بلکه تغییر طراحی سایت است. برای اینکه انتخابگر از تغییرهای کوچک صفحه جان سالم به در ببرد:

  • از نام کلاس‌های خودکارساخته پرهیز کنید. کلاس‌هایی مانند css-1x9k2ab یا sc-bdVaJa ممکن است با هر انتشار عوض شوند.
  • مسیر مطلق ننویسید. عبارتی که از مرورگر کپی شده، مانند /html/body/div[3]/div[2]/ul/li[4]/span، با افزوده شدن یک بنر به صفحه می‌شکند.
  • به معنا تکیه کنید نه به جایگاه. نه «سومین span»، بلکه «span با کلاس price» یا «خانه کنار عنوان Stock».
  • ویژگی‌های پایدار را ترجیح دهید. ویژگی‌هایی مانند data-sku، data-testid، itemprop و aria-label مستقل از طراحی بصری‌اند و کمتر تغییر می‌کنند.
  • نخست به داده ساخت‌یافته نگاه کنید. بسیاری از صفحه‌های محصول نام، قیمت و موجودی را در قالب schema.org درون <script type="application/ld+json"> دارند. خواندن این داده بسیار پایدارتر از تحلیل HTML بصری است.
  • نخست ظرف و سپس فیلد را انتخاب کنید. یافتن یک بار کارت و جست‌وجوی فیلدها درون آن از درهم‌آمیختن فیلدها جلوگیری می‌کند.
  • نتیجه خالی را خطا بدانید. به‌جای نوشتن بی‌صدای مقدار خالی وقتی انتخابگر چیزی نمی‌یابد، آن را ثبت کنید و هشدار بدهید؛ تغییر طراحی را همان روز اول متوجه می‌شوید.

خالی برگشتن انتخابگرها همیشه به معنای تغییر طراحی نیست؛ گاهی صفحه دیگری است که محافظت ربات برگردانده و گاهی محتوایی که بعداً با جاوااسکریپت بارگذاری می‌شود. برای تشخیص، فهرست موجود در وب اسکرپینگ بدون مسدود شدن را ببینید.

چگونه انتخابگر را در مرورگر بیازماییم؟

آزمودن انتخابگر در مرورگر پیش از نوشتن آن در کد وقت را ذخیره می‌کند. در ابزارهای توسعه‌دهنده (F12) کروم، Edge و فایرفاکس:

  • $$("div.product .price") در کنسول همه عنصرهای منطبق با انتخابگر CSS را به‌صورت آرایه برمی‌گرداند.
  • $x('//th[normalize-space()="Stock"]/following-sibling::td[1]') در کنسول یک عبارت XPath را اجرا می‌کند.
  • Ctrl + F در پنل Elements کادر جست‌وجویی باز می‌کند که متن ساده، انتخابگر CSS و XPath را می‌پذیرد و عنصرهای منطبق را یکی‌یکی برجسته می‌کند.

دو هشدار: صفحه‌ای که در مرورگر می‌بینید وضعیت پس از اجرای جاوااسکریپت است. اگر اسکرپر شما فقط HTML برگشتی سرور را از طریق کلاینت HTTP می‌گیرد، انتخابگری که در مرورگر کار می‌کند ممکن است در کد خالی برگردد؛ با دیدن کد منبع (Ctrl + U) بررسی کنید. دوم، قابلیت Copy XPath مرورگر معمولاً مسیرهای مطلق و شکننده می‌سازد؛ آن‌ها را نقطه شروع بدانید و ساده کنید.

کاربردها

  • پایش قیمت: کارت‌های محصول با CSS و فیلدهای برچسب و مقدار مانند «Stock» با XPath. ساختار آن در صفحه راه‌حل پایش قیمت آمده است.
  • جمع‌آوری کاتالوگ: نخست JSON-LD؛ اگر نبود، انتخابگرهای CSS با ویژگی‌های پایدار data-*. ساختار کلی جمع‌آوری داده در صفحه راه‌حل استخراج داده آمده است.
  • سایت‌های مقایسه: فایل تعریف انتخابگر برای هر سایت متناسب با ساختارهای متفاوت؛ XPath برای فیلدهای متنی.
  • خودکارسازی آزمون: انتخابگرهای CSS با ویژگی‌های data-testid در برنامه خودتان؛ آزمون‌هایی که از تغییر طراحی متأثر نمی‌شوند.
  • جمع‌آوری منظم از صفحه‌های فراوان: تا وقتی انتخابگرها پایدار بمانند، کار به پخش درخواست‌ها برمی‌گردد؛ برای IPهای خروجی متفاوت از یک آدرس می‌توان پروکسی چرخشی را به کار برد.

راهنمای انتخاب

نیاز شماپیشنهاد
انتخاب بر اساس کلاس، id یا ویژگیانتخابگر CSS
از BeautifulSoup استفاده می‌کنیدانتخابگر CSS (بدون پشتیبانی XPath)
یافتن عنصر با متنشXPath
رفتن از عنصر به والد یا هم‌نیای پیشینXPath
جدول‌های برچسب و مقدار و فهرست‌های تعریفXPath
برگرداندن مستقیم مقدار ویژگی/@attr در XPath یا ::attr() در parsel
هر دو در یک پروژهparsel
مرورگر headlesslocatorهای Playwright، نخست CSS
پایداری در برابر تغییر طراحیویژگی‌های data-* یا JSON-LD

پرسش‌های متداول

انتخابگر CSS بیاموزم یا XPath؟

نخست انتخابگرهای CSS را بیاموزید؛ کوتاه‌تر و رایج‌ترند و در توسعه وب هم به کار می‌آیند. وقتی در اسکرپینگ به انتخاب بر اساس متن و رفتن به والد نیاز پیدا کردید، آموختن محورها و توابع XPath کافی است. با کتابخانه‌هایی مانند parsel که هر دو را به کار می‌برند، جابه‌جایی آسان است.

آیا BeautifulSoup از XPath پشتیبانی می‌کند؟

نه. BeautifulSoup فقط انتخابگر CSS را پشتیبانی می‌کند. اگر به XPath نیاز دارید، همان HTML را با lxml یا parsel تحلیل کنید.

آیا CSS می‌تواند بر اساس متن انتخاب کند؟

در CSS استاندارد نه. برخی ابزارها افزونه‌های خودشان را دارند: has_text و :has-text() در Playwright و :-soup-contains() در BeautifulSoup. این افزونه‌ها فقط در همان ابزار کار می‌کنند و قابل انتقال نیستند.

چرا XPath کپی‌شده از مرورگر کار نمی‌کند؟

مسیری که مرورگر می‌سازد معمولاً مطلق است و بر صفحه پس از اجرای جاوااسکریپت تکیه دارد. ممکن است آن ساختار در HTML نخستین سرور وجود نداشته باشد یا یک تغییر کوچک طراحی مسیر را بشکند. مسیر را ساده کنید تا از کلاس، id یا ویژگی پایداری آغاز شود.

تفاوت text() و normalize-space() چیست؟

text() گره‌های متنی مستقیم عنصر را برمی‌گرداند و فاصله‌ها را همان‌طور که هستند نگه می‌دارد. normalize-space(.) همه متن عنصر را همراه تگ‌های تودرتو به هم می‌پیوندد، فاصله‌های آغاز و پایان را حذف می‌کند و فاصله‌های درونی را به یکی کاهش می‌دهد. برای مقایسه متن، normalize-space() معمولاً قابل‌اعتمادتر است.

چرا انتخابگرهای من گاهی خالی برمی‌گردند؟

سه دلیل رایج وجود دارد: طراحی صفحه عوض شده، محتوا بعداً با جاوااسکریپت بارگذاری می‌شود یا سایت به‌جای صفحه مورد انتظار صفحه بررسی یا خطا برمی‌گرداند. ذخیره HTML پاسخ در فایل و آزمودن انتخابگر روی همان فایل سریع‌ترین راه جدا کردن این سه حالت است.

خلاصه

انتخابگرهای CSS کوتاه و خوانا هستند و هر ابزاری از آن‌ها پشتیبانی می‌کند؛ برای انتخاب بر اساس تگ، کلاس، id و ویژگی باید گزینه پیش‌فرض باشند. XPath برای کارهایی لازم است که CSS نمی‌تواند، مانند انتخاب بر اساس متن، رفتن به والد و هم‌نیای پیشین و ساختارهای برچسب و مقدار؛ در مرورگرها و lxml به نسخه 1.0 محدود است. در بیشتر کارهای اسکرپینگ تفاوت کارایی کنار زمان شبکه ناچیز است؛ آنچه واقعاً اهمیت دارد نوشتن انتخابگرهایی است که از تغییر طراحی جان سالم به در ببرند. انواع پروکسی متناسب با کار جمع‌آوری داده را در خدمات پروکسی ما می‌یابید.

پرسش از ChatGPTپرسش از Claude