خانه/مقالات/اسکریپینگ با Scrapy برای جاب‌های LinkedIn
برنامه نویسی
وب اسکریپینگ
پروکسی و چرخش IP
برگشت به صفحه مقاله ها
اسکریپینگ با Scrapy برای جاب‌های LinkedIn

اسکریپینگ با Scrapy برای جاب‌های LinkedIn

این مقاله یک راهنمای عملی و فنی برای ساخت اسکریپر جاب‌های LinkedIn با Python Scrapy است؛ از کشف endpoint و نوشتن اسپایدر تا صفحه‌بندی، ادغام پراکسی و مانیتورینگ. نکات امنیتی، بهترین‌روش‌ها و مثال‌های کدی برای اجرای تولیدی نیز پوشش داده شده‌اند.
آسان اسکریپ
آسان اسکریپ
1404-12-05

مقدمه

در این مقاله قدم‌به‌قدم می‌فهمیم چگونه با Python Scrapy یک اسکریپر تولیدی برای جمع‌آوری آگهی‌های شغلی از LinkedIn بسازیم. هدف این راهنما این است که پس از خواندن آن بتوانید:

  • نقطهٔ ورود API یا درخواست‌های شبکهٔ مناسب را شناسایی کنید،
  • یک اسپایدر Scrapy بسازید که داده‌های ساختاریافتهٔ مشاغل را برمی‌گرداند،
  • صفحه‌بندی (pagination) را مدیریت کنید،
  • روش‌هایی برای عبور از محافظ‌های ضدربات و مانیتورینگ پروژه پیاده کنید،
  • و نکات عملی برای اجرا در محیط تولید را به‌کار ببرید.

لحن مقاله فنی و آموزشی است و فرض می‌کنیم خواننده با پایتون و مفاهیم پایهٔ Scrapy آشناست.

پیدا کردن endpoint مناسب (بررسی Network)

ایدهٔ کلی: قبل از نوشتن هر کدی، باید ببینیم آیا صفحه درخواست‌های API مستقیم (XHR/fetch) می‌فرستد که حاوی دادهٔ مشاغل است یا خیر. اگر چنین است، معمولاً ساده‌تر و مقیاس‌پذیرتر است که پاسخ API را بخوانیم به‌جای رندر کامل HTML صفحه.

مراحل عملی:

  1. در مرورگر Developer Tools > Network را باز کنید.
  2. فیلتر روی XHR یا Fetch بگذارید و یک جستجوی شغل انجام دهید (مثلاً عنوان "Python" و کشور "United States").
  3. به‌سمت پایین اسکرول کنید تا بارگذاری نتایج بعدی باعث یک درخواست جدید شود؛ معمولاً پارامتری مانند start یا offset برای صفحه‌بندی وجود دارد.
  4. در تب Preview یا Response، ساختار HTML یا JSON بازگشتی را بررسی کنید؛ اگر حاوی لیست عناصر شغلی است، آن endpoint هدف مناسبی است.

نکات:

  • بعضی از APIها حداکثر تعداد آیتم در هر درخواست را محدود می‌کنند (مثلاً 25 آیتم)، پس باید صفحه‌بندی را پیاده کنید.
  • همیشه Headerها و Payload را بررسی کنید تا ببینید چه هدرهای ضروری (مثل cookies یا رمزنگاری CSRF) باید ارسال شوند.

اسپایدر پایهٔ Scrapy برای اولین صفحه

در این بخش یک اسپایدر ساده را می‌بینیم که اولین صفحه (اولین 25 آگهی) را از endpoint بازخوانی و پارس می‌کند. کد نمونه و سپس توضیحات فنی را ارائه می‌کنیم.

import scrapy

class LinkedJobsSpider(scrapy.Spider):
    name = "linkedin_jobs"
    api_url = (
        'https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?'
        'keywords=python&location=United%2BStates&geoId=103644278&start='
    )

    def start_requests(self):
        first_job_on_page = 0
        first_url = self.api_url + str(first_job_on_page)
        yield scrapy.Request(url=first_url, callback=self.parse_job, meta={'first_job_on_page': first_job_on_page})

    def parse_job(self, response):
        first_job_on_page = response.meta['first_job_on_page']
        jobs = response.css('li')
        for job in jobs:
            job_item = {}
            job_item['job_title'] = job.css('h3::text').get(default='not-found').strip()
            job_item['job_detail_url'] = job.css('.base-card__full-link::attr(href)').get(default='not-found').strip()
            job_item['job_listed'] = job.css('time::text').get(default='not-found').strip()
            job_item['company_name'] = job.css('h4 a::text').get(default='not-found').strip()
            job_item['company_link'] = job.css('h4 a::attr(href)').get(default='not-found')
            job_item['company_location'] = job.css('.job-search-card__location::text').get(default='not-found').strip()
            yield job_item

