python

Используем недокументированное API сайта captionbot.ai

  • воскресенье, 8 мая 2016 г. в 03:14:14
https://habrahabr.ru/post/283112/
  • Реверс-инжиниринг
  • Python
  • API


В этой статье мы разберем, как получить и использовать API сайта, если по нему нет документации или оно еще не открыто официально. Руководство написано для новичков, которые еще не пробовали зареверсить простой API. Для тех же кто сам занимался подобным ничего нового здесь нет.


Разбор проведем на примере API сервиса https://www.captionbot.ai/ который недавно открыл Microsoft (спасибо им за это). Многие могли прочитать о нем в статье на Geektimes. Сайт использует ajax запросы в формате JSON, поэтому скопировать их будет легко и приятно. Поехали!


КДПВ


Анализируем запросы


В первую очередь открываем инструменты разработчика и анализируем запросы, которые сайт посылает на сервер.


image


В нашем случае все интересующие нас запросы имеют базовый URL https://www.captionbot.ai/api


Инициализация


При первом открытии сайта идет GET запрос на /api/init без параметров.
Ответ имеет Content-Type: application/json, при этом в теле ответа нам приходит просто строка вида:


"54cER5HILuE"

Запомним это и идем дальше.


Отправка URL


У нас есть два способа загрузить изображение: через 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)

Исходный код есть на Github, плюс готовый пакет в pip.