В этой статье мы обсудим простенький и относительно не извращённый способ сохранения информации о своей семье при помощи скриптов на Python. Для этого мы будем использовать модуль Diagrams.

Введение

"Кто я", "откуда я", и прочие бесполезные вопросы периодически возникают в головах, наверное, у каждого человека. И чтобы с ними разобраться, кто-то лезет в бутылку, кто-то платит кучу денег шарлатанам, чтобы узнать, что в прошлой жизни он был Цезарем, а она Нефертити, ну а кто-то идёт к своим родителям и прародителям, или в архивы, и таким образом восстанавливает своё генеалогическое древо, чтобы узнать о своих предках.

И есть много вариантов эти генеалогические деревья сохранить: нарисовать на бумажке и потом её продолбать, распечатать во всю стену на гобелене, чтобы уподобиться одной волшебной семье из Лондона, ну или сохранить её в каком-то удобочитаемом виде на этих наших компуктерах. На последнем мы и остановимся :) На Хабре уже были статьи о том, как нарисовать своё генеалогическое древо (например, вот или вот), но решений для любителей Python, я ещё здесь не видел (хотя в Сети и есть что-то похожее на искомое). Поэтому давайте попробуем заполнить этот пробел.

Естественно, писать всё с нуля в быстро меняющемся мире при наличии основной работы, это ещё то извращение. Поэтому хотелось бы найти какой-то фреймворк, который бы позволил бы сохранить память о предках и дальних родственниках в простом, но удобочитаемом коде. И чтобы это всё позволяло автоматически генерировать картинки для весьма разветвлённого генеалогического дерева и чтобы можно было корованы грабить.

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

Реальные причины написания поста

Собственно, причина, по которой я это пишу, чуть менее поэтическая. Год назад, внезапно, от обширного сердечного приступа умерла моя Бабушка. На похоронах, как водится, были немногие оставшиеся в живых её родственники, которых я знаю с детства, но конкретные родственные отношения с которыми мне не так хорошо знакомы.

Собственно, вот они, Бабушка и Дед :)
Собственно, вот они, Бабушка и Дед :)

Поэтому после похорон и поедания кутьи, я засел со своим овдовевшим Дедом на кухне его квартиры, и расспросил его под чай о всех-всех-всех родственниках с его стороны, и со стороны Бабушки, о которых он помнил. В результате получил четыре графа на 148 человек (и Бабушка и Дедушка были из деревенских семей). Результаты я записывал на свой телефон Redmi, на котором было приложение "Заметки" с возможностью рисования графов-деревьев (классное приложение). Но сам телефон был очень тормозным, поэтому через пару месяцев, я его решил поменять на более хороший.

И чтобы сохранить эти записи, я решил перевести это всё в какой-нибудь удобочитаемый код с вышеперечисленными требованиями. И чтобы его хранить при помощи Git-а, распределённым по нескольким устройствам. Собственно, найденное по-быстрому решение я и описываю здесь, в надежде, что может кому ещё это пригодится. Ну и как-то напомнить себе о Бабушке, пусть даже и таким странным способом.

Основы работы с Diagrams

Перед работой нам необходимо эти самые Diagrams и Graphviz установить. Это очень просто. Поскольку я использую Ubuntu, то я просто вбил в командную строку следующие две команды:

pip install diagrams

и

sudo apt-get install graphviz

Для других осей, это сделать тоже очень просто, и чтобы не искать, по этой ссылке можно найти способы установки Graphviz для Винды, MacOS, Fedora и прочего.

После установки, можем переходить к примерам. Концептуально, для графов, нам необходимо задавать, собственно, два основных типа элементов: вершины (nodes) и рёбра (edges). Вершины дают нам некоторый набор объектов, которые как‑то между собой соотносятся или сообщаются. Прикладных вариантов может быть много, эти вершины могут олицетворять компоненты облачной архитектуры (для чего и нужен Diagrams), людей, организации, города, атомы в молекулах, и т. д.

А рёбра показывают как именно соотносятся (соединены) между собой вершины. У нас может быть два типа рёбер:

  • «стрелочки» (их называют ориентированными рёбрами, или дугами),

  • и «палочки» (неориентированные рёбра).

Первые, по‑сути, показывают из какой в какую вершину что идёт: данные, родительские отношения, вода, электричество, в зависимости от того, что изображает наш граф. А вторыми мы можем показывать, что две вершины просто соединены. В прикладных случаях ими можно изображать, например, брак между людьми, или химические связи между атомами в молекулах.

Пример бессмысленной картинки, сгенерированной при помощи модуля Diagrams (смотри код ниже по тексту).
Пример бессмысленной картинки, сгенерированной при помощи модуля Diagrams (смотри код ниже по тексту).

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

from diagrams import Diagram
from diagrams.k8s.network import Service
from diagrams.onprem.inmemory import Redis
from diagrams.onprem.aggregator import Fluentd
from diagrams.onprem.network import Nginx

with Diagram("Habr"):
    node1 = Service("I")
    node2 = Redis("want")
    node3 = Fluentd("my")
    node4 = Nginx("medicine")

    node1 >> node2
    node2 >> node3
    node3 >> node4

Сохранив его в файле с названием "test.py", мы по исполнению команды "python test.py", мы получим PNG-картинку "habr.png", с названием нашей диаграммы.

Warning!

Не пугайтесь внезапно открывшемуся файлу картинки по завершению работы скипта. Чтобы это внезапное открытие убрать, достаточно изменить строку номер 7 на

with Diagram("Habr", show=False):

Разберём код по порядку. Чтобы построить диаграмму, нам надо импортировать собственно, сам объект "диаграмма" (Diagram), что мы и сделали в первой строке. Потом, мы наимпортировали кучу различных видов вершин, и это разнообразие и составляет, как я понимаю, основную фишку Diagramms. Ну а дальше, мы просто в строках 7-15 рисуем диаграмму. Сначала, задаём диаграмму (строка 7), определяем вершины (8-11), а потом добавляем рёбра графа (13-15). Подписи под вершинами задаются при их определении, но это не обязательно. Последние три строки можно было бы записать в одну, как

node1 >> node2 >> node3 >> node4

Результат был бы тем же. Мы можем рисовать и более хитрые картинки, например, такие:

Ещё одна бессмысленная картинка для иллюстрации возможностей модуля Diagrams (код под спойлером).
Ещё одна бессмысленная картинка для иллюстрации возможностей модуля Diagrams (код под спойлером).
Код к картинке выше
from diagrams import Diagram
from diagrams.aws.quantum import QuantumTechnologies as qtech

with Diagram("Хабрахабр"):
    node1 = qtech("my")

    qtech("I") - qtech("want") >> node1
    node1 >> qtech("medicine")
    node1 >> qtech("money for nothing")
    node1 >> qtech("tears back")
    node1 << qtech("you")

Как видно, можно из одной вершины делать несколько рёбер, причём сами рёбра могут быть не только направленными вперёд (в коде это ">>") и назад (аналогично, "<<"), так и быть ненаправленными ("-"). Сам формат рёбер (цвет, пунктир и прочее) тоже можно модифицировать (пример разберём ниже). И вершины в явном виде не обязательно определять, из-за чего конструкции типа приведённых в примере под спойлером выше, вполне допустимы. Разобравшись с этими азами, давайте перейдём к рисованию генеалогических древ при помощи Diagrams.

Генеалогические деревья с использованием Diagrams

Собственно, никто нам не мешает делать генеалогические деревья с использованием логотипов компонентов облачной архитектуры, но это не всем может понравиться эстетически. Поэтому, чтобы добавить свои собственные варианты вершин, в Diagrams существует модуль diagrams.custom. В отличие от предыдущих предзаданных вариантов изображения вершин, здесь появляется необходимость предоставлять картинку, которая будет рисоваться на месте соответствующей вершины.

Самым классным вариантом, было бы использование фотографий людей, но это весьма трудозатратно. Поэтому, я взял следующий вариант для генеалогического древа: нарисовал по-быстрому в Inkscape четыре следующие картинки и экспортировал их в формат PNG.

Четыре картинки, нужные нам для diagrams.custom.Custom
Четыре картинки, нужные нам для diagrams.custom.Custom

Первая картинка (family.png) будет использоваться для обозначения семьи, где родители неизвестны, вторая (fem.png), для обозначения женщин, третья (male.png) — мужчин, а четвёртая (unknown.png) для случаев когда пол/гендер человека неизвестен. При необходимости, в дополнение к этим картинкам можно нарисовать больше вариантов, но у меня такой необходимости не возникло. Полноразмерные варианты всех этих четырёх изображений спрятаны под спойлер.

Полноразмерные картинки для семейного древа

family: Можно использовать, например, для семьи
family: Можно использовать, например, для семьи
fem: Для женщин
fem: Для женщин
male: Для мужчин
male: Для мужчин
unknown: Например, для того, когда пол/гендер не известен.
unknown: Например, для того, когда пол/гендер не известен.

Теперь мы можем попробовать построить какое-нибудь генеалогическое древо. Например, возьмём следующий скрипт, иллюстрирующий одну известную киносемью:

HATE

Как человек, читавший книги серии Star Wars за много лет до выхода новой трилогии, я всё ещё не могу смириться с тем, что у Лейи и Хана какой-то Бен (Кайло Рен) вместо Джейсена, Джейны и Энакина! Но, поскольку 1<3, и кино смотрели больше народа, мы возьмём для иллюстрации более короткий киновариант.

from diagrams import Diagram,Edge
from diagrams.custom import Custom

with Diagram("Небоходцы", direction="TB"):
    InLaw = Edge(color="firebrick", style="dashed")
    
    Shmi   = Custom("Шми", "fem.png")
    Cliegg = Custom("Клигг", "male.png")
    Naboo  = Custom("Набу, пл.", "family.png")
    
    Anakin = Custom("Энакин", "male.png")
    Padme  = Custom("Падме", "fem.png")
    Luke   = Custom("Люк", "male.png")
    Leia   = Custom("Лея", "fem.png")
    Han    = Custom("Хан", "male.png")
    Kylo   = Custom("Кайло", "male.png")

    Owen   = Custom("Оуэн", "male.png")
    Beru   = Custom("Беру", "fem.png")

    Shmi >> Anakin >> Luke
    
    Anakin >> Leia
    Naboo >> Padme >> Luke
    Padme >> Leia


    Cliegg >> Owen
    
    Cliegg >> InLaw >> Anakin
    Shmi >> InLaw >> Owen

    Owen >> InLaw >> Luke
    Beru >> InLaw >> Luke

    Leia >> Kylo
    Han  >> Kylo

Для его исполнения, нам, естественно, нужны все картинки, приведённые выше, в той же директории, что и сам скрипт. По исполнении этого кода мы получим следующую картинку:

Семья Небооходцев
Семья Небооходцев

Разберём сам скрипт. Во-первых, мы изменили ориентацию диаграммы с дефолтной "LR" (слева направо, left-right) на "TB" (сверху вниз, top-bottom) в строке 4, т.е. Diagram("Небоходцы", direction="TB"). Обычную связь от родителя к ребёнку, мы обозначили стандартной стрелкой (>>), а для изображения связи между приёмными родителями, мы модифицировали эту стрелку, покрасив её в красный (firebrick) и сделав линию стрелки пунктирной (dashed). Для этого, мы экспортировали из diagrams объект Edge, и создали его экземпляр InLaw с соответствующими параметрами (строка 5).

В целом, генеалогическое древо выше уже весьма прилично: мы автоматически видим поколения, связи между родителями и детьми. Но мы эту картинку можем улучшить, используя кластеризацию некоторых из вершин, объединяя их в одну поддиаграмму, выделенную цветным прямоугольником. Для этого нам потребуется импортировать объект Cluster из того же diagram. Мы можем, например, объединить в кластеры супружеские пары. Код будет выглядеть следующим образом:

from diagrams import Diagram,Cluster,Edge
from diagrams.custom import Custom

with Diagram("Небоходцы", direction="TB"):
    InLaw = Edge(color="firebrick", style="dashed")
    
    with Cluster("Ш-К"):
        Shmi   = Custom("Шми", "fem.png")
        Cliegg = Custom("Клигг", "male.png")
        
    Naboo  = Custom("Набу, пл.", "family.png")

    with Cluster("Э-П"):    
        Anakin = Custom("Энакин", "male.png")
        Padme  = Custom("Падме", "fem.png")
        
    Luke   = Custom("Люк", "male.png")
    
    with Cluster("Л-Х"):  
        Leia   = Custom("Лея", "fem.png")
        Han    = Custom("Хан", "male.png")
        
    Kylo   = Custom("Кайло", "male.png")
    
    with Cluster("О-Б"):
        Owen   = Custom("Оуэн", "male.png")
        Beru   = Custom("Беру", "fem.png")

    Shmi >> Anakin >> Luke
    
    Anakin >> Leia
    Naboo >> Padme >> Luke
    Padme >> Leia


    Cliegg >> Owen
    
    Cliegg >> InLaw >> Anakin
    Shmi >> InLaw >> Owen

    Owen >> InLaw >> Luke
    Beru >> InLaw >> Luke

    Leia >> Kylo
    Han  >> Kylo

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

Семья Небоходцев с выделенными супружескими парами.
Семья Небоходцев с выделенными супружескими парами.

Мы можем выбрать и альтернативное расположение кластеров по какому-нибудь другому признаку (см. код под спойлером), получая другие картинки генеалогического древа:

Код альтернативной кластеризации
from diagrams import Diagram,Cluster,Edge
from diagrams.custom import Custom

with Diagram("Небоходцы", direction="TB"):
    InLaw = Edge(color="firebrick", style="dashed")
    
    with Cluster("Татуин"):
        Shmi   = Custom("Шми", "fem.png")
        Cliegg = Custom("Клигг", "male.png")
        Anakin = Custom("Энакин", "male.png")
        Luke   = Custom("Люк", "male.png")
        Owen   = Custom("Оуэн", "male.png")
        Beru   = Custom("Беру", "fem.png")

    with Cluster("Набу"):    
        Naboo  = Custom("Набу, пл.", "family.png")
        Padme  = Custom("Падме", "fem.png")
        
    
    with Cluster("Альдераан"):  
        Leia   = Custom("Лея", "fem.png")

    with Cluster("Кореллия"):  
        Han    = Custom("Хан", "male.png")

    Kylo   = Custom("Кайло", "male.png")
    


    Shmi >> Anakin >> Luke
    
    Anakin >> Leia
    Naboo >> Padme >> Luke
    Padme >> Leia


    Cliegg >> Owen
    
    Cliegg >> InLaw >> Anakin
    Shmi >> InLaw >> Owen

    Owen >> InLaw >> Luke
    Beru >> InLaw >> Luke

    Leia >> Kylo
    Han  >> Kylo

Альтернативная кластеризация генеалогического древа семьи Небоходцев
Альтернативная кластеризация генеалогического древа семьи Небоходцев

Но, к сожалению, Diagrams поддерживает только вложенные кластеры, поэтому объединить, например, Энакина и Падме на диаграмме выше, ещё одним кластером, соединяющим два независимых кластера, к сожалению, невозможно. То же самое и с рёбрами. Если мы попробуем объединить Энакина и Падме неориентированным ребром (-), то они станут неэквивалентны в поколениях, что будет, конечно, читаемо, но смотреться будет так-себе.

Вместо заключения

Приведённые выше примеры вполне легко можно использовать для построения генеалогических деревьев. Но опций у модуля Diagrams куда больше, хотя и недостаточно для всех целей генеалогии, поскольку создавался сей код для совершенно другого. Поэтому вместо завершения, я приведу пример скрипта (под спойлером), где опции объектов Diagram, Cluster , Edgeи Node вынесены в отдельные словари. Результат его работы не сильно отличается от того, что было дано выше, но по-крайней мере в финальном коде все доступные крутилки для регулировки построения диаграммы вынесены в явном виде.

