Получаем фотографии NASA с Марса с помощью aiohttp

Jun 29, 2017 20:04 · 1033 words · 5 minute read перевод python

Оригинал: ‘Getting Mars photos from NASA using aiohttp’ by PAWEŁ FERTYK; перевод был впервые опубликован на Хабрахабре.

Я большой фанат книги «Марсианин» Энди Вейера. Читая её, мне было интересно, что же Марк Уотни чувствовал, гуляя по красной планете. Недавно наткнулся на пост на Twillo, в котором упоминалось, что у NASA есть публичный API для доступа к фотографиям с марсоходов. Так что я решил написать собственное приложение для просмотра изображений непосредственно в браузере.

Создание aiohttp приложения

Начнём с простого — установим и запустим aiohttp. Для начала создадим виртуальное окружение. Я советую использовать Python 3.5, в котором появился новый синтаксис async def и await. Если вы хотите развивать это приложение в дальнейшем, чтобы лучше понимать асинхронное программирование, то можете ставить сразу Python 3.6. Наконец, установим aiohttp:

$ pip install aiohttp

Добавим файл в проект (назовём его nasa.py) с кодом:

from aiohttp import web


async def get_mars_photo(request):
    return web.Response(text='A photo of Mars')


app = web.Application()
app.router.add_get('/', get_mars_photo, name='mars_photo')

Если вы ещё не работали с aiohttp, то поясню несколько моментов:

  • корутина get_mars_photo — обработчик запросов; принимает HTTP запрос в качестве аргумента и подготавливает содержимое для HTTP ответа (ну или бросает исключение)
  • app — высокоуровневый сервер; он поддерживает роутинг, middleware и сигналы (в примере будет показан только роутинг)
  • app.router.add_get — регистрирует обработчик HTTP метода GET по пути ‘/’

Примечание: обработчиком запросов может также быть и обычная функция, а не только корутина. Но чтобы понять всю мощь asyncio, большинство функций будут определены как async def.

Запуск приложения

Для запуска приложения добавьте строчку в конец файла:

web.run_app(app, host='127.0.0.1', port=8080)

И запустите его как обычный python скрипт:

$ python nasa.py

Однако, есть способ получше. Среди множества сторонних библиотек я нашёл aiohttp-devtools. Она предоставляет замечательную команду runserver, которая запускает ваше приложение, а также поддерживает live reloading.

$ pip install aiohttp-devtools
$ adev runserver -p 8080 nasa.py

Теперь по адресу localhost:8080 вы должны увидеть текст «A photo of Mars».

Использование API NASA

Перейдём непосредственно к получению фотографий. Для этого будем использовать API NASA. У каждого ровера есть свой URL (для Curiosity это api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos [1]). Мы должны также передавать минимум 2 параметра при каждом вызове:

  • sol — марсианский день, на который была сделана фотография, начиная с приземления ровера (максимальное значение вы можете найти в разделе rover/max_sol)
  • API KEY — ключ, предоставляемый NASA (пока можно использовать тестовый DEMO_KEY)

В ответ мы получим список фото с URL, информацией о камере и марсоходе. Чуть подправим файл nasa.py:

import random

from aiohttp import web, ClientSession
from aiohttp.web import HTTPFound

NASA_API_KEY = 'DEMO_KEY'
ROVER_URL = 'https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos'


async def get_mars_image_url_from_nasa():
    while True:
        sol = random.randint(0, 1722)
        params = {'sol': sol, 'api_key': NASA_API_KEY}
        async with ClientSession() as session:
            async with session.get(ROVER_URL, params=params) as resp:
                resp_dict = await resp.json()
        if 'photos' not in resp_dict:
            raise Exception
        photos = resp_dict['photos']
        if not photos:
            continue
        return random.choice(photos)['img_src']


async def get_mars_photo(request):
    url = await get_mars_image_url_from_nasa()
    return HTTPFound(url)

Вот что здесь происходит:

  • мы выбираем случайный sol (для Curiosity max_sol на момент написания статьи равнялся 1722)
  • ClientSession создаёт новую сессию, которую мы используем для получения ответа от NASA API
  • распарсиваем JSON с помощью resp.json()
  • проверяем наличие ключа ‘photos’ в ответе; если его там нет, значит мы достигли лимита обращений, нужно немного подождать
  • если в текущих сутках нет снимков, запрашиваем другой случайный день
  • используем HTTPFound для редиректа на найденное фото

