Graphviz的使用及中文乱码问题

缘起

Linux下的绘图软件,自动的,半自动的,最后遇到神器Graphviz。整个流程还算波折,起初,编译docbook的部分遇到这个软件,后来,在看Korat: Automated Testing Based on Java Predicates2000ACM的最佳论文),发现其软件实现Korat中使用了Graphviz的实现来绘图程序依赖图,最后,在学习Latex的时候,考虑要嵌入图片,想起这个绘图工具,就对这个著名的工具做了一些研究,其实就是Google了一下,只能算是文献调研。

以下,是OpenFoundry上看到的介绍Graphviz的文章,原文为繁体字,用google translation转换为简体中文,修改一下一些术语,添加了一下内容(乱码问题)。

1. 简介

Graphviz 是一个运用广泛的命令行绘图软体,不过说是绘图软体,它能绘的图并不是一般人想像中的漫画或logo,而是数学意义上的"graph",比较通俗的说法就是「关系图」。

举例来说,像是下面这种图:

 

2. 安装

Graphviz 支援WindowsMac OS XFreeBSDSolarisLinux 等多种系统。UbuntuDebian下安装很简单,sudo apt-get install graphviz

Windows用户,请前往这里下载安装档:http://www.graphviz.org/Download_windows.php

Mac OS X的使用者请往这边走:http://www.graphviz.org/Download_macos.php

3. Graphviz的使用

# Graphviz 

cmd> <inputfile> -T format> -o outputfile

#举例:输出png

dot input.dot -T png -o output.png 

#举例:一样是输出png ,只不过档名是txt

dot input.dot -T png -o output.txt

首先,我们看看上面的<cmd> 部份。

Graphviz 的<cmd> 有好几种,每种使用方法都完全相同,差别只在于渲染出来的图片效果不一样。man中的简介是这样的:

dot

渲染的图具有明确方向性。

neato

渲染的图缺乏方向性。

twopi

渲染的图采用放射性布局。

circo

渲染的图采用环型布局。

fdp

渲染的图缺乏方向性。

sfdp

渲染大型的图,图片缺乏方向性。

可以透过man cmd> 取得进一步说明。但还是亲自用用比较容易理解。在本文中,凡没有说明的图,预设都是以dot渲染出来的。

继续往下看。在Graphviz中,若您不指定-T参数,Graphviz并不会自动猜测您想要产生什么格式,只会以预设格式渲染。可选格式相当多,包括(但不限于)jpgpngsvg等,全部列表可见官网说明页的最下方。

-o 可让您指定储存档案的档名。如果您不用-o 选项指定输出档名,Graphviz 则会将结果输出到标准输出上(图片格式输出到标准输出就是乱码的二进制文件)。

除非用法很特殊,否则这两个参数,每次都要输入并传递给dot。这样输入命令就是:

dot -Tpng demo.dot -o demo.png

为了减少打字的次数,编写简单的shell脚本dot.sh,其中涉及获取文件名和扩展,从网上搜的,工作的很好,但不太清楚实现原理:

#!/bin/sh

# dot.sh

file=$1

filename=${file%.*}

extension=${file##*.}

outfile=${filename}.png

dot -Tpng $file -o $outfile

#show generate image file

eog $outfile &

给脚本dot.sh加上执行权限,然后调用命令就简化为:

./dot.sh demo.dot

当然,如果整个命令很常用的话,可以将改作函数其写到.bashrc中。

4. dot语言说明

指挥Graphviz 绘图时,所使用的语言叫作"dot"。下边就来介绍如何使用它。

4.1. 有向图与无向图

使用dot 语言,第一步就是决定要画哪种图。

图分两种:有向图与无向图。

有向图以digraph申明图片,节点间的关系写为"->"

/*demo1。顺便一提,在dot语言中可使用C++中允许的注解。本行为C风格注解*/
digraph demo1{ //这也是注解,C++风格的。
a -> b -> c;
c -> a; 
}
结果图:


无向图以graph 宣告图片,节点间的关系可以写为"--"

//demo2
graph demo2{
a -- b -- c;
c -- a;
}

结果图:


其中demo1 demo2 是图片的名称。

4.2. 使用引号

上文中的a, b, c 除了作为程式内的识别字以外,也会成为节点的显示名称(label)。不过如果这名称中混了中文或夹了空格,Graphviz 就有可能搞错你的意思。

为防不必要的误解,所以平常最好都用英文引号括住。就像下面这样:

//demo3
digraph {
"总攻" -> "受";
"强攻" -> "受";
"健气攻" -> "受";
}

