Как отслеживать полярное сияние с помощью кода на Python? Инструкция для новичков

Продолжаем разбираться в том, как почувствовать себя айтишником и самостоятельно спрогнозировать появление полярного сияния.

Как отслеживать полярное сияние с помощью кода на Python? Инструкция для новичков

Привет! Это снова Александр, технический писатель в Selectel. Это вторая часть статьи о прогнозировании полярных сияний. В этот раз расскажу об автоматизации оценки состояния магнитосферы Земли с помощью Python и написании Telegram-бота, который будет присылать мне готовый прогноз по нажатию кнопки.

В первой статье я объяснил природу полярных сияний. Для понимания кода она не нужна, поэтому в этот раз мы не будем отвлекаться на выбросы корональной массы, свободные электроны и вот это все.

Используйте навигацию, если не хотите читать текст полностью:

Где брать данные

Большинство показателей собираются на Земле. Например, данные о радиопотоке от Солнца фиксирует обсерватория Национального исследовательского совета Канады. А Kp-индекс — это среднее значение К-индексов, зафиксированных в 13 геомагнитных обсерваториях, расположенных между 44 и 60 градусами северной и южной геомагнитных широт.

Тем не менее, что бы ни фиксировалось на Земле, нам нужны данные и из космоса. Допустим, на Солнце только что произошла вспышка, направленная точно на Землю. Через три-четыре дня (в зависимости от скорости потока) солнечные частицы достигнут первой орбитальной точки Лагранжа (L1) примерно в 1,5 млн км от Земли. Это такая точка между нашей планетой и Солнцем, в которой силы гравитации двух космических тел уравновешивают друг друга. Если туда поместить какой-нибудь объект, он будет оставаться почти неподвижным относительно Земли и двигаться вместе с ней вокруг звезды.

Собственно, лучшее, что туда можно поместить, — исследовательский спутник. Нам повезло: в точке Лагранжа находится аппарат DSCOVR от NASA и NOAA. Он передает на Землю важные данные о погоде в космосе.

Анимация движения Земли и спутника DSCOVR. Источник.

Кстати, есть пять точек Лагранжа в системе Земля-Солнце.

<i>Точки Лагранжа. В точке L1 находится DSCOVR и другие спутники. В точке L2 — телескоп Джеймс Уэбб. <a href="https://api.vc.ru/v2.8/redirect?to=https%3A%2F%2Fscience.nasa.gov%2Fsolar-system%2Fresources%2Ffaq%2Fwhat-are-lagrange-points%2F&postId=1601181" rel="nofollow noreferrer noopener" target="_blank">Источник</a>.</i>
Точки Лагранжа. В точке L1 находится DSCOVR и другие спутники. В точке L2 — телескоп Джеймс Уэбб. Источник.

NOAA собирает у себя данные с DSCOVR и исследовательского оборудования на Земле. То, что интересно нам, собрано на сайте управления в разделе Aurora Dashboard.

Более того, у NOAA есть свое мобильное приложение Aurora Forecast с красивой визуализацией. Там можно посмотреть как готовый прогноз, так и отдельные параметры, хотя почему-то не все. Так зачем вообще это все, если есть удобное приложение? Считаю вопрос риторическим. Это маленький веселый pet-проект.

Сбор данных для краткосрочного прогноза

Как я сказал в самом начале, это мой первый опыт работы с Python. Используя обрывки знаний с Хабра, Stack Overflow, YouTube и других источников, я написал код, который делает ровно то, что мне нужно. Но настоящий Python-разработчик сказал, что в таком виде показывать код никому нельзя. Проблема тут в том, что объяснить происходящее далее я могу только на том, что написал сам. А облагороженную версию от коллеги оставлю ниже.

Облагороженный код, который мы далее будем рассматривать по частям:

import re import json import requests from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By from selenium.common.exceptions import TimeoutException class AuroraParser: service = Service(ChromeDriverManager().install()) URL = "https://www.swpc.noaa.gov/communities/aurora-dashboard-experimental" def __init__(self, wind_speed, const_distance): self.driver = webdriver.Chrome(service=AuroraParser.service) self.wind_speed = wind_speed self.const_distance = const_distance def get_source_code(self) -> None: self.driver.get(AuroraParser.URL) while True: try: e = self.driver.find_element(By.ID, "WindSpeed") self.wind_speed = float(e.text) e2 = self.driver.find_element(By.ID, "Flux") e3 = self.driver.find_element(By.ID, "Bt") e4 = self.driver.find_element(By.ID, "Bz") e5 = self.driver.find_element(By.XPATH, "/html/body/div[4]/section/div/div/div/div/section[1]/div/div/div[1]/div[4]/div[1]/div/div[2]/div[4]/div[1]") p = requests.get("https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json") Kp = json.loads(p.text) with open("Kp.txt", "w") as l: l.write(str(Kp)) with open("Kp.txt", "r") as l: text = l.read() pattern = r"\d+\.\d+" numbers = re.findall(pattern, text) if numbers: last_number = numbers[-1] with open("parameters.txt", "w") as a: a.write("Solar Wind Speed: " + str(self.wind_speed) + " km/s" + "\n") a.write("Flux: " + e2.text + " sfu" + "\n") a.write("Bt: " + e3.text + " nT" + "\n") a.write("Bz: " + e4.text + " nT" + "\n") a.write("Storm: " + e5.text + " " "\n") a.write(f"Kp: {last_number}" + "\n") a.write("\n" + "Time to Earth: " + str(round(self.const_distance / self.wind_speed, 1)) + " minutes" + "\n") break except TimeoutException as _ex: print(_ex) break with open("parameters.txt", "r") as b: for line in b: location_vars = {"Storm: G ":65, "G1":60, "G2":55, "G3":50, "G4":45, "G5":40} with open("parameters.txt", "a", encoding="utf-8") as b: for loc_var in location_vars.keys(): if loc_var in line: b.write(f"Location: north to {location_vars[loc_var]}" + "° when Bz<0" + "\n") with open("parameters.txt", "r", encoding="utf-8") as c: content = c.read() with open("parameters.txt", "a", encoding="utf-8") as c: if re.search(r"-\d{1}", content): c.write("Low chance to see aurora 😾") elif re.search(r"(-1[0-9]|-19)", content): c.write("Good chance to see aurora 😺") elif re.search(r"(-[2-9][0-9]|-100)", content): c.write("WOW! High chance to see aurora! 🙀") else: c.write("No chance to see aurora 😿 (Bz ≥ 0)")

