تمرین و پروژه‌های پایتون

ساخت برنامه هواشناسی با پایتون؛ پروژه کامل با API رایگان Open-Meteo

ساخت برنامه هواشناسی با پایتون و API

🗓 آخرین به‌روزرسانی:

ساخت برنامه هواشناسی با پایتون یکی از بهترین پروژه‌ها برای یادگیری کار با API است. در این پروژه برنامه‌ای می‌نویسیم که نام یک شهر را می‌گیرد، مختصات جغرافیایی آن را پیدا می‌کند، دما، رطوبت و سرعت باد فعلی را نشان می‌دهد و پیش‌بینی چند روز آینده را چاپ می‌کند. برای دریافت داده از سرویس رایگان Open-Meteo استفاده می‌کنیم که برخلاف بیشتر سرویس‌های هواشناسی به ثبت‌نام و کلید API نیاز ندارد؛ پس می‌توانید همین حالا کدها را اجرا کنید. در طول مسیر کار با کتابخانه requests، خواندن JSON، مدیریت خطاهای شبکه، ساخت ابزار خط فرمان با argparse و حتی تست برنامه بدون اینترنت را یاد می‌گیرید. همه کدها روی پایتون ۳.۱۳ اجرا شده‌اند و خروجی‌ها واقعی هستند.

API چیست و چرا Open-Meteo؟

API یا رابط برنامه‌نویسی، دری است که یک سرویس برای برنامه‌های دیگر باز می‌کند. برنامه ما یک درخواست HTTP به آدرس مشخصی می‌فرستد، پارامترهایی مثل عرض و طول جغرافیایی را همراه آن ارسال می‌کند و سرویس پاسخ را در قالب JSON برمی‌گرداند. JSON در پایتون مستقیم به دیکشنری و لیست تبدیل می‌شود، پس خواندن داده‌ها بسیار ساده است.

  • بدون کلید و ثبت‌نام: برای پروژه‌های آموزشی و غیرتجاری نیازی به ساخت حساب ندارد.
  • دو سرویس جدا: Geocoding برای تبدیل نام شهر به مختصات و Forecast برای دریافت وضعیت هوا.
  • داده کامل: دما، رطوبت، باد، بارش و کد وضعیت هوا برای ساعت‌ها و روزهای آینده.
  • جایگزین‌ها: اگر بعداً سرویس دیگری مثل OpenWeatherMap خواستید، فقط تابع‌های دریافت داده عوض می‌شوند و بقیه برنامه ثابت می‌ماند.

هر پاسخ HTTP یک کد وضعیت هم دارد. کد ۲۰۰ یعنی درخواست موفق بوده، ۴۰۰ یعنی پارامترها ایراد دارند، ۴۰۴ یعنی آدرس پیدا نشده، ۴۲۹ یعنی در زمان کوتاه درخواست زیادی فرستاده‌اید و کدهای ۵۰۰ به بعد یعنی سرور دچار مشکل شده است. برنامه خوب قبل از خواندن داده، کد وضعیت را بررسی می‌کند و برای هر حالت پیام مناسبی نشان می‌دهد. نکته دیگر احترام به سرویس رایگان است: برای هر شهر بیش از نیاز درخواست نفرستید و اگر برنامه را زمان‌بندی می‌کنید، مثلاً هر چند دقیقه یک بار، فاصله معقولی بین درخواست‌ها بگذارید. همین اصول را در وب اسکرپینگ با پایتون هم رعایت می‌کنیم.

نصب کتابخانه requests

کتابخانه requests استاندارد غیررسمی ارسال درخواست HTTP در پایتون است. آن را داخل یک محیط مجازی نصب کنید:

python -m venv venv
# Windows: venv\Scripts\activate   macOS/Linux: source venv/bin/activate
pip install requests

قدم ۱: پیدا کردن مختصات شهر (Geocoding)