توضیح تابع‌ها و ورودی/خروجی:

  • start_requests: بدون پارامتر ورودی اجرا می‌شود؛ اولین URL را با پارامتر صفحه صفر می‌سازد و درخواست را ارسال می‌کند (yield می‌کند)؛ خروجی درخواست‌های Scrapy است.
  • parse_job(self, response): ورودی یک شیٔ response است؛ از response.meta مقدار ایندکس صفحه را می‌گیرد و با CSS selector المان‌های <li> را که حاوی آگهی‌ها هستند می‌خواند. خروجی این تابع آیتم‌های دیکشنری‌شده است که توسط Scrapy به pipeline یا خروجی فایل ارسال می‌شود.

تفسیر خط‌به‌خط (خلاصه):

  1. جمع‌آوری لی از تگ‌های <li> که هرکدام یک آگهی را نمایش می‌دهند.
  2. برای هر آگهی، با سلکتورهای CSS فیلدهای لازم استخراج و تمیز (strip) می‌شوند.
  3. هر آیتم با yield به خروجی فرستاده می‌شود.

صفحه‌بندی: درخواست صفحات بعدی

ایدهٔ کلی: چون هر پاسخ حداکثر 25 نتیجه برمی‌گرداند، باید تا زمانی که پاسخ خالی نشد یا وضعیت خطا نداد، پارامتر start را به‌صورت افزایشی (+25) ارسال کنیم.

# بخش‌های اضافه‌شده در داخل parse_job بعد از استخراج آیتم‌ها
num_jobs_returned = len(jobs)
if num_jobs_returned > 0:
    first_job_on_page = int(first_job_on_page) + 25
    next_url = self.api_url + str(first_job_on_page)
    yield scrapy.Request(url=next_url, callback=self.parse_job, meta={'first_job_on_page': first_job_on_page})

نکات عملی و بهترین روش‌ها:

  • برای جلوگیری از حلقهٔ نامتناهی، حتماً سقف منطقی برای تعداد صفحات تعیین کنید یا شرط توقف را (مثلاً تعداد کل آیتم‌های موردنظر) اعمال کنید.
  • مدیریت خطا: اگر API وضعیت 4xx یا 5xx بازگرداند، از منطق retry محدود و لاگ دقیق استفاده کنید؛ از middlewareهای Retry و کتابخانهٔ logging بهره ببرید.
  • تاخیر بین درخواست‌ها (download delay) و concurrency را متناسب با ظرفیت هدف و قراردادهای استفاده تنظیم کنید تا ریسک بلاک شدن کمتر شود.

عبور از محافظ‌های ضدربات (Anti-bot)

LinkedIn یکی از محافظ‌های پیچیدهٔ ضداسکریپ را دارد که بر مبنای ترکیبی از IP، هدرها، فینگرپرینت مرورگر و رفتار کاربر کار می‌کند. سه رویکرد عملی وجود دارد:

  1. استفاده از پراکسی‌های با کیفیت (rotating residential/mobile proxies) و چرخش IP
  2. چرخش User-Agent و هدرهای متناسب با مرورگر واقعی
  3. در صورت نیاز، رندر سمت‌کلاینت با headless browserهای تقویت‌شده و مدیریت فینگرپرینت

هشدار قانونی و اخلاقی: تلاش برای دسترسی به بخش‌هایی که نیاز به ورود (login) دارند ممکن است ریسک حقوقی و نقض شرایط سرویس داشته باشد؛ این راهنما بر جمع‌آوری داده‌های عمومی و بدون لاگین تمرکز دارد.

یک نمونهٔ سادهٔ curl که درخواست را از طریق یک پراکسی سرویس خارجی می‌فرستد (آدرس‌ها و کلیدها را جایگزین کنید):

curl 'PROXY_API_ENDPOINT?api_key=YOUR_API_KEY&url=https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=python&location=United%20States&start=0'

