خانه/مقالات/اسکریپینگ: مقایسه Scrapy و Pyppeteer
استخراج داده
پروکسی و چرخش IP
Playwright
برگشت به مقاله‌ها

اسکریپینگ: مقایسه Scrapy و Pyppeteer

اسکریپینگ: مقایسه Scrapy و Pyppeteer
این مقاله با لحن آموزشی مقایسه‌ای عملی بین اسکریپینگ با Scrapy و Pyppeteer ارائه می‌دهد: چه‌وقت هر کدام را انتخاب کنیم، چگونه راه‌اندازی و کدنویسی کنیم، نمونه‌های عملی پایتون برای هر ابزار و نکات فنی درباره همزمانی، مدیریت خطا، پراکسی و ذخیره‌سازی. پس از خواندن، می‌توانید ابزار مناسب را براساس نیازهای عملکردی و ساختاری پروژه‌تان انتخاب و پیاده‌سازی کنید.
آسان اسکریپ آسان اسکریپ
1405-05-23

مقدمه

در این مقاله مقایسه‌ای عملی و فنی بین دو رویکرد رایج در اسکریپینگ پایتون داریم: فریم‌ورک مبتنی بر HTTP و همزمانی بالا (Scrapy) و کنترل مرورگر واقعی برای صفحات سنگین JavaScript (Pyppeteer). در پایان این نوشته شما می‌دانید هر ابزار برای چه سناریویی مناسب است، چگونه آن‌ها را راه‌اندازی و بهره‌برداری کنید، و نمونه‌های کد عملی برای هر کدام خواهید داشت.

چیست Pyppeteer؟

Pyppeteer پورت پایتونی کتابخانه Puppeteer است که از طریق پروتکل DevTools مرورگر Chrome/Chromium را کنترل می‌کند. نکته کلیدی: Pyppeteer یک محیط مرورگر کامل را شبیه‌سازی می‌کند و برای صفحات با رندر سمت مشتری (SPA، AJAX، WebSocket) مناسب است.

مزایا و معایب Pyppeteer

  • مزایا:
    • پردازش کامل JavaScript و دریافت محتوای رندرشده.
    • اتماتیک‌سازی تعاملات: کلیک، فرم‌ها، اسکرول و گرفتن اسکرین‌شات/PDF.
    • پشتیبانی از سشن کامل مرورگر (کوکی‌ها، LocalStorage) و امکان فعال‌سازی حالت stealth برای کاهش شناسایی توسط سایت‌ها.
  • معایب:
    • مصرف بالای منابع و کندتر بودن نسبت به درخواست‌های خالص HTTP.
    • پیچیدگی بیشتر در مدیریت همزمانی و فرآیندهای مرورگر.
    • در زمان نگارش این متن پروژه پیپیتیر ممکن است به‌روز‌رسانی فعال نداشته باشد؛ برای پروژه‌های تولیدی بررسی وضعیت نگهداری ضروری است.

راه‌اندازی و مثال پایه Pyppeteer

نصب با pip ساده است:

pip install pyppeteer

مثال ساده برای باز کردن صفحه و بستن آن:

import asyncio
from pyppeteer import launch

async def main():
    browser = await launch(headless=True)
    page = await browser.newPage()
    await page.goto('https://example.com')
    await browser.close()

asyncio.run(main())

توضیح: ورودی این اسکریپ هیچ ورودی خارجی نیاز ندارد؛ خروجی هم صرفاً عمل مرور و بستن مرورگر است. مرحله‌بندی:

  1. ایجاد و راه‌اندازی مرورگر با launch.
  2. ایجاد یک صفحه با newPage.
  3. رفتن به URL با goto و سپس بستن مرورگر.

نکات عملی: از setRequestInterception برای فیلتر کردن منابع سنگین (مثل تصاویر) و از Semaphore برای محدود کردن تعداد تب‌های همزمان استفاده کنید تا منابع کنترل شوند.

چیست Scrapy؟

Scrapy یک فریم‌ورک کامل برای خزش و استخراج داده است که به‌صورت غیرهمزمان بر پایه Twisted کار می‌کند. مناسب پروژه‌های بزرگ، مقیاس‌پذیر و وقتی سرعت و مدیریت همزمانی مهم است.

