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