shikhalev.org
Скриншот репозитория

— Я зделяль. ©

Итак, прошу любить и жаловать — INat::Get — софтина для по­лу­че­ния и обработки данных с iNaturalist. Основное изначальное пред­наз­на­че­ние — подбивать всякую статистику для про­ек­тов на том же iNaturalist’е, но варианты использования гораздо шире.

Первым делом хочу отметить, что текущее со­сто­я­ние — это ранняя альфа. Я не ре­ко­мен­дую никому этим пользоваться иначе как из любопытства и желания поучаствовать. Тем не ме­нее делаю пост уже сейчас в на­деж­де, что любопытные желающие найдутся. Со сво­ей стороны готов подробно отвечать на во­про­сы и учитывать пожелания.

Зачем?

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

В ка­чес­т­ве примера таких отчетов, которые нельзя получить просто из ин­тер­фей­са, приведу свои посты в про­ек­те «Био­раз­но­об­ра­зие Артинского района». Не по­то­му, что они представляют собой что-то особо ценное, а именно как демонстрацию:

Эти посты были сформированы по дан­ным выгрузок, а не API, посредством мною же написанного inat-script, в про­цес­се работы с ко­то­рым (и над ко­то­рым) я осознал все недостатки механизма выгрузок:

  • ограниченность данных;
  • необходимость ручного создания процесса выгрузки и, потом, загрузки файла;
  • и глав­ное — неизвестный заранее срок го­тов­нос­ти — некоторые выгрузки делались вовсе несколько дней.

Кроме того, сам скрипт, как всякий первый блин, требовал существенной переработки для то­го, чтобы удобно внедрять в не­го новые варианты выборок и отчетов, и я решил написать с ну­ля новый инструмент, работающий непосредственно с API.

Принцип действия

На са­мом деле я сначала пытался сделать как-то так, чтобы отчеты формировались через конфигурационные файлы. Однако гибкости в та­ком подходе никакой (ну или потребуется senior-yaml-de­ve­lo­per для ис­поль­зо­ва­ния, ЕВПОЧЯ). В ито­ге пришел к вы­во­ду, что проще предположить в про­д­ви­ну­том пользователе базовые знания Ruby…

В об­щем, программа запускает ruby-скрипты, называемые задачами, которые работают на уров­не абстрактных выборок и списков, оставляя все обращения к API и кэширование ответов под ка­по­том. По боль­шо­му счету пользователь имеет дело (помимо объектов, представляющих собственно данные) с дву­мя классами: DataSet, который представляет собой набор наблюдений, уже отфильтрованный тем или иным образом; и List — по су­ти датасет, сгруппированный по неким объектам, как пра­ви­ло — таксонам.

Для фор­ми­ро­ва­ния вывода имеется специальный объект Table, который сначала определяется, т.е. задаются колонки с за­го­лов­ка­ми, шириной и выравниванием, а затем наполняется данными, которые должны представлять собой массив хэш-таб­лиц… Звучит страшно, но в дей­с­т­ви­тель­нос­ти скрипты могут быть совсем простые. Например, давайте получим список видов в Ар­тин­с­ком районе, которых я никогда не наблюдал.

user = User::by_login 'shikhalev'
place = Place::by_slug 'artinskiy-gorodskoy-okrug-osm-2023-sv-ru'

user_dataset = select user_id: user.id
place_dataset = select place_id: place.id

user_list = user_dataset.to_list
place_list = place_dataset.to_list

result_list = place_list - user_list

result_table = table do
  column '#', width: 3, align: :right, data: :line_no
  column 'Таксон', data: :taxon
  column 'К-во набл.', width: 6, align: :right, data: :count
end

result_rows = result_list.map { |ds| { taxon: ds.object, count: ds.count } }

result_table << result_rows

File.write 'notmy.htm', result_table.to_html

Файл я поместил в каталог примеров под именем notmy.inat, теперь мы можем его запустить командой:

$ inat-get notmy.inat

И через некоторое время получим результат.

Результат можно увидеть в мо­ем посте на iNa­tu­ra­list. Да, на дан­ный момент, все форматирование рассчитано именно и только на пос­ты в iNat, активно используя тамошние стили. Есть планы расширить данный момент, но об этом позже.

Что важно, если мы тут же запустим ту же команду, то результат получим практически мгновенно, причем идентичный. А ес­ли выждем сутки, то некоторое дополнительное время понадобится, но существенно меньшее, чем при пер­вом запуске. Это первый ключевой мо­мент — данные кэшируются.

