خانه/مقالات/اسکریپینگ با Scrapy و ذخیره داده در SQLite
استخراج داده
داده‌کاوی
برگشت به مقاله‌ها

اسکریپینگ با Scrapy و ذخیره داده در SQLite

اسکریپینگ با Scrapy و ذخیره داده در SQLite
این مقاله به‌صورت عملی نشان می‌دهد چگونه با استفاده از Scrapy Item Pipelines داده‌های استخراج‌شده را در SQLite ذخیره کنیم، از ایجاد جدول و درج ایمن تا جلوگیری از رکوردهای تکراری و نکات عملکردی/امنیتی را پوشش می‌دهد.
آسان اسکریپ آسان اسکریپ
1405-04-11

مقدمه

ذخیرهٔ داده‌های استخراج‌شده یکی از مراحل پایه در هر پروژهٔ وب اسکریپینگ است. برای پروژه‌های کوچک و متوسط، SQLite گزینه‌ای ساده، سبک و بدون نیاز به سرویس‌دهندهٔ جداست. در این راهنما با تمرکز روی Scrapy و SQLite به صورت عملی یاد می‌گیریم چگونه از Item Pipeline برای ساخت جدول، درج داده، جلوگیری از تکرار و نکات عملکردی/امنیتی استفاده کنیم.

پس از خواندن این مقاله شما با موارد زیر آشنا می‌شوید: تنظیم pipeline برای اتصال به دیتابیس، درج ایتم‌ها در process_item، جلوگیری از درج تکراری، و بهترین روش‌ها برای پایداری و کارایی.

چیست و چرا: Scrapy Item Pipelines

Item Pipelines مکانیزم Scrapy برای پردازش ایتم‌های استخراج‌شده است. زمانی که یک اسپایدر یک ایتم yield می‌کند، آن ایتم به ترتیب از میان pipelineها عبور می‌کند تا مراحل مانند پاک‌سازی، اعتبارسنجی و ذخیره‌سازی انجام شود.

  • کارهایی که معمولاً با pipelines انجام می‌دهیم: پاک‌سازی HTML، اعتبارسنجی فیلدها، حذف تکراری‌ها و ذخیره در دیتابیس.
  • مزیت استفاده از pipeline: جداسازی منطق ذخیره‌سازی از منطق استخراج، و امکان تست و مدیریت بهتر جریان داده.

بهترین ساختار اتصال به SQLite در Pipeline

بهتر است باز و بسته کردن کانکشن SQLite را در متدهای چرخهٔ زندگی pipeline انجام دهیم تا از مشکلاتی مثل اشتراک کانکشن بین فرآیندها یا threadها جلوگیری شود. به همین دلیل از open_spider و close_spider استفاده می‌کنیم.

# pipelines.py
import sqlite3
import json

class SqliteDemoPipeline:

    def open_spider(self, spider):
        # اتصال/ایجاد دیتابیس
        self.con = sqlite3.connect('demo.db')
        self.cur = self.con.cursor()
        # جدول با یک unique constraint روی متن تا امکان جلوگیری از تکرار ساده باشد
        self.cur.execute('''
            CREATE TABLE IF NOT EXISTS quotes(
                text TEXT UNIQUE,
                tags TEXT,
                author TEXT
            )
        ''')
        self.con.commit()

    def close_spider(self, spider):
        # بستن اتصال هنگام اتمام
        self.con.close()

    def process_item(self, item, spider):
        # تبدیل لیستِ tags به رشته (مثلاً JSON) قبل از ذخیره
        tags_str = json.dumps(item['tags'], ensure_ascii=False)
        # درج با پارامترایز شده برای جلوگیری از SQL injection
        self.cur.execute('INSERT OR IGNORE INTO quotes (text, tags, author) VALUES (?, ?, ?)',
                         (item['text'], tags_str, item['author']))
        self.con.commit()
        return item

توضیحات خط‌به‌خط:

  • open_spider: هنگام شروع اسپایدر فراخوانی می‌شود؛ کانکشن و کرسر را باز و جدول را ایجاد می‌کنیم.
  • CREATE TABLE ... UNIQUE: ستون text را به صورت UNIQUE تعریف کردیم تا بتوانیم از INSERT OR IGNORE برای عدم درج تکراری استفاده کنیم.
  • process_item: ورودی‌ها: item (قالب: دیکشنری/Item با کلیدهای text,tags,author)؛ خروجی: همان ایتم برای عبور به بقیه pipelineها. درون آن لیست تگ‌ها را با json.dumps به رشته تبدیل کرده و دستور پارامترایز شده اجرا می‌شود.
  • پارامترایز کردن query باعث جلوگیری از حملات تزریق و مشکلات فرمت می‌شود.

نسخهٔ مقابله با داده‌های تکراری (روش‌های مختلف)

دو رویکرد برای جلوگیری از ذخیرهٔ دادهٔ تکراری وجود دارد:

  1. استفاده از UNIQUE روی ستون‌ها و سپس INSERT OR IGNORE یا INSERT OR REPLACE.
  2. استعلام قبلی با SELECT و سپس درج فقط در صورت عدم وجود (مثل مثال مرجع).

