خانه/مقالات/اسکریپینگ با Scrapy و Selenium: راهنمای عملی
سلنیوم
Playwright
برگشت به مقاله‌ها

اسکریپینگ با Scrapy و Selenium: راهنمای عملی

اسکریپینگ با Scrapy و Selenium: راهنمای عملی
این راهنما نحوهٔ ادغام Selenium در پروژه‌های Scrapy را از نصب تا نمونه‌های عملی (wait_time، wait_until، اجرای JS، گرفتن اسکرین‌شات) با توضیحات فنی خط‌به‌خط و نکات بهینه‌سازی پوشش می‌دهد و توصیه‌هایی دربارهٔ مدیریت WebDriver، خطاها و جایگزین‌های مدرن مثل scrapy-playwright ارائه می‌کند.
آسان اسکریپ آسان اسکریپ
1405-04-13

مقدمه

این مقاله به صورت گام‌به‌گام نشان می‌دهد چگونه از Selenium داخل پروژه‌های Scrapy برای اسکریپ کردن صفحات سنگین با جاوااسکریپت استفاده کنید. پس از مطالعهٔ این راهنما شما یاد می‌گیرید چگونه محیط را آماده کنید، در تنظیمات Scrapy ادغام کنید، در اسپایدرها از SeleniumRequest استفاده کنید و روش‌های کنترل مرورگر (انتظارهای هوشمند، کلیک با جاوااسکریپت، اسکرول و گرفتن اسکرین‌شات) را عملی پیاده‌سازی کنید. در پایان نکات performance، امنیت و بهترین روش‌ها را خواهید دید.

نصب و پیش‌نیازها

برای شروع دو جز ضروری دارید: خودِ پکیج scrapy-selenium (یا در نظر داشته باشید که scrapy-playwright جایگزین مدرن و فعال‌تری است) و یک WebDriver سازگار (مثل ChromeDriver یا GeckoDriver). نسخهٔ پایتون 3.6 یا بالاتر توصیه می‌شود.

pip install scrapy-selenium

نکته: اگر نمی‌خواهید WebDriver را دستی مدیریت کنید، می‌توانید از ابزارهایی مثل webdriver-manager یا بسته‌های مشابه برای دانلود خودکار استفاده کنید؛ اما در این راهنما مثال‌ها با فرض نصب دستی آورده شده‌اند.

نصب ChromeDriver و ساختار پروژه

نسخهٔ ChromeDriver باید با نسخهٔ مرورگر شما همخوانی داشته باشد. عدد نسخهٔ مرورگر را از بخش About مرورگر پیدا کنید و نسخهٔ متناظر ChromeDriver را دانلود کنید. فایل اجرایی را بهتر است خارج از کنترل نسخه نگه دارید یا در مسیر مشخصی که از متغیر محیطی یا تنظیمات پروژه خوانده می‌شود قرار دهید.

├── scrapy.cfg
├── chromedriver.exe  ## ← پیشنهادی در مثال محلی
└── myproject
    ├── __init__.py
    ├── items.py
    ├── middlewares.py
    ├── pipelines.py
    ├── settings.py
    └── spiders
        └── __init__.py

به‌جای قرار دادن chromedriver در رپو، گزینهٔ بهتر نگهداری در یک پوشهٔ جدا یا استفاده از which برای یافتن مسیر اجرایی است.

یکپارچه‌سازی در settings.py

در settings.py باید نام درایور، مسیر اجرایی و آرگومان‌های مربوط به headless/argumentها را تعریف کنید و Middleware مربوطه را فعال نمایید. مثال برای Chrome:

# settings.py (برای Chrome)
from shutil import which

SELENIUM_DRIVER_NAME = 'chrome'
SELENIUM_DRIVER_EXECUTABLE_PATH = which('chromedriver')
SELENIUM_DRIVER_ARGUMENTS = ['--headless', '--no-sandbox', '--disable-dev-shm-usage']

DOWNLOADER_MIDDLEWARES = {
    'scrapy_selenium.SeleniumMiddleware': 800,
}

برای Firefox کافی است نام درایور و executable را به 'firefox' و 'geckodriver' تغییر دهید و آرگومان‌های مناسب را تنظیم کنید.

توضیح فیلدها:

  • SELENIUM_DRIVER_NAME: نوع درایور ('chrome' یا 'firefox').
  • SELENIUM_DRIVER_EXECUTABLE_PATH: مسیر اجرایی WebDriver (تابع which مسیر را پیدا می‌کند).
  • SELENIUM_DRIVER_ARGUMENTS: آرگومان‌های خط فرمان برای مرورگر (headless، غیرفعال‌سازی GPU، و غیره).
  • DOWNLOADER_MIDDLEWARES: فعال‌سازی Middleware که درخواست‌ها را به Selenium می‌فرستد.

