ساخت خزنده وب (Web Crawler) با فریمورک Scrapy — از صفر تا صد

۹۲۰ بازدید
آخرین به‌روزرسانی: ۲۹ شهریور ۱۴۰۲
زمان مطالعه: ۸ دقیقه
ساخت خزنده وب (Web Crawler) با فریمورک Scrapy — از صفر تا صد

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

Scrapy چیست؟

بر اساس تعریف ویکی‌پدیا، Scrapy که skray-pee تلفظ می‌شود یک فریمورک خزش وب اوپن‌سورس و رایگان است که به زبان پایتون نوشته شده است. این فریمورک در ابتدا برای وب اسکرپینگ طراحی شده بود و اکنون می‌توان از آن برای استخراج داده‌ها با استفاده از API-ها و یا به عنوان یک خزنده وب عمومی استفاده کرد. این فریمورک در حال حاضر از سوی شرکت Scrapinghub نگهداری می‌شود که ارائه‌دهنده خدمات برنامه‌نویسی وب اسکرپینگ است.

ایجاد یک پروژه

Scrapy ایده پروژه‌ای را مطرح کرد که چندین خزنده یا عنکبوت در یک پروژه منفرد داشته باشد. این مفهوم به طور خاص در مواردی مفید است که مشغول نوشتن چندین خزنده برای بخش‌های مختلف یا زیردامنه‌های متفاوت یک سایت باشیم. بنابراین در ابتدا یک پروژه می‌سازیم:

1Adnans-MBP:ScrapyCrawlers AdnanAhmad$ scrapy startproject olx
2New Scrapy project 'olx', using template directory '//anaconda/lib/python2.7/site-packages/scrapy/templates/project', created in:
3    /Development/PetProjects/ScrapyCrawlers/olx
4
5You can start your first spider with:
6    cd olx
7    scrapy genspider example example.com

ایجاد خزنده

با اجرای دستور زیر یک پروژه با نام olx ایجاد و اطلاعاتی را که برای مراحل بعدی کار مفید هستند ارائه می‌شوند.

1scrapy startproject olx

ابتدا به پوشه جدیداً ایجاد شده می‌رویم و سپس دستوری برای تولید نخستین عنکبوت با نام دامنه و سایتی که باید خزیده شود، وارد می‌کنیم:

1Adnans-MBP:ScrapyCrawlers AdnanAhmad$ cd olx/
2Adnans-MBP:olx AdnanAhmad$ scrapy genspider electronics  www.olx.com.pk
3Created spider 'electronics' using template 'basic' in module:
4  olx.spiders.electronics

ما کد عنکبوت نخست خود را با نام electronics ساختیم، زیرا قصد داریم به بخش electronics در سایت OLX دسترسی پیدا کنیم. شما می‌توانید نام عنکبوت خود را بر اساس نیازهای خود تعیین کنید.

ساختار پروژه نهایی چیزی مانند تصویر زیر خواهد بود:

Scrapy

همان طور که می‌بینید یک پوشه مستقل برای هر عنکبوت وجود دارد. شما می‌توانید چند عنکبوت را به یک پروژه منفرد اضافه کنید. اگر فایل عنکبوت electronics.py را باز کنیم با چیزی مانند زیر مواجه می‌شویم:

1# -*- coding: utf-8 -*-
2import scrapy
3
4
5class ElectronicsSpider(scrapy.Spider):
6    name = "electronics"
7    allowed_domains = ["www.olx.com.pk"]
8    start_urls = ['http://www.olx.com.pk/']
9
10    def parse(self, response):
11        pass

چنان که مشاهده می‌کنید، ElectronicsSpider یک زیرکلاس از scrapy.Spider است. مشخصه name در واقع نام عنکبوت است که در دستور تولید عنکبوت تعیین شده است. این نام در زمانی که خزنده، خود را اجرا می‌کند به کار می‌آید. مشخصه allowed_domains تعیین می‌کند که کدام دامنه‌ها در دسترس این خزنده هستند و start_urls جایی است که URL-های ابتدایی در آنجا نگه‌داری می‌شوند. این URL-های ابتدایی در زمان آغاز به کار عنکبوت مورد نیاز هستند. علاوه بر ساختار فایل، این یک قابلیت خوب برای ایجاد کران‌هایی برای خزنده است.

متد parse چنان که از نامش برمی‌آید، محتوای صفحه‌ای را که مورد دسترسی قرار داده است تحلیل خواهد کرد. ما می‌خواهیم خزنده‌ای بنویسیم که به چندین صفحه برود و به این منظور باید برخی تغییرات ایجاد کنیم.

1from scrapy.spiders import CrawlSpider, Rule
2from scrapy.linkextractors import LinkExtractor
3
4class ElectronicsSpider(CrawlSpider):
5    name = "electronics"
6    allowed_domains = ["www.olx.com.pk"]
7    start_urls = [
8        'https://www.olx.com.pk/computers-accessories/',
9        'https://www.olx.com.pk/tv-video-audio/',
10        'https://www.olx.com.pk/games-entertainment/'
11    ]
12
13    rules = (
14        Rule(LinkExtractor(allow=(), restrict_css=('.pageNextPrev',)),
15             callback="parse_item",
16             follow=True),)
17
18    def parse_item(self, response):
19        print('Processing..' + response.url)

برای این که خزنده به چندین صفحه سر بزند، به جای scrapy.Spider یک زیرکلاس از آن ایجاد می‌کنیم. این کلاس موجب می‌شود که خزش روی صفحه‌های چندگانه آسان‌تر باشد. شما می‌توانید با کد تولید شده هر کاری که دوست دارید انجام دهید، اما باید مواظب باشید که دوباره به صفحه‌های قبلی بازنگردید.

گام بعدی این است که متغیرهای قاعده خود را تنظیم کنید. در اینجا قواعد ناوبری وب‌سایت را بررسی می‌کنیم. LinkExtractor در واقع پارامترهایی برای رسم کران‌ها می‌گیرد. ما در این مثال از پارامتر restrict_css برای تعیین کلاسی جهت صفحه بعدی استفاده می‌کنیم. اگر به این صفحه (+) مراجعه کنید، چیزی مانند تصویر زیر را مشاهده خواهید کرد:

Scrapy

pageNextPrev کلاسی است که برای واکشی لینک‌ها صفحه‌های بعدی استفاده می‌شود. پارامتر call_back مشخصی می‌کند که کدام متد برای دسترسی به عناصر استفاده می‌شود. این متد را در ادامه بررسی می‌کنیم.

به خاطر داشته باشید که باید نام متد را از ()parse به ()parse_item با هر چیزی که دوست دارید تغییر دهید تا از override شدن کلاس مبنا جلوگیری شود. در غیر این صورت قاعده شما کار نخواهد کرد حتی اگر مقدار follow=True تنظیم کنید.

تا به اینجا همه چیز به خوبی پیش رفته است. در ادامه خزنده‌ای را که تا به اینجا ساخته‌ایم تست می‌کنیم. در دایرکتوری پروژه به ترمینال بروید و دستور زیر را وارد کنید:

1scrapy crawl electronics

پارامتر سوم در واقع نام عنکبوتی است که قبلاً در مشخصه name کلاس ElectronicsSpiders تعیین کرده‌ایم. در ترمینال اطلاعات مفید زیادی می‌یابید که برای دیباگ کردن خزنده مفید هستند. در صورتی که نخواهید اطلاعات دیباگ کردن را ببینید، می‌توانید گزینه debugger را غیرفعال کنید. دستور مشابهی با سوئیچ –nolog وجود دارد:

1scrapy crawl --nolog electronics

اگر این دستور را در حال حاضر اجرا کنید، خروجی چیزی مانند زیر خواهد بود:

1Adnans-MBP:olx AdnanAhmad$ scrapy crawl --nolog  electronics
2Processing..https://www.olx.com.pk/computers-accessories/?page=2
3Processing..https://www.olx.com.pk/tv-video-audio/?page=2
4Processing..https://www.olx.com.pk/games-entertainment/?page=2
5Processing..https://www.olx.com.pk/computers-accessories/
6Processing..https://www.olx.com.pk/tv-video-audio/
7Processing..https://www.olx.com.pk/games-entertainment/
8Processing..https://www.olx.com.pk/computers-accessories/?page=3
9Processing..https://www.olx.com.pk/tv-video-audio/?page=3
10Processing..https://www.olx.com.pk/games-entertainment/?page=3
11Processing..https://www.olx.com.pk/computers-accessories/?page=4
12Processing..https://www.olx.com.pk/tv-video-audio/?page=4
13Processing..https://www.olx.com.pk/games-entertainment/?page=4
14Processing..https://www.olx.com.pk/computers-accessories/?page=5
15Processing..https://www.olx.com.pk/tv-video-audio/?page=5
16Processing..https://www.olx.com.pk/games-entertainment/?page=5
17Processing..https://www.olx.com.pk/computers-accessories/?page=6
18Processing..https://www.olx.com.pk/tv-video-audio/?page=6
19Processing..https://www.olx.com.pk/games-entertainment/?page=6
20Processing..https://www.olx.com.pk/computers-accessories/?page=7
21Processing..https://www.olx.com.pk/tv-video-audio/?page=7
22Processing..https://www.olx.com.pk/games-entertainment/?page=7

از آنجا که مقدار follow=True را تنظیم کرده‌ایم، خزنده قاعده صفحه بعد را بررسی می‌کند و به ناوبری خود ادامه می‌دهد، مگر این که به صفحه‌ای برخورد کند که قاعده در مورد آن صدق نمی‌کند که معمولاً صفحه آخر لیست است.

اینک تصور کنید بخواهیم منطق مشابهی را با چیزهایی که در این صفحه (+) اشاره شده‌اند بنویسیم، ابتدا باید کدی بنویسیم که روی چندین پردازنده کار کند. همچنین باید کدی بنویسیم که نه تنها به صفحه بعد برود، بلکه اسکریپت را از طریق عدم دسترسی به URL های ناخواسته، در داخل کران‌های تعریف شده نگه دارد. Scrapy همه این وظایف را از دوش ما بر می‌دارد و کاری می‌کند که صرفاً روی منطق متمرکز شویم، یعنی خزنده‌ای برای استخراج اطلاعات بنویسیم. اینک قصد داریم کدی بنویسیم که لینک‌های آیتم منفرد مانند صفحه‌های فهرست‌بندی را واکشی کند بدین ترتیب کدی را که در متد parse_item داشتیم تغییر می‌دهیم:

1item_links = response.css('.large > .detailsLink::attr(href)').extract()
2        for a in item_links:
3            yield scrapy.Request(a, callback=self.parse_detail_page)

در این کد ما لینک‌ها را با استفاده از متد css. پاسخ واکشی می‌کنیم. چنان که گفتیم می‌توان از xpath نیز استفاده کرد و بستگی به نظر شما دارد.. در این حالت همه چیز کاملاً ساده خواهد بود:

Scrapy