مزایا و معایب Scrapy

  • مزایا:
    • عملکرد بالا با همزمانی داخلی، مدیریت نرخ درخواست و صف‌بندی.
    • معماری ساخت‌یافته: spiders، items، pipelines و middlewares.
    • خروجی‌ها و ذخیره‌سازی (JSON/CSV/XML/DB) و جامعهٔ بزرگ و منابع آموزشی فراوان.
  • معایب:
    • اجرای JavaScript به‌صورت پیش‌فرض ندارد (برای صفحات JS محور نیاز به ادغام با headless browser یا Splash/Playwright دارید).
    • پیچیدگی اولیه برای آشنایان تازه‌کار نسبت به اسکریپت‌های ساده.

راه‌اندازی و مثال پایه Scrapy

نصب:

pip install scrapy

ایجاد پروژه و ساختار پایه:

scrapy startproject myproject

نمونه کوتاه برای تعریف Item و یک Spider که نقل‌قول‌ها را از یک سایت نمونه استخراج می‌کند:

# items.py
import scrapy

class QuoteItem(scrapy.Item):
    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()

# spiders/quotes_spider.py
import scrapy
from myproject.items import QuoteItem

class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    start_urls = ['http://quotes.toscrape.com/page/1/']

    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

        next_page = response.css('li.next a::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

توضیح: ورودی این اسپایدر URLهای شروع است؛ خروجی مجموعهٔ آیتم‌هایی با فیلدهای تعریف‌شده. Scrapy به‌صورت خودکار صفحات را به‌صورت همزمان می‌گیرد، آیتم‌ها را به pipeline می‌فرستد و می‌توانید خروجی را با دستور scrapy crawl quotes ذخیره کنید.

مقایسه فنی و نکات عملی

  • اجرا و JavaScript: اگر صفحه به JS وابسته است از Pyppeteer (یا ادغام Scrapy با headless browser) استفاده کنید؛ برای صفحات استاتیک Scrapy سریع‌تر و سبک‌تر است.
  • همزمانی: Scrapy (Twisted) برای هزاران درخواست موازی بهتر ساخته شده؛ Pyppeteer با asyncio قابل موازی‌سازی است اما هر تب/مرورگر منابع قابل‌توجهی مصرف می‌کند.
  • ذخیره‌سازی داده: Scrapy خروجی‌ها و pipelineها را دارد؛ در Pyppeteer باید خودتان ذخیره‌سازی را پیاده‌سازی کنید (مثلاً نوشتن JSON، ارسال به DB یا صف پیام).
  • پراکسی و مدیریت هویت: هر دو از پراکسی پشتیبانی می‌کنند؛ Scrapy ادغام پراکسی و روتیشن را ساده‌تر می‌کند. در Pyppeteer معمولاً پراکسی را در آرگومان‌های راه‌اندازی مرورگر یا با پارامتر --proxy-server تنظیم می‌کنید.
  • پایداری و نگهداری: Scrapy جامعه بزرگتری دارد؛ در انتخاب Pyppeteer به وضعیت نگهداری پروژه توجه کنید و در صورت نیاز از کپی‌های فعالتر (مثل Playwright پایتون) بررسی کنید.
  • امنیت/قوانین: همیشه فایل robots.txt را بررسی کرده و سیاست‌های سایت و قوانین محلی را رعایت کنید. اسکریپینگ خودکار می‌تواند باعث بار اضافی سرورها شود؛ نرخ درخواست‌ها را محدود و از هدرهای صحیح (User-Agent و Referer) استفاده کنید.

مطالعه موردی: اسکریپینگ محصولات آمازون — مقایسه پیاده‌سازی

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

نمونه مختصر با Scrapy

# spider مختصر برای آمازون (نمونه آموزشی)
import scrapy
from urllib.parse import urljoin

class AmazonSpider(scrapy.Spider):
    name = 'amazon_sample'
    start_urls = ['https://www.amazon.com/s?k=jeans']

    custom_settings = {
        'CONCURRENT_REQUESTS': 3,
        'DOWNLOAD_DELAY': 1,
        'ROBOTSTXT_OBEY': True,
    }

    def parse(self, response):
        product_links = response.css('h2 a::attr(href)').getall()
        for rel in product_links:
            url = urljoin('https://www.amazon.com/', rel.split('?')[0])
            yield response.follow(url, self.parse_product)

        next_page = response.css('li.a-last a::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

و تابع استخراج محصول:

def parse_product(self, response):
    name = response.css('span#productTitle::text').get()
    price = response.css('.a-price-whole::text').get() or response.css('#priceblock_dealprice::text').get()
    yield {'product_name': name.strip() if name else 'not available',
           'product_price': price.strip() if price else 'not available'}

توضیح: در Scrapy شما از CSS/XPath برای استخراج استفاده می‌کنید؛ پیاده‌سازی pagination و follow ساده و مقرون‌به‌صرفه و مدیریت concurrency داخلی باعث کارایی بالا می‌شود.

نمونه مختصر با Pyppeteer

import asyncio
from pyppeteer import launch

async def extract_product_details(browser, url, sem):
    async with sem:
        page = await browser.newPage()
        await page.setRequestInterception(True)
        page.on('request', lambda req: asyncio.create_task(req.continue_({}) if req.resourceType != 'image' else req.abort()))
        try:
            await page.goto(url, waitUntil='domcontentloaded', timeout=120000)
            await page.waitForSelector('span#productTitle', timeout=60000)
            name = await page.evaluate('() => document.querySelector("span#productTitle").innerText')
            price = await page.evaluate('() => (document.querySelector(".a-price-whole")||{}).innerText')
            return {'name': name.strip(), 'price': price.strip()}
        except Exception as e:
            print('Error:', e)
            return {'name': 'not available', 'price': 'not available'}
        finally:
            await page.close()

async def main():
    browser = await launch(headless=True)
    page = await browser.newPage()
    all_urls = []
    for p in range(1, 3):
        await page.goto(f'https://www.amazon.com/s?k=jeans&page={p}')
        links = await page.querySelectorAll('h2 a.a-link-normal')
        urls = [await page.evaluate('(el)=>el.href', el) for el in links]
        all_urls.extend(urls)
    await page.close()

    sem = asyncio.Semaphore(3)
    tasks = [extract_product_details(browser, u, sem) for u in all_urls]
    products = await asyncio.gather(*tasks)
    print(products)
    await browser.close()

asyncio.run(main())

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

مقایسه سریع در عمل

  • سرعت: Scrapy معمولاً سریع‌تر است (خالص HTTP، بدون رندر کامل).
  • دقت محتوای JS: Pyppeteer بهتر است چون محتوای رندرشده را می‌بیند.
  • مقیاس‌پذیری: Scrapy برای پروژه‌های بزرگ آماده‌تر است؛ Pyppeteer برای مقیاس بزرگ هزینهٔ محاسباتی و زیرساختی بالایی می‌طلبد.
  • ذخیره‌سازی و pipeline: Scrapy دارای ابزارهای آماده است؛ Pyppeteer نیاز به پیاده‌سازی دستی دارد.

نکات امنیتی، قانونی و بهترین روش‌ها

  • قوانین و حقوق: پیش از اسکریپینگ قوانین سایت، فایل robots.txt و شرایط خدمات را بررسی کنید.
  • محدودیت نرخ درخواست: نرخ و تاخیر را تنظیم کنید تا از بلوک شدن جلوگیری شود.
  • پردازش خطا: هم در Scrapy و هم در Pyppeteer از retry، timeouts و بلوک try/except استفاده کنید تا منابع آزاد شوند.
  • پراکسی و هدرها: برای جلوگیری از بلاک شدن از روتویشن پراکسی و هدرهای معتبر استفاده کنید، اما مراقب قوانین استفاده از پراکسی‌ها باشید.
  • مدیریت منابع: در Pyppeteer حتماً صفحات و مرورگرها را در finally ببندید و از temp userDataDir استفاده‌شده را پاک کنید.

جمع‌بندی و توصیه‌های عملی

اگر هدف شما جمع‌آوری مقدار زیادی داده از صفحات نسبتاً استاتیک یا زمانی که سرعت و مقیاس‌پذیری مهم است، Scrapy معمولاً انتخاب بهتر است. اگر با SPAها یا صفحات سنگین JavaScript سروکار دارید و نیاز به تعامل کاربر-مانند (کلیک، فرم، اسکرین‌شات) دارید، Pyppeteer (یا راه‌حل‌های مشابه مثل Playwright) گزینه مناسبی است.

توصیه نهایی:

  • برای پروژه‌های تولیدی و مقیاس‌پذیر: Scrapy و استفاده از middlewares و pipelines.
  • برای تعامل با وب‌اپ‌های پیچیده یا گرفتن رندر کامل: Pyppeteer با مدیریت دقیق همزمانی و منابع.
  • همیشه جوانب قانونی و بار سرور را در نظر بگیرید و از روش‌های مودبانهٔ اسکریپینگ استفاده کنید.
مطالب مرتبط

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