如何将JSON数据转换为树形图片?

16
我正在使用 treelib 生成树形结构,现在我需要一个易于阅读的树形图像版本,所以我想将它们转换为图像。例如: enter image description here
下面是示例 JSON 数据,对应上述的树形结构: enter image description here
数据如下:
>>> print(tree.to_json(with_data=True))
{"Harry": {"data": null, "children": [{"Bill": {"data": null}}, {"Jane": {"data": null, "children": [{"Diane": {"data": null}}, {"Mark": {"data": null}}]}}, {"Mary": {"data": null}}]}}

没有数据:

>>> print(tree.to_json(with_data=False))
{"Harry": {"children": ["Bill", {"Jane": {"children": [{"Diane": {"children": ["Mary"]}}, "Mark"]}}]}}

有没有办法使用graphvizd3.js或其他Python库来使用此JSON数据生成树形图?


你看过 Plotly 吗?https://plot.ly/python/tree-plots/ - corn3lius
@corn3lius,没有看过,你能帮我吗?我对JSON很新! - Grimlock
顺便说一下,JSON 的 with_data=False 版本与你的图片匹配,但 with_data=True 版本则不匹配。 - PM 2Ring
是的@PM2Ring,但是with_data=False对我的使用已经足够了。 - Grimlock
3个回答

20
对于这样的树,没有必要使用库:您可以直接生成Graphviz DOT语言语句。唯一棘手的部分是从JSON数据中提取树边。为此,我们首先将JSON字符串转换回Python dict,然后递归解析该dict。
如果树字典中的名称没有子节点,则它是一个简单的字符串;否则,它是一个dict,我们需要扫描其“children”列表中的项。我们找到的每个(父、子)对都会附加到全局列表“edges”中。
这个有点神秘的行:
name = next(iter(treedict.keys()))

treedict中获取单个键。这将为我们提供人的姓名,因为这是treedict中唯一的键。在Python 2中,我们可以执行以下操作:

name = treedict.keys()[0]

但是之前的代码在 Python 2 和 Python 3 中都可以运行。

from __future__ import print_function
import json
import sys

# Tree in JSON format
s = '{"Harry": {"children": ["Bill", {"Jane": {"children": [{"Diane": {"children": ["Mary"]}}, "Mark"]}}]}}'

# Convert JSON tree to a Python dict
data = json.loads(s)

# Convert back to JSON & print to stderr so we can verify that the tree is correct.
print(json.dumps(data, indent=4), file=sys.stderr)

# Extract tree edges from the dict
edges = []

def get_edges(treedict, parent=None):
    name = next(iter(treedict.keys()))
    if parent is not None:
        edges.append((parent, name))
    for item in treedict[name]["children"]:
        if isinstance(item, dict):
            get_edges(item, parent=name)
        else:
            edges.append((name, item))

get_edges(data)

# Dump edge list in Graphviz DOT format
print('strict digraph tree {')
for row in edges:
    print('    {0} -> {1};'.format(*row))
print('}')

标准错误输出

{
    "Harry": {
        "children": [
            "Bill",
            {
                "Jane": {
                    "children": [
                        {
                            "Diane": {
                                "children": [
                                    "Mary"
                                ]
                            }
                        },
                        "Mark"
                    ]
                }
            }
        ]
    }
}

标准输出

strict digraph tree {
    Harry -> Bill;
    Harry -> Jane;
    Jane -> Diane;
    Diane -> Mary;
    Jane -> Mark;
}

上述代码可在 Python 2 和 Python 3 上运行。它将 JSON 数据打印到 stderr 中,以便我们可以验证其正确性。然后将 Graphviz 数据打印到 stdout 中,这样我们就可以将其捕获到文件中或直接将其导入到 Graphviz 程序中。例如,如果脚本名称为“tree_to_graph.py”,则可以在命令行中执行以下操作将图形保存为名为“tree.png”的 PNG 文件:

python tree_to_graph.py | dot -Tpng -otree.png

这里是PNG输出:

由Graphviz制作的树


这正是我需要的,但我一直收到错误提示:警告:<stdin>:第1行语法错误附近。你有什么想法是导致这个错误的原因吗? - Grimlock
也许是因为我的树是由整数而不是字符串构成的。所以每当我在控制台执行该命令时,就会出现错误:警告:<stdin>:第1行语法错误,附近为114,其中114是根节点。 - Grimlock
测试了你的代码,一切正常。问题是,我正在使用s = tree.to_json(with_data=False)来获取数据,这导致了上述错误,有什么办法可以解决吗?@PM2Ring - Grimlock
2
我自己解决了“警告:<stdin>:第1行附近的语法错误”错误,但问题是我正在使用整数,而这些整数有时可能会重复。在这种情况下应该怎么办?就像在这个例子中,可能会有一个名为“Diane”的孩子也叫“Diane”。如何解决这个问题? - Grimlock
@Grimlock:你不能有两个具有相同名称的不同节点。另一方面,Graphviz可以处理循环,包括指向自身的节点。如果你的treelib代码为两个或多个不同的节点分配了相同的名称,那么你需要修复该代码。 - PM 2Ring
显示剩余3条评论

