خانه/مقالات/اسکریپینگ سریع با Hrequests و ThreadPoolExecutor
برنامه نویسی
پروکسی و چرخش IP
برگشت به مقاله‌ها

اسکریپینگ سریع با Hrequests و ThreadPoolExecutor

اسکریپینگ سریع با Hrequests و ThreadPoolExecutor
در این راهنما روش‌های عملی برای ارسال درخواست‌های هم‌زمان با Hrequests در پایتون شرح داده شده است: از استفادهٔ سادهٔ ThreadPoolExecutor تا نسخه‌های امن‌تر با futures و یکپارچه‌سازی با پروکسی جمع‌گر. نکات مهم در انتخاب تعداد تردها، مدیریت خطا، timeout، و بهترین روش‌های جمع‌آوری نتایج نیز پوشش داده شده‌اند.
آسان اسکریپ آسان اسکریپ
1405-05-03

مقدمه

این مقاله به شما نشان می‌دهد چطور با کتابخانهٔ Hrequests در پایتون درخواست‌های هم‌زمان (concurrent) ارسال کنید تا سرعت اسکریپینگ افزایش یابد. در پایان این راهنما یاد می‌گیرید چگونه از ThreadPoolExecutor برای موازی‌سازی درخواست‌ها استفاده کنید، چطور خروجی‌ها را امن جمع‌آوری کنید، و چگونه Hrequests را با یک پروکسی جمع‌گر مانند ScrapeOps یکپارچه کنید.

خوانندهٔ فرضی: توسعه‌دهندهٔ پایتون با سطح متوسط که به دنبال مثال‌های عملی، نکات مربوط به خطا، مدیریت زمان‌بندی و توصیه‌های عملکردی است.

ایدهٔ کلی و چرا ThreadPoolExecutor

ایده ساده است: به‌جای ارسال درخواست‌ها به‌صورت ترتیبی، چند worker (نخ/ترد) هم‌زمان کار ارسال و پردازش پاسخ را انجام می‌دهند. در بسیاری از اسکریپ‌ها تاخیر اصلی در I/O (انتظار برای پاسخ سرور) است؛ پس با افزایش هم‌زمانی می‌توان صرفه‌جویی بزرگی در زمان داشت.

جایگزین‌ها: asyncio (برای I/O غیرهمزمان)، multiprocessing (برای CPU-bound)، یا کتابخانه‌های مبتنی بر gevent. ThreadPoolExecutor برای بسیاری از پروژه‌های اسکریپینگ ساده و خوانا است چون با API هم‌شکل requests/Hrequests سازگار است.

مثال پایه: ThreadPoolExecutor با Hrequests

ایدهٔ کلی:

  1. تعریف لیست URLها
  2. نوشتن تابعی که یک URL می‌گیرد و اطلاعات موردنظر را استخراج می‌کند
  3. ساخت یک ThreadPool و ارسال URLها برای اجرای تابع

نسخهٔ تمیز شده و با بهبودهای کوچک (timeout و قفل برای جمع‌آوری امن خروجی):

import hrequests
from bs4 import BeautifulSoup
import concurrent.futures
import threading

NUM_THREADS = 5
list_of_urls = [
    'http://quotes.toscrape.com/page/1/',
    'http://quotes.toscrape.com/page/2/',
    'http://quotes.toscrape.com/page/3/',
    'http://quotes.toscrape.com/page/4/',
    'http://quotes.toscrape.com/page/5/',
]

output_data_list = []
lock = threading.Lock()

def scrape_page(url):
    try:
        response = hrequests.get(url, timeout=10)
        if response.status_code == 200:
            soup = BeautifulSoup(response.text, "html.parser")
            title_tag = soup.find('h1')
            title = title_tag.text.strip() if title_tag else None
            with lock:
                output_data_list.append({'url': url, 'title': title})
    except Exception as e:
        # لاگ کردن خطا برای بررسی بعدی
        with lock:
            output_data_list.append({'url': url, 'error': str(e)})

with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
    executor.map(scrape_page, list_of_urls)

print(output_data_list)

توضیح اجزای مهم:

  • NUM_THREADS: تعداد workerهای هم‌زمان. این عدد را بسته به منابع محلی و محدودیت‌های سرور/پروکسی تنظیم کنید.
  • timeout=10: همیشه timeout مشخص کنید تا تردها برای همیشه در انتظار نمانند.
  • lock: دسترسی هم‌زمان به لیست خروجی را سینک می‌کند. دسترسی به ساختارهای مشترک بدون قفل می‌تواند موجب race condition شود.

نسخهٔ بهبودیافته: برگرداندن نتایج از futures و استفادهٔ امن از Session

یک الگوی بهتری که از global list اجتناب می‌کند، برگرداندن نتایج از هر ترد با استفاده از futures است. در این نمونه، برای هر URL نتیجهٔ تابع برگردانده می‌شود و در حلقهٔ اصلی جمع‌آوری می‌شود. همچنین الگوهایی برای retry ساده درون تابع نشان داده شده است:

import hrequests
from bs4 import BeautifulSoup
import concurrent.futures

NUM_THREADS = 5
list_of_urls = [...]

def scrape_page(url):
    last_exc = None
    for attempt in range(3):
        try:
            # در این نمونه، session درون تابع ساخته می‌شود تا از مشکلات thread-safety جلوگیری شود
            with hrequests.Session() as session:
                r = session.get(url, timeout=10)
                r.raise_for_status()
                soup = BeautifulSoup(r.text, "html.parser")
                title_tag = soup.find('h1')
                return {'url': url, 'title': title_tag.text.strip() if title_tag else None}
        except Exception as e:
            last_exc = e
    return {'url': url, 'error': str(last_exc)}

results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
    future_to_url = {executor.submit(scrape_page, u): u for u in list_of_urls}
    for future in concurrent.futures.as_completed(future_to_url):
        results.append(future.result())

print(results)

نکته دربارهٔ Session: برخی از کاربران session مشترک بین تردها را بدون مشکل استفاده می‌کنند تا از connection pooling بهره‌مند شوند، اما این رویکرد می‌تواند در برخی کتابخانه‌ها ناپایدار باشد. امن‌ترین راه، یا استفاده از session درون هر ترد یا استفاده از ابزارهایی مثل Thread-local session است.

اضافه کردن ScrapeOps (یا هر پروکسی جمع‌گر) برای افزایش هم‌زمانی واقعی

اگر از یک سرویس پروکسی جمع‌گر استفاده می‌کنید، ممکن است برنامهٔ پروکسی شما محدودیت هم‌زمانی یا thread pool مخصوص به خود را داشته باشد. مثال زیر نشان می‌دهد چطور URL را به شکل مورد نیاز پروکسی تبدیل کنید و تعداد تردها را مطابق پلان پروکسی تنظیم کنید:

import hrequests
from bs4 import BeautifulSoup
import concurrent.futures
from urllib.parse import urlencode

SCRAPEOPS_API_KEY = 'YOUR_API_KEY'
NUM_THREADS = 5
list_of_urls = [...]

def get_scrapeops_url(url):
    payload = {'api_key': SCRAPEOPS_API_KEY, 'url': url}
    proxy_url = 'https://proxy.scrapeops.io/v1/?' + urlencode(payload)
    return proxy_url

def scrape_page_via_proxy(url):
    try:
        proxy_url = get_scrapeops_url(url)
        response = hrequests.get(proxy_url, timeout=15)
        response.raise_for_status()
        soup = BeautifulSoup(response.text, "html.parser")
        title_tag = soup.find('h1')
        return {'url': url, 'title': title_tag.text.strip() if title_tag else None}
    except Exception as e:
        return {'url': url, 'error': str(e)}

with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
    results = list(executor.map(scrape_page_via_proxy, list_of_urls))

print(results)

نکات مهم:

  • تعداد NUM_THREADS را بر اساس محدودیت‌های پروکسی و پلن خود انتخاب کنید؛ اگر بیش از حد بالا برود، ممکن است درخواست‌ها ری‌جکت یا throttle شوند.
  • استفاده از پروکسی‌ها می‌تواند IP rotation و مدیریت بلوکه شدن را آسان کند، اما هزینه و محدودیت‌های API را هم دارد.

نکات عملی، مدیریت خطا و بهینه‌سازی

  • Timeout و retry: همیشه timeout تعیین کنید و برای خطاهای موقتی (502، 429، timeout) مکانیزم retry با backoff افزایشی داشته باشید.
  • Respect robots.txt و قوانین سایت: قبل از اسکریپ کردن، سیاست‌های سایت را بررسی کنید تا از مسائل قانونی یا اخلاقی جلوگیری شود.
  • User-Agent و هدرها: هدرهای مناسب بفرستید و در صورت نیاز هدرها را با هر درخواست/ترد تغییر دهید تا رفتار طبیعی‌تری داشته باشید.
  • Rate limiting: پروکسی یا سرور هدف را بار زیاد نکنید؛ اضافه کردن تاخیر یا محدود‌سازی نرخ بر اساس پاسخ سرور کمک می‌کند.
  • جمع‌آوری نتایج: به‌جای لیست‌های سراسری از futures یا صف‌های امن (مثل queue.Queue یا پایگاه داده) استفاده کنید تا پایداری افزایش یابد.
  • پروفایلینگ و مانیتورینگ: زمان پاسخ، نرخ خطا و درصد موفقیت را لاگ و مانیتور کنید تا گلوگاه‌ها را پیدا کنید.

ملاحظات امنیتی و عملیاتی

هر زمان کلید API یا پارامترهای حساس دارید از محیط‌های امن برای نگهداری متغیرها استفاده کنید (مثلاً متغیرهای محیطی یا secret manager). داده‌های جمع‌آوری‌شده را قبل از ذخیره‌سازی اعتبارسنجی کنید تا از injection یا دادهٔ مخرب جلوگیری شود.

جمع‌بندی

با استفاده از ThreadPoolExecutor می‌توانید به‌سرعت درخواست‌های وب را موازی کنید و زمان کلی اسکریپینگ را به‌طرز چشمگیری کاهش دهید. ولی انتخاب تعداد تردها، مدیریت خطا، نحوهٔ جمع‌آوری نتایج و استفادهٔ درست از session و پروکسی‌ها تعیین‌کنندهٔ موفقیت شماست. اگر به پایداری و مقیاس نیاز دارید، به الگوهایی مانند بازگشت نتایج با futures، retry با backoff، و یکپارچه‌سازی با پروکسی‌ها فکر کنید.

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

مطالب مرتبط

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