Инструменты

Парсинг погоды на Python: от Visual Crossing API до PostgreSQL

Почему мы используем сервис Visual Crossing?

Сначала кажется, что исторические данные о погоде – это как воздух: бесплатны, доступны всем и в любом количестве. Но через пять минут гугления приходит осознание – не тут-то было! Бесплатный сыр, как известно, бывает только в мышеловке, а бесплатные погодные данные – где-то в параллельной вселенной.

Следующая гениальная мысль: Росгидрометцентр! Ну вот же, государственный сервис, наверняка у них есть все данные, причем бесплатно и в доступном формате. Но после непростого квеста по их сайту остаётся главный вопрос – а есть ли у них API? Судя по всему, Гидрометцентр живёт по принципу «‎если API и существует, но ты его не нашел – значит, его для тебя нет»‎.

Дальше пошли платные сервисы. Яндекс.Погода? О да, там всё чётко, красиво, точно, но взглянув на ценник, начинаешь задумываться: «‎А не проще ли мне встать утром, посмотреть в окно и вручную записывать данные в таблицу? »‎ Другие сервисы выглядят либо подозрительно дешёвыми (и такими же подозрительными по качеству данных), либо какими-то устаревшими артефактами интернета.

И вот тут, среди этих тернистых поисков, мы наткнулись на Visual Crossing. И оказалось, что это редкий островок адекватности в океане либо слишком дорогих, либо слишком сомнительных сервисов.

У них:
✅ Удобный интерфейс (не нужно жертвовать своими глазами, чтобы получить данные)
✅ Приемлемые цены (наконец-то что-то дешевле Яндекса!)
✅ Данные, которым можно доверять (погода не из параллельной вселенной)

И как смешно это ни звучало, но в итоге мы смотрим на погоду в России через американский сервис)

Отбор ключевых признаков погоды

Было отобрано 10 ключевых признаков , которые наиболее полно описывают погодные условия:

  1. tempmax – максимальная температура. 
  2. temp – текущая или средняя температура за период. 
  3. tempmin – минимальная температура. 
  4. precip – количество осадков (дождь, снег и т.д.) за период, обычно измеряется в миллиметрах. 
  5. humidity – относительная влажность воздуха, выраженная в процентах. 
  6. windspeed – скорость ветра, обычно измеряется в км/ч или м/с. 
  7. cloudcover – облачность, т.е. доля неба, покрытая облаками, обычно в процентах. 
  8. solarenergy – количество солнечной энергии, полученное за период, может измеряться в киловатт-часах на квадратный метр (кВт·ч/м²) или аналогичной единицей. 
  9. uvindex  – индекс ультрафиолетового излучения, отражающий силу UV-лучей и потенциальный риск для здоровья. 
  10. preciptype – тип осадков (например, дождь, снег, град), если такая информация доступна.

Эти признаки позволяют не только получить общее представление о погоде, но и анализировать ее влияние на различные процессы – от сельского хозяйства до логистики. Теперь, вооруженные данными, можно делать предсказания, строить модели и, конечно, спорить с бабушками во дворе о том, будет ли завтра дождь! 😁🌤️

Для хранения погодных данных мы рассматриваем аренду базы данных (PostgreSQL) как оптимальный вариант, обеспечивающий баланс между удобством, стоимостью и надёжностью.

Аренда и настройка базы данных PostgreSQL для хранения данных о вакансиях

Этот этап довольно прост и понятен: я планирую арендовать базу данных PostgreSQL от провайдера Timeweb Cloud, который является самым бюджетным вариантом. Нам подойдет объем базы данных в 8 ГБ. 

Затем переходим в административную панель нашей базы данных и создаем таблицу для хранения данных.

SQL
CREATE TABLE IF NOT EXISTS weather (
    id SERIAL PRIMARY KEY,
    city VARCHAR(255),
    date DATE NOT NULL,
    tempmax NUMERIC,
    temp NUMERIC,
    tempmin NUMERIC,
    precip NUMERIC,
    humidity NUMERIC,
    windspeed NUMERIC,
    cloudcover NUMERIC,
    solarenergy NUMERIC,
    uvindex NUMERIC,
    preciptype VARCHAR(255),
    latitude DOUBLE PRECISION NOT NULL,
    longitude DOUBLE PRECISION NOT NULL,
    UNIQUE (date, latitude, longitude)
);

Инструкция по использованию скрипта для сбора исторических данных о погоде

Python
import time
import logging
import psycopg2
import pandas as pd
import urllib.parse
import urllib.request
from datetime import datetime
from geopy.geocoders import Nominatim
from typing import Optional, Dict, Any

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def filter_weather(df_weather: pd.DataFrame) -> pd.DataFrame:
    """
    Фильтрует нужные колонки, сортирует данные по дате и переводит типы осадков на русский язык.

    :param df_weather: DataFrame с данными о погоде.
    :return: Обработанный DataFrame.
    """
    columns = [
        'datetime', 'tempmax', 'temp', 'tempmin', 'precip', 'humidity',
        'windspeed', 'cloudcover', 'solarenergy', 'uvindex', 'preciptype'
    ]
    df_weather = df_weather[columns]
    translations = {
        'rain': 'дождь',
        'rain,snow': 'дождь, снег',
        'snow': 'снег',
        'rain,freezingrain,snow': 'дождь, заморозки, снег',
        'freezingrain': 'заморозки',
        'rain,snow,ice': 'дождь, снег, лед',
        'freezingrain,snow': 'замерзающий дождь, снег',
        'snow,ice': 'снег, лед',
        'freezingrain,snow,ice': 'замерзающий дождь, снег, лед',
        'rain,freezingrain,snow,ice': 'дождь, замерзающий дождь, снег, лед',
        'rain,freezingrain': 'дождь, град',
        'ice': 'лед',
        'rain,ice': 'дождь, лед'
    }
    df_weather.loc[:, 'preciptype'] = df_weather['preciptype'].replace(translations)
    return df_weather.sort_values(by='datetime')


def historical_weather(latitude: float, longitude: float, start_date: str,
                           end_date: str, api_key: str, pause: int = 2) -> Optional[pd.DataFrame]:
    """
    Получает исторические данные о погоде для заданных координат и диапазона дат.

    :param latitude: Широта.
    :param longitude: Долгота.
    :param start_date: Начальная дата (YYYY-MM-DD).
    :param end_date: Конечная дата (YYYY-MM-DD).
    :param api_key: API-ключ Visual Crossing.
    :param pause: Пауза между запросами в секундах.
    :return: DataFrame с данными или None при ошибке.
    """
    coords = f"{latitude},{longitude}"
    url = (
        f"https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/"
        f"{urllib.parse.quote(coords)}/{start_date}/{end_date}"
        f"?unitGroup=metric&include=days&key={api_key}&contentType=csv"
    )
    logging.info(f"Запрос: {url}")
    try:
        with urllib.request.urlopen(url) as response:
            df_weather = pd.read_csv(response)
        df_weather = filter_weather(df_weather)
        df_weather['latitude'] = latitude
        df_weather['longitude'] = longitude
        time.sleep(pause)
        return df_weather
    except urllib.error.HTTPError as e:
        logging.error(f"HTTPError (код {e.code}) для координат {latitude}, {longitude}")
    except urllib.error.URLError as e:
        logging.error(f"URLError ({e.reason}) для координат {latitude}, {longitude}")
    return None


def get_city_by_coordinates(latitude: float, longitude: float) -> str:
    """
    Определяет название города по координатам с использованием Nominatim.

    :param latitude: Широта.
    :param longitude: Долгота.
    :return: Название города или 'Не определен'.
    """
    try:
        geolocator = Nominatim(user_agent="weather_parser")
        location = geolocator.reverse((latitude, longitude), language='ru')
        if location:
            address = location.raw.get('address', {})
            return address.get('city') or address.get('town') or address.get('village') or "Не определен"
    except Exception as e:
        logging.error(f"Ошибка геокодирования ({latitude}, {longitude}): {e}")
    return "Не определен"


def process_data(df_input: pd.DataFrame, api_key: str,
                                   db_config: Dict[str, Any]) -> None:
    """
    Для каждой записи входного DataFrame получает данные о погоде, добавляет название города,
    и записывает данные в PostgreSQL.

    :param df_input: DataFrame с колонками 'latitude', 'longitude', 'start_date', 'end_date'.
    :param api_key: API-ключ Visual Crossing.
    :param db_config: Параметры подключения к БД.
    """
    all_data = pd.DataFrame()

    for idx, row in df_input.iterrows():
        lat, lon = row['latitude'], row['longitude']
        start, end = row['start_date'], row['end_date']
        logging.info(f"Обработка {idx+1}/{len(df_input)}: ({lat}, {lon}) с {start} по {end}")
        df_weather = historical_weather(lat, lon, start, end, api_key)
        if df_weather is not None:
            df_weather['city'] = get_city_by_coordinates(lat, lon)
            all_data = pd.concat([all_data, df_weather], ignore_index=True)
        else:
            logging.error(f"Нет данных для координат ({lat}, {lon})")

    if all_data.empty:
        logging.error("Нет данных для записи в БД.")
        return

    all_data.rename(columns={'datetime': 'date'}, inplace=True)
    cols = ['city', 'date', 'tempmax', 'temp', 'tempmin', 'precip', 'humidity',
            'windspeed', 'cloudcover', 'solarenergy', 'uvindex', 'preciptype',
            'latitude', 'longitude']
    data_to_insert = all_data[cols]

    insert_query = """
        INSERT INTO weather (
            city, date, tempmax, temp, tempmin, precip, humidity, windspeed,
            cloudcover, solarenergy, uvindex, preciptype, latitude, longitude
        )
        VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
        ON CONFLICT (date, latitude, longitude) DO UPDATE SET
            city = EXCLUDED.city,
            tempmax = EXCLUDED.tempmax,
            temp = EXCLUDED.temp,
            tempmin = EXCLUDED.tempmin,
            precip = EXCLUDED.precip,
            humidity = EXCLUDED.humidity,
            windspeed = EXCLUDED.windspeed,
            cloudcover = EXCLUDED.cloudcover,
            solarenergy = EXCLUDED.solarenergy,
            uvindex = EXCLUDED.uvindex,
            preciptype = EXCLUDED.preciptype
    """

    try:
        conn = psycopg2.connect(
            host=db_config['host'],
            port=db_config.get('port', 5432),
            dbname=db_config['dbname'],
            user=db_config['user'],
            password=db_config['password']
        )
        conn.autocommit = True
        cursor = conn.cursor()
        logging.info("Соединение с БД установлено.")
    except Exception as e:
        logging.error(f"Ошибка подключения к БД: {e}")
        return

    for _, row in data_to_insert.iterrows():
        params = (
            row['city'], row['date'], row['tempmax'], row['temp'],
            row['tempmin'], row['precip'], row['humidity'], row['windspeed'],
            row['cloudcover'], row['solarenergy'], row['uvindex'], row['preciptype'],
            row['latitude'], row['longitude']
        )
        try:
            cursor.execute(insert_query, params)
            logging.info(f"Данные за {row['date']} для ({row['latitude']}, {row['longitude']}), город: {row['city']} записаны")
        except Exception as e:
            logging.error(f"Ошибка вставки данных: {e}")

    cursor.close()
    conn.close()
    logging.info("Соединение с БД закрыто.")


def main() -> None:
    """
    Основная функция: получает данные о погоде по заданным координатам и датам, 
    и сохраняет их в базу данных PostgreSQL.
    """
    api_key = "YOUR_API_KEY_HERE"

    db_config = {
        'host': "your_host",
        'port': 5432,
        'dbname': "your_dbname",
        'user': "your_user",
        'password': "your_password"
    }
    process_data(df_input, api_key, db_config)
    logging.info("Сбор данных завершен.")


if __name__ == "__main__":
    main()

1. Получение API-ключа Visual Crossing

Прежде чем запустить скрипт, необходимо получить API-ключ:

  1. Заходим на сайт Visual Crossing.
  2. Регистрируемся или входим в аккаунт.
  3. Переходим в раздел API & Data Services.
  4. Создаём API-ключ (он будет использоваться в запросах к сервису).
  5. Копируем API-ключ и сохраняем его.

2. Настройка скрипта

После получения API-ключа его нужно вставить в скрипт в переменную api_key:

Python
api_key = "YOUR_API_KEY_HERE"

Также необходимо указать параметры подключения к PostgreSQL, заменив your_host, your_dbname, your_user, your_password:

Python
db_config = {
    'host': "your_host",
    'port': 5432,
    'dbname': "your_dbname",
    'user': "your_user",
    'password': "your_password"
}

3. Подготовка входных данных

Прежде чем запускать скрипт, необходимо подготовить датасет (например, df_input), содержащий следующие колонки:

  • latitude – широта (пример: 55.7558)
  • longitude – долгота (пример: 37.6173)
  • start_date – начальная дата сбора данных (формат: YYYY-MM-DD)
  • end_date – конечная дата сбора данных (формат: YYYY-MM-DD)

Почему используем координаты, а не название города?
Работа по координатам точнее, чем запрос по названию:

  • Не возникает проблем с транслитерацией (например, «Tverdysh» vs. «Твердыш»1).
  • Исключаются ошибки, когда у нескольких поселков одно и то же название.

Пример входного датасета:

latitudelongitudestart_dateend_date
55.755837.61732024-01-012024-02-01
59.934330.33512024-01-012024-02-01

4. Как работает скрипт?

  1. Проходит по списку координат и дат, собирая данные о погоде через Visual Crossing API.
  2. Фильтрует и переводит данные, приводя осадки (дождь, снег, град) к русскому языку.
  3. Определяет название города по координатам через сервис Nominatim (если не найдено, записывает «Не определено»).
  4. Записывает данные в PostgreSQL, обновляя их при повторном запуске.

5. Запуск скрипта

Просто запускаем команду:

pip install pandas psycopg2-binary geopy urllib3
python3 weather.py

Если всё настроено верно, в PostgreSQL появятся записи с историческими данными о погоде.


Теперь можно анализировать данные, строить прогнозы или просто доказывать соседу, что «в прошлом году было холоднее» — теперь у вас есть точные цифры! 😊🌦️

  1. https://ru.wikipedia.org/wiki/Твердыш ↩︎
Пред.
Математический аппарат Facebook Prophet

Математический аппарат Facebook Prophet

Содержание Показать 1