

در این مقاله یک راهنمای عملی و فنی برای ساخت یک اسپايدر تولیدی با Python Scrapy بهمنظور اسکریپ کردن آگهیهای شغلی از سایت Indeed ارائه میکنم. هدف این راهنما این است که بعد از خواندن آن بتوانید یک سیستم کشف آگهی (discovery crawler) و یک اسکرِیپر صفحات شغل بسازید، خروجیها را به CSV یا پایگاهداده منتقل کنید، با مکانیزمهای ضدربات مقابله کنید و عملیات را پایش و زمانبندی کنید.
آنچه یاد میگیرید:
انتخاب معماری وابسته است به هدف، فرکانس اجرای اسکریپ، حجم داده و پیچیدگی تحلیلی که میخواهید انجام دهید. برای یک نمونهٔ میانرده (صدها کلیدواژه، پایش منظم) معماری پیشنهادی ساده و خودمحور است:
مزایا/معایب این طراحی:
هدف این بخش ساخت یک crawler است که برای هر کلیدواژه و مکان، صفحهٔ جستجوی Indeed را درخواست دهد، JSON پنهان را بیرون بکشد و jobkeyها را استخراج کند. ابتدا ساختار URLهای جستجو را باید بشناسیم. پارامترهای مهم:
ایدهٔ کلّی: با یک تابع کمکی URL درست میکنیم، صفحهٔ اول را میزنیم، از تگ window.mosaic.providerData["mosaic-provider-jobcards"] JSON استخراج میکنیم، عدد نتایج را میگیریم و سپس برای هر صفحهٔ بعدی درخواست میفرستیم.
نمونهٔ سادهٔ تابع تولید URL و اسکلت اسپایدر:
import re
import json
import scrapy
from urllib.parse import urlencode
class IndeedJobSpider(scrapy.Spider):
name = "indeed_jobs"
def get_indeed_search_url(self, keyword, location, offset=0):
params = {"q": keyword, "l": location, "filter": 0, "start": offset}
return "https://www.indeed.com/jobs?" + urlencode(params)
def start_requests(self):
keyword_list = ['python']
location_list = ['texas']
for keyword in keyword_list:
for location in location_list:
url = self.get_indeed_search_url(keyword, location)
yield scrapy.Request(url=url, callback=self.parse_search_results, meta={'keyword': keyword, 'location': location, 'offset': 0})
def parse_search_results(self, response):
pass
توضیح ورودی/خروجی و نقش توابع:
در صفحات جستجو، دادهٔ نتایج داخل یک تگ <script id="mosaic-data"> و در متغیر window.mosaic.providerData["mosaic-provider-jobcards"] قرار دارد. از یک الگوی regex میتوان این بلاک JSON را جدا کرد و سپس با json.loads پارس کرد. الگوی معمولی (non-greedy) شکل زیر است:
pattern = r'window.mosaic.providerData\["mosaic-provider-jobcards"\]=(\{.+?\});'نمونهٔ کاملتر برای parse_search_results که هم صفحهبندی را محاسبه میکند و هم jobkeyها را استخراج میکند:
def parse_search_results(self, response):
location = response.meta['location']
keyword = response.meta['keyword']
offset = response.meta['offset']
script_tag = re.findall(r'window.mosaic.providerData\["mosaic-provider-jobcards"\]=(\{.+?\});', response.text)
if not script_tag:
return
json_blob = json.loads(script_tag[0])
# صفحهبندی: در صفحهٔ اول تعداد نتایج را محاسبه میکنیم
if offset == 0:
meta_data = json_blob['metaData']['mosaicProviderJobCardsModel']['tierSummaries']
num_results = sum(category['jobCount'] for category in meta_data)
num_results = min(num_results, 50) # برای مثال محدود به 50 میکنیم
for off in range(10, num_results + 10, 10):
url = self.get_indeed_search_url(keyword, location, off)
yield scrapy.Request(url=url, callback=self.parse_search_results, meta={'keyword': keyword, 'location': location, 'offset': off})
# استخراج لیست آگهیها
jobs_list = json_blob['metaData']['mosaicProviderJobCardsModel']['results']
for index, job in enumerate(jobs_list):
if job.get('jobkey'):
job_url = 'https://www.indeed.com/m/basecamp/viewjob?viewtype=embedded&jk=' + job.get('jobkey')
yield scrapy.Request(url=job_url, callback=self.parse_job, meta={'keyword': keyword, 'location': location, 'page': round(offset/10)+1 if offset>0 else 1, 'position': index, 'jobKey': job.get('jobkey')})
توضیح خطبهخط (خلاصه):
صفحهٔ هر شغل نیز دادهٔ اصلی را داخل یک تگ <script> در متغیر window._initialData نگهداری میکند. همینجا میتوانیم JSON را استخراج و فیلدهای مورد نیاز را خوانده و yield کنیم (که در Scrapy به خروجی تبدیل میشود).
def parse_job(self, response):
keyword = response.meta.get('keyword')
location = response.meta.get('location')
page = response.meta.get('page')
position = response.meta.get('position')
script_tag = re.findall(r'_initialData=(\{.+?\});', response.text)
if not script_tag:
return
json_blob = json.loads(script_tag[0])
job = json_blob['jobInfoWrapperModel']['jobInfoModel']
header = job.get('jobInfoHeaderModel', {})
sanitized = job.get('sanitizedJobDescription')
yield {
'keyword': keyword,
'location': location,
'page': page,
'position': position,
'company': header.get('companyName'),
'jobkey': response.meta.get('jobKey'),
'jobTitle': header.get('jobTitle'),
'jobDescription': sanitized,
}
نکات عملی دربارهٔ parse_job:
برای شروع ساده، از قابلیت Feed Exports در Scrapy استفاده کنید. در settings.py میتوانید مسیر داینامیک بسازید تا هر اجرا یک فایل جدید تولید شود:
# settings.py
FEEDS = {
'data/%(name)s_%(time)s.csv': {
'format': 'csv',
},
}
نکات و بهترینروشها:
اگر چند بار اسکریپ را اجرا کنید احتمال دیدن صفحهٔ بلاک یا CAPTCHA وجود دارد. راهکارهای معمول:
مثالی از مسیر کردن درخواست از طریق یک پراکسی (تابع کمکی) و استفادهٔ آن در Requestها:
def get_scrapeops_url(self, url, api_key):
# api_key باید از محیط یا vault خوانده شود، نه هاردکد
from urllib.parse import urlencode
payload = {'api_key': api_key, 'url': url, 'bypass': 'cloudflare_level_1'}
return 'https://proxy.scrapeops.io/v1/?' + urlencode(payload)
# استفاده در زمان ساخت درخواست:
# yield scrapy.Request(url=self.get_scrapeops_url(job_url, SCRAPEOPS_API_KEY), ...)
نکات امنیتی و عملی:
در محیط تولید، اگر از یک SDK پراکسی یا سامانهٔ مانیتورینگ استفاده میکنید، معمولاً باید یک Downloader Middleware و/یا Extension را در settings.py فعال کنید. مثال نمونهٔ پیکربندی برای فعال شدن میدلور و یک extension مانیتورینگ:
# settings.py (نمونه)
SCRAPEOPS_API_KEY = 'YOUR_API_KEY'
SCRAPEOPS_PROXY_ENABLED = True
DOWNLOADER_MIDDLEWARES = {
'scrapeops_scrapy_proxy_sdk.scrapeops_scrapy_proxy_sdk.ScrapeOpsScrapyProxySdk': 725,
}
EXTENSIONS = {
'scrapeops_scrapy.extension.ScrapeOpsMonitor': 500,
}
DOWNLOADER_MIDDLEWARES.update({
'scrapeops_scrapy.middleware.retry.RetryMiddleware': 550,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
})
توضیح: این تنظیمات باعث میشود درخواستها از طریق پراکسی SDK ارسال شوند و یک Extension ساده، متریکها و لاگهای اجرا را به داشبورد مانیتورینگ ارسال کند.
برای اجراهای منظم (مثلاً روزانه یا هفتگی) چند گزینه دارید: سرور اختصاصی، container در سرویسهای ابری یا استفاده از سرویسهای زمانبندی Jobs. نکات عملی:
خلاصهٔ مراحل عملی برای ساخت یک Indeed Spider قابلاستفاده در تولید:
توصیههای عملی سریع:
اگر بخواهید میتوانم نمونهٔ کاملتر پروژهٔ Scrapy با فایلهای settings.py، pipelines و مثالهایی برای ارسال به Postgres/S3 را هم تهیه کنم یا نسخهای از همین اسکریپ را بهصورت قابل اجرا برای شما بازنویسی کنم.