🌐 APIs و Web Scraping

🌐 APIs و Web Scraping ببايثون

اربط برنامجك بالعالم الخارجي — اجلب بيانات الطقس، الأسهم، الأخبار، وأي شيء على الإنترنت.

🔗 HTTP و REST API 📦 مكتبة requests 🗝️ API Keys 🕷️ BeautifulSoup 📊 تحليل JSON ⚡ Async Requests

١. HTTP — كيف يتحدث الإنترنت

كل مرة تزور موقعاً، برنامجك يرسل Request للخادم، والخادم يرد بـ Response. هذا هو HTTP.

💻 برنامجك
Client
📤 Request
GET /data
🖥️ الخادم
Server
📥 Response
200 OK + JSON
💻 برنامجك
يعالج البيانات

أنواع طلبات HTTP

الطلبات الأساسية: GET — اجلب بيانات · POST — أرسل بيانات جديدة · PUT — حدّث بيانات كاملة · PATCH — حدّث جزئياً · DELETE — احذف

رموز الحالة الشائعة

200

OK — طلب ناجح

201

Created — تم الإنشاء

301

Moved — تحويل دائم

400

Bad Request — طلب خاطئ

401

Unauthorized — غير مصرح

404

Not Found — غير موجود

500

Server Error — خطأ الخادم

429

Too Many Requests

٢. مكتبة requests — أسهل طريقة لجلب البيانات

ثبّت المكتبة: pip install requests

Python — requests أساسيات
import requests

# ===== GET — جلب بيانات =====
resp = requests.get("https://httpbin.org/get")

print(resp.status_code)   # 200
print(resp.headers)       # معلومات الاستجابة
print(resp.text)          # المحتوى كنص
print(resp.json())        # تحويل JSON تلقائياً

# ===== GET مع معاملات =====
params = {"q": "python tutorial", "page": 1, "limit": 10}
resp   = requests.get("https://httpbin.org/get", params=params)
# URL: https://httpbin.org/get?q=python+tutorial&page=1&limit=10

# ===== POST — إرسال بيانات =====
data = {"username": "khalil", "password": "secret"}
resp = requests.post("https://httpbin.org/post", json=data)
print(resp.json()["json"])   # يُرجع ما أرسلناه

# ===== Headers مخصصة =====
headers = {
    "User-Agent"    : "MyApp/1.0",
    "Accept-Language": "ar,en",
    "Content-Type"  : "application/json",
}
resp = requests.get("https://httpbin.org/headers", headers=headers)

# ===== Timeout — لا تنتظر إلى الأبد =====
resp = requests.get("https://httpbin.org/delay/1", timeout=5)
# إذا لم يرد الخادم في 5 ثوانٍ → Timeout error

# ===== Session — إعادة استخدام الاتصال =====
session = requests.Session()
session.headers["User-Agent"] = "MyBot/1.0"
r1 = session.get("https://httpbin.org/get")
r2 = session.get("https://httpbin.org/get")
# نفس الـ headers لكلا الطلبين

🧠 سؤال سريع

متى يجب استخدام POST بدل GET؟

عندما نريد جلب بيانات من API
عندما نريد إرسال بيانات جديدة للخادم — مثل تسجيل مستخدم أو نشر تعليق
POST أسرع دائماً من GET
عندما يكون الـ URL طويلاً جداً
✅ صحيح! GET لجلب البيانات، POST لإرسال بيانات جديدة. GET يضع المعاملات في الـ URL، POST يضعها في الـ body.
❌ خطأ. GET لجلب البيانات دون تعديل. POST لإرسال بيانات جديدة للخادم كتسجيل مستخدم أو نشر منشور.

٣. التعامل مع REST APIs الحقيقية

Python — API الطقس (OpenWeatherMap)
import requests

# ١. API مجانية — JSONPlaceholder (للتجربة)
BASE = "https://jsonplaceholder.typicode.com"

# جلب كل المنشورات
posts = requests.get(f"{BASE}/posts").json()
print(f"عدد المنشورات: {len(posts)}")
print(f"أول منشور: {posts[0]['title']}")

# جلب منشور محدد
post = requests.get(f"{BASE}/posts/1").json()
print(f"العنوان: {post['title']}")

# إنشاء منشور جديد
new_post = {
    "title"  : "تعلم بايثون",
    "body"   : "بايثون لغة رائعة للمبتدئين",
    "userId" : 1
}
resp = requests.post(f"{BASE}/posts", json=new_post)
print(f"تم الإنشاء — ID: {resp.json()['id']}, Status: {resp.status_code}")

# ٢. API الطقس (تحتاج مفتاح مجاني من openweathermap.org)
def get_weather(city: str, api_key: str) -> dict:
    url    = "https://api.openweathermap.org/data/2.5/weather"
    params = {"q": city, "appid": api_key, "units": "metric", "lang": "ar"}
    resp   = requests.get(url, params=params, timeout=10)
    resp.raise_for_status()   # يرمي خطأ إذا 4xx أو 5xx
    data   = resp.json()
    return {
        "مدينة"     : data["name"],
        "حرارة"     : f"{data['main']['temp']}°C",
        "وصف"       : data["weather"][0]["description"],
        "رطوبة"     : f"{data['main']['humidity']}%",
        "سرعة الرياح": f"{data['wind']['speed']} m/s",
    }

# weather = get_weather("Marrakech", "YOUR_API_KEY")
# for k, v in weather.items(): print(f"{k}: {v}")

API مجانية للتجربة — لا تحتاج مفتاح

Python — APIs مجانية
import requests

# ١. معلومات عن دولة
r    = requests.get("https://restcountries.com/v3.1/name/morocco")
data = r.json()[0]
print(f"الدولة  : {data['name']['common']}")
print(f"العاصمة : {data['capital'][0]}")
print(f"السكان  : {data['population']:,}")
print(f"العملة  : {list(data['currencies'].keys())[0]}")

# ٢. ترجمة نص (LibreTranslate — مجاني)
resp = requests.post(
    "https://libretranslate.de/translate",
    json={"q": "Hello World", "source": "en", "target": "ar"},
    timeout=10
)
if resp.ok:
    print(resp.json()["translatedText"])   # مرحباً بالعالم

# ٣. معلومات IP
ip_data = requests.get("https://ipapi.co/json/").json()
print(f"IP: {ip_data.get('ip')} | البلد: {ip_data.get('country_name')}")

٤. المصادقة — API Keys و Tokens

Python — المصادقة
import requests
import os

# ===== ١. API Key في Header (الأكثر شيوعاً) =====
API_KEY  = os.getenv("MY_API_KEY")   # من متغيرات البيئة
headers = {"Authorization": f"Bearer {API_KEY}"}
resp    = requests.get("https://api.example.com/data", headers=headers)

# ===== ٢. API Key في Params =====
params = {"api_key": API_KEY, "city": "Marrakech"}
resp   = requests.get("https://api.weather.com/current", params=params)

# ===== ٣. Basic Auth =====
resp = requests.get(
    "https://httpbin.org/basic-auth/user/pass",
    auth=("user", "pass")
)
print(resp.json())

# ===== حفظ API Key بأمان في .env =====
# ١. أنشئ ملف .env:
#     MY_API_KEY=abc123secretkey
# ٢. ثبّت python-dotenv: pip install python-dotenv
from dotenv import load_dotenv
load_dotenv()
key = os.getenv("MY_API_KEY")
# لا تكتب المفتاح مباشرة في الكود — خطر أمني!
لا تكتب API Key في الكود مباشرة! إذا رفعت الكود على GitHub سيُسرق المفتاح. دائماً استخدم متغيرات البيئة أو ملف .env مع إضافته لـ .gitignore.

٥. معالجة أخطاء الشبكة بشكل صحيح

Python — Error Handling للشبكة
import requests
from requests.exceptions import (
    Timeout, ConnectionError, HTTPError, RequestException
)

def safe_request(url: str, **kwargs) -> dict | None:
    try:
        resp = requests.get(url, timeout=10, **kwargs)
        resp.raise_for_status()   # يرمي HTTPError للـ 4xx/5xx
        return resp.json()

    except Timeout:
        print("⏱️ الخادم لم يرد خلال 10 ثوانٍ")
    except ConnectionError:
        print("🔌 لا يوجد اتصال بالإنترنت")
    except HTTPError as e:
        print(f"❌ خطأ HTTP: {e.response.status_code}")
        if e.response.status_code == 401:
            print("   المفتاح منتهي أو غير صحيح")
        elif e.response.status_code == 429:
            print("   تجاوزت حد الطلبات — انتظر قليلاً")
    except RequestException as e:
        print(f"❓ خطأ غير متوقع: {e}")
    return None

# Retry تلقائي مع backoff
import time

def request_with_retry(url: str, retries=3, backoff=2):
    for attempt in range(retries):
        result = safe_request(url)
        if result:
            return result
        wait = backoff ** attempt
        print(f"محاولة {attempt+1}/{retries} — انتظر {wait}s")
        time.sleep(wait)
    return None

٦. Web Scraping — استخراج بيانات المواقع

عندما لا يوجد API، يمكنك قراءة صفحة HTML وتحليلها. مع الالتزام دائماً بـ robots.txt وشروط الاستخدام.

ثبّت: pip install requests beautifulsoup4 lxml

Python — requests + HTML
import requests

# جلب صفحة HTML
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64)"}
resp    = requests.get("https://books.toscrape.com",
                      headers=headers, timeout=10)

print(f"Status : {resp.status_code}")
print(f"Encoding: {resp.encoding}")
print(resp.text[:300])   # أول 300 حرف من HTML
قواعد Web Scraping: تحقق من /robots.txt · لا تضغط على الخادم (أضف sleep بين الطلبات) · لا تستخدم البيانات لأغراض تجارية دون إذن · بعض المواقع يستخدم JavaScript لتحميل البيانات — حينها تحتاج Selenium.

٧. BeautifulSoup — تحليل HTML

Python — BeautifulSoup
import requests
from bs4 import BeautifulSoup

headers = {"User-Agent": "Mozilla/5.0"}
resp    = requests.get("https://books.toscrape.com", headers=headers)
soup    = BeautifulSoup(resp.text, "lxml")

# ===== الوصول للعناصر =====
print(soup.title.text)              # عنوان الصفحة

# عنصر واحد بـ CSS selector
first_h1 = soup.select_one("h1")
print(first_h1.text.strip())

# كل الروابط
links = soup.select("a[href]")
for a in links[:5]:
    print(a["href"], "|", a.text.strip())

# ===== مثال حقيقي: scraping كتالوج كتب =====
books = []
for article in soup.select("article.product_pod"):
    books.append({
        "title" : article.select_one("h3 a")["title"],
        "price" : article.select_one(".price_color").text.strip(),
        "rating": article.select_one("p.star-rating")["class"][1],
    })

print(f"وُجد {len(books)} كتاب:")
for b in books[:5]:
    print(f"  📚 {b['title'][:40]:<40} {b['price']:>10}  ⭐{b['rating']}")

# حفظ النتائج
import json
with open("books.json", "w", encoding="utf-8") as f:
    json.dump(books, f, ensure_ascii=False, indent=2)
print("✅ تم الحفظ في books.json")

🧠 سؤال سريع

ما الفرق بين soup.find() و soup.select()؟

لا فرق
find() يستخدم أسماء وسوم HTML، select() يستخدم CSS selectors مما يجعله أكثر مرونة
select() أبطأ لأنه يبحث في كل الصفحة
find() يعيد قائمة دائماً
✅ صحيح! select("h3 a") يفهم CSS selectors كاملاً — أقوى بكثير من find("h3").
❌ خطأ. select() يستخدم CSS selectors مثل "article.product h3 a"، بينما find() يستخدم اسم الوسم فقط مع attrs اختياري.

📝 تمارين تطبيقية

تمرين ١ — جلب وعرض أسعار العملات

استخدم API المجانية https://open.er-api.com/v6/latest/USD لجلب أسعار صرف الدرهم المغربي والريال السعودي والجنيه المصري مقابل الدولار.

