مقدمه
در اسکریپینگ مقیاسپذیر باید بتوانید با هر نوع سیستم صفحهبندی (pagination) که یک سایت استفاده میکند کنار بیایید. در این مقاله شش روش مرسوم که در عمل بیشتر بهکار میآیند را با مثالهای پایتون/Scrapy توضیح میدهیم، همراه با نکات پیادهسازی، مدیریت خطا، و بهترین روشها. پس از خواندن این راهنما شما قادر خواهید بود:
- نوع صفحهبندی یک سایت را تشخیص دهید.
- اسپایدرهای Scrapy را برای هر نوع صفحهبندی بنویسید.
- مسائل مربوط به عملکرد، خطا و قوانین اخلاقی را مدیریت کنید.
#1: تغییر شماره صفحه در URL (Change Page Number In URL)
سادهترین حالت وقتی است که پارامتر شماره صفحه در URL قرار دارد، مثلاً quotes.toscrape.com/page/2/. دو روش رایج وجود دارد: تولید همهٔ URLها از قبل یا پیمایش تا زمانی که خطا یا دادهای وجود نداشته باشد.
تولید URLها با عمق ثابت
وقتی تعداد صفحات را میدانید یا میخواهید مثلاً فقط ۱۰ صفحه را بخوانید، میتوانید start_urls را با فهرستی از URLها مقداردهی کنید. ورودی: هیچ (فقط start_requests از Scrapy). خروجی: درخواستها به Scheduler ارسال میشود و هر پاسخ به parse میرود.
import scrapy
from pagination_demo.items import QuoteItem
class QuotesFixedSpider(scrapy.Spider):
name = "quotes_fixed"
# تولید URL از صفحه 1 تا 10
start_urls = [f"http://quotes.toscrape.com/page/{i}/" for i in range(1, 11)]
def parse(self, response):
for quote in response.css('div.quote'):
item = QuoteItem()
item['text'] = quote.css('span.text::text').get()
item['author'] = quote.css('small.author::text').get()
item['tags'] = quote.css('div.tags a.tag::text').getall()
yield item
توضیح گامبهگام: لیست URLها ساخته میشود؛ Scrapy برای هر URL یک request میسازد؛ در parse عناصر استخراج و yield میشوند.
پیمایش تا دریافت 404 یا نبود داده
اگر تعداد صفحات نامشخص است، میتوانید از روش افزایش شماره صفحه و توقف روی 404 یا وقتی که دادهٔ موردنظر حاضر نیست استفاده کنید. در این روش از CloseSpider برای قطع کردن اسپایدر استفاده میکنیم.
import scrapy
from pagination_demo.items import QuoteItem
from scrapy.exceptions import CloseSpider
class Quotes404Spider(scrapy.Spider):
name = "quotes_404"
start_urls = ["http://quotes.toscrape.com/page/1/"]
handle_httpstatus_list = [404]
page_number = 1
def parse(self, response):
if response.status == 404:
raise CloseSpider('received 404')
quotes = response.css('div.quote')
if len(quotes) == 0:
raise CloseSpider('no quotes on page')
for quote in quotes:
item = QuoteItem()
item['text'] = quote.css('span.text::text').get()
item['author'] = quote.css('small.author::text').get()
item['tags'] = quote.css('div.tags a.tag::text').getall()
yield item
self.page_number += 1
next_page = f'http://quotes.toscrape.com/page/{self.page_number}/'
yield response.follow(next_page, callback=self.parse)
نکتههای عملی: این روش ساده است اما میتواند کند باشد چون صفحات یکییکی پردازش میشوند؛ اگر سایت محدودیت نرخ دارد بهتر است delay/auto-throttle را تنظیم کنید.
#2: دنبال کردن URL دکمهٔ Next از پاسخ
روش استاندارد Scrapy این است که URL «صفحهٔ بعد» را از HTML استخراج کنید و با response.follow آن را درخواست بزنید. این روش هم برای URLهای نسبی و هم برای query-parameterها کار میکند.
import scrapy
from pagination_demo.items import QuoteItem
class QuotesNextButtonSpider(scrapy.Spider):
name = "quotes_button"
start_urls = ["http://quotes.toscrape.com/"]
def parse(self, response):
for quote in response.css('div.quote'):
item = QuoteItem()
item['text'] = quote.css('span.text::text').get()
item['author'] = quote.css('small.author::text').get()
item['tags'] = quote.css('div.tags a.tag::text').getall()
yield item
next_page = response.css('li.next a::attr(href)').get()
if next_page:
# response.follow به صورت خودکار آدرس نسبی را به آدرس کامل تبدیل میکند
yield response.follow(next_page, callback=self.parse)
توضیح: response.css(...).get() یک رشتهٔ href نسبی یا None برمیگرداند. response.follow با پایهٔ URL فعلی آن را به URL کامل تبدیل و request میسازد.
- مزایا: انعطافپذیر، با تغییرات پارامترها و مسیرها سازگار است.
- معایب: اگر سایت لینک صفحه بعد را حذف کند یا JS تولید کند باید از رندر یا روشهای دیگر استفاده کنید.
#3: استفاده از نقشهٔ سایت (Sitemap)
اگر سایت نقشهٔ سایت XML داشته باشد میتوان با SitemapSpider همهٔ URLهای موردنظر را بدون حل مسئلهٔ صفحهبندی استخراج کرد؛ این روش اغلب سریع و کامل است.
from scrapy.spiders import SitemapSpider
from pagination_demo.items import BlogItem
class BlogSitemapSpider(SitemapSpider):
name = 'blog_sitemap'
sitemap_urls = ['https://www.example.com/sitemap.xml']
sitemap_rules = [('/blog/', 'parse')]
def parse(self, response):
item = BlogItem()
item['url'] = response.url
item['title'] = response.css('h1 > span::text').get()
yield item
نکات عملی: بررسی کنید sitemap شامل URL های مورد نظر باشد؛ بعضی سایتها چندین sitemap یا sitemap index دارند.
#4: استفاده از CrawlSpider
وقتی میخواهید مجموعهای از صفحات یک نوع خاص را بازدید کنید و نمیخواهید هر صفحه را دستی فهرست کنید، میتوانید از CrawlSpider و قوانین (Rule) استفاده کنید. این روش بیشتر به دنبال لینکها بر اساس الگو است تا حل مستقیم pagination.
import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from pagination_demo.items import QuoteItem
class QuotesCrawlSpider(CrawlSpider):
name = 'quotes_crawler'
allowed_domains = ['quotes.toscrape.com']
start_urls = ['http://quotes.toscrape.com/']
rules = (
Rule(LinkExtractor(allow=('page/',), deny=('tag/',)), callback='parse_item', follow=True),
)
def parse_item(self, response):
for quote in response.css('div.quote'):
item = QuoteItem()
item['text'] = quote.css('span.text::text').get()
item['author'] = quote.css('small.author::text').get()
item['tags'] = quote.css('div.tags a.tag::text').getall()
yield item
مزایا: خودکار کردن یافتن لینکها. معایب: ممکن است صفحات غیرضروری را هم بخزد یا برعکس بعضی لینکها را از دست بدهد؛ قواعد دقیق نیاز دارند.
#5: صفحهبندی APIها (Paginate API Requests)
APIها معمولاً در پاسخ خود اطلاعات صفحهبندی (مثل مقدار next یا تعداد صفحات) میفرستند. دو رویکرد اصلی: دنبال کردن URL بعدی از پاسخ یا پس از استخراج تعداد صفحات، تولید همهٔ درخواستها بهصورت همزمان.
دنبال کردن URL بعدی از JSON
فرض کنید پاسخ JSON شامل فیلد info.next است. ورودی: response JSON. خروجی: آیتمها و یک درخواست جدید در صورت وجود فیلد next.
import scrapy
from pagination_demo.items import CharacterItem
class ApiPaginationSpider(scrapy.Spider):
name = 'api_pagination'
start_urls = ['https://rickandmortyapi.com/api/character/']
def parse(self, response):
data = response.json()
for ch in data.get('results', []):
item = CharacterItem()
item['name'] = ch.get('name')
item['status'] = ch.get('status')
item['species'] = ch.get('species')
yield item
next_page = data.get('info', {}).get('next')
if next_page:
yield response.follow(next_page, callback=self.parse)
نکته عملکرد: این روش صف را پشت سر هم پیش میبرد؛ اگر میخواهید سرعت را افزایش دهید، میتوانید بعد از اولین پاسخ همهٔ URLهای صفحات را تولید کنید.
تولید همهٔ درخواستها بر اساس تعداد صفحات
اگر پاسخ اطلاعاتی مانند num_pages یا pages دارد میتوانید همهٔ URLها را از صفحهٔ دوم تا آخر تولید کنید تا Scrapy آنها را موازی پردازش کند.
import scrapy
from pagination_demo.items import CharacterItem
class ApiBulkSpider(scrapy.Spider):
name = 'api_bulk'
start_urls = ['https://rickandmortyapi.com/api/character/']
def parse(self, response):
data = response.json()
for ch in data.get('results', []):
item = CharacterItem()
item['name'] = ch.get('name')
item['status'] = ch.get('status')
item['species'] = ch.get('species')
yield item
num_pages = data.get('info', {}).get('pages')
if not num_pages:
return
# تولید و ارسال همهٔ درخواستها به scheduler برای پردازش موازی
for page in range(2, num_pages + 1):
next_page = f'https://rickandmortyapi.com/api/character/?page={page}'
yield response.follow(next_page, callback=self.parse)
نکات مهم: ارسال همهٔ URLها به scheduler میتواند سرعت را بالا ببرد اما مصرف حافظه و تعداد همزمانیهای شبکه را افزایش میدهد؛ محدودیتهای API و نرخ درخواست (rate limits) را در نظر بگیرید.
#6: استفاده از یادگیری ماشین با Autopager
کتابخانههایی مثل Autopager میتوانند لینکهای صفحهبندی را روی یک صفحه شناسایی کنند؛ مناسب برای تحقیق و تشخیص سریعِ الگوی صفحهبندی، نه همیشه یک راهحل اتوماتیک کامل.
pip install autopager
# نمونهٔ استفادهٔ اولیه برای تشخیص لینکهای صفحهبندی
import autopager
import requests
urls = autopager.urls(requests.get('http://quotes.toscrape.com'))
print(urls) # خروجی نمونه: ['/page/2/', '/tag/...']
راهبرد عملی: از Autopager برای تحلیل اولیه استفاده کنید تا ببینید آیا سایت از شمارهٔ صفحه در URL استفاده میکند، لینک Next دارد یا الگوی دیگری. سپس بسته به نوع الگو، یکی از روشهای بالا را پیاده کنید.
مسائل امنیتی، خطاها و بهترین روشها
- قوانین و اخلاق: قبل از اسکریپینگ، فایل robots.txt را بررسی و قوانین سایت و شرایط سرویس را رعایت کنید.
- هدرها و شبیهسازی مرورگر: برای جلوگیری از مسدود شدن از هدرهای منطقی مثل User-Agent و در صورت نیاز کوکیها استفاده کنید.
- در مقابل خطاها مقاوم باشید: از تنظیمات Retry و handle_httpstatus_list استفاده کنید و کد را برای پاسخهای غیرمنتظره (کدهای 429، 500، JSON ناصحیح) آماده کنید.
- مدیریت نرخ و همزمانی: از DOWNLOAD_DELAY، AUTOTHROTTLE و CONCURRENT_REQUESTS برای جلوگیری از overload کردن هدف استفاده کنید.
- پراکسی و چرخش آیپی: برای مقیاسپذیری و جلوگیری از بلاک شدن از پروکسی یا مدیریت پروکسیها استفاده کنید.
- دادگان بزرگ: برای خروجیهای بزرگ از استریم و pipelineها بهره ببرید (نوشتن مستقیم در دیتابیس یا استفاده از item pipelines)، نه بارگذاری همهٔ آیتمها در حافظه.
- امنیت دادهها: اطلاعات حساس (توکنها، کوکیها) را در کد hardcode نکنید؛ از تنظیمات یا سرویسهای مدیریت اسرار استفاده کنید.
جمعبندی
برای موفقیت در اسکریپینگ صفحات، ابتدا الگوی صفحهبندی را شناسایی کنید: آیا شمارهٔ صفحه در URL است، دکمهٔ Next وجود دارد، از sitemap استفاده میشود، یا API پاسخ میدهد؟ سپس روش مناسب (تولید URLها، دنبال کردن لینک Next، SitemapSpider، CrawlSpider، صفحهبندی API یا ابزارهای کمکی مثل Autopager) را انتخاب و پیادهسازی کنید. همیشه به عملکرد، مدیریت خطا، و قوانین سایت توجه کنید تا اسکریپری پایدار و اخلاقی بسازید.