Первая мысль — подключиться к API NOAA и написать Telegram-бота, который будет по команде ходить на сайт и забирать данные. В целом, ничего сложного. В Академии Selectel описан подобный пример с ботом, который собирает данные и присылает прогноз погоды для любого города.

Но API NOAA подойдет как раз для прогноза погоды на Земле. Все-таки это управление океанических и атмосферных исследований. Центр прогнозирования космической погоды NOAA свой API не предлагает. Значит, перейдем к другим способам. Похоже, это приключение на 20 минут.

Парсинг с помощью Selenium

Итак, нам нужно собрать данные о погоде в космосе. Сайт NOAA динамический, то есть нужные параметры магнитосферы и космоса там регулярно обновляются. Скорость солнечного ветра, Bz и Bt — каждую минуту, сила магнитных бурь и реальный Kp — каждые три часа, F 10.7 cm и прогноз Kp на три дня — раз в сутки.

Получается, обойтись библиотекой BeautifulSoup не выйдет. Она поможет вытащить со страницы статический HTML-код, но не меняющиеся данные. А нужны нам именно они.

<i>Здесь лежат нужные нам данные. <a href="https://api.vc.ru/v2.8/redirect?to=https%3A%2F%2Fwww.swpc.noaa.gov%2Fcommunities%2Faurora-dashboard-experimental&postId=1601181" rel="nofollow noreferrer noopener" target="_blank">Источник</a>.</i>
Здесь лежат нужные нам данные. Источник.

Очевидное решение — использовать библиотеку Selenium. Устанавливаем ее и webdriver_manager в терминале:

pip install selenium pip install webdriver_manager

Следом импортируем пакеты и объявляем переменные:

from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By from selenium.common.exceptions import TimeoutException import re import json import requests service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) URL = "https://www.swpc.noaa.gov/communities/aurora-dashboard-experimental"

Большинство данных будем забирать со страницы центра прогнозирования космической погоды NOAA. Там есть все, кроме реального и прогнозного Kp-индекса, но эту проблему мы решим чуть позже. А пока спарсим скорость солнечного ветра, Bt, Bz, F 10.7 cm и силу магнитной бури.

Первые четыре можно получить одной командой, поскольку все они в HTML-коде страницы находятся внутри id "summary". Удобно, но Selenium в таком случае выдаст результат одной строкой с той же пунктуацией, что на странице.

<i>Быстро, но читаемость, на мой взгляд, хромает.</i>
Быстро, но читаемость, на мой взгляд, хромает.

Мне это не нравится. Соберем все по отдельности и запишем каждый показатель с новой строки в текстовый файл:

def get_source_code(URL: str) -> None: driver.get(URL) while True: try: e = driver.find_element(By.ID, "WindSpeed") e2 = driver.find_element(By.ID, "Flux") e3 = driver.find_element(By.ID, "Bt") e4 = driver.find_element(By.ID, "Bz") e5 = driver.find_element(By.XPATH, "/html/body/div[4]/section/div/div/div/div/section[1]/div/div/div[1]/div[4]/div[1]/div/div[2]/div[4]/div[1]") with open("parameters.txt", "w") as a: a.write(e.text + "\n") a.write(e2.text + "\n") a.write(e3.text + "\n") a.write(e4.text + "\n") a.write(e5.text + "\n") break except TimeoutException as _ex: print(_ex) break def main() -> None: get_source_code(URL) if __name__ =="__main__": main() driver.quit()

Как видите, у каждого элемента есть свой id и только с одним что-то пошло не так. Это сила магнитной бури. В HTML-коде, конечно, есть id нужного элемента — "noaa_scale_info_effect". Но Selenium почему-то не может его достать. Так я пришел к идее спарсить показатель через XPATH. Почему нет? Копируем его из кода страницы — "/html/body/div[4]/section/div/div/div/div/section[1]/div/div/div[1]/div[4]/div[1]/div/div[2]/div[4]/div[1]". Здесь важно, что сила магнитной бури представлена в нескольких местах. Есть максимальная за прошедшие сутки, последняя зафиксированная и три прогнозных на ближайшие три дня. Нас интересует тот показатель, что зафиксирован на планете за последние три часа — Latest Observed.

Поскольку сила магнитной бури и Kp-индекс хорошо соотносятся друг с другом, на этом сбор данных можно было бы и закончить. Но я решил достать оба показателя. Забегая вперед, скажу, что это опциональное решение. Индекс я буду использовать, просто чтобы точнее отслеживать интенсивность бури. Например, уровню G3 обычно соответствуют Kp-индексы 6,67, 7,00 и 7,33. Таким образом, отслеживание Kp позволит судить, усиливается или слабеет буря. Но не забывайте, о чем я писал выше: иногда корреляция двух показателей отключается и при достаточно высоком Kp уровень G остается на нуле.

Итак, на основной странице Kp-индекс есть в интерактивном дашборде, но оттуда данные не достать. Зато они собраны на другой странице этого же сайта, не самой очевидной. Я не придумал ничего лучше, чем загрузить содержимое json-файла в новый текстовый документ, найти в нем все дробные числа и записать последнее в уже имеющийся файл с остальными параметрами. Это и есть самый поздний зафиксированный Kp-индекс. В коде это выглядит так:

p = requests.get("https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json") Kp = json.loads(p.text) with open("Kp.txt", "w") as l: l.write(str(Kp)) with open("Kp.txt", "r") as l: text = l.read() pattern = r"\d+\.\d+" numbers = re.findall(pattern, text) if numbers: last_number = numbers[-1] with open("parameters.txt", "w") as a: a.write(f"{last_number}" + "\n")

Удлиняет ли это код? Да. Замедляет ли в итоге время ответа бота? Наверное. Так нужно ли это делать? Необязательно, но ради большей информативности можно.

Оценка показателей

Теперь у нас есть текстовый файл, в котором собраны нужные данные. Классно? Классно. Но в текущем виде файл "parameters" не очень информативен. А ведь его содержимое мы и будем потом отправлять в мессенджер.

Как отслеживать полярное сияние с помощью кода на Python? Инструкция для новичков

Путем нехитрых манипуляций можно сделать его куда более понятным. Просто чуть пофиксим запись данных от Selenium в текстовый файл:

with open("parameters.txt", "w") as a: a.write("Solar Wind Speed: " + e.text + " km/s" + "\n") a.write("Flux: " + e2.text + " sfu" + "\n") a.write("Bt: "+ e3.text + " nT" + "\n") a.write("Bz: "+ e4.text + " nT" + "\n") a.write("Storm: "+ e5.text) a.write(f"Kp: {last_number}")

Благодаря этому мы получим уже гораздо более содержательный текст:

Как отслеживать полярное сияние с помощью кода на Python? Инструкция для новичков

Правда, первый же человек, которому я показал такое сообщение от бота в Telegram, спросил меня: «Так, ну а сияние-то где будет?». Кажется, приключение на 20 минут выходит из-под контроля.

Расчет времени

Добавим для ясности три строки.

  • В первой строке на основании данных о скорости потока рассчитаем время его прибытия на Землю.

  • Во второй — покажем, к северу от какой широты возможно появление сияния при актуальном состоянии магнитосферы.

  • В третьей — дадим финальную оценку вероятности появления авроры, основываясь на ориентации межпланетного магнитного поля и силе магнитной бури.

Как мы помним со школы, время = расстояние / скорость. Скорость потока солнечных частиц нам известна — ее мы получили от Selenium. Расстояние — тоже. Это около 1,5 млн км от Земли до спутника DSCOVR, который и фиксирует скорость солнечного ветра. Конечно, цифра постоянно варьируется, но в масштабах космоса этим можно пренебречь (как и временем, которое потребуется сигналу, чтобы добраться от спутника до Земли).

Сперва зададим две константы:

wind_speed = 0 const_distance = 25000 #чтобы получить время сразу в минутах, а не в секундах, заранее разделим 1500000 на 60 и получим 25000.

Затем, добавим формулу с округлением до одного знака после запятой:

str(round(const_distance/wind_speed, 1))

Добавим новую строку:

with open("parameters.txt", "a") as a: a.write("\n" + "Time to Earth: " + str(round(const_distance/wind_speed, 1)) + " minutes")

Почему я не написал текст по-русски? Из-за падежей. Мне не хочется получать сообщение с текстом «50 минуты» или «51 минут». Наиболее простым решением счел запись на английском. Ну и раз так, дальше тоже буду писать по-английски.

Здесь считаю уместным небольшое пояснение. Все важные для прогноза показатели собираются на Земле. То есть если мы видим, например, Kp = 7,33, то он уже есть. Так зачем рассчитывать время? Оно показывает, как скоро на магнитосферу начнет воздействовать солнечный ветер, только что зафиксированный спутником DSCOVR. Если видим, что его скорость падает, весьма вероятно (но не гарантировано) уменьшение активности магнитосферы. И наоборот. А время показывает, когда произойдет это изменение.