Python — الحل
import requests

def get_exchange_rates():
    url  = "https://open.er-api.com/v6/latest/USD"
    resp = requests.get(url, timeout=10)
    resp.raise_for_status()
    rates  = resp.json()["rates"]

    wanted = {
        "MAD": "🇲🇦 درهم مغربي",
        "SAR": "🇸🇦 ريال سعودي",
        "EGP": "🇪🇬 جنيه مصري",
        "EUR": "🇪🇺 يورو",
    }
    print("💵 1 دولار أمريكي =")
    for code, name in wanted.items():
        print(f"  {name:20} {rates.get(code, 'N/A'):.4f}")

get_exchange_rates()

تمرين ٢ — Scraper متعدد الصفحات

اكتب scraper يجلب الكتب من أول 3 صفحات من books.toscrape.com ويحفظها في CSV.

Python — الحل
import requests, csv, time
from bs4 import BeautifulSoup

BASE    = "https://books.toscrape.com/catalogue"
headers = {"User-Agent": "Mozilla/5.0"}
all_books = []

for page in range(1, 4):
    url  = f"{BASE}/page-{page}.html"
    resp = requests.get(url, headers=headers, timeout=10)
    soup = BeautifulSoup(resp.text, "lxml")

    for a in soup.select("article.product_pod"):
        all_books.append({
            "title" : a.select_one("h3 a")["title"],
            "price" : a.select_one(".price_color").text.strip(),
            "rating": a.select_one("p.star-rating")["class"][1],
            "page"  : page
        })

    print(f"✅ صفحة {page} — {len(all_books)} كتاب حتى الآن")
    time.sleep(1)   # نحترم الخادم

# حفظ CSV
with open("books.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["title","price","rating","page"])
    writer.writeheader()
    writer.writerows(all_books)
print(f"💾 تم الحفظ: {len(all_books)} كتاب")

🚀 المشروع — لوحة تحكم بيانات حية

Python — live_dashboard.py
import requests, json, time
from datetime import datetime

class LiveDashboard:
    """لوحة بيانات حية تجمع من عدة APIs"""

    def __init__(self):
        self.session = requests.Session()
        self.session.headers["User-Agent"] = "Dashboard/1.0"
        self.data = {}

    def _get(self, url, **kw):
        try:
            r = self.session.get(url, timeout=8, **kw)
            r.raise_for_status()
            return r.json()
        except Exception as e:
            print(f"  ⚠️ {url.split('/')[2]}: {e}")
            return None

    def fetch_country(self, name="Morocco"):
        d = self._get(f"https://restcountries.com/v3.1/name/{name}")
        if d:
            c = d[0]
            self.data["country"] = {
                "name"   : c["name"]["common"],
                "pop"    : f"{c['population']:,}",
                "capital": c["capital"][0],
                "region" : c["region"],
            }

    def fetch_rates(self):
        d = self._get("https://open.er-api.com/v6/latest/USD")
        if d:
            r = d["rates"]
            self.data["rates"] = {
                "MAD": r.get("MAD"), "EUR": r.get("EUR"),
                "SAR": r.get("SAR"), "GBP": r.get("GBP"),
            }

    def display(self):
        print("\n" + "═"*45)
        print(f"  📊 LIVE DASHBOARD — {datetime.now():%H:%M:%S}")
        print("═"*45)

        if c := self.data.get("country"):
            print(f"  🌍 {c['name']} | {c['capital']} | {c['region']}")
            print(f"     السكان: {c['pop']}")

        if r := self.data.get("rates"):
            print(f"\n  💱 أسعار الصرف (1 USD =)")
            for cur, val in r.items():
                print(f"     {cur}: {val:.4f}")

    def run(self):
        print("🔄 جلب البيانات...")
        self.fetch_country()
        self.fetch_rates()
        self.display()

LiveDashboard().run()

🎉 أتقنت APIs و Web Scraping!

الآن تعرف كيف تتصل بأي API في العالم وتجلب البيانات وتحللها وتحفظها. هذه المهارة تفتح لك أبواباً لا حد لها.

← العودة للبداية