Заключительный код
from diagrams import Diagram,Cluster,Edge
from diagrams.custom import Custom


graph_attr = {
        "pad": "2.0",
        "splines": "ortho",
        "nodesep": "1.90",
        "ranksep": "1.95",
        "fontname": "Sans-Serif",
        "fontsize": "20",
        "fontcolor": "#2D3436",
    }

node_attr= {
        "shape": "box",
        "style": "rounded",
        "fixedsize": "true",
        "width": "1.4",
        "height": "1.4",
        "labelloc": "b",
        "imagescale": "true",
        "fontname": "Sans-Serif",
        "fontsize": "14",
        "fontcolor": "#000000",
    }
    
edge_attr = {
        "color": "#7B8894",
    }    

cluster_graph_attr = {
        "shape": "box",
        "style": "rounded",
        "labeljust": "c",
        "pencolor": "#AEB6BE",
        "fontname": "Sans-Serif",
        "fontsize": "16",
        "fontcolor": "#000000",        
    }


with Diagram("Небоходцы", direction="TB", 
             graph_attr=graph_attr, node_attr=node_attr, edge_attr=edge_attr):
             
    InLaw = Edge(color="firebrick", style="dashed")
    
    with Cluster("Ш-К"):
        Shmi   = Custom("Шми", "fem.png")
        Cliegg = Custom("Клигг", "male.png")
        
    Naboo  = Custom("Набу, пл.", "family.png")

    with Cluster("Э-П"):    
        Anakin = Custom("Энакин", "male.png")
        Padme  = Custom("Падме", "fem.png")
        
    Luke   = Custom("Люк", "male.png")
    
    with Cluster("Л-Х"):  
        Leia   = Custom("Лея", "fem.png")
        Han    = Custom("Хан", "male.png")
        
    Kylo   = Custom("Кайло", "male.png")
    
    with Cluster("О-Б"):
        Owen   = Custom("Оуэн", "male.png")
        Beru   = Custom("Беру", "fem.png")

    Shmi >> Anakin >> Luke
    
    Anakin >> Leia
    Naboo >> Padme >> Luke
    Padme >> Leia


    Cliegg >> Owen
    
    Cliegg >> InLaw >> Anakin
    Shmi >> InLaw >> Owen

    Owen >> InLaw >> Luke
    Beru >> InLaw >> Luke

    Leia >> Kylo
    Han  >> Kylo

А вот и картинка
Финальное изображение семьи Небоходцев
Финальное изображение семьи Небоходцев

Комментарии (6)


  1. Jury_78
    11.04.2023 09:37

    Python это хорошо..., но почему не Gramps?


    1. madschumacher Автор
      11.04.2023 09:37
      +1

      Вот верьте, нет, я с таким объёмным и мощным проектом не смог по-быстрому разобраться как эффективно пользоваться (GUI это хорошо, но с интерфейсом тоже надо разбираться). Поэтому это было просто наилучшее (для меня) решение по соотношению выхлопа к затратам личных ресурсов.


      1. Jury_78
        11.04.2023 09:37

        Отчасти вы правы, если заносить 2 - 5 человек, если же больше, то после первого десятка идет проще. :)


  1. iklin
    11.04.2023 09:37

    Если Вам было в кайф и результат Вас устроил, то и замечательно, но Вы изобрели велосипед, поскольку программ для составления генеалогических деревьев довольно много. Например, «Древо жизни» (https://genery.com/ru/). А вышеназванный Grumps, действительно, своеобразен в плане интерфейса.


    1. madschumacher Автор
      11.04.2023 09:37

      Понятное дело, что такого софта пруд пруди. Но, например, на первой странице приведённого Вами большими буквами написано: "Для Windows и Mac", и к тому же платно. И вот так в большинстве: для конкретной оси (чаще всего Винды) и платно. Конечно, если этим приходится постоянно заниматься, то можно и скачать с торента купить лицензию, но для простых смертных иногда можно что-то более простое и элементарное взять :)


      1. iklin
        11.04.2023 09:37

        Резонно.