Расчет координат

Следующий шаг — добавить информацию о том, к северу от какой широты вероятно наблюдение сияния при имеющейся магнитной буре. Здесь я воспользовался функцией vars(), чтобы чрезмерно не раздувать код. Я хочу добавить строку «Location: north to X° when Bz<0», в которой Х подбирался бы в зависимости от записанной ранее силы магнитной бури. Уточнение «when Bz<0» просто будет напоминать, что даже при геомагнитном шторме межпланетное магнитное поле должно быть ориентировано на юг. Иначе чуда не случится.

with open("parameters.txt", "r") as b: for line in b: location_vars = {"Storm = G ":65, "G1": 60, "G2":55, "G3":50, "G4":45, "G5":40} with open("parameters.txt", "a", encoding="utf-8") as b: for loc_var in location_vars.keys(): if loc_var in line: b.write(f"Location: north to {location_vars[loc_var]}" + "° when Bz<0" + "\n")

Возникает проблема. При поиске "Storm = G" будут находиться "Storm = G1" и так далее. Проблема изящно решается добавлением пробела в конце строки при записи element5.text и location_vars.

Уточню, что прогнозирование места для наблюдений на основании оценки магнитосферы — это лишь определение широты, к северу от которой может возникнуть сияние. Чтобы найти наиболее удобную конкретную локацию, нужно выбрать пространство с безоблачным темным небом и без высоких объектов на горизонте. Здесь-то и помогут сервисы с прогнозом погоды, например тот же Windy.

Оценка вероятности сияния

Итак, у нас уже есть легкочитаемая информация о том, где в теории можно увидеть сияние. Но пока нет очевидного ответа на вопрос, а возникнет ли оно вообще при имеющихся условиях и какова будет его видимость. Чтобы дать оценку, нужно учесть величину Bz.

Наиболее простым вариантом я счел использование регулярных выражений. Так можно однозначно отличить, скажем, -23 от -2, 2, 3 и 23. При этом найти простое решение с функцией vars(), как это было выше, у меня не получилось. Хотя уверен, что оно есть. Как бы то ни было, вот результат:

with open("parameters.txt", "r", encoding="utf-8") as c: content = c.read() with open("parameters.txt", "a", encoding="utf-8") as c: if re.search(r"-\d{1}", content): c.write("Low chance to see aurora 😾") elif re.search(r"(-1[0-9]|-19)", content): c.write("Good chance to see aurora 😺") elif re.search(r"(-[2-9][0-9]|-100)", content): c.write("WOW! High chance to see aurora! 🙀") else: c.write("No chance to see aurora 😿 (Bz ≥ 0)")

Итак, выше описаны четыре условия, от которых зависит добавление еще одной строки.

  • В файле есть отрицательное однозначное число — добавляем строку «Low chance to see aurora 😾». Отрицательным числом здесь может быть только значение Bz. И если оно не ниже -9, ждать яркого сияния не стоит при любой буре.

  • В файле есть отрицательное число в диапазоне от -10 до -19 — добавляем строку «Good chance to see aurora 😺».

  • В файле есть отрицательное число от -20 до -100 — добавляем строку «WOW! High chance to see aurora! 😺». Как показывает практика, при Bz ниже -20 сияние точно появится и будет хорошо различимо на темном небе.

  • В файле не найдено отрицательных чисел. Что ж, прямо сейчас сияния точно не будет. А при отсутствии магнитных бурь (Storm: G) можно смело ложиться спать.

В целом здесь можно сделать более подробную градацию в зависимости от значения Bz. Но я решил, что и так достаточно. Число -100 не является каким-то сакральным. Чисто теоретически оно может быть и ниже, но шансы на это крайне малы. Даже Bz = -50 — чрезвычайно редкое явление.

После всего описанного выше мы получаем уже такой удобный файл:

<i>В таком виде мы и будем получать оценку вероятности сияния в Telegram.</i>
В таком виде мы и будем получать оценку вероятности сияния в Telegram.

Сбор данных для трехдневного прогноза

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

Выше я упомянул, что NOAA дает трехдневный прогноз геомагнитной активности планеты. Он выражается как в Kp-индексе, так и в оценке силы предстоящих магнитных бурь. Можно собирать и то, и другое, но прогноз Kp более информативен. Он формируется на каждые три часа в течение следующих трех суток. Очень удобно.

Данные о прогнозе Kp-индекса на три дня я собрал примерно так же, как уже зафиксированный Kp. Только этот прогноз лежит на другой странице. Загружаем все содержимое в отдельный файл:

t = request("get", "https://services.swpc.noaa.gov/text/3-day-geomag-forecast.txt").text with open("days.txt", "w") as x: x.write(t)

Эти строки я не добавляю в код сразу. Я использую их чуть позже и добавлю в код бота.

Итак. Мы собрали все данные и дали им необходимую оценку. Пора переходить к написанию Telegram-бота, чтобы получать прогноз в мессенджере по нажатию кнопки.

Написание Telegram-бота

Первым делом открываем Telegram, находим @BotFather и отправляем ему команду /newbot. Далее придумываем будущему боту имя и username. Итогом всех манипуляций будет уникальный токен. Обязательно его сохраните и постарайтесь не потерять. Токен нужен, чтобы управлять ботом.

<i>Процесс создания бота в Telegram.</i>
Процесс создания бота в Telegram.

Теперь переходим к написанию кода. Снова начинаем с установки библиотеки. Я использовал pyTelegramBotAPI:

pip install pyTelegramBotAPI

Импортируем пакеты. На этом шаге вместо "TOKEN" указываем токен, полученный от @BotFather.

import telebot from telebot import types bot = telebot.TeleBot("TOKEN") import os from requests import request

Настроим приветствие. По команде /start будут появляться две reply-кнопки. Одна для получения прогноза на основе актуального состояния магнитосферы, другая — для трехдневного прогноза Kp-индекса.

@bot.message_handler(commands=["start"]) def start(message): keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) keyboard.add(types.KeyboardButton("Актуальные параметры")) keyboard.add(types.KeyboardButton("Прогноз на три дня")) bot.send_message(message.chat.id, "Какие показатели изволите?", reply_markup=keyboard)

Следующий шаг — настройка действий бота в ответ на нажатие кнопок. Начнем со сценария, в котором мы хотим получить оценку текущих показателей.

@bot.message_handler(content_types=["text"]) def second(message): if message.text == "Актуальные параметры": os.system("python aurora.py") with open("parameters.txt", "r", encoding="utf-8") as m: d = m.read() bot.send_message(message.chat.id, text=d)

Здесь все просто: нажатие кнопки "Актуальные параметры" запускает скрипт с Selenium, записанный в файле aurora.py. Тот в свою очередь записывает текстовый файл, а бот отправляет его содержимое в ответ.

Почти то же самое делаем со второй кнопкой. Только при ее нажатии бот собирает данные о прогнозе Kp-индекса на три дня. Вот здесь и пригодился тот небольшой кусочек кода из раздела о сборе трехдневных показателей.

if message.text == "Прогноз на три дня": t = request("get", "https://services.swpc.noaa.gov/text/3-day-geomag-forecast.txt").text with open("days.txt", "w") as x: x.write(t) with open("days.txt", "r", encoding="utf-8") as n: lines = n.readlines() selected_lines = lines[15:25] bot.send_message(message.chat.id, text="\n".join(selected_lines))

Все строки из документа с прогнозом нам не так уж и нужны. На мой взгляд, чтобы отсеять лишнее, но сохранить информативность, хватит строк с 16 по 25. Не забываем, что индексация в Python начинается с 0, поэтому указываем диапазон [15:25]. Бот читает эти строки и отправляет их одним сообщением в ответ на нажатие кнопки.

Как и в предыдущем случае, заботливо собранный код отредактировал мой коллега. Результат ниже.

import telebot from telebot import types bot = telebot.TeleBot("TOKEN") #Замените "TOKEN" на токен от @BotFather from requests import request from aurora import AuroraParser @bot.message_handler(commands=["start"]) def start(message): keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) keyboard.add(types.KeyboardButton("Актуальные параметры")) keyboard.add(types.KeyboardButton("Прогноз на три дня")) bot.send_message(message.chat.id, "Какие показатели изволите?", reply_markup=keyboard) @bot.message_handler(content_types=["text"]) def second(message): if message.text == "Актуальные параметры": parser = AuroraParser(wind_speed=0, const_distance=25000) parser.get_source_code() parser.driver.quit() with open("parameters.txt", "r", encoding="utf-8") as m: d = m.read() bot.send_message(message.chat.id, text=d) if message.text == "Прогноз на три дня": t = request("get", "https://services.swpc.noaa.gov/text/3-day-geomag-forecast.txt").text with open("days.txt", "w") as x: x.write(t) with open("days.txt", "r", encoding="utf-8") as n: lines = n.readlines() selected_lines = lines[15:25] bot.send_message(message.chat.id, text="\n".join(selected_lines)) bot.polling(none_stop=True)

Теперь все работает так, как задумано. Остался последний шаг.

Деплой бота в облако

Telegram-бота нужно поселить на облачный сервер. Так он будет работать постоянно, а не только при запуске кода на компьютере. Казалось бы, ну уж этот-то процесс описан миллион раз во всех возможных вариациях. Но нет.

Сервер с Ubuntu

Обычно деплой Telegram-бота на облачный сервер и его запуск выглядят просто. Нужно создать сервер, подключиться через терминал по SSH, установить необходимые пакеты, завести виртуальное окружение, клонировать репозиторий и запустить бота. Но не в этом случае.

После запуска бота на сервере оказалось, что работает только кнопка для вызова прогноза Kp-индекса на три дня. А вот на нажатие кнопки для запроса актуального состояния магнитосферы бот не реагирует. Повторюсь, что локально все работает прекрасно. Ошибок сервер тоже не выдает.

Смотрим логи:

<i>Упс. Что-то пошло не так.</i>
Упс. Что-то пошло не так.

Как видим, Selenium не хочет работать. Оказывается, такая проблема встречается довольно часто — эта библиотека без нареканий запускается локально, но на сервере начинает капризничать. Вопрос уже поднимался на Stack Overflow. Есть три возможных решения.