لینک دیگر کلاسی به نام detailsLink دارد. اگر تنها از ('response.css('.detailsLink استفاده کنیم، در این صورت لینک‌های تکراری از یک مدخل منفرد گردآوری می‌شوند، زیرا لینک‌ها در تگ‌های img و h3 تکرار شده‌اند. همچنین به کلاس والد large اشاره کرده‌ایم تا لینک‌های یکتا دریافت کنیم. ما از (attr(href:: برای استخراج بخش href خود لینک استفاده می‌کنیم. سپس از متد ()extract استفاده می‌کنیم.

دلیل استفاده از این متد آن است که css. و xpath. شیء SelectorList را بازگشت می‌دهند و ()extract به بازگرداندن DOM واقعی برای پردازش بیشتر کمک می‌کند. در نهایت لینک‌ها را در scrapy.Request با یک callback به صورت کامل yield می‌کنیم. ما کد داخلی Scrapy را بررسی نکرده‌ایم، اما احتمالاً از yield به جای return استفاده می‌کند، زیرا می‌توانید چندین آیتم را return کنید. از آنجا که خزنده باید مراقب لینک‌های چندگانه همراه با هم نیز باشد، در این صورت yield بهترین انتخاب خواهد بود.

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

  • یک لیست از مدخل‌ها در parse_item به دست می‌آورید.
  • می‌توانید آن‌ها را در یک متد callback برای پردازش بیشتر ارسال کنید.

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

در نهایت قصد داریم اطلاعات واقعی را تحلیل کنیم که روی یکی از مدخل‌ها مانند این (+) در دسترس است.

تحلیل اطلاعات این صفحه کار دشواری نیست، اما این کاری است که باید روی اطلاعات ذخیره‌شده صورت بگیرد. ما باید model را برای داده‌های خود تعریف کنیم. این بدان معنی است باید به Scrapy بگوییم چه اطلاعاتی را می‌خواهیم برای استفاده‌های بعدی ذخیره کنیم. در ادامه فایل item.py را ویرایش می‌کنیم که قبلاً از سوی Scrapy ایجاد شده است:

1import scrapy
2
3class OlxItem(scrapy.Item):
4    # define the fields for your item here like:
5    # name = scrapy.Field()
6    pass

OlxItem کلاسی است که در آن فیلدهای مورد نیاز برای نگهداری اطلاعات را تنظیم خواهیم کرد. ما قصد داریم سه فیلد برای کلاس مدل خود تعریف کنیم.

1import scrapy
2
3class OlxItem(scrapy.Item):
4    # define the fields for your item here like:
5    # name = scrapy.Field()
6    pass

در این فیلدها عنوان مطلب، قیمت و خود URL را ذخیره می‌کنیم. در این مرحله به فایل کلاس خزنده بازمی‌گردیم و parse_detail_page را ویرایش می‌کنیم. اکنون یک متد برای آغاز نوشتن کد، یکی برای تست از طریق اجرای کل خزنده و دیگری برای مشاهده درست بودن مسیر است، اما ابزار جالب دیگری نیز وجود دارد که از سوی Scrapy عرضه شده است.

شل Scrapy

Shell یا پوسته Scrapy (+) یک ابزار خط فرمان است که فرصت تست کد تحلیل‌شده را بدون اجرای کلی خزنده در اختیار ما قرار می‌دهد. برخلاف خزنده که به همه لینک‌ها سر می‌زند، شل Scrapy اقدام به ذخیره‌سازی DOM یک صفحه منفرد برای استخراج داده‌ها می‌کند:

Adnans-MBP:olx AdnanAhmad$ scrapy shell https://www.olx.com.pk/item/asus-eee-pc-atom-dual-core-4cpus-beautiful-laptops-fresh-stock-IDUVo6B.html#4001329891

اکنون می‌توان به سادگی کد را بدون مراجعه چندباره به همان URL تست کرد. بدین ترتیب عنوان صفحه را با کد زیر واکشی کرده‌ایم:

In [8]: response.css('h1::text').extract()[0].strip()
Out[8]: u"Asus Eee PC Atom Dual-Core 4CPU's Beautiful Laptops fresh Stock"

آن response.css آشنا را اینجا هم می‌توانید مشاهده کنید. از آنجا که کل DOM موجود است می‌توان هر کاری با آن انجام داد. برای نمونه آن را می‌توان به صورت زیر واکشی کرد:

In [11]: response.css('.pricelabel > strong::text').extract()[0]
Out[11]: u'Rs 10,500'

نیازی به انجام هیچ کاری برای واکشی url نیست، زیرا response.url اقدام به بازگشت دادن URL-ی می‌کند که هم اینک مورد دسترسی قرار گرفته است.

اکنون که همه کد را بررسی کردیم، نوبت آن رسیده است که parse_detail_page را مورد استفاده قرار دهیم:

1title = response.css('h1::text').extract()[0].strip()
2price = response.css('.pricelabel > strong::text').extract()[0]
3item = OlxItem()
4item['title'] = title
5item['price'] = price
6item['url'] = response.url
7yield item

وهله OlxItem پس از تحلیل کردن اطلاعات لازم ایجاد می‌شود و مشخصه‌ها تعیین می‌شوند. اینک که نوبت اجرای خزنده و ذخیره‌سازی اطلاعات رسیده است، کمی تغییر در دستور باید ایجاد کرد:

scrapy crawl electronics -o data.csv -t csv

ما نام فایل و قالب‌بندی فایل را برای ذخیره‌سازی داده‌ها ارسال می‌کنیم. زمانی که دستور فوق اجرا شود فایل CSV برای شما می‌سازد. چنان که می‌بینید روند ساده‌ای است و برخلاف خزنده‌ای که خود می‌باید می‌نوشتیم، در اینجا کافی است رویه مورد نیاز برای ذخیره‌سازی داده‌ها را بنویسیم.

اما زیاد عجله نکنید! کار به همین جا ختم نمی‌شود. شما می‌توانید حتی داده‌ها را در قالب JSON نیز ذخیره کنید، تنها کاری که به این منظور لازم است ارسال مقدار json با سوئیچ t- است.

Scrapy قابلیت‌های دیگری نیز در اختیار ما قرار می‌دهد. برای نمونه می‌توان یک نام فایل ثابت ارسال کرد که در سناریوهای دنیای واقعی هیچ معنایی ندارد. چرا باید برنامه‌ای نوشت که نام فایل ثابتی تولید کند؟ یکی از موارد استفاده آن این است که باید فایل settings.py را اصلاح و این دو مدخل را اضافه کنید:

FEED_URI = 'data/%(name)s/%(time)s.json'
FEED_FORMAT = 'json'

در ادامه الگوی فایلی که ایجاد کرده‌ایم را ارائه می‌کنیم. %(name)% نام خود خزنده است، time زمان را نشان می‌دهد. اکنون زمانی که دستور زیر را اجرا کنیم:

scrapy crawl --nolog electronics

و یا دستور زیر را اجرا کنیم:

scrapy crawl electronics

یک فایل JSON در پوشه data مانند زیر ایجاد می‌شود:

1[
2{"url": "https://www.olx.com.pk/item/acer-ultra-slim-gaming-laptop-with-amd-fx-processor-3gb-dedicated-IDUQ1k9.html", "price": "Rs 42,000", "title": "Acer Ultra Slim Gaming Laptop with AMD FX Processor 3GB Dedicated"},
3{"url": "https://www.olx.com.pk/item/saw-machine-IDUYww5.html", "price": "Rs 80,000", "title": "Saw Machine"},
4{"url": "https://www.olx.com.pk/item/laptop-hp-probook-6570b-core-i-5-3rd-gen-IDUYejF.html", "price": "Rs 22,000", "title": "Laptop HP Probook 6570b Core i 5 3rd Gen"},
5{"url": "https://www.olx.com.pk/item/zong-4g-could-mifi-anlock-all-sim-supported-IDUYedh.html", "price": "Rs 4,000", "title": "Zong 4g could mifi anlock all Sim supported"},
6...
7]

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۶ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *