کتاب آنلاین رایگان • مقدماتی تا پیشرفته

آموزش جامع Scrapy به فارسی

از نصب و اولین درخواست تا ساخت خزنده‌ای سریع، قابل‌اعتماد و قابل‌نگهداری؛ همراه مثال واقعی و پروژه کامل.

۳۲فصل
۵۵+مثال
۱پروژه کامل
رایگاندسترسی
+
فصل ۲۴

مرجع کاربردی دستورات Scrapy

فرمان‌ها به دو گروه سراسری و وابسته به پروژه تقسیم می‌شوند. برای جزئیات هر فرمان scrapy command -h را اجرا کنید.

فرمانکاربرد
crawlاجرای Spider پروژه
runspiderاجرای یک فایل Spider بدون پروژه
fetch / viewمشاهده پاسخ از دید Downloader
parseاجرای callback و نمایش Item و Request
settingsمشاهده مقدار مؤثر تنظیمات
benchسنجش سریع توان Scrapy روی سیستم
scrapy fetch --headers https://example.com/
scrapy parse https://example.com/product/1 -c parse_product
scrapy settings --get DOWNLOAD_DELAY
scrapy runspider standalone.py -O data.json
scrapy version -v
فصل ۲۵

Cookie، Session و چند حساب

CookiesMiddleware کوکی‌های هر session را نگه می‌دارد. برای sessionهای مستقل از cookiejar متفاوت استفاده کنید و همان مقدار را در درخواست‌های بعدی منتقل کنید.

yield scrapy.FormRequest.from_response(
    response,
    formdata={"username": user, "password": password},
    callback=self.after_login,
    meta={"cookiejar": user_id},
)

yield response.follow(
    "/account/orders",
    callback=self.parse_orders,
    meta={"cookiejar": response.meta["cookiejar"]},
)
نکته امنیتی

مقدار Cookie را log نکنید. برای غیرفعال‌کردن ادغام Cookie از dont_merge_cookies استفاده کنید، نه دستکاری دستی هدر در همه درخواست‌ها.

فصل ۲۶

Spider Contracts و تست callback

Contractها آزمون‌های سبک داخل docstring هستند. URL نمونه، تعداد Item/Request و فیلدهای مورد انتظار را تعریف می‌کنند و با scrapy check اجرا می‌شوند.

def parse_product(self, response):
    """
    @url https://example.com/product/1
    @returns items 1 1
    @returns requests 0 0
    @scrapes name price url
    """
    yield {
        "name": response.css("h1::text").get(),
        "price": response.css(".price::text").get(),
        "url": response.url,
    }
scrapy check
scrapy check products
scrapy check -l

Contract جای تست واحد و fixtureهای آفلاین را نمی‌گیرد؛ برای کنترل سریع قرارداد خروجی callback مناسب است.

فصل ۲۷

Add-onها و توسعهٔ قابل‌نصب

Add-on می‌تواند چند component و تنظیم مرتبط را یک‌جا فعال کند. این روش از الزام کاربر به کپی‌کردن چند تنظیم پراکنده جلوگیری می‌کند.

# settings.py
ADDONS = {
    "my_package.addon.AnalyticsAddon": 500,
}

# داخل پکیج
class AnalyticsAddon:
    def update_settings(self, settings):
        settings.setdict(
            {"EXTENSIONS": {"my_package.ext.Analytics": 500}},
            priority="addon",
        )

قبل از نصب افزونه شخص ثالث، سازگاری نسخه، مجوز، فعالیت مخزن و دسترسی آن به Request، Cookie و خروجی را بررسی کنید.

فصل ۲۸

DevTools و محتوای پویا

در تب Network مرورگر، درخواست XHR/Fetch حامل داده را پیدا کنید. سپس URL، method، query، payload و هدرهای ضروری را بازسازی کنید. درخواست «Copy as cURL» نقطه شروع است، اما هدرهای غیرضروری را حذف کنید.

  1. صفحه را با Network باز و فیلتر Fetch/XHR را فعال کنید.
  2. عملی که داده را بارگذاری می‌کند انجام دهید.
  3. Response و Preview درخواست را بررسی کنید.
  4. pagination، cursor یا payload را پیدا کنید.
  5. همان endpoint را در Scrapy Shell آزمایش کنید.
yield scrapy.Request(
    "https://example.com/api/search?page=1",
    headers={"Accept": "application/json"},
    callback=self.parse_api,
)

def parse_api(self, response):
    data = response.json()
    yield from data["results"]

اگر داده داخل JavaScript صفحه جاسازی شده، ابتدا JSON یا متغیر موردنظر را استخراج کنید؛ اجرای مرورگر آخرین گزینه است.

فصل ۲۹

عیب‌یابی حافظه و Benchmark

افزایش حافظه همیشه leak نیست؛ صف Request، cache و Itemهای درحال‌پردازش هم حافظه مصرف می‌کنند. اگر پس از ثابت‌شدن workload حافظه همچنان رشد کرد، referenceها را بررسی کنید.

# سنجش پایه بدون منطق پروژه
scrapy bench

# آمار حافظه
scrapy crawl products -s MEMUSAGE_ENABLED=True

# در Telnet Console توسعه
prefs()
prefs(MySpider)
  • Response یا Selector را در collection سراسری نگه ندارید.
  • Requestهای صف‌شده و عمق Crawl را در آمار ببینید.
  • Pipeline کند می‌تواند Itemهای معلق زیادی بسازد.
  • قبل و بعد از هر بهینه‌سازی یک benchmark قابل‌تکرار بگیرید.
فصل ۰۱

نقشه راه و پیش‌نیازها

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

پیش‌نیاز

پایتون مقدماتی و آشنایی با HTML کافی است؛ XPath و مفاهیم async را در مسیر یاد می‌گیرید.

۱بررسی HTML
۲تست در Shell
۳ساخت Spider
۴ذخیره داده

چه زمانی Scrapy لازم نیست؟

برای یک صفحه کوچک شاید requests و BeautifulSoup کافی باشد. اگر API رسمی وجود دارد ابتدا همان را بررسی کنید. Scrapy برای صفحات زیاد، پیمایش و اجرای دوره‌ای مناسب است.

فصل ۰۲

معماری Scrapy به زبان ساده

Engine هماهنگ‌کننده است. درخواست از Spider به Scheduler می‌رود، Downloader صفحه را می‌گیرد، پاسخ به Spider می‌رسد و داده وارد Pipeline می‌شود. Middlewareها رفتار مسیر را تغییر می‌دهند.

SpiderEngineSchedulerDownloaderداده ← Pipeline
مدل ذهنی

شما نتیجه را yield می‌کنید و Scrapy زمان‌بندی را انجام می‌دهد.

فصل ۰۳

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

python -m venv .venv
source .venv/bin/activate  # macOS / Linux
.venv\Scripts\Activate.ps1 # Windows
python -m pip install --upgrade pip
python -m pip install scrapy
scrapy version
scrapy startproject shopbot
cd shopbot
scrapy genspider products example.com
scrapy list
shopbot/
  • settings.py — تنظیمات
  • items.py — مدل داده
  • pipelines.py — پردازش
  • spiders/ — خزنده‌ها
فصل ۰۴

قبل از کدنویسی: Scrapy Shell

تا وقتی انتخابگر در Shell درست کار نکرده، آن را وارد Spider نکنید.

scrapy shell "https://quotes.toscrape.com/"
response.status
response.css("title::text").get()
response.css("div.quote").getall()[:2]
view(response)

view(response) پاسخ واقعی دریافتی Scrapy را نشان می‌دهد؛ این پاسخ ممکن است با DOM ساخته‌شده توسط JavaScript فرق کند.

فصل ۰۵

ساخت اولین Spider

import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    allowed_domains = ["quotes.toscrape.com"]
    start_urls = ["https://quotes.toscrape.com/"]

    def parse(self, response):
        for quote in response.css("div.quote"):
            yield {
                "text": quote.css("span.text::text").get(),
                "author": quote.css("small.author::text").get(),
                "tags": quote.css("a.tag::text").getall(),
            }
        next_url = response.css("li.next a::attr(href)").get()
        if next_url:
            yield response.follow(next_url, callback=self.parse)
scrapy crawl quotes -O quotes.json:json

-O فایل قبلی را بازنویسی می‌کند؛ -o به خروجی موجود می‌افزاید.

فصل ۰۶

استخراج دقیق با CSS و XPath

هدفCSSXPath
عنوانtitle::text//title/text()
لینک‌هاa::attr(href)//a/@href
محصولarticle.product//article[contains(@class,'product')]
card = response.css("article.product")[0]
name = card.css("h2::text").get(default="").strip()
price = card.css("[data-price]::attr(data-price)").get()
links = response.css("a::attr(href)").getall()
absolute = response.urljoin(links[0])

برای مباحث پیشرفته، راهنمای جامع XPath را ببینید.

فصل ۰۷

Request، callback و پیمایش

داده مخصوص callback بعدی را با cb_kwargs و کنترل‌های فنی را با meta بفرستید.

def parse(self, response):
    for card in response.css(".product-card"):
        yield response.follow(
            card.css("a::attr(href)").get(),
            callback=self.parse_product,
            cb_kwargs={"category": "books"},
            errback=self.handle_error,
        )

def parse_product(self, response, category):
    yield {"category": category, "url": response.url}
دام رایج

کل response.meta را کپی نکنید؛ تعداد Retry ممکن است ناخواسته منتقل شود.

فصل ۰۸

Item و Item Loader

مدل داده قرارداد بین Spider و Pipeline است.

import scrapy
class ProductItem(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    currency = scrapy.Field()
    url = scrapy.Field()

item = ProductItem()
item["name"] = response.css("h1::text").get()
item["url"] = response.url
yield item

Item Loader برای انتخابگرهای جایگزین و پردازشگر ورودی/خروجی مفید است.

فصل ۰۹

Pipeline: اعتبارسنجی و پاک‌سازی

from decimal import Decimal
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem

class CleanProductPipeline:
    def process_item(self, item, spider):
        data = ItemAdapter(item)
        name = (data.get("name") or "").strip()
        if not name:
            raise DropItem("محصول بدون نام")
        data["name"] = name
        data["price"] = Decimal(str(data["price"]).replace(",", ""))
        return item
ITEM_PIPELINES = {
    "shopbot.pipelines.CleanProductPipeline": 300,
}
فصل ۱۰

خروجی فایل و پایگاه داده

برای CSV، JSON، JSON Lines و XML از Feed Export استفاده کنید.

FEEDS = {
    "exports/%(name)s-%(time)s.jsonl": {
        "format": "jsonlines",
        "encoding": "utf-8",
        "overwrite": False,
    }
}
نکته

JSON Lines برای داده حجیم و پردازش جریانی مناسب‌تر است.

فصل ۱۱

API، فرم و ورود

اگر داده از endpoint عمومی می‌آید، فراخوانی آن سریع‌تر از اجرای مرورگر است؛ مجوز دسترسی را بررسی کنید.

def parse(self, response):
    payload = response.json()
    yield from payload["results"]

برای فرم از FormRequest.from_response() استفاده کنید تا CSRF حفظ شود. رمز را در مخزن نگذارید.

فصل ۱۲

صفحات جاوااسکریپتی

Scrapy مرورگر نیست. ابتدا API صفحه را بررسی کنید؛ اگر اجرای مرورگر ضروری بود از scrapy-playwright کمک بگیرید.

python -m pip install scrapy-playwright
playwright install chromium
DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
yield scrapy.Request(url, meta={"playwright": True})
فصل ۱۳

سرعت و تنظیمات

ROBOTSTXT_OBEY = True
USER_AGENT = "shopbot/1.0 (+https://example.org/bot)"
CONCURRENT_REQUESTS_PER_DOMAIN = 4
DOWNLOAD_DELAY = 0.5
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_MAX_DELAY = 30
RETRY_TIMES = 2
HTTPCACHE_ENABLED = True  # فقط توسعه

سرعت را کورکورانه بالا نبرید؛ نرخ مناسب تابع ظرفیت میزبان و شرایط استفاده است.

فصل ۱۴

خطایابی و تست

scrapy settings --get CONCURRENT_REQUESTS
scrapy crawl products -s CLOSESPIDER_ITEMCOUNT=20 -L INFO
scrapy check

آمار status، Itemهای حذف‌شده، Retry، حافظه و زمان را بررسی کنید.

تست مقاوم

HTML نمونه را fixture کنید و خروجی callback را بسنجید تا تغییر انتخابگر زود پیدا شود.

فصل ۱۵

اجرا و نگهداری واقعی

  • وابستگی‌ها را قفل کنید.
  • لاگ ساختاریافته و خروجی تاریخ‌دار داشته باشید.
  • روی تعداد Item و نرخ خطا هشدار بگذارید.
  • رمزها را بیرون مخزن نگه دارید.
  • برای اجرای دوره‌ای از cron، CI یا orchestrator استفاده کنید.

با کلید پایدار upsert کنید تا اجرای دوباره داده تکراری نسازد.

+
فصل ۱۶

CrawlSpider و LinkExtractor

وقتی الگوی لینک‌های سایت منظم است، CrawlSpider و Rule منطق پیمایش را خواناتر می‌کنند. LinkExtractor لینک‌ها را با الگو و دامنه فیلتر می‌کند.

from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

class CatalogSpider(CrawlSpider):
    name = "catalog"
    allowed_domains = ["example.com"]
    start_urls = ["https://example.com/products/"]
    rules = (
        Rule(LinkExtractor(allow=r"/product/\d+"),
             callback="parse_product"),
        Rule(LinkExtractor(allow=r"/products\?page="), follow=True),
    )

    def parse_product(self, response):
        yield {"title": response.css("h1::text").get(),
               "url": response.url}
نکته

متد parse را در CrawlSpider بازنویسی نکنید؛ اجرای Ruleها به آن وابسته است.

فصل ۱۷

Downloader و Spider Middleware

Downloader Middleware پیش و پس از دانلود روی Request و Response اثر می‌گذارد؛ Spider Middleware ورودی و خروجی callbackها را پردازش می‌کند.

class CrawlerHeaderMiddleware:
    def process_request(self, request, spider):
        request.headers.setdefault("X-Crawler", spider.name)
        return None

DOWNLOADER_MIDDLEWARES = {
    "shopbot.middlewares.CrawlerHeaderMiddleware": 543,
}

Requestها صعودی و Responseها نزولی عبور می‌کنند. ابتدا Middlewareهای داخلی Retry، Redirect، Cookies و Proxy را بررسی کنید.

فصل ۱۸

Signals، Extensions و آمار

Signalها رویدادهای چرخه عمر را اعلام می‌کنند و برای مانیتورینگ مناسب‌اند.

from scrapy import signals

class RunSummary:
    @classmethod
    def from_crawler(cls, crawler):
        ext = cls()
        ext.stats = crawler.stats
        crawler.signals.connect(ext.closed, signal=signals.spider_closed)
        return ext

    def closed(self, spider, reason):
        count = self.stats.get_value("item_scraped_count", 0)
        spider.logger.info("items=%s reason=%s", count, reason)

تعداد Item، Retry، کدهای HTTP، مدت اجرا و حافظه را ذخیره و با اجرای قبلی مقایسه کنید.

فصل ۱۹

دانلود فایل و تصویر

FilesPipeline و ImagesPipeline دانلود، جلوگیری از تکرار و metadata را مدیریت می‌کنند.

yield {
    "image_urls": [
        response.urljoin(url)
        for url in response.css(".gallery img::attr(src)").getall()
    ]
}

ITEM_PIPELINES = {"scrapy.pipelines.images.ImagesPipeline": 100}
IMAGES_STORE = "data/images"
IMAGES_THUMBS = {"small": (120, 120)}

برای پردازش تصویر Pillow لازم است. URL نسبی را قبل از yield مطلق کنید.

فصل ۲۰

توقف و ادامهٔ Crawl

JOBDIR وضعیت Scheduler و درخواست‌های تکراری را روی دیسک نگه می‌دارد.

scrapy crawl catalog -s JOBDIR=jobs/catalog-2026
# توقف محترمانه: یک بار Ctrl+C
# ادامه با همان فرمان
پوشه مستقل

یک JOBDIR را هم‌زمان یا برای Spider دیگر استفاده نکنید. Requestها باید قابل serialization باشند.

فصل ۲۱

Crawl گسترده و بهینه‌سازی

ابتدا با آمار گلوگاه پهنای باند، DNS، حافظه یا callback را پیدا کنید؛ سپس هم‌زمانی را مرحله‌ای افزایش دهید.

CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 4
REACTOR_THREADPOOL_MAXSIZE = 20
DNS_TIMEOUT = 20
DOWNLOAD_TIMEOUT = 30
LOG_LEVEL = "INFO"
MEMUSAGE_ENABLED = True
  • خروجی را جریانی بنویسید.
  • پردازش سنگین CPU را جدا کنید.
  • نرخ هر دامنه را مستقل محدود کنید.
  • رشد حافظه را پایش کنید.
فصل ۲۲

Coroutine و اجرای Scrapy از کد

callbackها می‌توانند async def باشند. برای اجرای Spider از پایتون، Process چرخه اجرا را مدیریت می‌کند.

from scrapy.crawler import AsyncCrawlerProcess
from scrapy.utils.project import get_project_settings

process = AsyncCrawlerProcess(get_project_settings())
process.crawl("catalog", category="books")
process.start()

اگر برنامه از قبل event loop یا reactor دارد، Runner مناسب‌تر است.

فصل ۲۳

امنیت خزنده و داده

HTML، JSON، URL و نام فایل ورودی غیرقابل‌اعتمادند. آن‌ها را اعتبارسنجی کنید و secret را در log ننویسید.

  • Telnet Console را در محیط ناامن غیرفعال کنید.
  • Job و خروجی حاوی token را محافظت کنید.
  • Redirect و URLهای داخلی را کنترل کنید.
  • نسخه وابستگی‌ها را قفل کنید.
import os
API_TOKEN = os.environ["SHOP_API_TOKEN"]
TELNETCONSOLE_ENABLED = False
COOKIES_DEBUG = False
فصل ۲۴ • پروژه عملی

پروژه کامل: خزنده فروشگاه کتاب

import scrapy

class BooksSpider(scrapy.Spider):
    name = "books"
    allowed_domains = ["books.toscrape.com"]
    start_urls = ["https://books.toscrape.com/"]

    def parse(self, response):
        yield from response.follow_all(
            css="article.product_pod h3 a",
            callback=self.parse_book,
        )
        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

    def parse_book(self, response):
        yield {
            "name": response.css("h1::text").get(),
            "price": response.css(".price_color::text").re_first(r"[\d.]+"),
            "availability": response.css(".availability::text").getall()[-1].strip(),
            "url": response.url,
        }
scrapy crawl books -O data/books.jsonl:jsonlines
تمرین توسعه
  1. UPC و دسته‌بندی را اضافه کنید.
  2. امتیاز را عددی کنید.
  3. رکورد ناقص را حذف کنید.
  4. تست انتخابگر بنویسید.
فصل ۳۱

اخلاق، قانون و چک‌لیست

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

  • هدف و مبنای دسترسی روشن است.
  • User-Agent شفاف است.
  • نرخ درخواست محدود است.
  • داده حساس در لاگ نیست.
  • سیاست نگهداری داده تعریف شده.
فصل ۳۲

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

تفاوت Scrapy و BeautifulSoup چیست؟

Scrapy زمان‌بندی، پیمایش، Retry، Pipeline، خروجی و آمار اجرا را هم دارد.

آیا Scrapy جاوااسکریپت را اجرا می‌کند؟

خیر؛ API را بررسی کنید و در صورت ضرورت از scrapy-playwright استفاده کنید.

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

احتمالاً عنصر با JavaScript ساخته شده. response.text و view(response) را بررسی کنید.

با ۴۰۳ چه کنم؟

مجوز را بررسی، نرخ را کم و User-Agent را شفاف کنید؛ محدودیت را دور نزنید.

منابع و روش تدوین

محتوا با بازنویسی و بومی‌سازی مستندات رسمی تهیه شده است. جزئیات وابسته به نسخه را در مرجع رسمی ببینید.