Получение ключа для NASA API

Публичный ключ DEMO_KEY, предлагаемый NASA по умолчанию, работает нормально, но очень скоро вы упрётесь в лимит вызовов [2]. Я рекомендую получить здесь свой собственный ключ, который станет доступен после регистрации.

После запуска приложения вы можете быть перенаправлены на такое вот изображение с Марса:

что-то не так

Это немного не то, чего я ожидал…

Валидация изображения

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

Нам нужен какой-то механизм валидации получаемых изображений. Адаптируем код:

async def get_mars_photo_bytes():
    while True:
        image_url = await get_mars_image_url_from_nasa()
        async with ClientSession() as session:
            async with session.get(image_url) as resp:
                image_bytes = await resp.read()
        if await validate_image(image_bytes):
            break
    return image_bytes


async def get_mars_photo(request):
    image = await get_mars_photo_bytes()
    return web.Response(body=image, content_type='image/jpeg')

Вот что изменилось:

  • мы получаем сырой поток байт по url картинки с помощью resp.read()
  • проверяем достаточно ли хорошо выглядит изображение
  • если всё в порядке, то помещаем байты в web.Response. Обратите внимание — они передаются в body вместо text, а также задаётся content_type

К тому же мы удалили перенаправление (HTTPFound), так что можем получать следующую случайную картинку с помощью простой перезагрузки страницы.

Осталось описать валидацию фотографии. Самое простое — определить минимальные размеры.

Установим Pillow:

pip install pillow

Наша функция валидации превратится в:

import io
from PIL import Image


async def validate_image(image_bytes):
    image = Image.open(io.BytesIO(image_bytes))
    return image.width >= 1024 and image.height >= 1024

чёрно-белый пейзаж

Уже что-то! Идём дальше и отбрасываем все чёрно-белые изображения:

async def validate_image(image_bytes):
    image = Image.open(io.BytesIO(image_bytes))
    return image.width >= 1024 and image.height >= 1024 and image.mode != 'L'

Теперь наша программа начинает выдавать более интересные фотографии:

пейзаж

И даже иногда селфи:

селфи

Заключение

Исходный код программы

#!/usr/bin/env python3
import io
import random
from aiohttp import web, ClientSession
from aiohttp.web import HTTPFound
from PIL import Image


NASA_API_KEY = 'DEMO_KEY'
ROVER_URL = 'https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos'


async def validate_image(image_bytes):
    image = Image.open(io.BytesIO(image_bytes))
    return image.width >= 1024 and image.height >= 1024 and image.mode != 'L'


async def get_mars_image_url_from_nasa():
    while True:
        sol = random.randint(0, 1722)
        params = {'sol': sol, 'api_key': NASA_API_KEY}
        async with ClientSession() as session:
            async with session.get(ROVER_URL, params=params) as resp:
                resp_dict = await resp.json()
        if 'photos' not in resp_dict:
            raise Exception
        photos = resp_dict['photos']
        if not photos:
            continue
        return random.choice(photos)['img_src']


async def get_mars_photo_bytes():
    while True:
        image_url = await get_mars_image_url_from_nasa()
        async with ClientSession() as session:
            async with session.get(image_url) as resp:
                image_bytes = await resp.read()
        if await validate_image(image_bytes):
            break
    return image_bytes


async def get_mars_photo(request):
    image = await get_mars_photo_bytes()
    return web.Response(body=image, content_type='image/jpeg')


app = web.Application()
app.router.add_get('/', get_mars_photo, name='mars_photo')
web.run_app(app, host='127.0.0.1', port=8080)

Есть много вещей для усовершенствования (например, получение значения max_sol через API, просмотр изображений с нескольких роверов, кеширование URL), но пока программа выполняет свою работу: мы можем получать случайное фото с Марса и представлять себя на месте будущих поселенцев.

Надеюсь, вам понравилось это короткий туториал. Если заметили ошибку или у вас есть вопросы, дайте знать.

Примечания переводчика:

  • [1] На самом деле роверов всего 3, их список можно посмотреть запросом api.nasa.gov/mars-photos/api/v1/rovers/?API_KEY=DEMO_KEY.
  • [2] Я б ещё добавил, что программа работает очень долго из-за случайного перебора изображений