В этой статье мы вам расскажем, как можно получить информацию о погоде с одного из известных сайтов без регистрации и получения каких-либо API-ключей. Мы покажем, как, используя только стандартную библиотеку D и ряд самописных структур, разобрать ответ с сайта wttr.in и что с ним можно сделать дальше.
Идея этой статьи у одного из авторов блога зрела очень давно и первоначально что-то подобное было реализовано нами в качестве плагина для голосового ассистента Ирина. Несмотря на то, что работа с wttr.in была освоена, понимания работы с этим ресурсом практически не было и у нас даже была мысль, что до D реализация работы с wttr не дойдет никогда. Тем не менее, мы решили разобраться с тем, как получить с этого ресурса данные и при этом не используя сторонних библиотек и рассказать об этом вам.
Для тех, кто не знает, wttr.in – это очень простой погодный сервис, который ориентирован на запрос погоды через командную строку и который может выдавать самую разную информацию в зависимости от поданного к нему запроса. Формат вывода, а также набор получаемых данных определяются той строкой, которая направляется сервису с помощью обычных GET-запросов. В основном, wttr дает текстовые строки, но может также давать и информацию в виде изображений, что можно применять в создании виджетов для рабочего стола.
Прежде всего, нас интересовал текстовой формат, так как он является универсальным и больше подходит для разного рода скриптов. Но, wttr выдает всю информацию в виде пригодного для анализа JSON, а остальная обработка остается за скриптом или приложением, которое получает данные с этого сайта. С учетом этого, получается что для работы с wttr.in нужны две вещи: нечто, что умеет делать GET-запросы заданного формата и средство деконструкции JSON в что-то более удобное.
Обе этих вещи достижимы с помощью стандартной библиотеки D, в которой есть два крайне полезных (порой) модуля: std.net.curl, в котором есть метод get, и std.json, в котором есть весь необходимый инструментарий для работы с JSON.
Сначала необходимо сделать запрос к wttr.in и получить ответ в виде JSON, который требуется сначала разобрать:
void main()
{
import std.net.curl : get;
import std.conv : to;
import std.format : format;
import std.json;
string location = "ваш населенный пункт (название на английском)";
auto w = get("https://wttr.in/%s?Q?m&format=j1&lang=en".format(location))
.parseJSON;
}
Тут все достаточно просто, кроме формата самого запроса к wttr, о котором стоит сказать подробнее.
Сам вывод wttr.in ориентирован на выгрузку данных в терминале Linux (или ином другом) и рассчитан на применение утилит наподобие curl, которые в строку адреса добавляют параметры. Данные параметры говорят сайту, что за данные следует отдать и в каком виде и прекрасно описаны в документации. В нашем примере мы используем как раз параметры из документации, которые разделены знаком вопроса и амперсандами, которые обозначают следующее (знаки вопроса/амперсанды в параметрах сохранены, но в документации параметры идут без них):
?Q - "супертихий" режим (нет некоторых надписей и нет терминальной псевдографики) ?m - метрические единицы в параметрах &format=j1 - выдать результат в виде JSON &lang=en - язык выдаваемого текста
Это тот минимальный набор параметров в GET-запросе, который тем не менее позволяет получить огромный набор параметров, описывающий погоду по конкретному местоположению (оно задается через добавление в строку формата строки с описанием населенного пункта). И вы даже не представляете, насколько обширный ответ можно получить! Чтобы увидеть весь объем информации своими глазами, можете перейти по ссылке с примером погоды для Ярославля.
Как видите, это огромный массив информации, который имеет определенную и достаточно продуманную структуру. Но разобраться в ней оказалось непросто, так как это приличный объем данных…
Мы получили данные в виде JSON, который содержит следующую информацию: текущая погодная сводка, т.е. сводка на конкретные сутки, а также сведения о погоде еще на два дня, которые следуют за сутками, в которые был сделан запрос. Условно говоря, можно сделать вывод, что результат содержит текущие условия и прогноз погоды, и при этом в каждом из этих блоков есть множество разнообразных значений.
Чтобы лучше разобраться с информацией в виде JSON используйте древовидное представление в браузере (большинство браузеров поддерживают его) или специальные сайты вроде Online JSON Viewer.
Для удобного оперирования всеми этими данными в коде, нам пришлось реализовать ряд структур и распарсить JSON в эти структуры.
Мы выделили несколько таких структур. К примеру определение структуры Astronomy, которая содержит данные об некоторых астрономических показателях (см. комментарии в самой структуре):
// Астрономические условия
struct Astronomy {
// освещенность Луны
float moonIllumination;
// фаза Луны
string moonPhase;
// восход Луны
string moonrise;
// закат Луны
string moonset;
// восход Солнца
string sunrise;
// закат Солнца
string sunset;
}Также выделили структуру для хранения данных о погодных условиях в течении 3 часов. Дело в том, что в полученном ранее JSON, одни сутки представлены в виде 8 таких блоков, каждый из которых представляет погодные данные в интервале времени от 00:00 до 24:00 с разбивкой на интервалы в три часа:
// Сводка погоды за три часа
struct Hourly {
// точка росы (по Цельсию)
float dewPointC;
// точка росы (по Фаренгейту)
float dewPointF;
// чувствуется как (по Цельсию)
float feelsLikeC;
// чувствуется как (по Фаренгейту)
float feelsLikeF;
// тепловой индекс (по Цельсию)
float heatIndexC;
// тепловой индекс (по Фаренгейту)
float heatIndexF;
// охлаждение ветром (по Цельсию)
float windChillC;
// охладение ветром (по Фаренгейту)
float windChillF;
// порывы ветра (в километрах/час)
float windGustKmph;
// порывы ветра (в милях/час)
float windGustMiles;
// вероятность появления тумана
float chanceOfFog;
// вероятность появления заморозков
float chanceOfFrost;
// вероятность появления высоких температур
float chanceOfHighTemp;
// вероятность появления облачной погоды
float chanceOfOvercast;
// вероятность появления дождя
float chanceOfRain;
// вероятность появления сухой погоды
float chanceOfRemDry;
// вероятность появления снега
float chanceOfSnow;
// вероятность появления солнечной погоды
float chanceOfSunshine;
// вероятность появления грозы
float chanceOfThunder;
// вероятность появления сильного ветра
float chanceOfWindy;
// облачность (в процентах)
float cloudCover;
// относительная влажность воздуха
float humidity;
// количество осадков (в дюймах)
float precipInches;
// количество осадков (в миллиметрах)
float precipMM;
// атмосферное давление (в миллибарах)
float pressure;
// атмосферное давление (в дюймах ртутного столба)
float pressureInches;
// температура (по Цельсию)
float tempC;
// температура (по Фаренгейту)
float tempF;
// время наблюдения за погодой
float time;
// индекс УФ-излучения
float uvIndex;
// видимость (в километрах)
float visibility;
// видимость (в милях)
float visibilityMiles;
// код погоды
int weatherCode;
// описание погоды
string weatherDesc;
// иконка погоды
string weatherIconUrl;
// направление ветра (по Розе Ветров)
string windDir16Point;
// азимут ветра (в градусах)
float windDirDegree;
// скорость ветра (в километрах/ч)
float windSpeedKmph;
// скорость ветра (в милях/ч)
float windSpeedMiles;
}Стоит заметить, что в исходном JSON некоторые из наших структур не имеют имени (т.е именованного ключа, а только цифровой) и потому название пришлось изобрести самостоятельно. В остальном же отступлений от API нет.
Далее мы создали еще одну структуру, которая содержит данные по одному дню:
// Сводка погоды за сутки
struct Daily {
// астрономические условия
Astronomy astronomy;
// средняя температура (по Цельсию)
float avgTempC;
// средняя температура (по Фаренгейту)
float avgTempF;
// дата
string date;
// отдельные сводки по каждым трем часам в сутках
Hourly[8] hourly;
// максимальная температура (по Цельсию)
float maxTempC;
// максимальная температура (по Фаренгейту)
float maxTempF;
// минимальная температура (по Цельсию)
float minTempC;
// минимальная температура (по Фаренгейту)
float minTempF;
// Количество солнечных часов
float sunHour;
// Количество снега (в сантиметрах)
float totalSnowCm;
// Индекс УФ-излучения
float uvIndex;
}Таких структур внутри нашего JSON три, как уже было упомянуто ранее, и мы их упаковали в структуру, представляющую собой погодную сводку:
// Сводка погоды за три дня
struct Weather {
Daily[3] daily;
}Вы даже не представляете, как замучился один из авторов блога, выписывая все эти параметры из JSON и выясняя для чего нужен каждый!
Но само по себе наличие структур, пусть даже и удобных, не делает погоду. Самое интересное происходит далее, после создания и добавлении функции, которая объединяет в себе получение данных из wttr.in и их парсинг в наши структуры:
auto fromWttrJSON(string location) {
import std.net.curl : get;
import std.conv : to;
import std.format : format;
import std.json;
auto wttrContent = get("https://wttr.in/%s?Q?m&format=j1&lang=en".format(location))
.parseJSON;
auto fromJSON(T = string)(JSONValue json, string key) {
static if (is(T : string))
return json[key].str;
else
return json[key].str.to!T;
}
Weather weather;
foreach (i, e; wttrContent["weather"].array)
{
// данные по погоде на день
Daily daily;
// Астрономические условия
Astronomy astronomy;
with (astronomy)
{
auto q = e["astronomy"][0];
moonIllumination = fromJSON!float(q, "moon_illumination");
moonPhase = fromJSON(q, "moon_phase");
moonrise = fromJSON(q, "moonrise");
moonset = fromJSON(q, "moonset");
sunrise = fromJSON(q, "sunrise");
sunset = fromJSON(q, "sunset");
}
daily.astronomy = astronomy;
with (daily)
{
avgTempC = fromJSON!float(e, "avgtempC");
avgTempF = fromJSON!float(e, "avgtempF");
date = fromJSON(e, "date");
maxTempC = fromJSON!float(e, "maxtempC");
maxTempF = fromJSON!float(e, "maxtempF");
minTempC = fromJSON!float(e, "mintempC");
minTempF = fromJSON!float(e, "mintempF");
sunHour = fromJSON!float(e, "sunHour");
totalSnowCm = fromJSON!float(e, "totalSnow_cm");
uvIndex = fromJSON!float(e, "uvIndex");
foreach (j, w; e["hourly"].array)
{
Hourly hourly;
with (hourly)
{
dewPointC = fromJSON!float(w, "DewPointC");
dewPointF = fromJSON!float(w, "DewPointF");
feelsLikeC = fromJSON!float(w, "FeelsLikeC");
feelsLikeF = fromJSON!float(w, "FeelsLikeF");
heatIndexC = fromJSON!float(w, "HeatIndexC");
heatIndexF = fromJSON!float(w, "HeatIndexF");
windChillC = fromJSON!float(w, "WindChillC");
windChillF = fromJSON!float(w, "WindChillF");
windGustKmph = fromJSON!float(w, "WindGustKmph");
windGustMiles = fromJSON!float(w, "WindGustMiles");
chanceOfFog = fromJSON!float(w, "chanceoffog");
chanceOfFrost = fromJSON!float(w, "chanceoffrost");
chanceOfHighTemp = fromJSON!float(w, "chanceofhightemp");
chanceOfOvercast = fromJSON!float(w, "chanceofovercast");
chanceOfRain = fromJSON!float(w, "chanceofrain");
chanceOfRemDry = fromJSON!float(w, "chanceofremdry");
chanceOfSnow = fromJSON!float(w, "chanceofsnow");
chanceOfSunshine = fromJSON!float(w, "chanceofsunshine");
chanceOfThunder = fromJSON!float(w, "chanceofthunder");
chanceOfWindy = fromJSON!float(w, "chanceofwindy");
cloudCover = fromJSON!float(w, "cloudcover");
humidity = fromJSON!float(w, "humidity");
precipInches = fromJSON!float(w, "precipInches");
precipMM = fromJSON!float(w, "precipMM");
pressure = fromJSON!float(w, "pressure");
pressureInches = fromJSON!float(w, "pressureInches");
tempC = fromJSON!float(w, "tempC");
tempF = fromJSON!float(w, "tempF");
time = fromJSON!float(w, "time");
uvIndex = fromJSON!float(w, "uvIndex");
visibility = fromJSON!float(w, "visibility");
visibilityMiles = fromJSON!float(w, "visibilityMiles");
weatherCode = fromJSON!int(w, "weatherCode");
weatherDesc = w["weatherDesc"][0]["value"].str;
weatherIconUrl = w["weatherIconUrl"][0]["value"].str;
windDir16Point = fromJSON(w, "winddir16Point");
windDirDegree = fromJSON!float(w, "winddirDegree");
windSpeedKmph = fromJSON!float(w, "windspeedKmph");
windSpeedMiles = fromJSON!float(w, "windspeedMiles");
}
daily.hourly[j] = hourly;
}
}
weather.daily[i] = daily;
}
return weather;
}Теперь же для получения погоды можно сделать так:
void main()
{
import std.net.curl : get;
import std.conv : to;
import std.format : format;
import std.json;
fromWttrJSON("Ваш населенный пункт")
.writeln;
}Результат, конечно, немногим лучше изначального JSON, при том, что выглядит почти также (мы старались!), но имеет несомненное преимущество – можно теперь из структур с упрощенным синтаксисом доставать интересующие значения и делать из них выборки. Кроме того, детали взаимодействия с wttr.in спрятаны и вам нет нужды писать такой код. Теперь достаточно просто указать для функции интересующее место и выбрать из полученной структуры день, из которого достать интересующую информацию уже намного проще и она уже нужного типа (за исключением, конечно, дат – тут еще нужно доработать).
Также, пользуясь случаем, можем сказать, что даже код приведенный в статье, вы не обязаны использовать, так как мы подготовили для вас библиотеку wttrd, которая уже есть в реестре dub!
С данной библиотекой, описанное в статье можно сделать так:
#!/usr/bin/env dub
/+ dub.sdl:
dependency "wttrd" version="~main"
+/
import std.stdio;
import wttrd;
void main()
{
// three days weather forecast
auto w = weatherFromWttr("Yaroslavl");
// current conditions
auto c = conditionFromWttr("Yaroslavl");
w.writeln;
c.writeln;
}И при этом еще получить и данные на текущий момент!
Однако, мы решили оставить функционал библиотеки минимальным и не добавили даже удобного текстового отображения, поскольку предназначение библиотеки – это удобный парсинг (десериализация) JSON от wttr.in в необходимые структуры, а их обработку каждый волен производить по своему.