Однажды я наткнулся на интерактивную карту lastfm и решил обязательно сделать подобный проект для фильмов. Под катом история о том, как собрать данные, построить граф и создать своё интерактивное демо на примере данных с кинопоиска и imdb. Мы рассмотрим фреймворк для скрапинга Scrapy, пробежимся по методам визуализации больших графов и разберёмся с инструментами для интерактивного отображения больших графов в браузере. В качестве источника данных я выбрал кинопоиск. Однако, потом оказалось, что этого очень мало и я соскрапил IMDb. Чтобы составить граф, для каждого фильма нужно знать список рекомендуемых фильмов. Если поискать, то можно найти достаточно парсеров кинопоиска и всевозможных неофициальных api, но нигде нет возможности достать рекомендации. IMDb открыто делится своим датасетом, но и там рекомендаций нет. Поэтому выбор остаётся только один: писать своего паука.  На хабре уже есть несколько статей о скрапинге, поэтому я пропущу обзор возможных подходов. В двух словах: если вы пишете на python и не хотите писать свой фреймворк, используйте Scrapy. Практически всё, что вам может понадобиться, в нём уже предусмотрено.  Scrapy действительно очень мощный и в то же время очень простой инструмент. Порог входа довольно низкий, но в то же время Scrapy легко масштабируется на проекты любого объёма и сложности. В нём действительно собрано всё что нужно. От инструментов непосредственно парсинга и HTTP запросов, обработки и сохранения полученных элементов, до управления работой проекта, включая способы обхода блока, паузы и возобновления скрапинга и т. д.  Создание проекта начинается с команды scrapy startproject mycoolproject, после чего вы получаете готовую структуру с шаблонами необходимых элементов и файлы минимальной рабочей конфигурации. Чтобы сделать из этого рабочий проект достаточно описать способ парсинга страницы — то есть создать паука и положить его в папку spiders внутри проекта, и описать какую именно информацию вы хотите извлечь — то есть унаследовать свой класс от класса scrapy.item в скрипте items.py. Таким образом можно сделать полностью рабочий проект меньше чем за час. Для сохранения результатов есть встроенные средства: например запись в csv или json, но лучше всё-таки использовать внешнюю БД, если проект не на пять минут. Поведение связанное с обработкой результатов, включая сохранение задаётся в pipelines.py. Остаётся последний важный файл — settings.py назначение которого понятно из названия. Здесь можно задать конфигурацию проекта связанную например с использованием прокси, таймингом между запросами и многим другим.  И так, по шагам: Кинопоиск забанил меня ещё на этапе отладки паука, когда я отправлял раз в несколько минут пачку из 5-ти запросов с таймаутом в 1 секунду. Чтобы обойти ограничания существует множество опций. Для скрапи легко нагугливаются готовые примеры использования тора, случайного перебора прокси из списка, либо подключения к платным сервисам rotating proxy. Так как у нас ”проект выходного дня”, я выбрал rotating proxy — самый быстрый в реализации вариант, правда приходится подключаться к платному сервису. Как это работает: вы подключаетесь к конкретному ip:port вашего провайдера прокси, а на выходе получаете новый ip на каждый запрос. Со стороны scrapy вам нужно добавить одну строку в файле settings.py своего проекта и в каждом запросе передавать параметр для пары ip:port.  Ищем в settings.py соответствующий раздел и добавляем туда строчку: Затем в каждом запросе вашего паука: Автор вдохновившего меня проекта начал с Nightwish и дальше обходил рекомендации lastfm в ширину, как дерево — поэтому у него получился связный граф. Мой подход был похожим. С кинопоиска можно доставать фильмы по id, который судя по всему просто соответствует порядковому номеру фильма на сайте. Если просто брать все id подрят, то выйдет не очень хорошо, потому что большинство фильмов не имеют рекомендаций и будут одиночными точками, которые превратятся в шум на карте. ID свежих фильмов имеют порядок 500000 — это довольно много для наглядной отрисовки, поэтому начнём со списка топ 250 фильмов и будем итеративно обходить списки рекомендаций каждого фильма.  Я ожидал получить порядка 100 000 фильмов, но к после ночи скрапинга оказалось, что паук остановился на ~12600. Рекомендации в кинопоиске на этом кончаются. Как было упомянуто вначале, за новыми данными я полез на IMDb. Скрапинг IMDb оказался ещё проще. Пара часов на переписывание готового проекта и новый паук готов к старту. Через два-три дня кроулинга (8 запросов в секунду, чтобы не наглеть) паук остановился, собрав 173+ тысячи фильмов. Полный код пауков можно посмотреть на гитхабе: кинопоиск и IMDb. С одной стороны, инструментов визуализации графов целый зоопарк. С другой стороны, когда у речь идёт об очень больших графах, этот зоопарк вдруг куда-то разбегается. Я выбрал для себя два инструмента на такие случаи: это sfdp из graphviz и gephi. SFDP — это CLI утилита с широким набором параметров, способен отрисовывать графы на миллион узлов, но в нашем случае — это не самый удобный инструмент, потому что нам нужно контролировать процесс укладки. Для случаев вроде нашего отлично подходит Gephi — это приложение с графическим интерфейсом и универсальным набором укладок, практически на любой вкус. Экспорт данных для графа делаем простым скриптом на python. Я обычно использую формат dot, потому что он очень простой, что называется "human readable". Изначально формат предназначен для использования в graphviz, но сейчас поддерживается многими другими приложениями для работы с графами. Описание формата
В начале мы пишем заголовок digraph kinopoisk {\n и не забываем в конце файла записать закрывающую скобку }. В каждой строке описываем рёбра графа node1 -> node2; и закрываем список. Описание формата здесь: официальные доки и простые примеры в википедии. digraph — значит, что мы объявляем направленный граф. Если нужен не направленный, то пишем просто graph. sample — это имя нашего графа (не обязательно). В каждой строке обявлены рёбра или вершины. Если рёбра не направленные, то вместо -> пишем --. В квадратных скобках можно объявить параметры ребра или вершины. В этом примере мы задаём ребру между узлами 5 и 4 вес равный пяти, а вершине 4 форму круга. Имена вершин не обязательно обозначать числами, это могут быть и строки. Больше примеров и параметров смотрите в документации. В нашем случае вполне достаточно возможностей описанных выше. Для больших графов в gephi есть две разумные опции: OpenOrd и ForceAtlas 2. OpenOrd — очень быстрый приближённый алгоритм, но имеет мало настраиваемых параметров. ForceAtlas похож на другие классические force-directed алгоритмы, даёт более точные результаты, очень гибкий в настройке, но за это приходится платить временем. Ниже примеры работы обоих алгоритмов на графе, представляющем собой сетку.  Можно подумать, что OpenOrd вообще не стоит использовать если есть время ждать более точный результат. На самом деле не редки случаи графов, когда ForceAtlas собирает все узлы в один плотный комок, а OpenOrd показывает хоть какую-нибудь структуру.  Для ускорения процесса я использовал OpenOrd в качестве начального приближения и затем "размазывал" граф с помощью ForceAtlas. Чтобы на изображении было хоть что-то понятно, нужно устранить наложение узлов друг на друга. Для этого удобно использовать укладку Yifan Hu — чтобы немного размазать кластера, и noverlap, чтобы совсем устранить наложение. На устранение наложения в графе кинопоиска ушла ночь, с imdb не удалось справиться и за целые выходные.  Gephi умеет экспортировать картинки в svg, png и много других форматов. Но с большими данными приходят большие сложности. Одной только красивой картинки мало. Мы хотим видеть названия фильмов и как они связаны. Если рисовать метки узлов, то при таком их количестве мы получим совершенно нечитаемое облоко букв. Есть опции использовать SVG и масштабировать его пока не станет что-то видно, или рисовать только самые важные метки. Но есть вариант лучше, на котором я и решил сосредоточиться. Делаем интерактивную карту. sigma.js
Первая опция, одна из самых простых и в то же время самая наглядная — это плагин для gephi с экспортом в sigma.js шаблон. На гифке выше как раз оно. Устанавливаем плагин через меню gephi, после чего у нас появляется новый пункт меню экспорта во вкладке file. Заполняем форму, экспортируем и получаем готовую рабочую визуализацию. Просто и мощно. Результат можно посмотреть здесь. Недостаток: на больших графах браузер едва справляется. gefx-js
Следующая опция ещё проще предыдущей и в целом очень похожа. gefx-js — нужно просто экспортировать ваш проект из gephi в формат gexf и положить в папку с шаблоном. Готово. Недостаток точно такой же как в предудщем случае. Более того, если с помощью sigmajs я мог просмотривать граф imdb хотя бы локально, то с gefx-js он просто не загрузился. openseadragon
Для случаев, когда нужно показать очень большую картинку есть seadragon. Принцип точно такой же как при рендеринге географических карт: при масштабировании подгружаются новые тайлы, соответствующие текущему увеличению и области просмотра. Именно так и сделал автор вдохновившего меня проекта. Недостаток один: интерактивности минимум. Невозможно выделить узлы, сложно увидеть куда идут рёбра. Невозможно "заглянуть за" перекрытия узлов и рёбер. shinglejs
А что, если сделать что-то вроде смеси предыдущих вариантов, чтобы при масштабировании граф подгружался тайлами, но не картинками, а как в первом случае, с интерактивностью узлов и рёбер? Готовое решение нашлось буквально чудом, это shinglejs.
Плюсы: можно рендерить очень (очень-очень) большие графы в браузере, сохраняя интерактивность.
Минусы: Не так красиво, как sigmajs, подготовка данных не тривиальна. Для визуализации графа imdb я выбрал последний вариант. Выбора в общем-то и не было. Результат можно посмотреть здесь, а дальше немного о том, как подготовить данные для такой визуализации. Экспорт данных в shinglejs:
Как я уже сказал, экспорт данных в последнем случае не очень простой, поэтому я приведу пример, как выгрузить граф из gephi для shinglejs. На графе lastfm есть явная кластеризация связанная со странами происхождения музыкальных коллективов, например японский поп и рок, греческий метал и т. д. В точности то же самое происходит с фильмами. Очень чётко отделяется корейское кино, турецкое, японское и бразильское. В imdb далеко от основной массы выделяется большой кластер мультфильмов. На обоих графах очень плотно собирается кластер фильмов о супергероях из комиксов. Вроде бы очевидно, но тем не менее неожиданно, что плохие фильмы собираются в одно большое облако. Есть отдельные кластера музыкальных клипов, детских youtube-блогов и фанатских фильмов по вселенной Гарри Поттера. Я уверен, что читатели смогут придумать и сделать на полученных данных ещё много интересных проектов. Мне в голову сразу приходят такие идеи: Для кластеризации графов есть много специальных методов, и все они достойны отдельных статей. Я в качестве эксперимента использовал метод для графов не предназначенный. Рассуждения были такими: раз визуально граф разложился на облака похожих фильмов, то области где фильмы особенно плотно сблизились можно найти с помощью DBSCAN. Давайте разберёмся, что этот метод делает, не вдаваясь в глубокие подробности. Название DBSCAN расшифровывается как density-based scan, то есть применяя этот метод, мы объединяем точки, которые расположены друг к другу достаточно плотно. Формализуется это через два основных гиперпараметра — это радиус, в котором мы ищем соседей для каждой точки и минимальное число соседей. 1. Получаем координаты.
Для этого экспортируем наш граф из gephi в формате gdf. Читаем файл как csv с помощью pandas: Давайте теперь нарисуем и посмотрим, на что это похоже. Отлично, DBSCAN с таким должен справиться.  2. Кластеризуем.
Подбираем параметры и смотрим на распределение размеров кластеров. Никакой серьёзной работы не планировалось, поэтому качество я оценивал "на глаз". Распределение по размерам кластеров Давайте раскрасим наши точки в цвета кластеров и посмотрим насколько результат похож на правду. Выглядит как то, что мы и хотели получить.  Попробуем получить кластер какого-нибудь фильма в виде списка. Я не позаботился о том, чтобы сделать удобный способ получения списка, поэтому в этот раз без кода. Ниже список фильмов, который попали в один кластер с "реальными упырями". По-моему неплохо.