به‌روزرسانی اسپایدرها: استفاده از SeleniumRequest

برای رندر صفحه توسط Selenium باید به‌جای scrapy.Request از SeleniumRequest استفاده کنید. مثال سادهٔ اسپایدری که نقل‌قول‌ها را از صفحهٔ جاوااسکریپتی می‌خواند:

# spider.py
import scrapy
from scrapy_selenium import SeleniumRequest
from quotes_js_scraper.items import QuoteItem

class QuotesSpider(scrapy.Spider):
    name = 'quotes'

    def start_requests(self):
        url = 'https://quotes.toscrape.com/js/'
        yield SeleniumRequest(url=url, callback=self.parse)

    def parse(self, response):
        for quote in response.css('div.quote'):
            item = QuoteItem()
            item['text'] = quote.css('span.text::text').get()
            item['author'] = quote.css('small.author::text').get()
            item['tags'] = quote.css('div.tags a.tag::text').getall()
            yield item

توضیح رفتار کد:

  • ورودی: URL صفحهٔ هدف.
  • خروجی: آیتم‌های استخراج‌شده (متن، نویسنده، برچسب‌ها).
  • نقش SeleniumRequest: قبل از بازگرداندن پاسخ، مرورگر را باز و صفحه را رندر می‌کند و HTML رندرشده را به Scrapy می‌دهد.

کنترل رفتار مرورگر: waiting، کلیک، اسکرین‌شات

سخت‌ترین بخش اسکریپینگ صفحات جاوااسکریپتی مدیریت زمان و همگام‌سازی است. بهتر است از انتظارهای صریح (explicit waits) استفاده کنید تا از خواب‌های ثابت و اضافی جلوگیری شود.

انتظار بر اساس زمان (wait_time)

می‌توانید با پارامتر wait_time به Selenium زمان مشخصی برای صبر کردن پس از لود اولیه بدهید. این روش ساده است اما مؤثر نیست اگر صفحه سریع یا بسیار کند باشد.

# استفاده از wait_time
yield SeleniumRequest(url=url, callback=self.parse, wait_time=10)

در این حالت Selenium تا 10 ثانیه پس از لود اولیه منتظر می‌ماند تا احتمالا داده‌ها بارگذاری شوند.

انتظار بر اساس عنصر (wait_until)

روش بهتر استفاده از wait_until با expected_conditions است. حتماً همراه با wait_time از آن استفاده کنید تا در صورت عدم ظهور عنصر، درخواست معلق نماند.

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC

yield SeleniumRequest(
    url=url,
    callback=self.parse,
    wait_time=10,
    wait_until=EC.element_to_be_clickable((By.CLASS_NAME, 'quote'))
)

توضیح: این ترکیب به Selenium می‌گوید حداکثر تا 10 ثانیه برای قابل کلیک شدن المان با کلاس 'quote' صبر کند؛ در صورتی که پیش از زمان مقرر المان آماده شود، ادامه سریع انجام خواهد شد.

اجرای جاوااسکریپت و کلیک

گاهی لازم است قبل از خواندن HTML، اسکریپتی اجرا کنید (مثلاً کلیک روی دکمهٔ next). می‌توانید با پارامتر script جاوااسکریپت را اجرا کنید.

# کلیک با اجرای JS
yield SeleniumRequest(
    url=url,
    callback=self.parse,
    script="document.querySelector('.pager .next > a').click()",
    wait_time=5,
)

اگر نیاز به تعامل پیچیده‌تر دارید می‌توانید در تابع parse به درایور دسترسی پیدا کنید:

def parse(self, response):
    driver = response.meta.get('driver')  # webdriver instance
    # مثال: اجرای اسکریپت مستقیم با درایور
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    # سپس از response برای استخراج استفاده کنید.

نکته: دسترسی به driver اجازهٔ اجرای عملیات خاص و سریع‌تر را می‌دهد، اما مراقب مدیریت منابع و بستن درایورها باشید.

گرفتن اسکرین‌شات

برای دیباگ یا نگهداری تصویری از صفحهٔ رندرشده می‌توانید از پارامتر screenshot=True استفاده کنید؛ دادهٔ تصویر به صورت بایت در response.meta['screenshot'] قرار می‌گیرد.

yield SeleniumRequest(url=url, callback=self.parse, screenshot=True)

# در parse
with open('image.png', 'wb') as f:
    f.write(response.meta['screenshot'])

