Практическое руководство по использованию skyfield, timezonefinder, geopy и pytz, а также другие практические приложения
Делиться

Вы планируете отметить дни рождения трёх друзей: Габриэля, Жака и Камиллы. Все трое родились в 1996 году в Париже, Франция, поэтому в следующем, 2026 году, им исполнится 30 лет. Габриэль и Жак будут в Париже в день своего рождения, а Камилла — в Токио, Япония. Габриэль и Камилла обычно отмечают дни рождения в «официальные» дни, указанные в их свидетельствах о рождении — 18 января и 5 мая соответственно. Жак, родившийся 29 февраля, предпочитает отмечать свой день рождения (или гражданскую годовщину) 1 марта в невисокосные годы.
Мы используем високосные годы, чтобы синхронизировать наш календарь с орбитой Земли вокруг Солнца. Солнечный год — время, необходимое Земле для совершения одного полного оборота вокруг Солнца — составляет приблизительно 365,25 дня. По соглашению, в григорианском календаре каждый год длится 365 дней, за исключением високосных лет, которым дается 366 дней для компенсации дробного дрейфа во времени. Это заставляет вас задуматься: будет ли кто-нибудь из ваших друзей праздновать свой день рождения в «настоящую» годовщину своего дня рождения, т. е. в день, когда Солнце будет в том же положении на небе (относительно Земли), что и в день их рождения? Может быть, ваши друзья в конечном итоге отпразднуют свое 30-летие — особую веху — на день раньше или на день позже?
В данной статье на примере задачи о днях рождения читатели познакомятся с некоторыми интересными и широко применимыми пакетами Python с открытым исходным кодом для астрономических вычислений и геопространственно-временной аналитики, включая skyfield, timezonefinder, geopy и pytz. Чтобы получить практический опыт, мы используем эти пакеты для решения нашей увлекательной задачи — точного предсказания «реального дня рождения» (или даты возвращения Солнца) в заданном будущем году. Затем мы обсудим, как такие пакеты можно использовать в других реальных приложениях.
Настоящий предсказатель дня рождения
Настройка проекта
Все шаги реализации, описанные ниже, были протестированы на macOS Sequoia 15.6.1 и должны быть примерно одинаковыми на Linux и Windows.
Начнём с настройки каталога проекта. Для управления проектом мы будем использовать uv (инструкции по установке см. здесь). Проверьте установленную версию в Терминале:
uv —версия
Инициализируйте каталог проекта с именем real-birthday-predictor в подходящем месте на вашем локальном компьютере:
uv init —bare real-birthday-predictor
В каталоге проекта создайте файл requirements.txt со следующими зависимостями:
skyfield==1.53 timezonefinder==8.0.0 geopy==2.4.1 pytz==2025.2
Вот краткий обзор каждого из этих пакетов:
- Skyfield предоставляет функции для астрономических вычислений. Его можно использовать для вычисления точного положения небесных тел (например, Солнца, Луны, планет и спутников), что помогает определить время восхода/захода, затмений и орбитальных траекторий. Программа использует так называемые эфемериды (таблицы данных о положении различных небесных тел, экстраполированные за многие годы), которые ведутся такими организациями, как Лаборатория реактивного движения НАСА (JPL). В этой статье мы будем использовать облегченный файл эфемерид DE421, охватывающий даты с 29 июля 1899 года по 9 октября 2053 года.
- timezonefinder имеет функции для сопоставления географических координат (широты и долготы) с часовыми поясами (например, «Европа/Париж»). Это можно делать офлайн.
- geopy предлагает функции геопространственной аналитики, такие как сопоставление адресов и географических координат. Мы будем использовать его вместе с геокодером Nominatim для данных OpenStreetMap, чтобы сопоставить названия городов и стран с координатами.
- pytz предоставляет функции для временной аналитики и преобразования часовых поясов. Мы будем использовать его для преобразования времени между UTC и местным временем, используя региональные правила перехода на летнее время.
Мы также будем использовать несколько других встроенных модулей, таких как datetime для анализа и обработки значений даты/времени, calendar для проверки високосных лет и time для сна между попытками геокодирования.
Затем создайте виртуальную среду Python 3.12 внутри каталога проекта, активируйте среду и установите зависимости:
uv venv —python=3.12 source .venv/bin/activate uv add -r requirements.txt
Проверьте, установлены ли зависимости:
список uv-пипов
Выполнение
В этом разделе мы по частям разберём код для прогнозирования «реальной» даты и времени дня рождения в заданном будущем году и месте празднования. Сначала мы импортируем необходимые модули:
из datetime импорт datetime, timedelta из skyfield.api импорт load, wgs84 из timezonefinder импорт TimezoneFinder из geopy.geocoders импорт Nominatim из geopy.exc импорт GeocoderTimedOut импорт pytz импорт calendar импорт time
Затем мы определяем метод, используя осмысленные имена переменных и текст строки документации:
def get_real_birthday_prediction( official_birthday: str, official_birth_time: str, birth_country: str, birth_city: str, current_country: str, current_city: str, target_year: str = None ): «»» Предсказывает «реальный» день рождения (солнечное возвращение) для заданного года с учётом часового пояса в месте рождения и часового пояса в текущем местоположении. В невисокосные годы для гражданской годовщины используется 1 марта, если официальная дата рождения — 29 февраля. «»»
Обратите внимание, что current_country и current_city совместно обозначают место, в котором будет отмечаться день рождения в указанном году.
Мы проверяем входные данные перед началом работы с ними:
# Определить целевой год, если target_year равен None: target_year = datetime.now().year else: try: target_year = int(target_year) except ValueError: raise ValueError(f»Недопустимый целевой год '{target_year}'. Используйте формат 'yyyy'.») # Проверить и проанализировать дату рождения try: birth_date = datetime.strptime(official_birthday, «%d-%m-%Y») except ValueError: raise ValueError( f»Недопустимая дата рождения '{official_birthday}'. » «Используйте формат 'dd-mm-yyyy' с допустимой календарной датой.» ) # Проверить и проанализировать время рождения try: birth_hour, birth_minute = map(int, official_birth_time.split(«:»)) except ValueError: raise ValueError( f»Недопустимое время рождения '{official_birth_time}'. » «Пожалуйста используйте 24-часовой формат «чч:мм». ) если нет (0 <= birth_hour <= 23): raise ValueError(f"Час '{birth_hour}' выходит за пределы диапазона (0-23).") если нет (0 <= birth_minute <= 59): raise ValueError(f"Минута '{birth_minute}' выходит за пределы диапазона (0-59).")
Затем мы используем geopy с геокодером Nominatim для определения места рождения и текущего местоположения. Чтобы избежать ошибок тайм-аута, мы устанавливаем достаточно большое значение тайм-аута — десять секунд; именно столько времени наша функция safe_geocode ожидает ответа от службы геокодирования, прежде чем вызвать исключение geopy.exc.GeocoderTimedOut. Для дополнительной безопасности функция пытается выполнить процедуру поиска три раза с задержкой в одну секунду, прежде чем прекратить выполнение:
geolocator = Nominatim(user_agent=»birthday_tz_lookup», timeout=10) # Вспомогательная функция для вызова API геокодирования с повторными попытками def safe_geocode(query, retries=3, delay=1): for attempt in range(retries): try: return geolocator.geocode(query) except GeocoderTimedOut: if attempt < retries - 1: time.sleep(delay) else: raise RuntimeError( f"Не удалось получить местоположение для '{query}' после {retries} попыток." "Служба геокодирования может быть медленной или недоступной. Повторите попытку позже." ) birth_location = safe_geocode(f"{birth_city}, {birth_country}") current_location = safe_geocode(f"{current_city}, {current_country}") if not birth_location or not current_location: raise ValueError("Не удалось найти координаты для одного (мест. Пожалуйста, проверьте орфографию.)
Используя географические координаты места рождения и текущего местоположения, мы определяем соответствующие часовые пояса, а также дату и время рождения по UTC. Мы также предполагаем, что такие люди, как Жак, родившийся 29 февраля, предпочтут отмечать свой день рождения 1 марта в невисокосные годы:
# Получить часовые пояса tf = TimezoneFinder() birth_tz_name = tf.timezone_at(lng=birth_location.longitude, lat=birth_location.latitude) current_tz_name = tf.timezone_at(lng=current_location.longitude, lat=current_location.latitude) if not birth_tz_name or not current_tz_name: raise ValueError(«Не удалось определить часовой пояс для одного из местоположений.») birth_tz = pytz.timezone(birth_tz_name) current_tz = pytz.timezone(current_tz_name) # Установить гражданскую годовщину на 1 марта для дней рождения 29 февраля в невисокосные годы birth_month, birth_day = birth_date.month, birth_date.day if (birth_month, birth_day) == (2, 29): if not calendar.isleap(birth_date.year): raise ValueError(f»{birth_date.year} не високосный год, поэтому 29 февраля недопустимо.») civil_anniversary_month, civil_anniversary_day = ( (3, 1) if not calendar.isleap(target_year) else (2, 29) ) else: civil_anniversary_month, civil_anniversary_day = birth_month, birth_day # Анализируем дату и время рождения по местному времени места рождения birth_local_dt = birth_tz.localize(datetime( birth_date.year, birth_month, birth_day, birth_hour, birth_minute )) birth_dt_utc = birth_local_dt.astimezone(pytz.utc)
Используя данные эфемерид DE421, мы вычисляем, где находилось Солнце (т. е. его эклиптическую долготу) в точное время и в месте рождения человека:
# Загрузить данные эфемерид и получить эклиптическую долготу Солнца в момент рождения eph = load(«de421.bsp») # Охватывает даты с 1899-07-29 по 2053-10-09 ts = load.timescale() sun = eph[«sun»] earth = eph[«earth»] t_birth = ts.utc(birth_dt_utc.year, birth_dt_utc.month, birth_dt_utc.day, birth_dt_utc.hour, birth_dt_utc.minute, birth_dt_utc.second) # Долгота рождения в тропической системе от точки зрения наблюдателя рождения на поверхности Земли birth_observer = earth + wgs84.latlon(birth_location.latitude, birth_location.longitude) ecl = birth_observer.at(t_birth).observe(sun).apparent().ecliptic_latlon(epoch='date') birth_longitude = ecl[1].degrees
Обратите внимание, что при первом выполнении строки eph = load(«de421.bsp») файл de421.bsp будет загружен и помещен в каталог проекта; во всех последующих запусках загруженный файл будет использоваться напрямую. Также можно изменить код для загрузки другого файла эфемерид (например, de440s.bsp, охватывающего годы до 22 января 2150 года).
Теперь переходим к интересной части функции: мы сделаем первоначальное предположение о «реальной» дате и времени дня рождения в целевом году, определим безопасные верхнюю и нижнюю границы для истинного значения даты и времени (например, два дня в каждую сторону от первоначального предположения) и выполним двоичный поиск с ранней остановкой, чтобы эффективно нацелиться на истинное значение:
# Начальное предположение для целевого года солнечного возвращения approx_dt_local_birth_tz = birth_tz.localize(datetime( target_year, civil_anniversary_month, civil_anniversary_day, birth_hour, birth_minute )) approx_dt_utc = approx_dt_local_birth_tz.astimezone(pytz.utc) # Вычислить долготу Солнца из точки зрения текущего наблюдателя на поверхности Земли current_observer = earth + wgs84.latlon(current_location.latitude, current_location.longitude) def sun_longitude_at(dt): t = ts.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) ecl = current_observer.at(t).observe(sun).apparent().ecliptic_latlon(epoch='date') return ecl[1].degrees def angle_diff(a, b): return (a — b + 180) % 360 — 180 # Устанавливаем безопасные верхнюю и нижнюю границы для пространства поиска dt1 = approx_dt_utc — timedelta(days=2) dt2 = approx_dt_utc + timedelta(days=2) # Используем двоичный поиск с ранней остановкой для решения задачи точного возврата солнца в UTC old_angle_diff = 999 for _ in range(50): mid = dt1 + (dt2 — dt1) / 2 curr_angle_diff = angle_diff(sun_longitude_at(mid), birth_longitude) if old_angle_diff == curr_angle_diff: # Условие ранней остановки break if curr_angle_diff > 0: dt2 = mid else: dt1 = mid old_angle_diff = curr_angle_diff real_dt_utc = dt1 + (dt2 — dt1) / 2
В этой статье вы найдете больше примеров использования бинарного поиска и поймете, почему этот алгоритм так важен для специалистов по работе с данными.
Наконец, дата и время «реального» дня рождения, определенные двоичным поиском, преобразуются в часовой пояс текущего местоположения, форматируются по мере необходимости и возвращаются:
# Преобразовать в местное время текущего местоположения и отформатировать output real_dt_local_current = real_dt_utc.astimezone(current_tz) date_str = real_dt_local_current.strftime(«%d/%m») time_str = real_dt_local_current.strftime(«%H:%M») return date_str, time_str, current_tz_name
Тестирование
Теперь мы можем предсказать «настоящие» дни рождения Габриэля, Жака и Камиллы в 2026 году.
Чтобы сделать вывод функции более удобным для восприятия, вот вспомогательная функция, которую мы будем использовать для наглядного вывода результатов каждого запроса:
def print_real_birthday(official_birthday: str, official_birth_time: str, birth_country: str, birth_city: str, current_country: str, current_city: str, target_year: str = None): «»»Красивый вывод со скрытием подробных сообщений об ошибках.»»» print(«Официальный день рождения и время:», official_birthday, «at», official_birth_time) try: date_str, time_str, current_tz_name = get_real_birthday_prediction(official_birthday, official_birth_time, birth_country, birth_city, current_country, current_city, target_year ) print(f»В году {target_year} ваш настоящий день рождения приходится на {date_str} в {time_str} ({current_tz_name})n») except ValueError as e: print(«Ошибка:», e)
Вот тестовые случаи:
# Габриэль print_real_birthday( official_birthday=»18-01-1996″, official_birth_time=»02:30″, birth_country=»France», birth_city=»Paris», current_country=»France», current_city=»Paris», target_year=»2026″ ) # Жак print_real_birthday( official_birthday=»29-02-1996″, official_birth_time=»05:45″, birth_country=»France», birth_city=»Paris», current_country=»France», current_city=»Paris», target_year=»2026″ ) # Камиль print_real_birthday( official_birthday=»05-05-1996″, official_birth_time=»20:30″, birth_country=»Paris», birth_city=»France», current_country=»Japan», current_city=»Токио», target_year=»2026″ )
И вот результаты:
Официальная дата рождения и время: 18-01-1996 в 02:30 В 2026 году ваш настоящий день рождения 17/01 в 09:21 (Европа/Париж) Официальная дата рождения и время: 29-02-1996 в 05:45 В 2026 году ваш настоящий день рождения 28/02 в 12:37 (Европа/Париж) Официальная дата рождения и время: 05-05-1996 в 20:30 В 2026 году ваш настоящий день рождения 06/05 в 09:48 (Азия/Токио)
Как мы видим, «реальный» день рождения (или момент солнечного возвращения) отличается от официального дня рождения всех троих ваших друзей: Габриэль и Жак теоретически могут начать праздновать за день до своего официального дня рождения в Париже, в то время как Камилле следует подождать еще один день, прежде чем отмечать свое 30-летие в Токио.
В качестве более простой альтернативы описанным выше действиям автор этой статьи создал библиотеку Python под названием solarius для достижения того же результата (подробности см. здесь). Установите библиотеку с помощью команды pip install solarius или uv add solarius и используйте её, как показано ниже:
from solarius.model import SolarReturnCalculator calculator = SolarReturnCalculator(ephemeris_file=»de421.bsp») # Прогноз без печати date_str, time_str, tz_name = calculator.predict( official_birthday=»18-01-1996″, official_birth_time=»02:30″, birth_country=»France», birth_city=»Paris», current_country=»France», current_city=»Paris», target_year=»2026″ ) print(date_str, time_str, tz_name) # Или используйте удобный принтер calculator.print_real_birthday( official_birthday=»18-01-1996″, official_birth_time=»02:30″, birth_country=»France», birth_city=»Paris», current_country=»France», current_city=»Paris», target_year=»2026″ )
Конечно, дни рождения — это не только предсказание солнечного движения — эти особые дни имеют вековую историю. Вот короткое видео об увлекательном происхождении дней рождения:
За пределами дней рождения
Целью этого раздела было предоставить читателям увлекательный и интуитивно понятный пример использования различных пакетов для астрономических вычислений и геопространственно-временной аналитики. Однако полезность таких пакетов выходит далеко за рамки предсказания дней рождения.
Например, все эти пакеты могут быть использованы для прогнозирования других астрономических событий (например, для определения времени восхода, заката или затмения в определённую дату в определённом месте). Прогнозирование движения спутников и других небесных тел также может играть важную роль в планировании космических миссий.
Эти пакеты также можно использовать для оптимизации размещения солнечных панелей в конкретном месте, например, в жилом районе или на коммерческой территории. Целью будет прогнозирование вероятного количества солнечного света, падающего на это место в разное время года, и использование этих данных для корректировки размещения, наклона и графика использования солнечных панелей для максимального сбора энергии.
Наконец, эти пакеты могут быть использованы для реконструкции исторических событий (например, в контексте археологических или исторических исследований, или даже судебной экспертизы). Целью здесь является воссоздание условий неба для конкретной даты и места в прошлом, чтобы помочь исследователям лучше понять условия освещения и видимости в то время.
В конечном итоге, комбинируя эти пакеты с открытым исходным кодом и встроенные модули различными способами, можно решать интересные проблемы, затрагивающие ряд областей.
Источник: towardsdatascience.com



