Первое решение (правильное) — пофиксить код, опираясь на документацию Selenium, и соответствующим образом донастроить сервер. Плюс такого подхода — возможность нормально запустить бота в облаке. Хотя реализация потребует некоторого количества времени и усилий.

Второе решение — оставить все как есть и запускать код локально. Плюс — все будет работать вообще бесплатно. Можно вечером запустить код на ноутбуке и уехать ловить сияние, отслеживая параметры в Telegram. Минус — остановка кода приведет к остановке бота. А я хочу поделиться ботом с друзьями и знакомыми. Будет странно, если они смогут им пользоваться, только когда мне удобно.

Третье решение — арендовать сервер не на Ubuntu, а на Windows. Плюсы — это самый быстрый и простой способ добиться результата. Не потребуются никакие доработки. С другой стороны, сервер на Windows дороже сервера на Ubuntu, а его ресурсы даже в минимальной комплектации могут быть избыточны для простого Telegram-бота, Google Chrome и IDE (одно ядро, 2 ГБ RAM и 32 ГБ SSD). Хотя если есть другие проекты, для которых нужен Windows Server, решение может оказаться вполне рабочим.

Сервер с Windows

Вероятно, в ближайшее время я все же пойду правильным путем: немного перепишу код и донастрою облачный сервер с Ubuntu. А пока добавим еще немного костылей.

Переходим в панель управления, открываем раздел Облачная платформа → Серверы. Нажимаем на кнопку Создать сервер. В поле Источник выбираем Windows и подходящую версию ОС (в моем случае Windows Server 2019 Standard).

Как отслеживать полярное сияние с помощью кода на Python? Инструкция для новичков

Минимальной конфигурации хватит с запасом. По умолчанию SSH-ключ не требуется, т. к. подключаться мы будем по RDP.

Как только статус сервера сменится на ACTIVE, можно открыть Подключение к удаленному рабочему столу на компьютере. Данные для входа доступны в панели управления. Публичный IP-адрес можно посмотреть на вкладке Порты, а логин и пароль — на вкладке Консоль.

После подключения нужно выполнить несколько простых операций.

1. Нажмите Пуск и откройте Server Manager.

Как отслеживать полярное сияние с помощью кода на Python? Инструкция для новичков

2. Перейдите во вкладку Local Server. Найдите справа строку IE Enhanced Security Configuration, переключите настройки на Off и нажмите кнопку ОК.

Как отслеживать полярное сияние с помощью кода на Python? Инструкция для новичков
Как отслеживать полярное сияние с помощью кода на Python? Инструкция для новичков

3. Скачайте и установите Google Chrome и любой IDE (в моем случае PyCharm).

4. Скопируйте файлы .py в проект и запустите код.

На этом подключение можно разорвать, бот продолжит работать. При необходимости вы можете доработать бота, добавить асинхронности в функции и обработчики исключений, которые могут возникать, например, из-за проблем с сайтом NOAA.

Как отслеживать полярное сияние с помощью кода на Python? Инструкция для новичков

UPD: делаем то же самое без Selenium

Спустя несколько дней после публикации статьи получил совет с Хабра — обратить внимание на страницу на сайте NOAA. Там собраны ссылки на файлы json, в которых аккумулируются все нужные данные (и ненужные тоже). Это избавляет от необходимости использовать Selenium и позволяет уложить весь проект в один файл.

Правда, из нескольких json придется отсеять все лишнее, но в целом в этом нет ничего сложного. Вот что получилось в итоге:

import json import requests import telebot from telebot import types from requests import request import re bot = telebot.TeleBot("TOKEN") #вместо TOKEN вставляем токен от BotFather @bot.message_handler(commands=["start"]) #здесь, как и раньше, создаем приветствие и две кнопки def start(message): keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) keyboard.add(types.KeyboardButton("Актуальные параметры")) keyboard.add(types.KeyboardButton("Прогноз на три дня")) bot.send_message(message.chat.id, "Какие показатели изволите?", reply_markup=keyboard) @bot.message_handler(content_types=["text"]) def second(message): wind_speed = 0 #теперь зададим переменные для расчета времени прибытия солнечного выброса const_distance = 25000 if message.text == "Актуальные параметры": try: #завернем все в обработчик ошибок и начнем последовательно ходить в файлы json за данными a = requests.get("https://services.swpc.noaa.gov/products/summary/solar-wind-speed.json") speed = json.loads(a.text) b = requests.get("https://services.swpc.noaa.gov/products/summary/10cm-flux.json") flux = json.loads(b.text) c = requests.get("https://services.swpc.noaa.gov/products/summary/solar-wind-mag-field.json") field = json.loads(c.text) d = requests.get("https://services.swpc.noaa.gov/products/noaa-scales.json") storm = json.loads(d.text) e = requests.get("https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json") Kp = json.loads(e.text) with open("params.txt", "w") as f: #запишем данные из json в текстовый файл f.write(str(speed) + "\n") f.write(str(flux) + "\n") f.write(str(field) + "\n") f.write(str(storm) + "\n") f.write(str(Kp)) with open("params.txt", "r") as g: #откроем файл для чтения и найдем в нем нужные числа с помощью регулярных выражений text = g.read() pattern = r"\d+" #целые положительные числа - это скорость потока, Flux, Bt и G pattern2 = r"-\d+|\d+" #целое число, которое может быть и отрицательным, и положительным - Bz pattern3 = r"\d+\.\d+" #дробное положительное число - Kp-индекс numbers = re.findall(pattern, text) numbers2 = re.findall(pattern2, text) numbers3 = re.findall(pattern3, text) if numbers: #далее нужно лишь знать, какая цифра на каком месте в файле. Это место указано в квадратных скобках. По порядку: скорость потока, Flux, Bt, Bz, G, Kp-индекс first_number = numbers[0] eighth_number = numbers[8] fifteenth_number = numbers[15] sixteenth_number = numbers2[16] thirtythird_number = numbers[33] last_number = numbers3[-1] #находим цифры и копируем их в новый текстовый файл, не забыв добавить пояснения для лучшей читаемости with open("parameters.txt", "w") as h: h.write(f"Solar Wind Speed: {first_number}" + " km/s" + "\n") wind_speed = float(first_number) h.write(f"Flux: {eighth_number}" + " sfu" + "\n") h.write(f"Bt: {fifteenth_number}" + " nT" + "\n") h.write(f"Bz: {sixteenth_number}" + " nT" + "\n") h.write(f"G-Storm: {thirtythird_number}" + "\n") h.write(f"Kp: {last_number}" + "\n") h.write("\n" + "Time to Earth: " + str(round(const_distance / wind_speed, 1)) + " minutes" + "\n") #здесь задаем формулу, как описано выше, для расчета времени with open("parameters.txt", "r") as i: #эта процедура также уже описана выше: добавляем пояснение о том, к северу от какой широты видно сияние при имеющейся магнитной буре for line in i: location_vars = {"G-Storm: 0": 65, "G-Storm: 1": 60, "G-storm: 2": 55, "G-Storm: 3": 50, "G-Storm: 4": 45, "G-Storm: 5": 40} for loc_var in location_vars.keys(): if loc_var in line: with open("parameters.txt", "a", encoding="utf-8") as i: i.write(f"Location: north to {location_vars[loc_var]}" + "° when Bz < 0" + "\n") with open("parameters.txt", "r", encoding="utf-8") as j: #наконец, даем оценку вероятности увидеть полярное сияние в зависимости от Bz: content = j.read() with open("parameters.txt", "a", encoding="utf-8") as k: if re.search(r"Bz: -[1-9] nT", content): k.write("Low chance to see aurora 😾") if re.search(r"Bz: -1[0-9] nT", content): k.write("Good chance to see aurora 😺") if re.search(r"Bz: -[2-9][0-9]|-100 nT", content): k.write("WOW! High chance to see aurora! 🙀") if not re.search(r"-\d+", content): k.write("No chance to see aurora 😿 (Bz ≥ 0)") with open("parameters.txt", "r", encoding="utf-8") as l: m = l.read() bot.send_message(message.chat.id, text=m) except: bot.send_message(message.chat.id, "Ooops, NOAA feels bad. Try again later") #а если на сайте NOAA что-то пойдет не так, выполнение кода не завершится. Просто бот пришлет вот такое сообщение if message.text == "Прогноз на три дня": #здесь ничего нового. Все описано выше. Разве что тоже добавил обработчик ошибок try: n = request("get", "https://services.swpc.noaa.gov/text/3-day-geomag-forecast.txt").text with open("days.txt", "w") as o: o.write(n) with open("days.txt", "r", encoding="utf-8") as p: lines = p.readlines() selected_lines = lines[15:25] bot.send_message(message.chat.id, text="\n".join(selected_lines)) except: bot.send_message(message.chat.id, "Ooops, NOAA feels bad. Try again later") bot.polling(none_stop=True)

И вот такой код уже легко можно запустить на облачном сервере с Ubuntu без дополнительных манипуляций и настроек. Описанный апдейт, безусловно, имеет очевидные преимущества. Во-первых, это дешевле за счет ОС. Во-вторых, бот теперь отвечает значительно быстрее, поскольку не парсит данные через Selenium. Единственный обнаруженный минус — скорость солнечного ветра записывается в файл json раз в три минуты, а не раз в минуту, как на сайте. Но будем честны — это не критично. Приятный бонус — во время доработки кода я нашел и исправил несколько ошибок.

Как арендовать сервер и какой выбрать

Наш Telegram-бот не требует больших вычислительных мощностей, поэтому нас устроит облачный сервер в минимальной конфигурации. Чтобы арендовать его, перейдите в панель управления, выберите раздел Облачная платформа и нажмите на кнопку Создать сервер.

<i>Панель управления Selectel.</i>
Панель управления Selectel.

Далее необходимо настроить конфигурацию. В поле Источник выберите Ubuntu. Как я сказал, большие мощности нам не нужны, поэтому в разделе Конфигурация переключитесь на вкладку Shared и задайте настройки, как на скриншоте ниже:

  • доля vCPU — 10%,
  • RAM — 512 МБ,
  • Диск — 5 ГБ.
<i>Такой конфигурации вполне достаточно для нашей задачи.</i>
Такой конфигурации вполне достаточно для нашей задачи.

Не забудьте также создать и добавить SSH-ключ. О том, что это такое и как этим пользоваться, можно прочитать в документации Selectel.

Как задеплоить бота на сервер

Вот теперь точно последний шаг. Подключаемся к серверу по SSH и проводим нехитрую настройку. Чтобы не перегружать никого деталями, я просто приведу список команд, которые нужно поочередно ввести в терминале:

  • sudo apt update,
  • sudo apt upgrade,
  • sudo apt --reinstall install python3-pip -y,
  • sudo apt install nodejs,
  • sudo apt install npm,
  • npm install pm2 -g,
  • pip install virtualenv,
  • mkdir <название папки> (создаем папку на сервере, внутри которой будет жить бот),
  • cd <название папки>,
  • virtualenv venv.

В этом месте нужно поместить файл с ботом на сервер. Это можно сделать разными путями. Например, если вы предварительно загрузили проект на GitHub, можно просто клонировать его на сервер командой git clone. Либо можно воспользоваться одной из бесплатных утилит, скажем, WinSCP. Она интуитивно понятно, весь процесс выглядит как обычное копирование папок и файлов на компьютере.

После того, как файл .py с ботом размещен на сервере, остается еще несколько шагов. Продолжаем вводить команды в терминале:

  • source venv/bin/activate,
  • pip install -r requirements.txt,
  • pm2 start <название файла с ботом>.py --interpreter=python3

Вы могли заметить, что в предпоследней команде появился какой-то requirements.txt. Крайне полезная штука, кстати. Это файл с зависимостями проекта. Прочитав его, сервер поймет, какие версии библиотек нужно использовать в проекте. Это поможет избежать неожиданных ошибок. Чтобы создать файл в IDE, после написания кода введите команду в его терминале:

pip freeze > requirements.txt

Файл появится на компьютере в директории проекта. Просто скопируйте его вместе с основным файлом на сервер.

Если в какой-то момент потребуется остановить выполнение кода, введите в консоли сервера или в терминале команду:

pm2 stop <название файла с ботом>.py

Весь код, представленный в статье, рабочий. Если захотите воспроизвести описанный опыт, просто скопируйте его, замените TOKEN и повторите все шаги по аренде сервера и деплою проекта.

Немного о тонкостях наблюдений

Увы, идеальные условия бывают далеко не всегда. И в большинстве случаев сияние на небе будет вообще едва различимо невооруженным глазом. Мы увидим какую-то белесую пелену, похожую на туман, а вовсе не то, что нам показывают на картинках.

По своему опыту могу сказать, что для наблюдения полярных сияний на широте Санкт-Петербурга Kp-индекс должен достичь хотя бы 5 баллов (отметки слабой магнитной бури G1). В этом случае будет видна та самая едва различимая и слегка мерцающая пелена. Но если вооружиться камерой, пусть даже в смартфоне, на выдержке от 10 секунд вполне получится снять яркое зеленое свечение с выраженными фиолетовыми акцентами. При тех же параметрах, скажем, в Мурманске (68°58’ северной широты) видимость авроры будет намного лучше.

<i>Снято на смартфон в окрестностях Лебяжьего пляжа.</i>
Снято на смартфон в окрестностях Лебяжьего пляжа.

Советую запастись терпением. Капризное межпланетное магнитное поле может внезапно развернуться и несколько часов «смотреть» на север, не давая разгореться авроре. Выезжая за город для наблюдений, стоит рассчитывать, что провести там придется не меньше 2-3 часов. Есть смысл прихватить с собой горячий чай или кофе в термосе и тепло одеться, потому что вы точно не поедете за сиянием жаркой июльской ночью.

<i>Слабая аврора над северным горизонтом на берегу Ладожского озера.</i>
Слабая аврора над северным горизонтом на берегу Ладожского озера.

Также заранее подумайте, куда именно вы поедете для наблюдений. Так вы спланируете время на дорогу, сможете подобрать место без городской засветки и избежите необходимости внезапно штурмовать бездорожье. Например, мои любимые места под Петербургом — Первый северный форт в Кронштадте, окрестности Лебяжьего пляжа на южном берегу Финского залива и Осиновецкий маяк на Ладожском озере. До всех этих локаций можно относительно быстро добраться из любой точки города на любом автомобиле. Если благоприятные условия прогнозируются на выходные, можно отправиться с ночевкой на север Ленобласти, а еще лучше — в Карелию.

Доводилось ли вам видеть полярное сияние? Делитесь фотографиями, лайфхаками и опытом в комментариях. А если остались вопросы, задавайте.

99
55
5 комментариев

Статья интересна, но жаль, что не повторю инструкцию. Просто не смогу увидеть полярное сияние(

Ответить

Рады, что вам понравилось! Уверены, когда-нибудь вы его увидите ❤️🦖

1
Ответить

А цена сервера на скрине - это в день?

Ответить

Александр, здравствуйте! Цена указана за месяц)

Также в панели управления можно посмотреть стоимость за час и день: my.selectel.ru

Ответить

для жителей лен области полезно будет!

Ответить