Получаем погоду из wttr.in с помощью D

В этой статье мы вам расскажем, как можно получить информацию о погоде с одного из известных сайтов без регистрации и получения каких-либо 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 в необходимые структуры, а их обработку каждый волен производить по своему.

aquaratixc

Программист-самоучка и программист-любитель

Добавить комментарий