Кроме того, следует обратить внимание на то, что на са­мом-то деле API отдает не бо­лее 200 наб­лю­де­ний за один запрос. Здесь же этого ограничения мы не ви­дим и работаем с пол­ны­ми да­та­се­та­ми (объемом 3k+ и 4k+) — организация последовательной постраничной загрузки так же находится под капотом.

Некоторые детали

  • Метод select выполняет запрос и возвращает объект класса DataSet. Именованные параметры данного метода примерно соответствуют параметрам API, правда, реализованы не все.

  • Класс DataSet инкапсулирует набор наблюдений, плюс опционально ассоциирует его с не­ко­то­рым объектом. В ос­нов­ном его поведение определяется включенным модулем Enumerable, и бинарными операциями:

    • | — объединение;
    • & — пересечение;
    • - — разность.

    Также у класса DataSet имеется важный метод to_list, который создает объект класса List, группируя наблюдения по то­му или иному параметру. Для груп­пи­ров­ки используется proc-объ­ект, который должен выдавать по на­блю­де­нию собственно ключ группировки. Наиболее полезные (на мой взгляд) группировки уже определены как константы модуля Listers:

    • Listers::SPECIES возвращает таксон, «приведенный к ви­ду». В ка­выч­ках потому, что результатом может быть как вид, так и гибрид или комплекс.
    • Listers::YEAR возвращает год.
    • И так далее.

    Если вызвать to_list без параметров, то по умол­ча­нию будет использован Listers::SPECIES.

  • Класс List представляет собой список датасетов с ас­со­ци­и­ро­ван­ны­ми значениями. Также реализует модуль Enumerable, только итерируемыми элементами будут объекты класса DataSet, а не Observation. Для спис­ков также определены некоторые бинарные операции:

    • + — объединение;
    • * — пересечение;
    • - — разность (что и использовано в примере).

    Объединенный датасет из списка можно получить посредством метода to_dataset.

  • Классы данных: Observation, Taxon, Place, User и так далее предоставляют собственно данные. Свойств там много, их следует от­до­ку­мен­ти­ро­вать, но пока руки не до­шли. Впрочем, это тот случай, когда код действительно является документацией, так что см. ка­та­лог entity.

    Отмечу, что все они имеют метод класса by_id для по­лу­че­ния соответствующего объекта, и в до­пол­не­ние классы Place и Project имеют метод by_slug, а класс User — метод by_login.

Установка

Установка максимально проста:

# gem install inat-get

Правда, на дан­ный момент все это гарантированно работает только под Li­nux. Тестирование под Win­dows в пла­нах есть, но скорее ближе к весне.

Планы

Вообще, на гит­ха­бе есть такой замечательный раздел «Issues», где можно посмотреть процесс планирования в ре­аль­ном времени. Здесь постараюсь дать сводную картину.

К версии 1.0

Документация

На первом этапе — полноценное руководство пользователя на рус­с­ком языке. Затем расширенное руководство как на рус­с­ком, так и на ан­г­лий­с­ком.

Данные

Сейчас многие поля, в том числе такие важные, как охранный статус, просто игнорируются. Это категорически неправильно и, естественно, будет исправлено уже в бе­та-вер­сии.

Сю­да же отнесу поддержку всех ключей запроса доступных в API. Тут есть некоторые нюансы, которые следует продумать, почему собственно на дан­ном этапе их и нет, но все решаемо.

Доделки и исправления

Уже замеченные баги есть, и их, само собой, исправлять нужно, причем в первую очередь. К счас­тью, критичный, т.е. приводящий к вы­ле­ту, только один и возникает он редко.

Есть запланированная, но пока нереализованная базовая функциональность, такая как чистка устаревших данных.

Оптимизация запросов

Есть мысли, как можно уменьшить количество запросов к API, что существенно ускорит работу в целом.

Дальнейшее развитие

Разнообразные возможности вывода

Тут с од­ной стороны, явно нужно сделать поддержку не толь­ко упрощенной разметки, используемой в жур­на­лах iNa­tu­ra­list, но и форматов, которые можно использовать в раз­лич­ных местах независимо.

С другой стороны, на­до бы добавить что-то типа вывода иерархических списков, а возможно и каких-то еще вариантов оформления.

Все это пока на уров­не исследования и формулирования задачи.

Оптимизация кэширования

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

Обратная связь

Буду рад вопросам, замечаниям и предложениям, как в ком­мен­та­ри­ях к это­му посту, так и в со­от­вет­с­т­ву­ю­щем разделе на Git­Hub. Можно так же писать в лич­ные сообщения на iNa­tu­ra­list, хотя предпочтительно все же общаться в от­кры­тых комментариях, чтобы не воз­ни­ка­ло дублирования.