سرویس پیش‌بینی هوا با نام شهر کار نمی‌کند و عرض و طول جغرافیایی می‌خواهد. پس اول از سرویس Geocoding می‌پرسیم شهر تهران کجاست. پارامترها را با آرگومان params می‌فرستیم تا requests خودش آن‌ها را درست در آدرس قرار دهد. آرگومان timeout هم مهم است؛ بدون آن، اگر سرور جواب ندهد، برنامه برای همیشه منتظر می‌ماند:

import requests

r = requests.get(
    "https://geocoding-api.open-meteo.com/v1/search",
    params={"name": "Tehran", "count": 1, "language": "en"},
    timeout=10,
)
print("status:", r.status_code)
city = r.json()["results"][0]
print(city["name"], city["country"], city["latitude"], city["longitude"])
print("timezone:", city["timezone"])

خروجی:

status: 200
Tehran Iran 35.69439 51.42151
timezone: Asia/Tehran

متد r.json() متن پاسخ را به دیکشنری پایتون تبدیل می‌کند. کلید results فهرست شهرهای پیداشده است و چون count را ۱ گذاشته‌ایم، فقط نزدیک‌ترین نتیجه را می‌گیریم.

قدم ۲: دریافت وضعیت فعلی هوا

حالا مختصات را به سرویس Forecast می‌دهیم و با پارامتر current مشخص می‌کنیم کدام داده‌های لحظه‌ای را می‌خواهیم. پارامتر timezone=auto باعث می‌شود زمان‌ها به وقت محلی همان شهر برگردند:

import requests

params = {
    "latitude": 35.69, "longitude": 51.42, "timezone": "auto",
    "current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
}
r = requests.get("https://api.open-meteo.com/v1/forecast", params=params, timeout=10)
data = r.json()
current = data["current"]
print("time:", current["time"])
print("temperature:", current["temperature_2m"], data["current_units"]["temperature_2m"])
print("humidity:", current["relative_humidity_2m"], "%")
print("weather code:", current["weather_code"])

خروجی:

time: 2026-10-20T05:45
temperature: 20.1 °C
humidity: 26 %
weather code: 0

عددهای خروجی شما با این‌جا فرق می‌کند، چون داده واقعی و لحظه‌ای است. این خروجی در زمان نوشتن مقاله گرفته شده است. سرویس واحد هر مقدار را هم در کلید current_units برمی‌گرداند.

قدم ۳: تبدیل کد وضعیت هوا به متن

Open-Meteo وضعیت هوا را به‌صورت یک عدد استاندارد به نام کد WMO برمی‌گرداند؛ مثلاً ۰ یعنی آسمان صاف و ۶۱ یعنی باران سبک. برای اینکه خروجی برای کاربر قابل‌فهم باشد، یک دیکشنری می‌سازیم که هر کد را به توضیح متنی نگاشت کند. متد get دیکشنری هم برای کدهایی که در فهرست ما نیستند، یک مقدار پیش‌فرض برمی‌گرداند تا برنامه با خطای KeyError متوقف نشود:

from weather import describe

for code in (0, 2, 45, 61, 95, 42):
    print(code, "->", describe(code))

خروجی:

0 -> Clear sky
2 -> Partly cloudy
45 -> Fog
61 -> Light rain
95 -> Thunderstorm
42 -> Unknown (42)

قدم ۴: پیش‌بینی چند روز آینده

برای پیش‌بینی روزانه، پارامتر daily را با دمای کمینه و بیشینه و کد وضعیت هوا می‌فرستیم. پاسخ برای هر مقدار یک لیست جدا برمی‌گرداند: لیست تاریخ‌ها، لیست دمای کمینه و غیره. تابع parse_forecast این لیست‌ها را با zip کنار هم می‌گذارد و برای هر روز یک دیکشنری مرتب می‌سازد. برای اینکه بتوانیم این بخش را دقیق تست کنیم، از یک نمونه پاسخ ثابت استفاده می‌کنیم که دقیقاً شکل پاسخ واقعی را دارد:

import json
from weather import parse_forecast

sample = json.loads(open("sample.json").read())
current, daily = parse_forecast(sample)
print(current)
for day in daily:
    print(day)

خروجی:

{'temp': 21.4, 'humidity': 30, 'wind': 9.7, 'text': 'Partly cloudy'}
{'date': '2026-10-20', 'min': 15.2, 'max': 27.9, 'text': 'Mainly clear'}
{'date': '2026-10-21', 'min': 14.8, 'max': 25.1, 'text': 'Light rain'}

جدا کردن «دریافت داده» از «پردازش داده» یکی از مهم‌ترین عادت‌های برنامه‌نویسی خوب است. تابع parse_forecast اصلاً نمی‌داند داده از اینترنت آمده یا از فایل؛ برای همین هم ساده‌تر تست می‌شود و هم اگر روزی سرویس را عوض کنید، تغییرات کمتری لازم است. اگر با List Comprehension آشنا نیستید، ساختار حلقه داخل این تابع را در آن آموزش ببینید.

قدم ۵: مدیریت خطاهای شبکه و ورودی اشتباه

برنامه‌ای که با اینترنت کار می‌کند همیشه ممکن است با مشکل روبه‌رو شود: اینترنت قطع است، سرور دیر جواب می‌دهد یا کاربر نام شهر را اشتباه نوشته است. در فایل نهایی همه این حالت‌ها را به یک خطای اختصاصی به نام WeatherError تبدیل می‌کنیم تا بخش اصلی برنامه فقط یک نوع خطا را مدیریت کند. متد raise_for_status هم پاسخ‌های ۴۰۴ و ۵۰۰ را به خطا تبدیل می‌کند:

from weather import find_city, WeatherError

for name in ("Isfahan", "Atlantisxyz"):
    try:
        city = find_city(name)
        print("found:", city["name"], city["country"], round(city["lat"], 2), round(city["lon"], 2))
    except WeatherError as error:
        print("Error:", error)

خروجی:

found: Isfahan Iran 32.65 51.67
Error: City not found: Atlantisxyz

عبارت raise WeatherError(...) from error خطای اصلی را هم نگه می‌دارد تا هنگام عیب‌یابی بدانید دقیقاً چه چیزی باعث مشکل شده است. برای آشنایی بیشتر با ساخت و مدیریت خطاها، آموزش مدیریت خطا در پایتون را ببینید.

قدم ۶: برنامه کامل خط فرمان با argparse

حالا همه قطعه‌ها را در تابع main کنار هم می‌گذاریم. ماژول استاندارد argparse آرگومان‌های خط فرمان را می‌خواند؛ نام شهر اجباری است و تعداد روزهای پیش‌بینی با --days بین ۱ تا ۷ قابل تنظیم است. argparse خودش راهنمای --help و پیام خطا برای ورودی اشتباه را هم می‌سازد. اجرای برنامه از ترمینال این‌طور است:

python weather.py Tehran --days 3
python weather.py "New York" --days 5

همین کار را می‌توانیم از داخل پایتون هم انجام دهیم و لیست آرگومان‌ها را مستقیم به main بدهیم:

from weather import main

exit_code = main(["Tehran", "--days", "3"])
print("exit code:", exit_code)

خروجی:

Weather in Tehran, Iran
Now: 19.2°C, Clear sky
Humidity: 26%  Wind: 6.6 km/h
Forecast:
  2026-10-20   18.6 ..  29.3°C  Overcast
  2026-10-21   17.4 ..  27.4°C  Overcast
  2026-10-22   16.8 ..  25.8°C  Overcast
exit code: 0

کد خروج ۰ یعنی برنامه موفق بوده و ۱ یعنی خطا رخ داده است؛ این قرارداد به اسکریپت‌ها و ابزارهای دیگر کمک می‌کند بفهمند اجرای برنامه موفق بوده یا نه.

نمایش مرتب خروجی با f-string

