آشنایی با Scrapy

آشنایی با Scrapy

نوشته شده توسط: میثم عقیلی
تاریخ انتشار: ۲۴ فروردین ۱۳۹۹
آخرین بروزرسانی: 17 تیر 1403
زمان مطالعه: 14 دقیقه
۴.۵
(۲)

مقدمه

به استخراج اطلاعات از وب با ایجاد الگو و روشی خودکار را Web Crawling می‌گویند. برای استفاده از این روش که حجم زیادی از اطلاعات را با سرعت مناسب برای ما فراهم می‌کند ,ابزارهای مختلفی در زبان‌های برنامه نویسی گوناگون وجود دارد مانند:
• Scrapy
• Beautiful Soap
• GRUB
• Selenium

Scrapy

Scrapy یکی از محبوب‌ترین و ساده‌ترین ابزارهای Web Crawling در زبان Python می‌باشد که:

  • Open Source
  • رایگان
  • Cross-Platform
  • سرعت زیاد نسبت به دیگر Crawlerها

Scrapy با ایجاد Request به وبسایت‌ها اطلاعات صفحات مختلف را جمع آوری می‌کند که می‌توان آنرا در قالب فایل CSV,Json,Xml,.. و یا در دیتابیس‌های SQL Server,MongoDB,MySql,… ذخیره کرد.
در این مطلب به استخراج اطلاعات مناسبت‌های سال جاری از سایت time.ir می‌پردازیم.

مرحله ۱: نصب Scrapy در cmd

pip install scrapy

در ادامه با ایجاد یک پروژه کار خود را آغاز می‌کنیم.

scrapy startproject <Project Name> scrapy startproject Nikamooz
cd <Project Name> Example cd Nikamooz
Scrapy genspider <Spider Name> <Url> scrapy genspider time_ir https://time.ir

Spider یک class در پروژه می‌باشد که ما با دستوراتی که درآن می‌نویسیم راه و روش استخراج و ذخیره اطلاعات مورد نیاز خود را تعیین می‌کنیم. با ایجاد Spider ازطریق کد بالا در پوشه Spiders فایلی با نام Spiderای که وجود دارد.
در مسیری که پروژه شما ایجاد شده است تعدادی فایل با فرمت .py وجود دارد که آنهارا مورد بررسی قرار می‌دهیم.

  • Items.py: برای ایجاد فیلد یا ستونی برای ذخیره اطلاعات
  • Middlewares.py: اضافه کردن Proxy به پروژه و یا تغییر اطلاعاتی که در حال ذخیره آنها هستیم.
  • Piplines.py: برای ذخیره اطلاعات به دست آمده در دیتابیس‌های مختلف مانند SQL Server , MySQL MongoDB , ..
  • Settings.py: تنظیمات مربوط به پروژه شما برای مثال فعال کردنpiplines و middlewares و یا تنظیم User Agent

Scrapy اطلاعات درون کدهای Html هر page که در Page Source آن قابل مشاهده است را ذخیره می‌کند در این پروژه برای این که بتوانیم تمام مناسبت‌های امسال را به دست بیاوریم باید بر روی دکمه ماه قبل یا بعد با استفاده از کدهای scrapy کلیلک کرده تا اطلاعات هر ماه هر بار بارگذاری شود تا بتوان آنها را به دست آورد .از طرفی در سایت time.ir با کلیک روی ماه قبل یا بعد اطلاعات جدید در کد Html قابل مشاهده نیست و فقط یک Response می‌باشد که با inspect می‌توان اطلاعات بارگذاری شده را دید.به این نوع صفحات JavaScript Pages می‌گویند که با Scrapy به تنهایی نمی‌توان اطلاعات این صفحات را به دست آورد. برای این کار می‌توان از Scrapy-Splash استفاده کرد.
مرحله ۲: نصب و راه اندازی Scrapy-Splash

pip install scrapy-splash

 در فایل Settings.py کدهای زیر را جهت فعال کردن و تنظیمات Splash اضافه می‌کنیم

