如何读取包含 boost::read_graphviz 子图的 DOT 文件中给出的图形?

How to read a graph given in a DOT file containing subgraphs with boost::read_graphviz?

我想弄清楚如何使用 boost::read_graphviz 来访问给定点文件中的子图结构。我已经可以使用 boost::read_graphviz 来通过 属性 映射 (boost::dynamic_properties) 读取一些简单的属性。但是,我不知道如何使用 属性 映射来访问子图。如何访问下面给出的图的子图集群?

digraph G {

subgraph cluster_0 {
    a0 -> a1;
    a1 -> a2;
    a2 -> a3;
}

subgraph cluster_1 {
    b0 -> b1;
    b1 -> b2;
    b2 -> b3;
}
c1 -> a0;
c1 -> b0;
a1 -> b3;
b2 -> a3;
a3 -> a0;
a3 -> c2;
b3 -> c2;

}

好的。 确实 read_graphviz 支持已经 dropped somewhere in history:

#if 0
  // This interface has not worked for a long time
  ...

  // These four require linking the BGL-Graphviz library: libbgl-viz.a
  // from the /src directory.
  // Library has not existed for a while
  extern void read_graphviz(const std::string& file, GraphvizDigraph& g);
  extern void read_graphviz(FILE* file, GraphvizDigraph& g);

  extern void read_graphviz(const std::string& file, GraphvizGraph& g);
  extern void read_graphviz(FILE* file, GraphvizGraph& g);
#endif

真可惜。

计划

正如我评论的那样,我曾经写过一个非常全面的 graphviz 解析器:。我做了/一些/工作来将生成的 AST 转换为图形模型(不是 BGL,但它可能是)。

但是,对于给定的示例,这似乎是一种过于复杂的方法。

简单的 Graphviz 解析器

让我们为给定的子集推出一个非常简单的 X3 语法:

namespace x3 = boost::spirit::x3;

using Id = std::string;

namespace Parser {
    using namespace x3;

    auto kw = [](auto... p) {
        return lexeme[(x3::as_parser(p) | ...) >> !(alnum | '_')];
    };

    x3::rule<struct _> graph{"graph"};

    auto subgraph  = kw("subgraph") >> graph;
    auto semi      = &('}'|subgraph) | ';';

    auto id        = lexeme[                            //
        !kw("edge", "node", "graph", "subgraph") //
        >> +char_("A-Za-z0-9_")];
    auto edge      = id >> *(kw("->") >> id) >> semi;
    auto node      = id >> semi;
    auto element   = subgraph | edge | node;
    auto graph_def = -id >> '{' >> *element >> '}' >> eps;

    auto simple_graphviz = skip(space)[kw("digraph") >> graph];

    BOOST_SPIRIT_DEFINE(graph)

} // namespace Parser

这就足够了(甚至不是最小的)。注意

  • 有智慧对某些关键字进行特殊处理并且
  • 并在某些情况下将 ';' 视为可选,但
  • 它还引入了一个空的根图——我把它留作 reader
  • 的练习

Live On Wandbox,打印"Parse success"

接线事件

为了构建/anything/,我们应该能够注入解析器事件处理程序。例如:

struct Events{};
struct Handlers {
    void node(Id id) { last = id; }
    void edge(Id id) { std::cerr << stack.back() << ": " << last << " -> " << id << "\n"; }
    void enter(auto optional_id) { stack.push_back(optional_id.value_or("(unnamed)")); }
    void leave(unused_type)      { stack.pop_back(); } 
  private:
    std::deque<std::string> stack;
    Id last{};
};

现在,Events 将用作上下文键:

#define ON(event) ([](auto& ctx) { get<Events>(ctx).event(_attr(ctx)); })

然后我们可以在语义动作中连接事件:

auto edge      = id[ON(node)] >> *(kw("->") >> id[ON(edge)]) >> semi;
auto node      = id[ON(node)] >> semi;
// ...
auto graph_def = (-id >> '{')[ON(enter)] >> *element >> '}' >> eps[ON(leave)];

在实际调用中我们应该绑定一个实例 Handler:

auto const bound_grammar = //
    x3::with<Parser::Events>(Parser::Handlers{})[Parser::simple_graphviz];

try {
    if (/*bool ok =*/parse(f, l, bound_grammar))

现在输出是 (Live On Wandbox):

cluster_0: a0 -> a1
cluster_0: a1 -> a2
cluster_0: a2 -> a3
cluster_1: b0 -> b1
cluster_1: b1 -> b2
cluster_1: b2 -> b3
G: c1 -> a0
G: c1 -> b0
G: a1 -> b3
G: b2 -> a3
G: a3 -> a0
G: a3 -> c2
G: b3 -> c2
Parse success

将它们放在一起:构建图表

我们的图形模型不需要太多,但正如我们在较早的答案中发现的那样,write_graphviz 的子图重载确实 强制所有属性映射:

using Attrs = std::map<std::string, std::string>;
using VProp = property<vertex_name_t, std::string,
                       property<vertex_attribute_t, Attrs>>;
using EProp = property<edge_attribute_t, Attrs, //
                       property<edge_index_t, int>>;

using GProp = property<graph_graph_attribute_t, Attrs,
             property<graph_vertex_attribute_t, Attrs,
                      property<graph_edge_attribute_t, Attrs,
                               property<graph_name_t, std::string>>>>;

using Graph = subgraph< //
    adjacency_list<vecS, vecS, undirectedS, VProp, EProp, GProp>>;

using Digraph = subgraph< //
    adjacency_list<vecS, vecS, directedS, VProp, EProp, GProp>>;

现在我们可以实现 Handlers 接口来构建其中之一:

template <typename G> struct Builder {
    using V = typename G::vertex_descriptor;
    using E = typename G::edge_descriptor;

    Builder(G& g) : g(g) {}

    V node(Id id) // return global descriptor to optionally locally added
    {
        if (auto it = vertices.find(id); it != vertices.end()) {
            return last = it->second;
        } else {
            auto& sub = current();
            V vd = add_vertex(sub);
            put(boost::vertex_name, sub, vd, id);

            // translate to global descriptors for later use
            vd = sub.local_to_global(vd);
            [[maybe_unused]] auto [_, ok] = vertices.emplace(id, vd);
            assert(ok);
            return last = vd;
        }
    }
    void edge(Id id) {
        V s = last;
        V t = node(id); // updates last, sequence point is important!
        add_edge(s, t, g);
    }
    void enter(auto optional_id)
    {
        auto id = optional_id.value_or("");

        if (!hasChild(current(), id))
            get_property(current().create_subgraph(), boost::graph_name) = id;

        stack.push_back(std::move(id));
    }
    void leave(x3::unused_type) { stack.pop_back(); }

  private:
    using Stack = std::deque<std::string>;

    G&              g;
    Stack           stack;
    std::map<Id, V> vertices;
    V               last;

    static G* hasChild(G& g, Id childId) {
        for (auto& child : make_iterator_range(g.children()))
            if (childId == get_property(child, boost::graph_name))
                return &child;
        return nullptr;
    }

    using F = Stack::const_iterator;
    static G& descend(G& g, F f, F l) {
        if (f == l)
            return g;
        if (G* c = hasChild(g, *f))
            return descend(*c, ++f, l);
        else
            throw std::range_error("descend");
    }

    G& current() { 
        return descend(g, stack.cbegin(), stack.cend());
    }
};

请注意,它可以进行优化,我对多个未命名子图的语义很草率。但是,解析器保持不变,只需将 Builder 绑定到 Events:

BGL::Digraph g;
auto const   bound_grammar = //
    x3::with<Parser::Events>(BGL::Builder{g})[Parser::simple_graphviz];

if (/*bool ok =*/parse(f, l, bound_grammar))
    std::cerr << "Parse success\n";
else
    std::cerr << "Parse failed\n";

auto name  = get(boost::vertex_name, g);

print_graph(g, name);
write_graphviz(std::cout << "\n ---- GRAPHVIZ:\n", g);

这导致输出 (Live On Wandbox):

Parse success
a0 --> a1
a1 --> a2 b3
a2 --> a3
a3 --> a0 c2
b0 --> b1
b1 --> b2
b2 --> b3 a3
b3 --> c2
c1 --> a0 b0
c2 -->

 ---- GRAPHVIZ:
digraph "" {
subgraph G {
subgraph cluster_0 {
0;
1;
2;
3;
0 -> 1;
1 -> 2;
2 -> 3;
3 -> 0;
}
subgraph cluster_1 {
4;
5;
6;
7;
4 -> 5;
5 -> 6;
6 -> 7;
}
8;
9;
1 -> 7;
3 -> 9;
6 -> 3;
7 -> 9;
8 -> 0;
8 -> 4;
}
}

锦上添花

所以现在我们有 structure-preserving,而不是 id-preserving。我们至少可以确保 id 被用作节点标签然后:

    auto name  = get(boost::vertex_name, g);
    auto attrs = get(boost::vertex_attribute, g);

    for (auto v : make_iterator_range(vertices(g))) {
        attrs[v]["label"] = name[v];
    }

    write_graphviz(std::cout, g);

现在打印:

digraph "" {
subgraph G {
subgraph cluster_0 {
0[label=a0];
1[label=a1];
2[label=a2];
3[label=a3];
0 -> 1;
1 -> 2;
2 -> 3;
3 -> 0;
}
subgraph cluster_1 {
4[label=b0];
5[label=b1];
6[label=b2];
7[label=b3];
4 -> 5;
5 -> 6;
6 -> 7;
}
8[label=c1];
9[label=c2];
1 -> 7;
3 -> 9;
6 -> 3;
7 -> 9;
8 -> 0;
8 -> 4;
}
}

完整列表

Live On Wandbox

#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graphviz.hpp>
#include <boost/graph/subgraph.hpp>
#include <boost/spirit/home/x3.hpp>

namespace x3 = boost::spirit::x3;
using boost::make_iterator_range;

using Id = std::string;

namespace BGL {
    using namespace boost;
    using Attrs = std::map<std::string, std::string>;
    using VProp = property<vertex_name_t, std::string,
                           property<vertex_attribute_t, Attrs>>;
    using EProp = property<edge_attribute_t, Attrs, //
                           property<edge_index_t, int>>;

    using GProp = property<graph_graph_attribute_t, Attrs,
                 property<graph_vertex_attribute_t, Attrs,
                          property<graph_edge_attribute_t, Attrs,
                                   property<graph_name_t, std::string>>>>;

    using Graph = subgraph< //
        adjacency_list<vecS, vecS, undirectedS, VProp, EProp, GProp>>;

    using Digraph = subgraph< //
        adjacency_list<vecS, vecS, directedS, VProp, EProp, GProp>>;

    template <typename G> struct Builder {
        using V = typename G::vertex_descriptor;
        using E = typename G::edge_descriptor;

        Builder(G& g) : g(g) {}

        V node(Id id) // return global descriptor to optionally locally added
        {
            if (auto it = vertices.find(id); it != vertices.end()) {
                return last = it->second;
            } else {
                auto& sub = current();
                V vd = add_vertex(sub);
                put(boost::vertex_name, sub, vd, id);

                // translate to global descriptors for later use
                vd = sub.local_to_global(vd);
                [[maybe_unused]] auto [_, ok] = vertices.emplace(id, vd);
                assert(ok);
                return last = vd;
            }
        }
        void edge(Id id) {
            V s = last;
            V t = node(id); // updates last, sequence point is important!
            add_edge(s, t, g);
        }
        void enter(auto optional_id)
        {
            auto id = optional_id.value_or("");

            if (!hasChild(current(), id))
                get_property(current().create_subgraph(), boost::graph_name) = id;

            stack.push_back(std::move(id));
        }
        void leave(x3::unused_type) { stack.pop_back(); }

      private:
        using Stack = std::deque<std::string>;

        G&              g;
        Stack           stack;
        std::map<Id, V> vertices;
        V               last;

        static G* hasChild(G& g, Id childId) {
            for (auto& child : make_iterator_range(g.children()))
                if (childId == get_property(child, boost::graph_name))
                    return &child;
            return nullptr;
        }

        using F = Stack::const_iterator;
        static G& descend(G& g, F f, F l) {
            if (f == l)
                return g;
            if (G* c = hasChild(g, *f))
                return descend(*c, ++f, l);
            else
                throw std::range_error("descend");
        }

        G& current() { 
            return descend(g, stack.cbegin(), stack.cend());
        }
    };
} // namespace BGL

namespace Parser {
    using namespace x3;

    auto kw = [](auto... p) {
        return lexeme[(x3::as_parser(p) | ...) >> !(alnum | '_')];
    };

    struct Events{};

#define ON(event) ([](auto& ctx) { get<Events>(ctx).event(_attr(ctx)); })

    x3::rule<struct _> graph{"graph"};

    auto subgraph  = kw("subgraph") >> graph;
    auto semi      = &('}'|subgraph) | ';';

    auto id        = lexeme[                            //
        !kw("edge", "node", "graph", "subgraph") //
        >> +char_("A-Za-z0-9_")];
    auto edge      = id[ON(node)] >> *(kw("->") >> id[ON(edge)]) >> semi;

    auto node      = id[ON(node)] >> semi;
    auto element   = subgraph | edge | node;
    auto graph_def = (-id >> '{')[ON(enter)] >> *element >> '}' >> eps[ON(leave)];

    auto simple_graphviz = skip(space)[kw("digraph") >> graph];

    BOOST_SPIRIT_DEFINE(graph)

#undef ON

} // namespace Parser

int main() {
    std::ifstream ifs("input.txt");
    std::string const s(std::istreambuf_iterator<char>(ifs >> std::noskipws), {});
    auto              f = s.begin(), l = s.end();

    BGL::Digraph g;
    auto const   bound_grammar = //
        x3::with<Parser::Events>(BGL::Builder{g})[Parser::simple_graphviz];

    try {
        if (/*bool ok =*/parse(f, l, bound_grammar))
            std::cerr << "Parse success\n";
        else
            std::cerr << "Parse failed\n";

        auto name  = get(boost::vertex_name, g);
        auto attrs = get(boost::vertex_attribute, g);

        for (auto v : make_iterator_range(vertices(g))) {
            attrs[v]["label"] = name[v];
        }

        write_graphviz(std::cout, g);

        if (f != l)
            std::cerr << "Remaining unparsed input: '" << std::string(f,l) << "'\n";
    } catch (x3::expectation_failure<std::string::const_iterator> const& e) {
        std::cerr << e.what() << ": " << e.which() << " at "
                  << std::string(e.where(), l) << "\n";
    }
}

结果图 renders: