پس از دانلود صفحه، کار اصلی اسکرپینگ آغاز میشود: یافتن و استخراج نام محصول، قیمت و پیوند از میان صدها تگ. اینکه کدام عنصر را میخواهید با یک انتخابگر توصیف میکنید. دو زبان رایج وجود دارد: انتخابگرهای 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/@srctext()گره متنی://h2/text()- محورها:
parent::،ancestor::،following-sibling::،preceding-sibling:: - توابع:
contains()،starts-with()،normalize-space()،last()،string-length()
در مرورگر، XPath با تابع جاوااسکریپت document.evaluate() اجرا میشود؛ شیوه کاربرد آن در مستندات MDN آمده است.
مقایسه نحو
| معیار | انتخابگر CSS | XPath |
|---|---|---|
| خوانایی | کوتاه، آشنا برای توسعهدهندگان وب | بلند، منحنی یادگیری تندتر |
| جهت | رو به پایین و جلو (با :has() تا حدی رو به بیرون) | هر جهت: والد، همنیای پیشین، نیا |
| انتخاب بر اساس متن | در استاندارد نیست | دارد: text()، contains() |
| برگرداندن مقدار ویژگی | نه (با افزونه کتابخانهها) | دارد: /@href |
| برگرداندن گره متنی | نه (با افزونه کتابخانهها) | دارد: /text() |
| تطبیق کلاس | .product دقیق و کوتاه | contains(@class, ...) دقت لازم دارد |
| پشتیبانی مرورگر | querySelectorAll | document.evaluate با XPath 1.0 |
| پشتیبانی پایتون | BeautifulSoup، lxml با cssselect، parsel | lxml، parsel؛ BeautifulSoup پشتیبانی نمیکند |
| مرورگر headless | Playwright، Puppeteer، Selenium | Playwright، Puppeteer، Selenium |
| کاربرد رایج | استخراج فهرست بر اساس کلاس و ویژگی | جدولهای برچسب و مقدار، انتخاب متنی |
جدول مرجع سریع
هر سطر جدول زیر یک انتخاب یکسان در دو زبان است. در سطرهایی که معادل CSS ندارند، کارهای اضافه XPath آمده است.
| # | چه انتخاب میشود؟ | انتخابگر CSS | XPath |
|---|---|---|---|
| 1 | همه عنصرهای div | div | //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 نه
انتخاب بر اساس متن
در صفحه یک فروشگاه اینترنتی، دکمه افزودن به سبد و دکمه ناموجود ممکن است کلاس یکسانی داشته باشند. تنها چیزی که آنها را از هم جدا میکند متنشان است:
//button[text()="Add to cart"]
//span[contains(normalize-space(.), "Discount")]text() فقط به گره متنی مستقیم عنصر نگاه میکند؛ اگر متن میان تگهای تودرتو پخش شده باشد تطبیق ندارد. normalize-space(.) همه متن عنصر را به هم میپیوندد و فاصلههای آغاز و پایان را حذف میکند، پس معمولاً قابلاعتمادتر است.
CSS استاندارد انتخاب بر اساس متن ندارد. برخی ابزارها افزونههای خودشان را دارند: گزینه has_text و :has-text() در Playwright و :-soup-contains() در BeautifulSoup. این افزونهها فقط در همان ابزار کار میکنند.
رفتن به والد و همنیای پیشین
اگر در کارت محصول فقط فیلد قیمت کلاس پایداری دارد، باید از قیمت آغاز کنید و به کل کارت برسید:
//span[@class="price"]/..
//span[@class="price"]/ancestor::div[@data-sku][1].. والد مستقیم و ancestor:: نیایی در هر سطح بالاتر را انتخاب میکند. [1] نزدیکترین نیا را برمیدارد.
ساختارهای برچسب و مقدار
جدولهای مشخصات محصول و فهرستهای تعریف از رایجترین ساختارهاییاند که اسکرپینگ با آنها روبهرو میشود. اینکه کدام سطر با چه ترتیبی بیاید از محصولی به محصول دیگر فرق میکند؛ آنچه ثابت میماند متن برچسب است:
//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 پشتیبانی ندارد:
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() نتیجه را بهصورت متن برمیگرداند:
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 استفاده کنید:
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 میداند:
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 |
| مرورگر headless | locatorهای 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 محدود است. در بیشتر کارهای اسکرپینگ تفاوت کارایی کنار زمان شبکه ناچیز است؛ آنچه واقعاً اهمیت دارد نوشتن انتخابگرهایی است که از تغییر طراحی جان سالم به در ببرند. انواع پروکسی متناسب با کار جمعآوری داده را در خدمات پروکسی ما مییابید.