#js readable--------------------------------------------------------------------
SPLASH_URL='http://127.0.0.1:8050'
DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}
SPIDER_MIDDLEWARES={
    'scrapy_splash.SplashDeduplicateArgsMiddleware':100,
}
DUPEFILTER_CLASS='scrapy_splash.SplashAwareDupeFilter'
HTTPCACHE_STORAGE='scrapy_splash.SplashAwareFSCacheStorage'
#----------------------------------------------------------------------------

همچنین Splash نیاز به یک Instance دارد که می‌توان آنرا از طریق Docker دانلود و استفاده کرد.برای دانلود از کد زیر استفاده کنید.

docker pull scrapinghub/splash

بعد از دانلود با دستور زیر image دانلود شده را درip 127.0.0.1 و port 8050 می‌توان اجرا کرد.

docker run -p 8050:8050 scrapinghub/splash

در خط آخر خروجی می‌توانید نتیجه را ببینید.برای ادامه کار cmd دیگری را اجرا کنید.

۲۰۱۹-۱۲-۱۶ ۱۶:۴۱:۰۶.۱۴۴۰۹۷ [-] Server listening on http://0.0.0.0:8050

مرحله ۳: تاریخ و مناسبت اطلاعاتی هستند که نیاز داریم پس در فایل items.py برای هر کدام فیلدی ایجاد می‌کنیم.

Date=scrapy.Field()
Title=scrapy.Field()

مرحله ۴: سپس برای ذخیره اطلاعات در دیتابیس SQL Server می‌توان در فایل piplines.py تغییرات زیر را انجام داد.

# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
import pyodbc
class NikamoozPipeline(object):
    def __init__(self):
        self.create_con()
        self.create_table()
    def create_con(self):
        try:
            self.con= pyodbc.connect('DRIVER={ODBC Driver 17 for SQL Server};SERVER=MEYSAM_LAP\MEYSAM;DATABASE=temp;UID=sa;PWD=Sh!@#123')
            self.cur=self.con.cursor()
        except pyodbc.Error as e:
            print (e)
    def create_table(self):
        self.cur.execute("""drop table if exists time_tb""")
        self.cur.execute("""create table time_tb(
                        [Date] nvarchar(100),
                        Title nvarchar(100)
                        )  """)
    def process_item(self,item,spider):
        self.store_db(item)
        print("--------------Stored---------------")
        return item
    def store_db(self,item):
        self.cur.execute("""insert into time_tb values(?,?)""",(item['Date'],item['Title']))
        self.con.commit()
    def close_spider(self, spider):
        self.con.close()

سپس برای اینکه بتوانید از piplines.pyاستفاده کنید باید در فایل settings.py کدهای زیر را از حالت comment خارج کنید.

ITEM_PIPELINES = {
‘Nikamooz.pipelines.NikamoozPipeline’: ۳۰۰,
}

مرحله ۵: شروع کد نویسی در Spider
در فایل time_ir.py با ایجاد تابع start_requests() می‌توان با ارسال SplashRequest به سایت time.ir کد‌های Html و اطلاعات مورد نیاز صفحه اول که مربوط به ماه جاری است را دریافت کرد و آن را به سمت تابع parse() فرستاد.درون script_thism یک تابع به زبان برنامه نویسی lua می‌باشد که Splash را تبدیل به یک Browser می‌کند و می‌توانیم یک url به تابع به عنوان ورودی بدهیم تا یک button را کلیک کند و منتظر شود تا اطلاعات load شوند و خروجی Html آنرا دریافت کنیم. برای استفاده از این تابع باید آنرا در SplashRequest خود به صورت زیر بیاوریم و endpoint را برابر execute قراردهیم.

# -*- coding: utf-8 -*-
import scrapy
import time
from scrapy_splash import SplashRequest
class TimeIrSpider(scrapy.Spider):
    name = 'time_ir'
    start_urls = ["https://time.ir"]
    def start_requests(self):
        script_thism = """
        function main(splash)
      splash.private_mode_enabled=false
               local url = splash.args.url
               assert(splash:go(url))
             assert(splash:wait(2))
               return {
                   html = splash:html()
               }
        end
        """
        url='https://time.ir'
        yield SplashRequest(url, self.parse, meta={
            'splash': {
                'args': {'lua_source': script_thism},
                'endpoint': 'execute',
                }
        })