6

根据PM 2Ring的答案,我创建了一个可以通过命令行使用的脚本:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Convert a JSON to a graph."""

from __future__ import print_function
import json
import sys


def tree2graph(data, verbose=True):
    """
    Convert a JSON to a graph.

    Run `dot -Tpng -otree.png`

    Parameters
    ----------
    json_filepath : str
        Path to a JSON file
    out_dot_path : str
        Path where the output dot file will be stored

    Examples
    --------
    >>> s = {"Harry": [ "Bill", \
                       {"Jane": [{"Diane": ["Mary", "Mark"]}]}]}
    >>> tree2graph(s)
    [('Harry', 'Bill'), ('Harry', 'Jane'), ('Jane', 'Diane'), ('Diane', 'Mary'), ('Diane', 'Mark')]
    """
    # Extract tree edges from the dict
    edges = []

    def get_edges(treedict, parent=None):
        name = next(iter(treedict.keys()))
        if parent is not None:
            edges.append((parent, name))
        for item in treedict[name]:
            if isinstance(item, dict):
                get_edges(item, parent=name)
            elif isinstance(item, list):
                for el in item:
                    if isinstance(item, dict):
                        edges.append((parent, item.keys()[0]))
                        get_edges(item[item.keys()[0]])
                    else:
                        edges.append((parent, el))
            else:
                edges.append((name, item))
    get_edges(data)
    return edges


def main(json_filepath, out_dot_path, lr=False, verbose=True):
    """IO."""
    # Read JSON
    with open(json_filepath) as data_file:
        data = json.load(data_file)

    if verbose:
        # Convert back to JSON & print to stderr so we can verfiy that the tree
        # is correct.
        print(json.dumps(data, indent=4), file=sys.stderr)

    # Get edges
    edges = tree2graph(data, verbose)

    # Dump edge list in Graphviz DOT format
    with open(out_dot_path, 'w') as f:
        f.write('strict digraph tree {\n')
        if lr:
            f.write('rankdir="LR";\n')
        for row in edges:
            f.write('    "{0}" -> "{1}";\n'.format(*row))
        f.write('}\n')


def get_parser():
    """Get parser object for tree2graph.py."""
    from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
    parser = ArgumentParser(description=__doc__,
                            formatter_class=ArgumentDefaultsHelpFormatter)
    parser.add_argument("-i", "--input",
                        dest="json_filepath",
                        help="JSON FILE to read",
                        metavar="FILE",
                        required=True)
    parser.add_argument("-o", "--output",
                        dest="out_dot_path",
                        help="DOT FILE to write",
                        metavar="FILE",
                        required=True)
    return parser


if __name__ == "__main__":
    import doctest
    doctest.testmod()
    args = get_parser().parse_args()
    main(args.json_filepath, args.out_dot_path, verbose=False)

2
以下是将雪花算法的JSON数据直接转换为树形结构的解决方案,使用graphviz实现:
您的输入数据:
json_data = {"Harry": {"data": None, "children": [{"Bill": {"data": None}}, {"Jane": {"data": None, "children": [{"Diane": {"data": None}}, {"Mark": {"data": None}}]}}, {"Mary": {"data": None}}]}}

我们可以使用广度优先搜索遍历树,以确保所有边都被遍历,这是使用graphviz构建树的要求:
import graphviz

def get_node_info(node):
    node_name = list(node.keys())[0]
    node_data = node[node_name]['data']
    node_children = node[node_name].get('children', None)
    return node_name, node_data, node_children

traversed_nodes = [json_data] # start with root node

# initialize the graph 
f = graphviz.Digraph('finite_state_machine', filename='fsm.gv')
f.attr(rankdir='LR', size='8,5')
f.attr('node', shape='rectangle')

while (len(traversed_nodes) > 0):
    cur_node = traversed_nodes.pop(0)
    cur_node_name, cur_node_data, cur_node_children = get_node_info(cur_node)
    if (cur_node_children is not None): # check if the cur_node has a child
        for next_node in cur_node_children: 
            traversed_nodes.append(next_node)
            next_node_name = get_node_info(next_node)[0]
            f.edge(cur_node_name, next_node_name, label='') # add edge to the graph

f.view()


输出图如下(我认为你的示例树图中Marie的节点位置有误):

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接