توضیح: خروجیِ response.meta['screenshot'] باینری PNG است؛ آن را به‌صورت باینری در فایل می‌نویسیم.

خطاها، تایم‌اوت‌ها و بازیابی مجدد (retries)

در محیط‌های واقعی باید خطاها را مدیریت کنید: زمان‌های طولانی لود، کرش درایور، یا محدودیت‌های شبکه. توصیه‌ها:

  • برای هر SeleniumRequest از wait_time منطقی استفاده کنید و در صورت نیاز از مکانیزم retry Scrapy بهره ببرید.
  • در middleware ترتیب را طوری تنظیم کنید که SeleniumMiddleware قبل یا بعد از middlewareهای پروکسی درست کار کند (ترتیب عددی مهم است).
  • از try/except در parse برای مدیریت استثناهای احتمالی و پاکسازی منابع استفاده کنید.
def parse(self, response):
    try:
        # استخراج داده
        ...
    except Exception as e:
        self.logger.error('parse error: %s', e)
        # در صورت لزوم بازگشت به URL برای retry
        yield SeleniumRequest(response.url, callback=self.parse)

مزایا و معایب استفاده از scrapy-selenium

  • مزایا:
    • دسترسی کامل به یک مرورگر واقعی (Selenium API).
    • قابلیت اجرای جاوااسکریپت پیچیده و تعامل با صفحه.
  • معایب:
    • مصرف منابع بالا (هر درایور یک مرورگر دارد).
    • راه‌اندازی و مدیریت WebDriver می‌تواند خطاپذیر باشد (نسخه‌ها باید تطابق داشته باشند).
    • پروژهٔ scrapy-selenium ممکن است در برخی مواقع کمتر به‌روز باشد؛ scrapy-playwright گزینهٔ فعال‌تری است که قابلیت‌های قوی‌تر و مدیریت خودکار browser binaries ارائه می‌کند.

بهینه‌سازی عملکرد و نکات امنیتی

چند نکتهٔ عملی برای بهبود پایداری و کارایی:

  • اگر فقط تعداد محدودی از صفحات نیاز به رندر دارند، فقط برای همان درخواست‌ها از SeleniumRequest استفاده کنید و بقیه را با درخواست‌های معمولی Scrapy بگیرید.
  • از انتظارهای صریح (wait_until) به جای sleeps ثابت استفاده کنید.
  • در محیط پراکسی یا با IP های مختلف کار کنید تا از بلاک شدن جلوگیری شود.
  • اطمینان حاصل کنید که اطلاعات حساس (مثل توکن‌ها) به صورت ایمن مدیریت شوند و خروجی‌های خام صفحهٔ رندرشده ذخیرهٔ تصادفی نداشته باشند.
  • در محیط تولید، از محدودسازی همزمانی (CONCURRENT_REQUESTS) و صف‌بندی مناسب استفاده کنید تا سربار RAM/CPU کاهش یابد.

مثال تکمیلی: ترکیب چند تکنیک

مثال زیر نشان می‌دهد چگونه می‌توان منتظر المان شد، اسکرول کرد، و سپس اسکرین‌شات گرفت:

yield SeleniumRequest(
    url=url,
    callback=self.parse_after_actions,
    wait_time=10,
    wait_until=EC.presence_of_element_located((By.CSS_SELECTOR, 'div.quote')),
    screenshot=True
)

def parse_after_actions(self, response):
    driver = response.meta.get('driver')
    # اسکرول به پایین برای بارگذاری محتواهای تنبل (lazy-loaded)
    driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
    # ذخیرهٔ اسکرین‌شات
    with open('final.png', 'wb') as f:
        f.write(response.meta['screenshot'])
    # استخراج داده‌ها از response
    for q in response.css('div.quote'):
        yield {
            'text': q.css('span.text::text').get(),
            'author': q.css('small.author::text').get()
        }

جمع‌بندی

ادغام Selenium با Scrapy ابزار قدرتمندی برای اسکریپینگ صفحات جاوااسکریپتی فراهم می‌کند اما باید با احتیاط از آن استفاده کنید: مدیریت WebDriver، انتظارهای هوشمند، و اصلاح سیاست‌های concurrency و retry اهمیت بالایی دارند. اگر به دنبال گزینه‌ای مدرن‌تر و نگهداری‌شده‌تر هستید، scrapy-playwright را بررسی کنید؛ در غیر این صورت با رعایت بهترین روش‌ها می‌توانید از scrapy-selenium به خوبی بهره ببرید.

مطالب مرتبط

مقاله‌های مرتبط