قبل از این که کار خود را با استخراج اطلاعات از خروجی SplashRequest در تابع Parse شروع کنیم باید با Css Selector آشنا شویم.
برای مشخص کردن و پیدا کردن اطلاعات مورد نیاز, باید به Scrapy با استفاده از Css Selector و XPath آدرس اطلاعات را بدهیم. برای پیداکردن این آدرس می‌توان از ابزاری به نام Selector Gadget کمک گرفت .

(https://chrome.google.com/webstore/detail/selectorgadget/mhjhnkcfbdhnjickkkdbjoemdmbfginb?hl=en)

پس از دانلود آن به صورت زیر عمل می‌کنیم.در scrapy به صورت زیر می‌توان عمل کرد.

m=response.css(.list-unstyled span::text).extract()

با اینکار در کد Html و element مربوطه را پیدا می‌کنیم و اطلاعات آنرا به صورت یک list در m ذخیره می‌کنیم.
به پروژه خود کد زیر را اضافه می‌کنیم تا بتوانیم از فیلدهایی که در items.py ایجاد کردیم استفاده کنیم.

from ..items import NikamoozItem

در تابع parse ابتدا با ایجاد NikamoozItem () فیلدهای خود را فراخوانی می‌کنیم سپس با استفاده از css selector مناسب برای اطلاعات تاریخ و مناسبت آن اطلاعات را به صورت یک list استخراج می‌کنیم.درادامه فرایند مرتب سازی دیتا را انجام می‌دهیم.
در لیست تاریخ اطلاعاتی همچون [۲۵ December] و یا رکوردهای empty وجود دارد که باید آنها را حذف کنیم.
در آخر اطلاعات را در دو list جدا ذخیره می‌کنیم و با یک حلقه هر بار یک رکورد را در فیلد مربوط به خود ریخته و آنها را با استفاده از Yield به سمت دیتابیس می‌فرستیم.هربار که Yield اجرا می‌شود Scrapy فایل piplines را اجرا می‌کند.

def parse(self,response):
        items=NikamoozItem()
        m=response.css('.list-unstyled li::text').extract()
        m2=response.css('.list-unstyled span::text').extract()
        y=0
        x=0
        z=0
        r=[]
        t=[]
        for i in range(0,len(m)):
            if m[y].strip()!='':
                r.append(m[y].strip())
            y=y+1
        for j in range(0,len(m2)):
            if m2[z].strip()!='' and '[' not in m2[z]:
                t.append(m2[z].strip())
            z=z+1
        for o in range(0,len(t)):
            b=t[x].strip()
            a=r[x].strip()
            items['Date']=b
            items['Title']=a
            x=x+1
            yield items

با ایجاد یک request جدید به سایت اطلاعات ماه قبل را استخراج می‌کنیم

for o in range(0,len(t)):
            b=t[x].strip()
            a=r[x].strip()
            items['Date']=b
            items['Title']=a
            x=x+1
            yield items
        url='https://time.ir'
        script_pre1 = """
        function main(splash)
      splash.private_mode_enabled=false
            local url = splash.args.url
            assert(splash:go(url))
            assert(splash:wait(2))
            assert(splash:runjs("$('#ctl00_cphTop_Sampa_Web_View_EventUI_EventCalendarSimple30cphTop_3732_ecEventCalendar_pnlPrevious').click()"))
            assert(splash:wait(2))
            -- return result as a JSON object
            return {
                html = splash:html()
            }
        end
        """
               yield SplashRequest(url, self.parse, meta={
            'splash': {
                'args': {'lua_source': script_pre1},
                'endpoint': 'execute',
            }
        })

در ادامه تابع parse تابع script_pre1 با کلیک روی button ماه قبل با id آن کدهای Html را دوباره به تابع parse جهت مرتب سازی و ذخیره اطلاعات جدید می‌فرستد. هم چنین برای دو ماه قبل باید دو بار button ماه قبل را کلیلک کنیم که به صورت زیر می‌باشد

  script_pre2 = """
  function main(splash)
splash.private_mode_enabled=false
      local url = splash.args.url
      assert(splash:go(url))
      assert(splash:wait(2))
      assert(splash:runjs("$('#ctl00_cphTop_Sampa_Web_View_EventUI_EventCalendarSimple30cphTop_3732_ecEventCalendar_pnlPrevious').click()"))
    assert(splash:wait(2))
      assert(splash:runjs("$('#ctl00_cphTop_Sampa_Web_View_EventUI_EventCalendarSimple30cphTop_3732_ecEventCalendar_pnlPrevious').click()"))
      assert(splash:wait(2))
      -- return result as a JSON object
      return {
          html = splash:html()
      }
  end
  """
  yield SplashRequest(url, self.parse, meta={
      'splash': {
          'args': {'lua_source': script_pre2},
          'endpoint': 'execute',
      }
  })

تابع runjs() button ای که id آن به عنوان ورودی داده شده است را کلیک می‌کند و با اضافه کردن wait(2) زمان کافی برای بارگذاری اطلاعات جدید را فراهم می‌کنیم.به همین صورت برای هر ماه دلخواه باید request آنرا فرستاد.
مرحله ۶: در آخر برای اجرای پروژه و به دست آوردن اطلاعات در cmd به صورت زیر عمل می‌کنیم. توجه داشته باشید که مرحله ۲ را در cmd دیگری اجرا کرده باشید.

cd <Project Address>
scrapy crawl <Spider Name>  example scrapy crawl time_ir

می‌توانید خروجی حاصل را در دیتابیس خود ببینید و یا مانند زیر آنرا در قالب یک فایل csv ذخیره کنید.

scrapy crawl <Spider Name> -o <File Name>.csv  scrapy crawl time_ir myfile.csv (or .json /.xml)

فایل شما در مسیر پروژه شما قرار می‌گیرد.
نتیجه به صورت زیر خواهد بود.

SQL Server

به دلیل ساختار منحصر به فرد سایت time.ir پروژه ما با lua_Script برای کلیک بر روی buttonها و گرفتن خروجی JavaScriptای و SplashRequest برای خواندن خروجی به نتیجه رسید ولی درحالت ساده تر می‌توان از کد زیر فقط برای ذخیره اطلاعات صفحه نخست که اطلاعات ماه جاری است استفاده کرد. یعنی نیازی به نوشتن Splash Settings در فایل Settings.pyو اجرا کردن نسخه docker , instance splash نیست.

# -*- coding: utf-8 -*-
import scrapy
from ..items import NikamoozItem
class TimeIrSpider(scrapy.Spider):
    name = 'time_ir'
    start_urls = ["https://time.ir"]
    def parse(self,response):
        items=NikamoozItem()
        m=response.css('.list-unstyled li::text').extract()
        m2=response.css('.list-unstyled span::text').extract()
        y=0
        x=0
        z=0
        r=[]
        t=[]
        for i in range(0,len(m)):
            if m[y].strip()!='':
                r.append(m[y].strip())
            y=y+1
        for j in range(0,len(m2)):
            if m2[z].strip()!='' and '[' not in m2[z]:
                t.append(m2[z].strip())
            z=z+1
        for o in range(0,len(t)):
            b=t[x].strip()
            a=r[x].strip()
            items['Date']=b
            items['Title']=a
            x=x+1
            yield items

 

چه رتبه ای می‌دهید؟

میانگین ۴.۵ / ۵. از مجموع ۲

اولین نفر باش

title sign
معرفی نویسنده
میثم عقیلی
مقالات
1 مقاله توسط این نویسنده
محصولات
0 دوره توسط این نویسنده
میثم عقیلی
title sign
معرفی محصول

دوره یادگیری علم داده

1.780.000 تومان 1.246.000 تومان
title sign
دیدگاه کاربران

close-image