هر کدام مزایا و معایبی دارند:

  • UNIQUE + INSERT OR IGNORE: ساده، سریع‌تر و کمتر مستعد race condition در شرایط ساده؛ اما در برخی موارد نیاز به جزئیات کنترلی بیشتر (مثلاً بروزرسانی فیلدها) دارد.
  • SELECT قبل از INSERT: انعطاف‌پذیرتر و می‌توان لاگ‌ یا تصمیمات پیچیده‌تر گرفت، اما دو کوئری بیشتر اجرا می‌کند و در شرایط هم‌زمانی ممکن است نیاز به transaction قوی‌تری داشته باشد.
# alternative pipeline: explicit duplicate check
class SqliteNoDuplicatesPipeline:

    def open_spider(self, spider):
        self.con = sqlite3.connect('demo.db')
        self.cur = self.con.cursor()
        self.cur.execute('''
            CREATE TABLE IF NOT EXISTS quotes(
                text TEXT,
                tags TEXT,
                author TEXT
            )
        ''')
        self.con.commit()

    def close_spider(self, spider):
        self.con.close()

    def process_item(self, item, spider):
        # بررسی وجود رکورد با SELECT
        self.cur.execute('SELECT 1 FROM quotes WHERE text = ?', (item['text'],))
        result = self.cur.fetchone()
        if result:
            # لاگ هشدار دربارهٔ مورد تکراری
            spider.logger.warning('Item already in database: %s', item['text'])
        else:
            tags_str = json.dumps(item['tags'], ensure_ascii=False)
            self.cur.execute('INSERT INTO quotes (text, tags, author) VALUES (?, ?, ?)',
                             (item['text'], tags_str, item['author']))
            self.con.commit()
        return item

توضیح: این روش مشابه مقاله مرجع است، با اضافه کردن لاگ با سطح warning و تبدیل امنِ tags.

فعال‌سازی Pipeline در تنظیمات

برای فعال‌سازی pipeline کافی است آن را در فایل settings.py اضافه کنید. مقدار عددی ترتیب اجرای pipelineها را مشخص می‌کند (کمتر = اول اجرا):

# settings.py
ITEM_PIPELINES = {
    'your_project.pipelines.SqliteDemoPipeline': 300,
    # یا برای جلوگیری از تکراری‌ها
    # 'your_project.pipelines.SqliteNoDuplicatesPipeline': 300,
}

نکات عملی: مطمئن شوید نام پکیج/نقشهٔ pipeline با ساختار پروژهٔ شما همخوانی داشته باشد (مثلاً sqlite_demo.pipelines.SqliteDemoPipeline).

نکات عملکردی، ایمنی و پایداری

  • Concurrency: SQLite برای دسترسی هم‌زمان قوی‌ترین گزینه نیست. اگر اسپایدر شما همزمانی بالایی دارد یا از چند پروسس استفاده می‌کنید، از یک دیتابیس سروری مانند Postgres/MySQL یا استفاده از queue/adbapi استفاده کنید.
  • Transactions: برای نوشتن حجم زیاد ایتم‌ها، بهتر است commit را به ازای هر N ایتم یا در پایان بسته شدن اسپایدر انجام دهید تا overhead I/O کاهش یابد. اما در صورت قطع ناگهانی ممکن است داده‌ها از دست بروند.
  • Index/Unique: برای جلوگیری از تکرار و افزایش سرعت جستجو، از ایندکس‌ها و قیدهای UNIQUE استفاده کنید.
  • فرمت‌سازی فیلدهای پیچیده: فیلدهایی مثل لیست تگ را با json.dumps ذخیره کنید تا نگهداری و بازیابی ساده باشد. هنگام خواندن از json.loads استفاده کنید.
  • امنیت: همیشه از کوئری‌های پارامترایز شده استفاده کنید تا از تزریق SQL جلوگیری شود.
  • خطایابی و لاگ: لاگ مناسب در process_item و open_spider کمک می‌کند مشکلات اتصال یا خطاهای درج را سریع‌تر پیدا کنید.

نمونهٔ سناریوهای واقعی

  • اسکریپ یک سایت کوچک خبری: SQLite برای ذخیرهٔ پیش‌نویس‌ها و نمونه‌گیری مناسب است و امکان بازبینی محلی به کمک ابزارهایی مثل DB Browser for SQLite را می‌دهد.
  • اسکریپ دیتای تستی قبل از مهاجرت به PostgreSQL: می‌توانید ابتدا با SQLite توسعه دهید و بعد با تغییر چند خط pipeline به دیتابیس سروری مهاجرت کنید.
  • اسکریپ با نرخ بالا: بهتر است از اتصال غیرهمزمان یا یک دیتابیس سروری استفاده شود تا قفل‌های SQLite به مشکل نخورد.

جمع‌بندی

ذخیرهٔ داده‌ها از طریق Scrapy Item Pipelines به SQLite ساده و سریع است: کانکشن را در open_spider باز کنید، جدول را ایجاد کنید، و در process_item از کوئری‌های پارامترایز شده برای درج استفاده کنید. برای جلوگیری از تکرار می‌توانید از قیدهای UNIQUE و INSERT OR IGNORE یا چک قبلی با SELECT بهره ببرید. همیشه مواردی مانند هم‌زمانی، تراکنش‌ها و فرمت‌گذاری فیلدهای پیچیده را در طراحی pipeline در نظر بگیرید.

اگر حجم داده یا نیاز به هم‌زمانی افزایش یافت، مهاجرت به دیتابیس سروری یا استفاده از لایهٔ غیرهمزمان توصیه می‌شود.

مطالب مرتبط

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