https://habrahabr.ru/post/331834/Я большой фанат книги «Марсианин» Энди Вейера. Читая её, мне было интересно, что же Марк Уотни чувствовал, гуляя по красной планете. Недавно наткнулся на пост на
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] Я б ещё добавил, что программа работает очень долго из-за случайного перебора изображений