Используем недокументированное API сайта captionbot.ai
- воскресенье, 8 мая 2016 г. в 03:14:14
В этой статье мы разберем, как получить и использовать API сайта, если по нему нет документации или оно еще не открыто официально. Руководство написано для новичков, которые еще не пробовали зареверсить простой API. Для тех же кто сам занимался подобным ничего нового здесь нет.
Разбор проведем на примере API сервиса https://www.captionbot.ai/ который недавно открыл Microsoft (спасибо им за это). Многие могли прочитать о нем в статье на Geektimes. Сайт использует ajax запросы в формате JSON, поэтому скопировать их будет легко и приятно. Поехали!
В первую очередь открываем инструменты разработчика и анализируем запросы, которые сайт посылает на сервер.
В нашем случае все интересующие нас запросы имеют базовый URL https://www.captionbot.ai/api
При первом открытии сайта идет GET запрос на /api/init
без параметров.
Ответ имеет Content-Type: application/json
, при этом в теле ответа нам приходит просто строка вида:
"54cER5HILuE"
Запомним это и идем дальше.
У нас есть два способа загрузить изображение: через URL и через загрузку файла. Для теста берем URL изображения Лены с вики и отсылаем. В сетевой активности появляется POST запрос на /api/message
со следующими параметрами:
{
"conversationId": "54cER5HILuE",
"waterMark": "",
"userMessage": "https://upload.wikimedia.org/wikipedia/ru/2/24/Lenna.png"
}
Ага, говорим себе мы, значит метод init
вернул нам строку для conversationId
, а в userMessage
попала наша ссылка. Что такое waterMark
пока непонятно. Смотрим на данные ответа:
"{\"ConversationId\":null,\"WaterMark\":\"131071012038902294\",\"UserMessage\":\"I am not really confident, but I think it's a woman wearing a hat\\nand she seems . \",\"Status\":null}"
Зачем-то закодировали JSON дважды, ну да ладно. В человеческом виде это выглядит так:
{
"ConversationId": null,
"WaterMark": "131071012038902294",
"UserMessage": "I am not really confident, but I think it's a woman wearing a hat\\nand she seems .",
"Status": null
}
Все параметры по пути поменяли манеру написания, но это мелочи жизни. Итак, нам вернули некоторое значение WaterMark
, почему-то пустой ConversationId
, собственно подпись к фото в поле UserMessage
и некий пустой статус.
Далее, не закрывая вкладку, пробуем ту же операцию с загрузкой фото из локального файла. Видим POST запрос на /api/upload
в формате multipart/form-data
с названием поля file
:
-----------------------------50022246920687
Content-Disposition: form-data; name="file"; filename="Lenna.png"
Content-Type: image/png
В ответ получаем строку URL нашего загруженного файла, можем перейти по нему и убедиться в этом:
"https://captionbot.blob.core.windows.net/images-container/2ogw3q4m.png"
Затем отсылается уже знакомый нам запрос на /api/message
:
{
"conversationId": "54cER5HILuE",
"waterMark": "131071012038902294",
"userMessage": "https://captionbot.blob.core.windows.net/images-container/2ogw3q4m.png"
}
Вот и пригодился waterMark
из предыдущего ответа, а URL тот, что нам вернул метод upload
. Данные ответа аналогичны предыдущим.
Чтобы использовать полученные знания с удобством, делаем простую обертку на вашем любимом языке программирования. Я сделаю это на Python. Для запросов к сайту использую requests, так как он удобный и в нем есть сессии, которые хранят cookie за меня. Сайт использует SSL, но по дефолту requests будет ругаться на сертификат:
hostname 'www.captionbot.ai' doesn't match either of '*.azurewebsites.net', '*.scm.azurewebsites.net', '*.azure-mobile.net', '*.scm.azure-mobile.net'
Решается это установкой флага verify=False при каждом запросе.
import json
import mimetypes
import os
import requests
import logging
logger = logging.getLogger("captionbot")
class CaptionBot:
BASE_URL = "https://www.captionbot.ai/api/"
def __init__(self):
self.session = requests.Session()
url = self.BASE_URL + "init"
resp = self.session.get(url, verify=False)
logger.debug("init: {}".format(resp))
self.conversation_id = json.loads(resp.text)
self.watermark = ''
def _upload(self, filename):
url = self.BASE_URL + "upload"
mime = mimetypes.guess_type(filename)[0]
name = os.path.basename(filename)
files = {'file': (name, open(filename, 'rb'), mime)}
resp = self.session.post(url, files=files, verify=False)
logger.debug("upload: {}".format(resp))
return json.loads(resp.text)
def url_caption(self, image_url):
data = json.dumps({
"userMessage": image_url,
"conversationId": self.conversation_id,
"waterMark": self.watermark
})
headers = {
"Content-Type": "application/json"
}
url = self.BASE_URL + "message"
resp = self.session.post(url, data=data, headers=headers, verify=False)
logger.debug("url_caption: {}".format(resp))
if not resp.ok:
return None
res = json.loads(json.loads(resp.text))
self.watermark = res.get("WaterMark")
return res.get("UserMessage")
def file_caption(self, filename):
upload_filename = self._upload(filename)
return self.url_caption(upload_filename)