نکته‌های فنی:

  • فقط به تغییر IP اکتفا نکنید؛ باید رفتار مرورگر و هدرها را نیز متنوع کنید تا TCP/Browser fingerprinting را کاهش دهید.
  • اگر از headless browser استفاده می‌کنید، از روش‌هایی برای مدیریت کلیک/اسکرول و اجرای JavaScript بهره ببرید، اما این روش هزینهٔ منابع را بالا می‌برد.

ادغام پراکسی و مانیتورینگ در پروژه Scrapy

برای ساده‌تر شدن کار در محیط تولید معمولاً از SDKها یا middlewareهایی استفاده می‌شود که پراکسی، چرخش UA و تشخیص بن‌ها را مدیریت می‌کنند. در Scrapy این چیزها معمولاً از طریق تغییرات در settings.py اضافه می‌شوند.

# settings.py (نمونه)
SCRAPEOPS_API_KEY = 'YOUR_API_KEY'
SCRAPEOPS_PROXY_ENABLED = True

DOWNLOADER_MIDDLEWARES = {
    'scrapeops_scrapy_proxy_sdk.scrapeops_scrapy_proxy_sdk.ScrapeOpsScrapyProxySdk': 725,
}

برای نصب پکیج پراکسی یا ابزار مانیتورینگ معمولاً از pip استفاده می‌شود:

pip install scrapeops-scrapy
pip install scrapeops-scrapy-proxy-sdk

مانیتورینگ: در محیط تولید ضروری است که عملکرد اسپایدرها را مانیتور کنید (نرخ خطا، زمان پاسخ، تعداد آیتم‌ها). با یک سیستم مانیتورینگ می‌توانید هشدار برای کاهش نرخ خروجی یا افزایش خطاها تنظیم کنید.

# مثال ادغام مانیتورینگ در settings.py
EXTENSIONS = {
    'scrapeops_scrapy.extension.ScrapeOpsMonitor': 500,
}

DOWNLOADER_MIDDLEWARES.update({
    'scrapeops_scrapy.middleware.retry.RetryMiddleware': 550,
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
})

تضمین پایداری و عملکرد (Performance)

نکات کلیدی برای اجرا در مقیاس:

  • از connection pooling و session reuse در صورت نیاز به درخواست‌های مکرر استفاده کنید.
  • محدودیت همزمانی (CONCURRENT_REQUESTS) و DOWNLOAD_DELAY را طوری تنظیم کنید که ترکیب مناسبی بین سرعت و پایداری حاصل شود.
  • در صورت نیاز خروجی را به صف یا دیتابیس (مثلاً یک pipeline سفارشی) ارسال کنید تا از حافظهٔ محلی پروژه محافظت کنید.
  • برای parsing سنگین، از استریم یا پردازش دسته‌ای استفاده کنید تا مصرف مموری کنترل شود.

استقرار و زمان‌بندی (Scheduling & Running In Cloud)

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

  • نسخه‌سازی کد اسپایدر و نگهداری تنظیمات اتصال به پراکسی/مانیتورینگ را به‌صورت امن (مثل متغیرهای محیطی) انجام دهید.
  • تنظیم هشدار برای خطاها و کاهش نرخ استخراج بسیار مهم است.
  • لاگ‌ها را به یک سیستم متمرکز ارسال کنید تا تحلیل مشکلات ساده‌تر شود.

نمونهٔ خروجی JSON یک آیتم

{
  "job_title": "Python",
  "job_detail_url": "https://www.linkedin.com/jobs/view/...",
  "job_listed": "1 day ago",
  "company_name": "Example Corp",
  "company_link": "https://www.linkedin.com/company/...",
  "company_location": "Texas, United States"
}

بهترین روش‌ها و هشدارها

  • همیشه حقوق مالک داده و شرایط استفادهٔ وب‌سایت را بررسی کنید؛ اسکریپ کردن محتوای نیازمند ورود ریسک قانونی دارد.
  • از تست محلی و بررسی دقیق network قبل از اجرای در مقیاس استفاده کنید.
  • مقیاس‌پذیری را از ابتدا در نظر بگیرید: ورود به pipeline ذخیره‌سازی، retry محدود، و timeouts مشخص.
  • برای تشخیص block و CAPTCHA یک استراتژی تشخیصی داشته باشید (مثلاً بررسی محتوای HTML بازگشتی یا کد وضعیت HTTP).

جمع‌بندی

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

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