 图4:混合了空白的示范


这样就没问题了!

子图与简化技巧

来看个复杂一点的例子,这是一份地中海海域的大略连接图:

//demo4

graph G{

"黑海" -- "亚速海";

"黑海" -- "博斯普鲁斯海峡"

"达达尼尔海峡" -- "爱琴海"

subgraph cluster_T{//新东西

label = "黑海海峡";//新东西

"达达尼尔海峡" -- "马尔马拉海" -- "博斯普鲁斯海峡";

}

subgraph cluster_M{

label = "地中海海域";

"中部地中海" -- {"爱琴海" "爱奥尼亚海" "西西里海峡"}; //也是新东西

"西部地中海" -- {"西西里海峡" "第勒尼安海" "利古里亚海" "伊比利海" "阿尔沃兰海"};

"爱奥尼亚海" -- "亚得里亚海";

"阿尔沃兰海" -- "直布罗陀海峡";

}

} 

结果图:


5:地中海海域连接图,使用dot渲染。

这张图有些新东西可以看。

第一个是subgraph 关键字。一如名字所示,他是用来定义「次级图片」用的。

次级图片在dot的官方文件中常被叫作cluster subgraph,特指图示中被方框包裹起来的那两块,其定义方式和一般的graph非常相似,不过使用上有两件事需要留意:

graph的命名得以cluster前缀开头,否则语法虽然能过关,但生不出图面上您预期的效果。

如果父图是无向图,他本身也得是无向图;反之如果父图是有向图,这边也得乖乖照着来。

第二个重点是下面这段:

1 "中部地中海" -- {"爱琴海" "爱奥尼亚海" "西西里海峡"};

用大括号括起,用空格分开-这是一口气将好几个节点群组起来同时操作的方法,其等效于:

1 "中部地中海" -- "爱琴海"; 2 "中部地中海" -- "爱奥尼亚海"; 3 "中部地中海" -- "西西里海峡";

您甚至可以用以下程式码画出图6

//demo5

digraph G{//{}中的注意空格

{ a b c} -> { d e f }

}


6:大括号效果示意图

这语法糖很方便好吃,可以灵活运用。

第三个不同处在于label=XXX 这行。这是「属性」的指定方式。

关于属性,我们下章再讲。

4.3. 属性

有了前面介绍过的技巧,所有图面关系都可以顺利地绘制出来。

然而,通常我们画图的时候,还会对图片做一些特别的处理。好比说把字加粗、把图变色、把标签或连接线的外型改变、把某些节点水平对齐......诸如此类。

要控制这些东西,就要用到属性。

属性有四种:

1. 用在节点上(Node, N)

2. 用在线段上(Edge, E)

3. 用在根图片上(Graph, G)

4. 用在子图片上(Cluster subgraph, C)

您可以阅读手册中的表,判断哪些属性能用在哪些地方。

那么,属性要怎么用呢?

4.3.1. 属性的套用

如果要设定根图片或子图片的属性,得像前面范例中所示的那样,在图片的大括号范围内指定..

属性名称=;

这样就行了。

对于节点(node) 的属性,有以下几种指定法:

1节点名[节点属性名=];

2节点名[节点属性名=,节点属性名=];

3 node [节点属性名=,节点属性名=];

属性指定的语句必须要被中括号括起。当一次指定多值时,需用英文逗点隔开。

第三行中的node 是个关键字,用来代称「图片范围内」所有「还没创建」的节点,或者您也可将它理解为:在当前大括号的范围内,所有尚未创建节点的属性预设值,会被这个语句给变更。

线段(edge)的属性指定,与节点属性指定方式很类似:

1节点名->节点名[线段属性名=];

2节点名--节点名[线段属性名=,线段属性名=];

3 edge [线段属性名=值,线段属性名=];

其中edge 是关键字。

这边顺便补充一个关于线段的观念:有些线段相关的属性,具有head值与tail值。而这边说的head tail,得将它想像成一个「箭头」的形状(就像是「a -> b」这样)。

对于线段来说,这个箭头指向的头部才是head。这可能和直觉上不一样,因为这边说的「Head」其实是两个节点中,后面的那一个。

4.3.2. 属性范例

把先前的看过的例子加上一些属性试试。

//demo6

graph G{

"黑海" [shape = circle, color = blueviolet, fontcolor = blueviolet, fontsize = 20];

"黑海" -- "亚速海" [label = "刻赤海峡"];

subgraph cluster_T{

label = "黑海海峡";

fontsize = 24;

fillcolor = darkslategray;

style = filled;

fontcolor = white;

node [fontcolor = white, color = white];

"博斯普鲁斯海峡" -- "马尔马拉海" -- "达达尼尔海峡" [color = white];

"博斯普鲁斯海峡" [shape = parallelogram];

"达达尼尔海峡" [shape = parallelogram];

}

"黑海" -- "博斯普鲁斯海峡" [color = red ,penwidth = 2];

"达达尼尔海峡" -- "爱琴海" [color = red ,penwidth = 2];

subgraph cluster_M{

label = "地中海海域";

fontsize = 24;

"西部地中海" [shape = Mcircle, style = filled, color = grey, fillcolor = aquamarine, fontsize = 20];

"中部地中海" [shape = Mcircle, style = filled, color = grey, fillcolor = aquamarine, fontsize = 20];

"直布罗陀海峡" [shape = parallelogram, fontcolor = red];

"西西里海峡" [shape = parallelogram ];

"中部地中海" -- {"爱琴海" "爱奥尼亚海" "西西里海峡"};

"西部地中海" -- {"西西里海峡" "第勒安海" "利古里亚海" "伊比利海" "阿尔沃兰海"};

"爱奥尼亚海" -- "亚得里亚海"; 30 "阿尔沃兰海" -- "直布罗陀海峡";

}

}


7:地中海海域连接图(加入属性)。

诸多属性中,最常用的大概是label 了。

label可以决定节点、线段或子图片上要显示些什么。如果您的节点名很长的话,可以在程序内部取个简短的名称,之后透过短名称操作它,另外透过label 指定它的显示内容。

colorfillcolorfontcolor 这些属性都是控制颜色用的,不过fillcolor 只有在style 被指定为"filled" 时才会生效。

shape可以指定节点的形状,形状列表参考这里(http://www.graphviz.org/content/node-shapes)。

线段属性方面。有向图中的箭头可透过arrowhead arrowtail 属性来指定头尾样式。至于线段本身,则可透过style 属性,指定不同类型的虚线与短截线。使用者还可以用dir 属性让箭头方向反过来。

另外还有一个image 属性,可以指定让node 显示图片,需要时也可参考看看。

属性很多,无法一一介绍,请查官网手册(http://www.graphviz.org/doc/info/attrs.html)

4.3.3. rank

dot 语言中有一个叫作rank 的概念。

所谓的rank,在dot 语言中,含意比较接近于「等级」。他主要用在dot渲染器中。

请看以下的图:


8rank 示例。

很明显可以看出来,图片被从上到下分为四层-这就是rank

下方是与上图对应的dot 陈述:

//demo7

digraph demo{

a -> b -> c -> d;

b -> { e f };

}

观察程序,可看出rank是如何被指定的。

其基本规则在于:每个线段的头端,都会比尾端多出一个等级(在图上面就是往下面一层)。

但等等,如果等级指定的语句彼此矛盾呢?

修改以上程式码为:

//demo8

digraph demo{

a -> b -> c -> d [label = "rank增加"];

b -> { e f } [label = "rank增加"];

f -> a [label = "不影响rank"];

}


9rank 示例。

看上面的结果,显然rank 的指定是「先说先赢」的。

除了基本规则外,rank也可以透过属性来加以调节,有必要时要参看手册。

5. 在脚本中使用Graphviz

5.1. 在shell中使用Graphviz

在了解了 Graphviz DOT 语言的基础知识之后,您可以开始创建脚本,从而动态创建一个 DOT 文件。这允许您动态创建始终准确且保持最新的图表。

示例1是一个 bash shell 脚本,它连接到 Hardware Management Console (HMC),收集托管服务器和逻辑分区 (LPAR) 的相关信息,然后使用这些信息来创建 DOT 输出。

示例1 hmc_to_dot.sh

#!/bin/bash

 

HMC="$1"

 

serverlist=`ssh -q -o "BatchMode yes" $HMC lssyscfg -r sys -F "name" | sort`

 

echo "graph hmc_graph{"

 

for server in $serverlist; do

    echo " \"$HMC\" -- \"$server\" "

    lparlist=`ssh -q -o "BatchMode yes" $HMC lssyscfg -m $server -r lpar -F "name" | sort`

    for lpar in $lparlist; do

             echo "    \"$server\" -- \"$lpar\" "

    done

done

 

echo "}"

通过提供一个 HMC 服务器名称作为参数传递给脚本,便可运行此脚本。该脚本将传递的第一个参数设置为 $HMC 变量。设置 $serverlist 变量的方法是连接到 HMC 并获得该 HMC 控制的所有托管服务器的清单。在这些托管服务器上进行循环,而脚本将为每台托管服务器打印一行 "HMC" -- "server" ,这表明 Graphviz 在每台 HMC 与其托管服务器之间绘制了一条直线。此外针对每台托管服务器,脚本再次连接到 HMC 并获得该托管系统上的 LPAR 清单,然后通过它们循环打印一行 "server" -- "LPAR"。这表明 Graphviz 在每台托管服务器与其 LPAR 之间都绘制了一条直线。

此脚本要求您在运行脚本的服务器与 HMC 之间设置 Secure Shell (SSH) 密钥身份验证。

事例1`脚本的输出:

graph hmc_graph{

 "hmc01" -- "test520"

    "test520" -- "lpar2"

    "test520" -- "lpar3"

 "hmc01" -- "test570"

    "test570" -- "aixtest01"

    "test570" -- "aixtest02"

    "test570" -- "aixtest03"

 "hmc01" -- "test510"

    "test510" -- "lpar1"

}

从脚本生成图的具体方法是运行以下命令:./hmc_to_dot.shhmc_server_name|dot-Tpng-ohmc_graph.png

上述命令运行脚本,并动态创建 DOT 语言的文本,然后将这些输出传递给 dot 命令,以便让它创建一个文件名为 hmc_graph.png 的图表。图 10 显示了创建的图表。

图 10 从 hmc_to_dot.sh 脚本创建的图表


备注:这里的脚本的使用和具体的环境有关,详细参考[3],具体的语境为AIX小型机上的Virtual I/O Servers 有关,很明显,我不能可能有的AIX小型机来测试,所以这个图不是我自己生成的,但是,这里在shell脚本中生成dot的源码确实值得学习。

5.2. 在Python中使用Graphviz

dot不是一个真正的编程语言,但它是很容易与真正的编程交互。可以绑定多种编程语言,包括JavaPerlPython。更轻量级替代是从喜欢的语言中生成dot代码。 这样做将使您自动化整个图形生成过程。

Python写的用来生成dot的例子。此示例脚本展示了如何以最小的努力绘制的Python类层次

# dot.py 

"Require Python 2.3 (or 2.2. with from __future__ import generators)"

def dotcode(cls):

    setup='node [color=Green,fontcolor=Blue,fontname=Courier]\n'

    name='hierarchy_of_%s' % cls.__name__

    code='\n'.join(codegenerator(cls))

    return "digraph %s{\n\n%s\n%s\n}" % (name, setup, code)

 

def codegenerator(cls):

    "Returns a line of dot code at each iteration."

    # works for new style classes; see my Cookbook

    # recipe for a more general solution

    for c in cls.__mro__:

        bases=c.__bases__

        if bases: # generate edges parent -> child

            yield ''.join([' %s -> %s\n' % ( b.__name__,c.__name__)

                           for b in bases])

        if len(bases) > 1: # put all parents on the same level

            yield " {rank=same; %s}\n" % ''.join(

                ['%s ' % b.__name__ for b in bases])

if __name__=="__main__": 

    # returns the dot code generating a simple diamond hierarchy

    class A(object): pass

    class B(A): pass

    class C(A): pass

    class D(B,C): pass

    print dotcode(D)

 

函数dotcode需要一个类,并返回绘制类的系谱树所需的dot源代码。codegenerator生成的代码,遍历类的祖先的列表(类的方法解析序列中),并确定边和节点的层次结构。Codegenerator是一个在每次迭代中返回一行的dot代码的生成器。返回一个迭代产生点阵码在每次迭代。生成器是最近添加到Python中的工具,在处理生成文本或源代码的目的问题时特别方便。脚本的输出是如下不言自明的dot代码:

digraph hierarchy_of_D {

node [color=Green,fontcolor=Blue,font=Courier]

 B -> D

 C -> D

 {rank=same; B C }

 A -> B

 A -> C

 object -> A

}

处理命令:python dot.py | dot -Tpng -o x.png

生成的图片如下:

备注:例子来自参考4。

6. 中文乱码问题

如果graphviz中出现了中文乱码问题,可以采用两种方法来处理,一种就是上面的将文本保存为UTF8Ubuntu下默认为UTF8Windows下默认为ASNI),并将中文包含在英文的引号(“”)中。另一种方法多一个将图或结点的字体属性设置为中文字体的步骤,具体步骤如下:

l 保存为UTF8格式

l 设置中文字体,常见的中文字体的对应的名字如下:

黑体:SimHei 宋体:SimSun 新宋体:NSimSun 仿宋:FangSong 楷体:KaiTi

仿宋_GB2312FangSong_GB2312 楷体_GB2312KaiTi_GB2312

下列以编译原理的处理流程图为例演示,第二种中文处理方法,此外,其中还演示了使用一些复杂的属性。

compile_process.dot程序代码:

//compile_process.dot

digraph G {

edge [fontname="FangSong"];

node [shape=box, fontname="FangSong" size="20,20"];

{

Lexical_Analyzer [label="词法分析器"];

Syntax_Analyzer [label="语法分析器"];

Semantic_Analyzer [label="语义分析"];

Intermediate_Code_Generator [label="中间代码生成器" ];

Machine_Independent_Code_Optimizer [label="机器无关代码优化器" ];

Code_Generator [label="代码生成器" ];

Machine_Dependent_Code_Optimizer [label="机器相关代码优化器"];

}

node[shape=plaintext, fontname="KaiTi" ]{

character_stream [label="字符流"];

target_machine_code [label="目标机器语言" ];

}

    character_stream ->Lexical_Analyzer;

Lexical_Analyzer ->Syntax_Analyzer [label="符号流"];

Syntax_Analyzer  ->Semantic_Analyzer  [label="语法树"];

Semantic_Analyzer  ->Intermediate_Code_Generator  [label="语法树"] ;

Intermediate_Code_Generator->Machine_Independent_Code_Optimizer  [label="中间表示形式"];

Machine_Independent_Code_Optimizer  ->Code_Generator  [label="中间表示形式"];

Code_Generator ->Machine_Dependent_Code_Optimizer [label="目标机器语言"];

Machine_Dependent_Code_Optimizer->target_machine_code ;

}

dot渲染生成的图,其他的格式(fdp等)渲染的效果不太好:


fdp渲染的该图(效果非常不好):


小结

Graphviz的确实一个非常的强大的工具,而且其官方的文档还非常全面。更多的内容,参考其官方的文档,地址为:http://www.graphviz.org/Documentation.php

参考文献

[1] Graphviz -用指令来画关系图吧!http://www.openfoundry.org/en/foss-programs/8820-graphviz-

[2]Graphviz中文乱码问题

[3]使用 Graphviz 生成自动化系统图:http://www.ibm.com/developerworks/cn/aix/library/au-aix-graphviz/

[4]An Introduction to GraphViz and dot :http://www.linuxdevcenter.com/pub/a/linux/2004/05/06/graphviz_dot.html

没有更多推荐了,返回首页