وب اسکرپینگ با پایتون
وب اسکرپینگ با پایتون، هنر استخراج داده از وب است — از مباحث حقوقی و اخلاقی تا requests، BeautifulSoup، Selenium و Scrapy. همان مسیری که در پروژههای
اولین بار که به وب اسکرپینگ نیاز پیدا کردم، برای یک پروژهی کوچک قیمتگذاری بود: کارفرما میخواست قیمت رقبای خود را در بازار آنلاین، هفتگی پایش کند و اگر کسی قیمتش را شکست، سریع مطلع شود. با PHP و curl شروع کردم، ولی در همان هفتهی اول به دیوار خوردم: صفحههای مقصد جاوااسکریپت داشتند، ساختار HTML مدام عوض میشد، و وقتی درخواستها را پشتسرهم زدم، IP من بلاک شد. آن تجربه به من یاد داد که وب اسکرپینگ با پایتون فقط نوشتن چند خط کد برای دانلود HTML نیست؛ یک فرآیند دقیق است که از ملاحظات حقوقی و اخلاقی شروع میشود و تا مدیریت پروکسی، تحمل خطا و ذخیرهسازی داده ادامه دارد. در این مقاله، همان مسیری را میروم که در پروژههای واقعی طی کردهام: از اولین درخواست تا یک اسکرپر پایدار که ماهها بدون دردسر کار میکند.
وب اسکرپینگ دقیقاً چیست؟
اگر با مفاهیم پایهی پایتون آشنا نیستید، اول آموزش پایتون از صفر را بخوانید. اما فرض کنیم پایتون را میشناسید. وب اسکرپینگ فرآیند استخراج داده از صفحات وب است به شکلی خودکار — یعنی بهجای اینکه یک انسان صفحه را باز کند و اطلاعات را کپی کند، برنامه این کار را انجام میدهد. این کار میتواند در مقیاس کوچک (چند صفحه در هفته) یا بزرگ (میلیونها صفحه در روز) باشد.
یک تفکیک مهم که همیشه به تازهکارها میگویم: اسکرپینگ با API گرفتن متفاوت است. اگر یک وبسایت API رسمی دارد، همیشه اول سراغ آن بروید — چون قانونی، پایدارتر و سریعتر است. اگر با مفهوم API آشنا نیستید، API چیست و چه کاربردی دارد تفاوت این دو را روشن میکند. اسکرپینگ تنها زمانی منطقی است که API وجود ندارد یا دادهای که میخواهید در API نیست.
سه سناریوی رایج که در پروژههای واقعی دیدهام:
- پایش قیمت رقبا: استخراج قیمت محصولات از سایتهای فروشگاهی برای تحلیل بازار یا تنظیم قیمت خودکار.
- جمعآوری داده برای تحلیل: استخراج نظرات کاربران، اخبار، یا دادههای عمومی برای پروژههای داده و یادگیری ماشین. اگر به این حوزه علاقه دارید، شروع یادگیری ماشین مسیر دادهمحور را نشان میدهد.
- اتوماسیون کارهای دستی: پر کردن فرمهای تکراری، دانلود گزارشهای دورهای، یا استخراج داده از پنلهای داخلی که API ندارند.
وب اسکرپینگ یک تکنیک نیست، یک مجموعهی تصمیم است: اول تصمیم میگیرید که اصلاً باید این کار را بکنید، بعد با چه ابزاری، بعد چطور پایدار نگهش دارید.
مرزهای حقوقی و اخلاقی که جدی میگیرم
قبل از نوشتن هر خط کد، این بخش را جدی بگیرید. وب اسکرپینگ، از نظر حقوقی و اخلاقی، یک منطقهی خاکستری است که در آن چند اصل روشن وجود دارد:
فایل robots.txt
هر سایت، یک فایل robots.txt دارد که مشخص میکند کدام بخشها برای رباتها آزاد و کدام ممنوع است. اگرچه robots.txt از نظر فنی فقط یک توصیه است، ولی از نظر اخلاقی و در برخی حوزههای قضایی، از نظر حقوقی هم مهم است. قبل از هر پروژه، به example.com/robots.txt بروید و بخش مرتبط با User-agent: * را بخوانید. اگر مسیری Disallow است، از آن صرفنظر کنید.
شرایط استفاده
بسیاری از سایتها در Terms of Service خود، اسکرپینگ را ممنوع اعلام کردهاند. اگر سایت هدف، این ممنوعیت را صریحاً بیان کرده، ادامه ندهید — چون در صورت شکایت، ممکن است به مشکل حقوقی بربخورید. یک قاعدهی شخصی که در پروژهها رعایت میکنم: اگر سایت API دارد یا فایل دادهی رسمی منتشر میکند، از همان استفاده کنم و اسکرپینگ را رها کنم.
بار سرور و نرخ درخواست
اگر سایت کوچکی با منابع محدود است، درخواستهای پشتسرهم میتواند بهمعنی واقعی به آن آسیب بزند. اصل اخلاقی ساده است: مثل یک کاربر عادی رفتار کنید، نه مثل یک حملهی DDoS. در بخش نرخ درخواست به این نکته مفصل برمیگردم.
دادههای شخصی
هرگز دادههای شخصی کاربران را جمعآوری نکنید، مخصوصاً اطلاعاتی که در اتحادیهی اروپا تحت GDPR محافظت میشوند. اگر مخاطب پروژهتان اروپایی است، حتی دادههای عمومی میتوانند مشمول قوانین باشند — همان اصولی که در GDPR و تأثیر آن بر وبسایتهای ایرانی رویشان تأکید کردهام. حتی اگر کاربران ایرانی هستند، اصل احترام به حریم خصوصی را نقض نکنید.
قانونی بودن، لازم است ولی کافی نیست؛ اخلاقی بودن، همان مرزی است که پروژهی شما را از یک ابزار مزاحم، به یک ابزار مفید تبدیل میکند.
ابزارهای اصلی پایتون برای اسکرپینگ
پایتون اکوسیستم غنیای برای اسکرپینگ دارد. انتخاب ابزار درست، بیشتر از هر چیز، به نوع سایت هدف و مقیاس پروژه بستگی دارد:
| ابزار | مناسب برای | مزیت اصلی |
|---|---|---|
| requests | درخواستهای HTTP ساده | سبک و ساده |
| BeautifulSoup | تجزیهی HTML و XML | رابط پایتونیک و قابل تحمل |
| lxml | تجزیهی سریعتر | عملکرد بالا |
| Selenium | سایتهای پویا با جاوااسکریپت | کنترل مرورگر واقعی |
| Playwright | سایتهای پویا (جایگزین مدرن Selenium) | API تمیزتر و سریعتر |
| Scrapy | پروژههای بزرگ | معماری کامل و مقیاسپذیر |
ترکیب پیشفرض من برای ۸۰٪ پروژهها: requests + BeautifulSoup. برای آن ۲۰٪ باقیمانده، اگر سایت پویا باشد، Playwright؛ اگر مقیاس بزرگ باشد، Scrapy. نصب این کتابخانهها با pip انجام میشود:
pip install requests beautifulsoup4 lxml
pip install playwright
python -m playwright install
اگر با مدیریت وابستگی در پایتون آشنا نیستید، آموزش پایتون از صفر محیط مجازی و pip را توضیح میدهد. عادت کنید هر پروژهی اسکرپینگ را در یک venv جدا بسازید، چون تعداد کتابخانههای آن زیاد میشود.
اولین قدم: درخواست با requests
سادهترین درخواست، یک خط کد است:
import requests
response = requests.get("https://example.com")
print(response.status_code)
print(response.text[:500])
ولی در پروژههای واقعی، هرگز بدون تنظیمات اضافی درخواست نمیفرستم. حداقل چهار کار باید انجام دهید:
۱) تنظیم User-Agent
بسیاری از سایتها درخواستهای بدون User-Agent معتبر یا با User-Agent پیشفرض requests را بلاک میکنند. یک User-Agent مرورگر واقعی بگذارید:
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
}
response = requests.get(url, headers=headers, timeout=10)
۲) تنظیم timeout
هرگز بدون timeout درخواست نفرستید. اگر سرور مقصد پاسخ ندهد، برنامهی شما ممکن است ساعتها معلق بماند. یک timeout ده ثانیهای، در اکثر موارد کافی است.
۳) مدیریت کد وضعیت
فقط کد ۲۰۰ بهمعنی موفقیت است. کدهای ۳xx نیاز به دنبالکردن ریدایرکت دارند، ۴xx خطای درخواست است، و ۵xx خطای سرور. برای هر کدام، رفتار متفاوتی طراحی کنید:
if response.status_code == 200:
handle_success(response.text)
elif response.status_code == 404:
log_missing(url)
elif response.status_code >= 500:
retry_later(url)
else:
log_unexpected(url, response.status_code)
۴) مدیریت استثناها
کتابخانهی requests در شرایط مختلف، استثناهای متفاوتی پرتاب میکند. در پروژههای واقعی، همیشه اینها را میگیرم:
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
except requests.Timeout:
log_error(url, "timeout")
except requests.ConnectionError:
log_error(url, "connection error")
except requests.HTTPError as e:
log_error(url, f"HTTP error: {e}")
این نوع مدیریت خطا، در پروژههای اسکرپینگ بزرگ، تفاوت بین یک اسکریپت شکننده و یک ابزار پایدار است. اصول کلی مدیریت خطا را در مدیریت خطا در پایتون با جزئیات بیشتری توضیح دادهام. اگر با یکی از خطاهای رایج روبرو شدید، رفع خطای ConnectionError در پایتون مسیر تشخیص و رفع را نشان میدهد.
تجزیهی HTML با BeautifulSoup
بعد از دریافت HTML، باید دادهی مورد نظر را از آن استخراج کنید. BeautifulSoup این کار را ساده میکند:
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, "lxml")
# یافتن یک عنصر
title = soup.find("h1").get_text(strip=True)
# یافتن همهی عناصر
links = soup.find_all("a", href=True)
# استفاده از CSS selector
prices = soup.select(".product .price")
چند ترفند که در پروژههای واقعی بهکارم آمده:
- انتخاب parser مناسب: از
lxmlاستفاده کنید نهhtml.parser. سرعتش چند برابر است و خطاهای HTML ناقص را تحمل میکند. - استفاده از CSS selector بهجای نام تگ:
soup.select(".product-price")ازsoup.find("span", class_="price")خواناتر است و کمتر میشکند. - استخراج متن با strip:
.get_text(strip=True)فاصلههای اضافی و شکست خط را حذف میکند. بدون آن، دادهها با فاصلههای عجیب ذخیره میشوند. - مراقب نبود عنصر باشید: اگر
soup.find("h1")نتیجهNoneبدهد و بعد.get_text()صدا بزنید، خطایAttributeErrorمیگیرید. همیشه بعد از find، بررسی کنید که نتیجهNoneنباشد.
title_tag = soup.find("h1")
title = title_tag.get_text(strip=True) if title_tag else None
الگوی دوم (با if else) در پروژههای واقعی، جلوی بسیاری از خطاها را گرفته است. برای یادگیری بیشتر دربارهی مدیریت دادههای ناقص، مدیریت خطا در پایتون نمونههای بیشتری دارد.
محتوای پویا و جاوااسکریپتی: Selenium
در سالهای اخیر، بخش بزرگی از سایتها محتوای اصلی خود را با جاوااسکریپت بارگذاری میکنند. در این حالت، requests فقط HTML خام را میگیرد که در آن هیچ دادهای نیست. راهحل، استفاده از یک مرورگر واقعی است که جاوااسکریپت را اجرا کند. Playwright گزینهی مدرنتر است، ولی Selenium هنوز در پروژههای زیادی استفاده میشود:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
page.wait_for_selector(".product-list")
products = page.eval_on_selector_all(
".product",
"els => els.map(el => el.innerText)"
)
browser.close()
سه نکتهی مهم در استفاده از این ابزارها:
- سرعت پایینتر: مرورگر واقعی، دهها برابر کندتر از یک درخواست HTTP است. اگر سایت مقصد پویا نیست، از آن استفاده نکنید.
- مصرف منابع: هر نمونهی مرورگر، چند صد مگابایت حافظه مصرف میکند. در پروژههای بزرگ، این مصرف باید در بودجهبندی سرور لحاظ شود.
- headless یا نه: در سرور، معمولاً حالت
headless=Trueاستفاده میکنم. ولی بعضی سایتها این حالت را تشخیص میدهند. در آن موارد، حالت headless با تنظیمات مشابه مرورگر واقعی، گاهی جواب میدهد.
قاعدهی من: تا وقتی که با requests و BeautifulSoup میتوانید داده را بگیرید، سراغ Selenium نروید. فقط وقتی سایت واقعاً نیاز به اجرای جاوااسکریپت دارد، سراغش بروید.
پروژههای بزرگ: Scrapy و معماری آن
وقتی مقیاس پروژه از چند هزار صفحه عبور میکند، نوشتن یک اسکریپت پایتونی ساده دیگر جواب نمیدهد. Scrapy یک چارچوب کامل است که مدیریت صف درخواستها، خروجی، تکراریگیری، و نرخ درخواست را خودش انجام میدهد. ساختار یک پروژهی Scrapy:
scrapy startproject myproject
cd myproject
scrapy genspider example example.com
سپس در فایل spider:
import scrapy
class ExampleSpider(scrapy.Spider):
name = "example"
start_urls = ["https://example.com/products"]
def parse(self, response):
for product in response.css(".product"):
yield {
"title": product.css("h2::text").get(),
"price": product.css(".price::text").get(),
}
next_page = response.css("a.next::attr(href)").get()
if next_page:
yield response.follow(next_page, self.parse)
و اجرا با:
scrapy crawl example -o products.json
مزیت Scrapy در پروژههای بزرگ، سه چیز است: مدیریت خودکار صف، امکان اجرای موازی چند سایت، و خروجی مستقیم به فرمتهای مختلف. اگر پروژهتان به این سطح از پیچیدگی رسیده، Scrapy انتخاب درست است. برای پروژههای کوچک، استفاده از آن شبیه به کشتن مگس با تانک است. اگر با مفهوم شیگرایی در پایتون راحت نیستید، آموزش شیگرایی در PHP مفاهیم پایه را میرساند که با کمی تطبیق، در پایتون هم مستقیم استفاده میشود — چون Scrapy کاملاً شیءگرا طراحی شده است.
احترام به سرور: نرخ درخواست و پروکسی
یکی از مهمترین بخشهای هر اسکرپر حرفهای، احترام به سرور مقصد است. اگر درخواستها را پشتسرهم بفرستید، سه اتفاق میافتد: سرور کند میشود، IP شما بلاک میشود، و اخلاقاً کار اشتباهی کردهاید.
تأخیر بین درخواستها
حداقل تأخیر یک ثانیه بین درخواستها، استاندارد اخلاقی است. در Scrapy این کار با تنظیم DOWNLOAD_DELAY انجام میشود:
# settings.py
DOWNLOAD_DELAY = 1
RANDOMIZE_DOWNLOAD_DELAY = True
در اسکریپتهای ساده، این کار را دستی انجام میدهم:
import time
import random
for url in urls:
response = requests.get(url, headers=headers, timeout=10)
process(response)
time.sleep(random.uniform(1, 3))
تأخیر تصادفی، از تأخیر ثابت بهتر است — چون الگوی درخواست را طبیعیتر میکند و تشخیصش برای سیستمهای ضدربات سختتر میشود.
پروکسی و چرخش IP
اگر بعد از مدتی، سرور مقصد IP شما را بلاک کرد، دو راه دارید: کمتر درخواست بفرستید (که همیشه انتخاب اولم است)، یا از پروکسی استفاده کنید. پروکسیهای رایگان اکثراً ناپایدارند و کند هستند. اگر پروژه واقعاً نیاز به پروکسی دارد، سرویسهای پولی مثل Bright Data یا Smartproxy منطقیترند. ولی قبل از آن، همیشه از خودتان بپرسید: آیا نیاز پروژه، این حجم از درخواست را واقعاً توجیه میکند؟
شناسایی و احترام به Retry-After
بسیاری از سرورها، وقتی کد وضعیت 429 Too Many Requests برمیگردانند، هدری به نام Retry-After میفرستند که میگوید چند ثانیه صبر کنید. این هدر را جدی بگیرید:
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", 60))
time.sleep(wait)
continue
اگر این کار را نکنید، سرور معمولاً شما را بهطور کامل بلاک میکند و ماهها طول میکشد تا دوباره اجازه بگیرید. تجربهی من در پروژههای واقعی: احترام به Retry-After، هم برای سرور مفید است و هم برای شما — چون فرصت دوباره میگیرید.
ذخیرهسازی داده: CSV، JSON و دیتابیس
پس از استخراج داده، آن را ذخیره میکنید. انتخاب فرمت، بستگی به کاربرد بعدی دارد:
- CSV: بهترین گزینه برای دادهی جدولی. با Excel یا Google Sheets قابل مشاهده است. حواستان به انکودینگ باشد — برای متن فارسی، UTF-8 با BOM بگذارید تا Excel درست بخواند.
- JSON: مناسب برای دادههای تودرتو یا وقتی میخواهید ساختار را حفظ کنید. برای API و انتقال داده به سرویسهای دیگر عالی است.
- دیتابیس: مناسب برای دادههای بزرگ یا وقتی میخواهید روی داده کوئری بزنید. SQLite برای شروع کافی است؛ برای پروژههای بزرگتر، PostgreSQL.
import csv
with open("products.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["title", "price", "url"])
writer.writeheader()
for row in data:
writer.writerow(row)
نکتهی مهم دربارهی encoding="utf-8-sig": اگر این کار را نکنید، Excel در ویندوز متن فارسی را بهصورت کاراکترهای عجیب نشان میدهد. برای دیتابیس، اگر با اتصال پایتون به MySQL آشنا هستید، میتوانید بهجای فایل، مستقیماً داده را به دیتابیس بریزید. مزیت دیتابیس این است که برای اجرای مجدد اسکرپر، میتوانید فقط دادههای جدید را ذخیره کنید — بهجای رونویسی کل فایل.
اگر دادهی حجیم دارید و میخواهید تحلیل کنید، کتابخانه pandas در پایتون مسیر پردازش سریع را نشان میدهد. پایپلاین طبیعی یک پروژهی اسکرپینگ بزرگتر: Scrapy برای استخراج، ذخیرهسازی در دیتابیس، و pandas برای تحلیل.
تحمل خطا در اسکرپرهای واقعی
یک اسکرپر واقعی، در ۹۹٪ موارد جواب میدهد؛ ولی در آن یک درصد باقیمانده، شکستهای غیرمنتظره رخ میدهد. سه الگو که در پروژههای خودم پیاده کردهام:
۱) سیستم log قابل جستجو
هر خطا، با URL، زمان، و پیام دقیق ثبت شود. اگر فقط print کنید، در پروژهی بزرگ، پیدا کردن منبع خطا ساعتها طول میکشد. من از logging استاندارد پایتون استفاده میکنم:
import logging
logging.basicConfig(
filename="scraper.log",
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logging.info(f"Fetched {url}")
logging.error(f"Failed {url}: timeout")
۲) Retry هوشمند
همهی خطاها را نمیتوان با یک retry ساده حل کرد. مثلاً Timeout یا ConnectionError معمولاً با یک بار تلاش دوباره حل میشوند، ولی خطای 404 بهمعنی عدم وجود صفحه است و retry فایدهای ندارد:
from requests.exceptions import Timeout, ConnectionError
def fetch_with_retry(url, retries=3):
for attempt in range(retries):
try:
return requests.get(url, timeout=10)
except (Timeout, ConnectionError):
if attempt < retries - 1:
time.sleep(2 ** attempt)
else:
raise
الگوی 2 ** attempt (تأخیر نمایی) در پروژههای واقعی جواب داده — اگر سرور موقتاً از کار افتاده، فاصلهی بین تلاشها دو برابر میشود و فرصت بیشتری برای بازیابی میدهد.
۳) Checkpoint برای پروژههای بزرگ
اگر پروژهی اسکرپینگ شما هزاران صفحه دارد و در صفحهی ۵۰۰۰ سرور قطع میشود، نمیخواهید از اول شروع کنید. راهحل: ثبت مداوم وضعیت در یک فایل یا دیتابیس:
import json
def load_checkpoint():
try:
with open("checkpoint.json") as f:
return json.load(f)
except FileNotFoundError:
return {"completed_urls": []}
def save_checkpoint(state):
with open("checkpoint.json", "w") as f:
json.dump(state, f)
اگر با خطای FileNotFoundError در پایتون روبرو شدید، رفع خطای FileNotFoundError در پایتون مسیر تشخیص را نشان میدهد. این نوع مدیریت خطا، در پروژههای اسکرپینگ بزرگ تفاوت بین یک روز کاری و یک هفته کاری است.
اشتباهاتی که در پروژههای واقعی دیدهام
در بازبینی پروژههای اسکرپینگ، این اشتباهات را زیاد دیدهام و هر کدام، یک درس عملی است:
- نادیده گرفتن robots.txt: توسعهدهنده با شور و شوق شروع میکند و بعد از یک هفته میفهمد سایت مقصد، اسکرپینگ را ممنوع کرده. قبل از شروع، این فایل را بخوانید.
- عدم تنظیم User-Agent: درخواستهای بدون User-Agent، در بیش از نیمی از سایتها بلاک میشوند — اغلب بدون توضیح.
- نرخ درخواست تهاجمی: درخواستهای پشتسرهم، حتی با پروکسی، به بلاک شدن دامنهی هدف منجر میشود. تأخیر و احترام، پروژه را طولانیمدتتر میکند.
- اتکا به CSS selector بدون fallback: اگر سایت مقصد، ساختار HTML را تغییر دهد، selector شما میشکند. بهترین راه، استفاده از چند selector جایگزین یا تغییر منطق استخراج به شکلی که کمتر شکننده باشد.
- نادیده گرفتن دادهی ناقص: در برخی صفحات، یک فیلد وجود دارد ولی در برخی دیگر ندارد. اگر این را در کد در نظر نگیرید، در نیمهی پروژه با
AttributeErrorروبرو میشوید. همان الگویif elseکه در بخش BeautifulSoup گفتهام، همین مشکل را حل میکند. - نبود monitoring: اگر اسکرپر شما در سرور اجرا میشود، باید سیستم هشدار داشته باشد که وقتی متوقف شد، شما مطلع شوید. ابزارهایی مثل
healthchecks.ioیا حتی یک اسکریپت ساده که هر ساعت ایمیل میفرستد، این کار را انجام میدهند. - پرینت جای log: در پروژههای بزرگ،
printجایloggingرا نمیگیرد. log متمرکز، در دیباگ و تحلیل خطاهای گذشته، بینظیر است.
یک توصیهی عملی که در پروژههای واقعی بهکارم آمده: قبل از نوشتن کد اصلی، یک اسکرپر کوچک برای پنج صفحهی نمونه بنویسید. اگر با همان پنج صفحه مشکلات پایداری یا ساختاری بروز کرد، بدانید که در مقیاس بزرگ، همین مشکلات چند برابر میشوند. این تست کوچک، از ساعتها اشتباه در پروژهی اصلی جلوگیری میکند.
سخن آخر
وب اسکرپینگ با پایتون، از یک خط requests.get شروع میشود ولی در پروژههای واقعی، به یک سیستم پایدار تبدیل میشود که چند بخش دارد: احترام به سرور مقصد، مدیریت خطا و retry، ذخیرهسازی درست، log و پایش. سه نکتهی مهم که در این مقاله به آنها رسیدیم: اول، مسائل حقوقی و اخلاقی (robots.txt، Terms of Service، دادههای شخصی) قبل از هر خط کد باید بررسی شوند؛ دوم، انتخاب ابزار باید بر اساس نوع سایت هدف باشد — ساده با requests، پویا با Playwright، بزرگ با Scrapy؛ سوم، پایداری پروژهی اسکرپینگ در تکنیکهای retry، log و checkpoint نهفته است، نه در پیچیدگی استخراج داده.
اگر امروز میخواهید شروع کنید، سه کار کوچک پیشنهاد میکنم: robots.txt یک سایت آموزشی ساده را بخوانید، یک اسکریپت کوچک با requests و BeautifulSoup بنویسید که تیتر چند مقاله را استخراج کند، و آن را با تأخیر دو ثانیهای بین درخواستها اجرا کنید. همین تمرین کوچک، همهی مفاهیم پایهی این مقاله را زنده میکند. اگر تجربهای از اسکرپینگ در پروژههای خودتان دارید — مخصوصاً اگر با چالشهای حقوقی یا فنی غیرمنتظره روبرو شدهاید — در دیدگاهها بنویسید؛ همین نکتههای میدانی، برای خوانندهی بعدی از هر مستند رسمی ارزشمندتر است. 🕷️