تابع report داده‌های خام را به متنی خوانا تبدیل می‌کند. در آن از قالب‌بندی f-string استفاده کرده‌ایم: عبارت {temp:.1f} عدد را با یک رقم اعشار نمایش می‌دهد و {day['min']:>5.1f} علاوه بر یک رقم اعشار، عدد را در فضای پنج‌کاراکتری راست‌چین می‌کند. نتیجه این است که ستون‌های دمای کمینه و بیشینه در همه روزها زیر هم قرار می‌گیرند، حتی اگر یکی از دماها تک‌رقمی یا منفی باشد. این تابع هم مثل parse_forecast هیچ وابستگی‌ای به اینترنت ندارد و فقط ورودی می‌گیرد و رشته برمی‌گرداند:

from weather import report

city = {"name": "Tabriz", "country": "Iran"}
current = {"temp": 8.4, "humidity": 64, "wind": 12.0, "text": "Light rain"}
daily = [
    {"date": "2026-10-20", "min": -1.5, "max": 9.0, "text": "Rain"},
    {"date": "2026-10-21", "min": 3.0, "max": 14.2, "text": "Partly cloudy"},
]
print(report(city, current, daily))

خروجی:

Weather in Tabriz, Iran
Now: 8.4°C, Light rain
Humidity: 64%  Wind: 12.0 km/h
Forecast:
  2026-10-20   -1.5 ..   9.0°C  Rain
  2026-10-21    3.0 ..  14.2°C  Partly cloudy

داده ساعتی: دمای چند ساعت آینده

Open-Meteo علاوه بر داده لحظه‌ای و روزانه، پیش‌بینی ساعتی هم دارد. کافی است پارامتر hourly را بفرستید و با forecast_hours تعداد ساعت‌ها را محدود کنید. پاسخ ساعتی هم مثل روزانه شامل چند لیست هم‌طول است؛ پس با zip می‌توانیم زمان و دمای هر ساعت را کنار هم چاپ کنیم. این داده برای رسم نمودار دمای روز یا هشدار باران در ساعت‌های آینده بسیار مفید است:

import requests

params = {"latitude": 35.69, "longitude": 51.42, "timezone": "auto",
          "hourly": "temperature_2m", "forecast_hours": 4}
data = requests.get("https://api.open-meteo.com/v1/forecast", params=params, timeout=10).json()
for time, temp in zip(data["hourly"]["time"], data["hourly"]["temperature_2m"]):
    print(time.replace("T", " "), f"{temp:.1f}°C")

خروجی:

2026-10-20 05:00 20.3°C
2026-10-20 06:00 19.7°C
2026-10-20 07:00 20.9°C
2026-10-20 08:00 25.0°C

تست برنامه بدون اینترنت

تست کردن کدی که به اینترنت وابسته است سخت است: نتیجه هر بار عوض می‌شود و اگر شبکه قطع باشد تست شکست می‌خورد. برای همین تابع‌های find_city و get_forecast یک پارامتر session دارند. در حالت عادی همان requests است، اما در تست می‌توانیم یک شیء ساختگی بدهیم که پاسخ‌های از پیش تعیین‌شده برمی‌گرداند. به این تکنیک «تزریق وابستگی» می‌گویند:

from weather import find_city

class FakeResponse:
    def __init__(self, data):
        self.data = data
    def raise_for_status(self):
        pass
    def json(self):
        return self.data

class FakeSession:
    def get(self, url, params=None, timeout=None):
        print("GET", url.split("/")[2], params["name"])
        return FakeResponse({"results": [{"name": "Shiraz", "country": "Iran",
                                          "latitude": 29.61, "longitude": 52.53}]})

print(find_city("Shiraz", session=FakeSession()))

خروجی:

GET geocoding-api.open-meteo.com Shiraz
{'name': 'Shiraz', 'country': 'Iran', 'lat': 29.61, 'lon': 52.53}

در خروجی می‌بینید که هیچ درخواست واقعی ارسال نشده و تابع با داده ساختگی درست کار کرده است. در پروژه‌های حرفه‌ای همین ایده با ماژول unittest.mock یا کتابخانه pytest انجام می‌شود.

سورس کامل برنامه هواشناسی

کد کامل برنامه در ادامه آمده است. به ساختار آن دقت کنید: هر تابع فقط یک کار انجام می‌دهد. find_city شهر را پیدا می‌کند، get_forecast داده خام را می‌گیرد، parse_forecast آن را مرتب می‌کند، report متن خروجی را می‌سازد و main فقط این قطعه‌ها را به هم وصل می‌کند. این تقسیم‌بندی باعث می‌شود اضافه کردن رابط گرافیکی یا نسخه وب بدون بازنویسی کل برنامه ممکن باشد. فایل ZIP شامل همین کد، requirements.txt و راهنمای اجراست:

⬇ دانلود سورس کامل برنامه هواشناسی (ZIP)

"""Weather app with Python and the free Open-Meteo API - Dev Source (dev-source.ir)"""
import argparse
import sys

import requests

GEO_URL = "https://geocoding-api.open-meteo.com/v1/search"
FORECAST_URL = "https://api.open-meteo.com/v1/forecast"

WEATHER_CODES = {
    0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
    45: "Fog", 48: "Rime fog", 51: "Light drizzle", 53: "Drizzle", 55: "Dense drizzle",
    61: "Light rain", 63: "Rain", 65: "Heavy rain", 71: "Light snow", 73: "Snow",
    75: "Heavy snow", 80: "Rain showers", 81: "Heavy showers", 82: "Violent showers",
    95: "Thunderstorm", 96: "Thunderstorm with hail", 99: "Severe thunderstorm",
}


class WeatherError(Exception):
    pass


def describe(code):
    return WEATHER_CODES.get(code, f"Unknown ({code})")


def find_city(name, session=requests):
    try:
        r = session.get(GEO_URL, params={"name": name, "count": 1, "language": "en"}, timeout=10)
        r.raise_for_status()
    except requests.RequestException as error:
        raise WeatherError(f"Network problem: {error}") from error
    results = r.json().get("results")
    if not results:
        raise WeatherError(f"City not found: {name}")
    c = results[0]
    return {"name": c["name"], "country": c.get("country", ""),
            "lat": c["latitude"], "lon": c["longitude"]}


def get_forecast(lat, lon, days=3, session=requests):
    params = {
        "latitude": lat, "longitude": lon, "timezone": "auto", "forecast_days": days,
        "current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
        "daily": "temperature_2m_max,temperature_2m_min,weather_code",
    }
    try:
        r = session.get(FORECAST_URL, params=params, timeout=10)
        r.raise_for_status()
    except requests.RequestException as error:
        raise WeatherError(f"Network problem: {error}") from error
    return r.json()


def parse_forecast(data):
    cur = data["current"]
    current = {
        "temp": cur["temperature_2m"], "humidity": cur["relative_humidity_2m"],
        "wind": cur["wind_speed_10m"], "text": describe(cur["weather_code"]),
    }
    d = data["daily"]
    daily = [
        {"date": day, "min": lo, "max": hi, "text": describe(code)}
        for day, lo, hi, code in zip(d["time"], d["temperature_2m_min"],
                                     d["temperature_2m_max"], d["weather_code"])
    ]
    return current, daily


def report(city, current, daily):
    lines = [f"Weather in {city['name']}, {city['country']}",
             f"Now: {current['temp']:.1f}°C, {current['text']}",
             f"Humidity: {current['humidity']}%  Wind: {current['wind']:.1f} km/h",
             "Forecast:"]
    for day in daily:
        lines.append(f"  {day['date']}  {day['min']:>5.1f} .. {day['max']:>5.1f}°C  {day['text']}")
    return "\n".join(lines)


def main(argv=None):
    parser = argparse.ArgumentParser(description="Show the weather for a city")
    parser.add_argument("city", nargs="+", help="city name, e.g. Tehran")
    parser.add_argument("--days", type=int, default=3, choices=range(1, 8))
    args = parser.parse_args(argv)
    try:
        city = find_city(" ".join(args.city))
        current, daily = parse_forecast(get_forecast(city["lat"], city["lon"], args.days))
    except WeatherError as error:
        print("Error:", error)
        return 1
    print(report(city, current, daily))
    return 0


if __name__ == "__main__":
    sys.exit(main())

ایده‌هایی برای توسعه پروژه

  • رابط گرافیکی: با Tkinter یک پنجره با کادر نام شهر و دکمه «نمایش هوا» بسازید.
  • ربات تلگرام: همین تابع‌ها را در یک ربات قرار دهید تا با فرستادن نام شهر، وضعیت هوا را جواب دهد؛ پایه کار را در ساخت ربات تلگرام با پایتون ببینید.
  • نسخه وب: با Flask یک صفحه وب بسازید که فرم نام شهر دارد و نتیجه را نمایش می‌دهد.
  • کش کردن نتیجه: پاسخ هر شهر را چند دقیقه در فایل JSON نگه دارید تا درخواست‌های تکراری ارسال نشود.
  • نمودار دما: دمای ساعتی را با Matplotlib رسم کنید.

خطاهای رایج و راه رفع

  • ModuleNotFoundError: No module named requests: کتابخانه در محیط فعلی نصب نیست؛ رفع ModuleNotFoundError را ببینید.
  • ConnectionError یا Timeout: اتصال اینترنت یا دسترسی به سرویس را بررسی کنید؛ برنامه ما این خطا را با پیام «Network problem» نشان می‌دهد.
  • KeyError روی results: شهر پیدا نشده و کلید results در پاسخ وجود ندارد؛ همیشه با get بررسی کنید.
  • دمای عجیب: بعضی سرویس‌ها دما را به کلوین برمی‌گردانند؛ واحد را در مستندات و کلید units بررسی کنید.
  • خطای JSONDecodeError: پاسخ سرور JSON نیست، معمولاً به‌خاطر خطای سرور یا آدرس اشتباه؛ قبل از json() کد وضعیت را چک کنید.

سوالات متداول

برای ساخت برنامه هواشناسی با پایتون چه چیزی لازم است؟

پایتون، کتابخانه requests و یک سرویس هواشناسی. Open-Meteo رایگان است و کلید API نمی‌خواهد؛ پس با pip install requests می‌توانید شروع کنید.

آیا Open-Meteo برای استفاده رایگان است؟

برای پروژه‌های شخصی و غیرتجاری بدون ثبت‌نام و کلید رایگان است. برای استفاده تجاری شرایط آن را در سایت سرویس بررسی کنید.

چطور نام شهر را به مختصات تبدیل کنم؟

از سرویس Geocoding خود Open-Meteo استفاده کنید و نام شهر را با پارامتر name بفرستید؛ پاسخ شامل latitude و longitude است.

چرا باید در requests.get از timeout استفاده کنم؟

بدون timeout، اگر سرور جواب ندهد برنامه برای همیشه منتظر می‌ماند. با timeout=10 بعد از ۱۰ ثانیه خطا می‌گیرید و می‌توانید آن را مدیریت کنید.

آیا می‌توانم این برنامه را گرافیکی کنم؟

بله. تابع‌های دریافت و پردازش داده را نگه دارید و فقط بخش نمایش را با Tkinter، Flask یا یک ربات تلگرام عوض کنید.

اگر سرویس در دسترس نبود چه کنم؟

برنامه خطای شبکه را می‌گیرد و پیام مناسب نشان می‌دهد. می‌توانید آخرین پاسخ موفق را در فایل ذخیره کنید و در زمان قطعی همان را نمایش دهید.

آموزش‌های مرتبط

آموزش‌های تازه، کد آماده و رفع خطاهر هفته در کانال Dev Source؛ رایگان.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *