آموزش جامع 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» نقطه شروع است، اما هدرهای غیرضروری را حذف کنید.
- صفحه را با Network باز و فیلتر Fetch/XHR را فعال کنید.
- عملی که داده را بارگذاری میکند انجام دهید.
- Response و Preview درخواست را بررسی کنید.
- pagination، cursor یا payload را پیدا کنید.
- همان 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 را در مسیر یاد میگیرید.
چه زمانی Scrapy لازم نیست؟
برای یک صفحه کوچک شاید requests و BeautifulSoup کافی باشد. اگر API رسمی وجود دارد ابتدا همان را بررسی کنید. Scrapy برای صفحات زیاد، پیمایش و اجرای دورهای مناسب است.
معماری Scrapy به زبان ساده
Engine هماهنگکننده است. درخواست از Spider به Scheduler میرود، Downloader صفحه را میگیرد، پاسخ به Spider میرسد و داده وارد Pipeline میشود. Middlewareها رفتار مسیر را تغییر میدهند.
شما نتیجه را 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
| هدف | CSS | XPath |
|---|---|---|
| عنوان | 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
- UPC و دستهبندی را اضافه کنید.
- امتیاز را عددی کنید.
- رکورد ناقص را حذف کنید.
- تست انتخابگر بنویسید.
اخلاق، قانون و چکلیست
قابلدسترسیبودن URL به معنی مجازبودن هر نوع جمعآوری نیست. قانون، شرایط استفاده، حق نشر، حریم خصوصی و robots.txt را بررسی کنید.
- هدف و مبنای دسترسی روشن است.
- User-Agent شفاف است.
- نرخ درخواست محدود است.
- داده حساس در لاگ نیست.
- سیاست نگهداری داده تعریف شده.
پرسشهای متداول
تفاوت Scrapy و BeautifulSoup چیست؟
Scrapy زمانبندی، پیمایش، Retry، Pipeline، خروجی و آمار اجرا را هم دارد.
آیا Scrapy جاوااسکریپت را اجرا میکند؟
خیر؛ API را بررسی کنید و در صورت ضرورت از scrapy-playwright استفاده کنید.
چرا انتخابگر در مرورگر کار میکند ولی در Scrapy نه؟
احتمالاً عنصر با JavaScript ساخته شده. response.text و view(response) را بررسی کنید.
با ۴۰۳ چه کنم؟
مجوز را بررسی، نرخ را کم و User-Agent را شفاف کنید؛ محدودیت را دور نزنید.
منابع و روش تدوین
محتوا با بازنویسی و بومیسازی مستندات رسمی تهیه شده است. جزئیات وابسته به نسخه را در مرجع رسمی ببینید.