Читать нас в Telegram

Примерно год назад в Европейском Университете открылась школа искусств, а в ней — лаборатория “Искусство и искусственный интеллект”. В нашей лаборатории мы занимаемся вопросами применения методов машинного обучения для решения задач искусствоведов.

В машинном обучении все начинается с данных, с них мы и начнем. Если мы хотим найти все картины с изображениями котиков, детектировать ордена и медали на портретах или сопоставлять изображения картин с их описанием, нам так или иначе придется откуда-то взять изображения картин. Хвала провидению, у нас есть большой каталог экспонатов, хранящихся в музеях России — портал Госкаталог!

Disclaimer: Все собранные данные будут использоваться только в научных и исследовательских целях. Все собранные изображения будут подвергнуты автоматической обработке.

Итак, мы все же решились в исключительно исследовательских целях получить некое подмножество картинок (здесь и далее под этим словом будет подразумеваться изображение предмета искусства) с портала. На самом деле, портал содержит просто уйму полезной для исследователей информации и может служить отличным подспорьем при решении большого числа поисковых задач.

Приступим. Для начала тривиальное решение. Я предлагаю сразу же отмести подход с хождением по сайту и ручным скачиванием изображений и перейти к более интересным сценариям, ибо одних только предметов живописи (на момент написания статьи) в базе более полу миллиона.

План «Паук»

Часто, когда исследователи хотят получить данные с какого-то сайта они пишут маленькую программу или скрипт (он же скраппер, он же паук) для получения данных. Все что делает этот паук — просто ходит по страничкам сайта и «выдирает» нужную информацию из него. Мы пойдем классическим путем без эмуляции браузера на стороне клиента и внимательно посмотрим на запросы, которые отправляет сайт при загрузке страницы и обнаруживаем, что на самом деле у госкаталога есть REST API (способ программного взаимодействия при котором сообщения пересылаются в машиночитаемом виде между клиентом и сервером). Это здорово, потому что нам не придется опираться на верстку сайта при сборе информации, ведь эта верстка может измениться в любой момент, а API более статичны. “Естественно”, этот API нигде не задокументирован, но нас уже не остановить такими мелочами.

Методом пристального взгляда мы узнали, что для каждого экспоната существует запрос, по которому можно узнать все данные о нем, а так же сконструировать правильную ссылку на скачивание самого изображения. Вот пример такого запроса:

https://goskatalog.ru/muzfo-rest/rest/exhibits/10363202

Красным выделен id экспоната. В ответ мы получим json объект (примеры таких объектов можно найти чуть ниже, но вообще json объект — это просто набор пар ключ-значение) с описанием экспоната. Получается, чтобы скачивать что-то с Госкаталога, нам нужен только список этих id. Сам же список id можно получать по следующему адресу:

https://goskatalog.ru/muzfo-rest/rest/exhibits/ext?&statusIds=6&publicationLimit=false&calcCountsType=0&dirFields=desc&limit=100&offset=0&sortFields=id

Красным тут выделен отступ от начала всех записей. То есть, для того что бы продвигаться по записям, нужно просто прибавлять с каждым запросом величину limit к отступу и получать все новые и новые списки id. Красота!

Но здесь нас поджидал подвох. При посещении страницы со списком экспонатов браузер постоянно (раз в 5 секунд) отсылает специальные сообщения на сторонний сервер. Эти сообщения оповещают систему о том, что пользователь находится на страничке и все еще взаимодействует с сайтом. А если такие сообщения не отсылаются, то запросы на получение списка id просто не выполняются и пользователь получает ошибку. Быстро разобраться, что же такое хитрое нужно отправлять на сторонний сервер, мне не удалось, поэтому было принято решение посмотреть в другую сторону.

Выгрузка

Вот удача! Госкаталог предоставляет выгрузку своих данных в архиве. Звучит слишком хорошо, чтобы быть правдой, не так ли?

Идем по ссылке и видим прекрасную кнопку «скачать» и выбираем CSV. Получаем архив и пытаемся его разархивировать. И тут нас поджидает очередная неудача. Архив битый. Возможно, при скачивании что-то пошло не так и просто что-то не скачалось, ок, попробуем еще раз. Нет, получаем ровно такой же файл. Но нас не остановить на пути к познанию! Попробуем его восстановить. Попробуем при помощи команд в терминале и утилиты zip восстановить испорченный архив:

zip -F data-4-structure-3.csv.zip -> Error
zip -FF data-4-structure-3.csv.zip -> Error

Не вышло, попробуем переупаковать архив и тогда его восстановить

zip -FF data-4-structure-3.csv.zip --out New.zip
unzip New.zip

И снова неудача, ну что ж, давайте расчехлим клавиатуры и напишем немного кода на python. Цель скрипта — последовательно читать файл архива и восстанавливать данные строчка за строчкой (спасибо моему коллеге Ивану за помощь и наводку).

from libarchive.adapters.archive_entry import ArchiveEntry
from libarchive.adapters.archive_read import file_pour
 
....
 
for entry in file_pour(archive_file_name):
    e: ArchiveEntry = entry
    try:
        last_line_of_block = b''
        for block in e.get_blocks():
            try:
                batch = []
                exhibits = block.split(b'\n')

Тут мы опираемся на знание о том, что наш архив содержит csv файл. Этот метод позволяет добыть около 10% записей, но и он ломается при чтении этого файла. Кроме того, мне так и не удалось подобрать правильный символ-разделитель между колонками. Обычно для разделения колонок используют запятую или точку с запятой. И похоже, что в конкретно этом файле используются запятые. Однако, запятые в тексте описаний не экранированы (запись должна выглядеть как-то так `»,»`). Поэтому разобрать строчки в восстановленном csv файле достаточно сложно. Все вышеперечисленное справедливо только для последней выгрузки.

Нас постигла очередная неудача…

Открытые данные

Врагу не сдается наш гордый «Варяг»,
Пощады никто не желает!

Песня на стихи австрийского поэта Рудольфа Грейнца (в переводе Е. М. Студенской)

Если мы взглянем более пристально на ссылку, которую мы уже видели, то обнаружим, что на самом деле мы на портале открытых данных. У этого портала есть API! Давайте же воспользуемся им. Для этого нужно зарегистрироваться и получить токен доступа. Дальше все просто, нужно сформировать правильный запрос.

typology = 'живопись'
headers = {'X-API-KEY': KEY} # Тот самый токен
limit = 1000

# url для первого запроса
url = f'{prefix}/$?f={{"data.typology.name":{{"$eq":"{typology}"}}}}&l={limit}'

# Сам запрос для получения данных
resp = requests.get(url, headers=headers)

# Ответ приходит в формате json
jsn = resp.json()

# Следующая страница для запроса
url = jsn['nextPage']

Мы получаем в ответ набор вот таких объектов:

{
    "_id": "5c3e197893fa687ca4a51c43",
    "nativeId": "10000000",
    "hash": "2017-12-11T22:59:39.625Z",
    "data": {
        "id": 10000000,
        "name": "Инструмент музыкальный. Погремушка.  Нанайцы.",
        "productionPlace": "Хабаровский край, с. Сикачи-Алян",
        "description": "На выструганной, круглой рукоятке крепится небольшой барабан, обод которого выполнен из бересты и орнаментирован. Барабан обтянут с двух сторон рыбьей кожей, внутри полый, наполнен крупой для создания ритмичного звука. В берестяном ободе на противоположной стороне от рукоятки имеется отверстие, которое закрыто деревянной резной деталью в виде головы человека.",
        "partsCount": 1,
        "regNumber": 9881829,
        "invNumber": "ЭТН-34",
        "gikNumber": "ОКМ КП-1824/4",
        "type": 0,
        "statusId": 6,
        "museum": {
            "id": 2279,
            "name": "Муниципальное бюджетное учреждение \"Охинский краеведческий музей\"",
            "code": "122265",
            "inn": ""
        },
        "typology": {
            "id": 13,
            "name": "предметы прикладного искусства, быта и этнографии",
            "obsolete": false
        },
        "dimStr": "Д - 8,5 см, Н- 6 см, длина рукоятки 11,5 см",
        "startDate": "2009-01-01T12:00:00.000Z",
        "precision": "YEAR",
        "periodStr": "2009",
        "regDate": "2017-12-11T22:59:39.625Z",
        "extSystem": {
            "id": "1234"
        },
        "technologies": [
            "дерево, кожа, береста",
            "Резьба, склеивание"
        ],
        "images": [
            {
                "url": "http://goskatalog.ru/muzfo-imaginator/rest/images/original/6874045"
            },
            {
                "url": "http://goskatalog.ru/muzfo-imaginator/rest/images/original/6874046"
            }
        ]
    },
    "status": 2,
    "errorFields": [
        {
            "keyword": "type",
            "dataPath": ".startDate",
            "schemaPath": "#/properties/startDate/type",
            "params": {
                "type": "number"
            },
            "message": "should be number"
        }
    ],
    "nativeName": "Инструмент музыкальный. Погремушка.  Нанайцы.",
    "activated": "2019-01-15T16:13:03.971Z",
    "created": "2019-01-15T17:33:43.790Z",
    "modified": "2019-01-15T17:33:43.790Z",
    "odSetVersions": [
        "5c3dfb260b2cb0575b725713"
    ],
    "odSetVersion": "5c3dfb260b2cb0575b725713",
    "updateSession": "5c3dfb260b2cb0575b725714",
    "odSchema": "5c0d7ae03ad95ad9d3a0bee5",
    "dataset": "57f53f621f556893054aace6"
}

Выглядит хорошо. Более того, тут есть искомые нами ссылки на изображения и все сопутствующие данные! Дело за малым — просто ходить по указанным путям и скачивать изображения.

Но есть нюанс… Примерно в половине случаев вместо картинки мы получаем ошибку 404 — «не найдено» или 400 — «плохой запрос». Неужели это тупик?

Собираем все в кучу

Снова прибегнем к методу пристального взгляда, но на этот раз обратим свой взор на открытые данные. В полученном нами json ответе мы видим поле «regNumber», и это то, что нам нужно. Это и есть тот id экспоната из Госкаталога. То есть, перед скачиванием изображений нам нужно запросить Rest API Госкаталога по этому id json с информацией об экспонате и сконструировать правильную ссылку на скачивание изображений. Бинго!

Теперь мы знаем как программно забирать изображения с сайта Госкаталога. Прошу относиться к этому тексту не как к готовой инструкции, а скорее как к описанию одного маленького приключения по получению данных. И помните, одновременно скачивая много картинок с сайта, вы нагружаете его, делайте перерывы, иначе обычные пользователи будут страдать, а вы будете забанены.

P.S. Все изображения в Госкаталоге охраняются авторским правом и у каждого изображения есть правообладатель.