GraphViz 简介
GraphViz 是一个用于操作图结构和生成图布局的工具集合。图可以是定向的或非定向的。GraphViz 提供图形化和命令行工具。Perl 接口也可用,但出于通用性考虑,此处不作介绍。本文也不讨论图形化工具。相反,本文重点介绍如何从命令行使用 GraphViz。
一些用户喜欢使用命令行工具而不是图形化工具,因为命令行工具可以与脚本语言一起使用。这意味着您可以创建脚本来创建动态图。动态图的一些可能用途包括
跟踪路由表示数据包到达多个目的地,采用树的形式
数据库表之间的关系图
给老板的图形化报告,从 cron 作业自动通过电子邮件发送
网站地图
UML 图
基于 RPM 的系统上的 RPM 包依赖项
源代码结构图
该软件包已编译为大多数 Linux 和 UNIX 发行版(请参阅“资源”)。它也可以以源代码形式提供,因此您可以自行编译。使用您喜欢的软件安装方法在您的系统上获取 GraphViz。
GraphViz 由以下程序和库组成。
dot 程序:一个用于绘制有向图的实用程序。它接受 dot 语言的输入。dot 语言可以定义三种类型的对象:图、节点和边。dot 使用 Sugiyama 风格(请参阅“资源”)的分层布局。
NEATO 程序:一个用于绘制无向图的实用程序。这种类型的图通常用于电信和计算机编程任务。NEATO 使用 Kamada-Kawai(请参阅“资源”)算法的实现来进行对称布局。
twopi 程序:一个用于使用圆形布局绘制图的实用程序。选择一个节点作为中心,其他节点以圆形模式放置在中心周围。如果一个节点连接到中心节点,则将其放置在距离 1 处。如果一个节点连接到直接连接到中心节点的节点,则将其放置在距离 2 处,依此类推。
dotty、tcldot 和 lefty:三个图形化程序。dotty 是一个用 lefty 编写的 X Window 系统的可定制界面。tcldot 是一个用 Tcl 7 编写的可定制图形界面。lefty 是一个用于技术图片的图形编辑器。
libgraph 和 libagraph:绘图库。它们的存在意味着应用程序可以将 GraphViz 用作库而不是软件工具。
必须展示执行 dot、NEATO 和 twopi 程序的方式。这些程序具有相似的命令行参数。运行 dot、NEATO 或 twopi 的通用方法如下
toolname -Tps filename -o output.ps
此命令执行工具(toolname),声明输出应为 PostScript(使用 filename 作为输入)并生成 (-o) output.ps 文件。此处不讨论其他可能的命令行参数,但您可以查看手册页或 GraphViz 网站(请参阅“资源”)以获取更多信息。
图 G(V,E) 是顶点 V 或节点和边 E 的有限非空集合。如果边 E 具有 (a,b) 的形式,是顶点的有序对,那么我们有一个有向图。如果边 E 是顶点的无序对,我们有一个无向图(请参阅“资源”)。
要使用 dot 制作二叉树,我们应该了解以下理论。没有循环的有向图称为有向无环图。如果一个有向无环图满足以下属性,则它是一棵树
存在一个且仅存在一个顶点(根),没有边进入
除了根顶点之外,每个顶点只有一个进入边。
从根到每个顶点都存在唯一路径。
如果树中每个顶点的子节点都是有序的,则该树称为有序树。二叉树是一棵有序树,使得
顶点的每个子节点都被区分为左子节点或右子节点。
每个顶点最多可以有一个左子节点和一个右子节点。
shape 参数的 record 参数是此示例中的关键参数。此外,label 参数有一些指针(命名为 f0、f1 和 f2 - dot 语言代码中的 <f0>、<f1> 和 <f2>)来声明我们要连接的记录的特定部分。我们使用 | 符号来分隔指针。请注意,在定义每个节点之后,我们使用 nodename: 指针来表示我们要连接的记录部分。有关输出,请参见图 1,有关 dot 源代码,请参见列表 1。

图 1. 二叉树
列表 1. 图 1 的 Dot 源代码
digraph G
{
node [shape = record];
node0 [ label ="<f0> | <f1> J | <f2> "];
node1 [ label ="<f0> | <f1> E | <f2> "];
node4 [ label ="<f0> | <f1> C | <f2> "];
node6 [ label ="<f0> | <f1> I | <f2> "];
node2 [ label ="<f0> | <f1> U | <f2> "];
node5 [ label ="<f0> | <f1> N | <f2> "];
node9 [ label ="<f0> | <f1> Y | <f2> "];
node8 [ label ="<f0> | <f1> W | <f2> "];
node10 [ label ="<f0> | <f1> Z | <f2> "];
node7 [ label ="<f0> | <f1> A | <f2> "];
node3 [ label ="<f0> | <f1> G | <f2> "];
"node0":f0 -> "node1":f1;
"node0":f2 -> "node2":f1;
"node1":f0 -> "node4":f1;
"node1":f2 -> "node6":f1;
"node4":f0 -> "node7":f1;
"node4":f2 -> "node3":f1;
"node2":f0 -> "node5":f1;
"node2":f2 -> "node9":f1;
"node9":f0 -> "node8":f1;
"node9":f2 -> "node10":f1;
}
哈希表是一个数组,它保存指向由哈希值索引的条目的指针。这样的条目可以是内存地址、保存所需信息的数据库记录、文件中作为所需记录起点的特定位置等等。哈希表用于易于查找目的的搜索。例如,它们通常用作 DBMS 系统和编译器中的索引。
在rotate=90下面的列表中的命令返回一个横向输出页面。请注意,label 参数内的 {} 告诉 dot 语言将记录部分从左到右连接,而不是一个在另一个之上。以这种方式绘制哈希表比使用图形化工具更容易。
在rankdir=LR命令是列表 1 中的关键命令。它告诉 dot 语言从左到右放置节点。有关输出,请参见图 2,有关 dot 源代码,请参见列表 2。
列表 2. 图 2 的 Dot 源代码
digraph G
{
rankdir = LR;
node [shape=record, width=.1, height=.1];
rotate=90;
node0 [label = "<p0> | <p1> | <p2> | <p3> \\
| <p4> | | ", height = 3];
node[ width=2 ];
node1 [label = "{<e> r0 | 123 | <p> }" ];
node2 [label = "{<e> r10 | 13 | <p> }" ];
node3 [label = "{<e> r11 | 23 | <p> }" ];
node4 [label = "{<e> r12 | 326 | <p> }" ];
node5 [label = "{<e> r13 | 1f3 | <p> }" ];
node6 [label = "{<e> r20 | 123 | <p> }" ];
node7 [label = "{<e> r40 | b23 | <p> }" ];
node8 [label = "{<e> r41 | 12f | <p> }" ];
node9 [label = "{<e> r42 | 1d3 | <p> }" ];
node0:p0 -> node1:e;
node0:p1 -> node2:e;
node2:p -> node3:e;
node3:p -> node4:e;
node4:p -> node5:e;
node0:p2 -> node6:e;
node0:p4 -> node7:e;
node7:p -> node8:e;
node8:p -> node9:e;
}
您可以将前面的任何一个示例与 twopi 一起使用。使用 twopi 的主要原因是它允许您轻松定义和更改图的中心项。这可以使用 center 命令,后跟文件开头的节点名称来完成。有关输出,请参见图 3,有关 twopi 源代码,请参见列表 3。
列表 3. 图 3 的源代码
digraph G
{
center = v21;
center -> v11;
center -> v12;
center -> v13;
v11 -> v21;
v11 -> v22;
v11 -> v23;
v21 -> v22;
v22 -> v23;
v23 -> v21;
v21 -> v31;
v21 -> v32;
v21 -> v33;
v32 -> v41;
v32 -> v42;
v33 -> v43;
}
在这里,我们将逐步完成使用小型 Perl 脚本创建月度日历的简单任务。有关脚本的命令行参数的更多信息,请参见下面的相关 Perl 代码。
您可以尝试输出的大小、颜色和形状来自定义日历并使其看起来符合您的要求。这个小例子只是简单地展示了脚本语言与智能工具结合可以做什么。有关输出,请参见图 4,有关 Perl 源代码,请参见列表 4。
列表 4. 图 4 的 Perl 源代码
#!/usr/bin/perl -w
# $Id: MYdot.pl,v 3.1 2004/02/05 22:03:32 mtsouk Exp mtsouk $
#
# A perl script to generate monthly
# calendars using dot.
#
# Mihalis Tsoukalos 2004
# * * * * * * * * * * * * * * * * * * * * * * * *
# Command line arguments
# MYdot.pl MonthName MonthYear NofDays StartingDay
#
# MonthName: This is given by the user.
# Year: This is given by the user.
# NofDays: The number of the month's days.
# Month Starting Day: 0 for Sunday etc.
die <<Thanatos unless @ARGV;
usage:
$0 MonthName MonthYear NofDays StartingDay
MonthName: The name of the month in the output.
Year: The Year to appear in the output.
NofDays: The number of the month's days.
Month first Day: 0 for Sunday, 1 for Monday etc.
Thanatos
if ( @ARGV != 4 )
{
die <<Thanatos
usage info:
Please use exactly 4 arguments!
Thanatos
}
# Get the values of the command line arguments
($MonthName, $Year, $NofDays, $SDay) = @ARGV;
# This will tell how many weeks exist in the month
$NofWeeks = (($NofDays + $SDay) / 7);
if ( $NofWeeks > int($NofWeeks) )
{
$NofWeeks = int($NofWeeks) + 1;
}
my $filename = $MonthName.".dot";
open(OUTPUT, "> $filename " ) ||
die "Cannot create $filename: $!\n";
$temp = $SDay;
#
# Fix the first week
#
while ( $temp > 0 )
{
$month[$temp] = "em".$temp;
$temp--;
}
for ( $i = 0; $i<$NofDays; $i++ )
{
$month[$i+$SDay+1] = "day".($i+1)." ";
}
print OUTPUT <<PRE;
digraph G
{
subgraph weekdays
{
node [ style=filled, color=lightgray, \\
height=.60, width=1.15];
sun->mon->tue->wed->thu->fri->sat;
label = "WeekDays";
}
"$MonthName $Year" [shape=Msquare];
"$MonthName $Year" -> sun;
"$MonthName $Year" -> $month[1];
"$MonthName $Year" -> $month[8];
"$MonthName $Year" -> $month[15];
"$MonthName $Year" -> $month[22];
PRE
#
# All months have at least 4 weeks (not full).
# Checking is being done here to see
# if the current months has more that 4 weeks.
#
if (defined $month[29] )
{
print OUTPUT <<WEEK5;
"$MonthName $Year" -> $month[29];
WEEK5
}
#
# The maximum number of weeks a month can have is 6
#
if( defined $month[36] )
{
print OUTPUT <<WEEK6;
"$MonthName $Year" -> $month[36];
WEEK6
}
for ( $i=0; $i<$NofWeeks; $i++ )
{
print OUTPUT "subgraph week".($i+1);
print OUTPUT "{";
print OUTPUT "node [ height=.60, width=.85];";
for ( $j=1; $j<7; $j++)
{
if (defined $month[$i*7+$j])
{
print OUTPUT $month[$i*7+$j];
if (defined $month[$i*7+$j+1])
{
print OUTPUT " -> ";
}
}
}
#
# The 7th day of each week is a special case
# as it does not have a -> after.
#
if (defined $month[$i*7+7] )
{
print OUTPUT $month[$i*7+7].";\n";
}
print OUTPUT "}\n";
}
# Type the empty days into the output file
$temp = $SDay;
while ( $temp > 0 )
{
print OUTPUT "em".$temp;
print OUTPUT " [ shape=box, label=\"\" ];";
print OUTPUT "\n";
$temp--;
}
# Type the days into the output file
for ( $i=1; $i <= $NofDays; $i++ )
{
print OUTPUT "\t";
print OUTPUT "day".$i;
print OUTPUT " [shape=box, label=\"".$i."\"];";
print OUTPUT "\n";
}
print OUTPUT <<ENDING;
sun [shape=egg, label="Sunday"];
mon [shape=egg, label="Monday"];
tue [shape=egg, label="Tuesday"];
wed [shape=egg, label="Wednesday"];
thu [shape=egg, label="Thursday"];
fri [shape=egg, label="Friday"];
sat [shape=egg, label="Saturday"];
}
ENDING
close(OUTPUT) ||
die "Cannot close $filename: $!\n";
exit 0;
现在我们将看看您可以使用 GraphViz 完成的一些更复杂的示例。我们首先使用 NEATO 构建实体关系图(用于 DBMS 系统)。实体关系图是表示实体集、属性和关系的图。通常的做法是将实体集表示为矩形,将属性表示为椭圆形,将关系表示为菱形。GraphViz 可以轻松地用于构建此类图,如下例所示。
这里要记住的关键点是在不同类型的元素之间更改形状——矩形、椭圆形和菱形。这可以使用 shape 属性来完成。有关输出,请参见图 5,有关 NEATO 源代码,请参见列表 5。

图 5. 实体关系图
列表 5. 图 5 的源代码
graph G
{
rotate=90;
id[label="id"];
email[label="email"];
name[label="name"];
uname[label="Name"];
address[label="Address"];
node[shape=rectangle];
university[label="University Department"];
student[label="Student"];
StUnDe[shape=diamond];
university -- StUnDe [label="1", len=3];
StUnDe -- student [label="n", len=2];
student -- id;
student -- name;
student -- email;
university -- uname;
university -- address;
}
接下来是使用 NEATO 的边上带有成本的无向图的示例。这是一个有用的示例,因为这种类型的图可以用于表示网络结构,而成本值有助于定义本地或广域网络的最佳路由表。它应该有助于轻松理解网络拓扑。此图取自 计算机算法的设计和分析 (请参阅“资源”)的第 174 页。有关输出,请参见图 6,有关 NEATO 源代码,请参见列表 6。
列表 6. 图 6 的源代码
graph G
{
edge [len=2];
V1 -- V2 [label="20"];
V2 -- V3 [label="15"];
V3 -- V4 [label="3"];
V4 -- V5 [label="17"];
V5 -- V6 [label="28"];
V6 -- V1 [label="23"];
V7 -- V1 [label="1"];
V7 -- V2 [label="4"];
V7 -- V3 [label="9"];
V7 -- V4 [label="16"];
V7 -- V5 [label="25"];
V7 -- V6 [label="36"];
}
如果您首先放置六个 V7 连接,则输出效果不佳;至少,它与原始图形不相似。当然,信息量是相同的。您可以对文件进行一些更改,看看输出效果如何。
以下示例展示了 RCS 修订树结构的图形表示。同样,Perl 是首选的编程语言。您可以想象,Perl 脚本并不完美,因为它没有涵盖所有可能的修订树结构。在此示例中,展示了小型修订树以进行说明。对于复杂的修订树,在编写 Perl 脚本时必须更多地考虑修订树的特殊性。
脚本背后的基本思想是首先从 rlog 命令获取我们想要的输出。rlog 命令返回有关文件更改历史记录的大量信息,因此我们必须 grep 输出以获取所需的数据。rlog 实用程序包含在 RCS 修订控制系统中。
脚本中的关键点是通过修订号的第一部分分隔修订分支。这样,每个修订分支都有自己的子图。
请注意,您必须在修订名称周围加上引号。否则,输出将无法正确显示。有关输出,请参见图 7,有关 Perl 源代码,请参见列表 7。
列表 7. 图 7 的 Perl 源代码
#!/usr/bin/perl -w
# $Id: RCS.pl,v 1.2 2004/02/05 12:21:40 mtsouk Exp mtsouk $
#
# Command line arguments
# program_name.pl file_with_RCS_info
use strict;
my $filename="";
my $COMMAND="";
my %revision=();
# Change that according to your system
my $RLOG="/usr/bin/rlog";
my $rev=0;
my $count=0;
my %branch=();
my $date=0;
die <<Thanatos unless @ARGV;
usage:
$0 file_with_RCS_info
Thanatos
if ( @ARGV != 1 )
{
die <<Thanatos
usage info:
Please use exactly 1 argument!
Thanatos
}
# Get the file name
($filename) = @ARGV;
$COMMAND = "$RLOG $filename |";
#
# Do not forger to change the path
# of the grep command
#
$COMMAND = $COMMAND." /bin/grep ^revision -A 1 ";
open (RCSINFO, "$COMMAND |")
|| die "Cannot run the ".$COMMAND.": $!\n";
my $line = "";
my $connect="";
while ($line = <RCSINFO>)
{
if ( $line =~ /^revision/ )
{
$rev = (split " ", $line)[1];
$count = (split /\./, $rev)[0];
if ( defined $branch{$count} )
{
my $number = $branch{$count};
$number++;
$branch{$count} = $number;
}
else
{
$branch{$count}=1;
$connect.="\"$filename\" -> \"rev$rev\";";
}
$line = <RCSINFO>;
if ( $line =~ /^date:/ )
{
$date = (split / /, $line)[1];
$revision{$rev} = $date;
}
}
}
close(RCSINFO)
|| die "Cannot close RCSINFO: $!\n";
my $FILE = $filename.".dot";
open (OUTPUT, "> $FILE" )
|| die "Cannot create file: $!\n";
print OUTPUT <<START;
digraph G
{
"$filename" [shape=Msquare];
node [ style=filled, color=lightgray];
node [ height=.50, width=.65];
START
#
# Now we will process the output of the rlog command
# We want to get the revision number,
# the date and the username
#
my $k="";
foreach $k (sort keys %revision)
{
print "$k => $revision{$k}\n";
print OUTPUT <<DATA;
"rev$k" [shape=egg, label="$k\\n$revision{$k}"];
DATA
}
my $ll="";
foreach $ll (keys %branch)
{
print OUTPUT "\t";
foreach $k (sort keys %revision)
{
my $major = (split /\./, $k)[0];
if ( $ll == $major )
{
my $counter = $branch{$ll};
$counter--;
$branch{$ll} = $counter;
print OUTPUT "\"rev".$k."\"";
if ( $counter > 0 )
{
print OUTPUT " -> ";
}
else
{
print OUTPUT ";\n";
}
}
}
}
print OUTPUT <<END;
\t$connect
}
END
close (OUTPUT)
|| die "Cannot close file: $!\n";
exit 0;
我们的下一个示例是更常用的图形:操作系统目录结构的图形表示。请记住,由于页面尺寸限制,可以呈现的目录数量受到限制。此 Perl 脚本已用少量目录进行过测试。此外,目录结构本身是输出质量的最重要因素。意思是,如果我们可以将目录结构想象成树结构,那么如果树的最大级别是 4,并且树的最大级别是 10,而大多数分支的深度为 3,则可能会有很大的不同。
请注意,每个框都不显示完整路径名,仅显示目录名称的最后一部分。您可以通过跟踪链接找到完整路径名。请查看源代码以了解脚本的工作原理。请记住,源代码尚未完全测试;它只是用来展示 dot 语言在脚本语言的少量帮助下可以做什么。有关输出,请参见图 8,有关 Perl 源代码,请参见列表 8。
列表 8. 图 8 的 Perl 源代码
#!/usr/bin/perl -w
# $Id: DIR.pl,v 1.3 2004/02/06 15:18:13 mtsouk Exp mtsouk $
#
# Please note that this is alpha code
#
# Command line arguments
# program_name.pl directory
use strict;
my $directory="";
my $COMMAND="";
my %DIRECTORIES=();
die <<Thanatos unless @ARGV;
usage:
$0 directory
Thanatos
if ( @ARGV != 1 )
{
die <<Thanatos
usage info:
Please use exactly 1 argument!
Thanatos
}
# Get the file name
($directory) = @ARGV;
$COMMAND = "/usr/bin/find $directory -type d | ";
open (INPUT, "$COMMAND")
|| die "Cannot run the ".$COMMAND.": $!\n";
#
# The reason for putting OUTPUT in front of the
# directory name is that we
# can have . as directory name
#
my $OUTPUT="OUTPUT$directory.dot";
$OUTPUT =~ s/\//-/g;
open (OUTPUT, "> $OUTPUT")
|| die "Cannot create output file $OUTPUT: $!\n";
print OUTPUT <<START;
digraph G
{
rotate=90;
nodesep=.05;
node[height=.05, shape=record, fontsize=5];
START
# Make nodes for the command line argument directory
my @split = split /\//, $directory;
my $key="";
my $prev=undef;
for $key (@split)
{
my $KEY=$key;
$key =~ s/[^[a-zA-Z0-9]/_/g;
$key = $prev."_".$key;
$prev = $key;
print OUTPUT "\t".$prev;
print OUTPUT " [shape=box, label=\"$KEY\"];";
print OUTPUT "\n";
}
my $lastpart = "";
while (<INPUT>)
{
chomp;
my $orig=$_;
# Get the right label
my @split = split /\//, $_;
$lastpart = pop @split;
$_ =~ s/\//_/g;
#
# The _ is accepted as a valid node character
# . , + - are not accepted
#
$_ =~ s/[^a-zA-Z0-9]/_/g;
my @split = split /_/, $_;
print OUTPUT "\t_".$_;
print OUTPUT " [shape=box,label=\"$lastpart\"];";
print OUTPUT "\n";
$DIRECTORIES{$orig}=0;
}
my $subdir="";
my %TEMP=();
foreach $key ( sort keys %DIRECTORIES )
{
print "KEY: $key\n";
my @split = split /\//, $key;
my $prev = undef;
for $subdir (@split)
{
$subdir =~ s/[^a-zA-Z0-9_]/_/g;
my $next = $prev."_".$subdir;
# print "NEXT: $next\n";
if ( !defined($prev) )
{
$prev = $next;
next;
}
my $val = "$prev->$next;\n";
# print "VAL: $val\n";
if ( !defined( $TEMP{$val} ))
{
print OUTPUT "$prev->$next;\n";
}
$prev .= "_".$subdir;
$TEMP{$val}=1;
}
}
close(INPUT)
|| die "Cannot close input file: $!\n";
print OUTPUT <<END;
}
END
close (OUTPUT)
|| die "Cannot close file: $!\n";
exit 0;
使用 dot、twopi 和 NEATO 创建图形并不困难。这三个实用程序是根据 UNIX 哲学制作的:它们易于使用,它们很好地完成一件事情,并且使用它们的唯一要求是一个简单的文本编辑器。
也存在使用 dot、twopi 和 NEATO 语言制作图形的图形化工具,但我仍然更喜欢命令行。所提供的工具既可以用于简单图形,也可以用于复杂图形。
我要感谢 Nikos Platis 校对本文。
“用于可视化理解分层系统结构的方法”。Sugiyama, K., Tagawa, S. 和 Toda, M. 人和控制论。 IEEE Trans. Systems,1981 年 2 月。
绘制有向图的技术。Gansner, E. R., Koutsofios, E., North, S. C. 和 Vo, K. IEEE Trans. Software Engineering,1993 年 5 月。
“用于绘制一般无向图的算法”。Kamada, T. 和 Kawai, S. 信息处理快报,1989 年 4 月。
计算机算法的设计和分析。 Aho, Hopcroft 和 Ullman。Addison Wesley,1974 年。
Mihalis Tsoukalos 与他的妻子 Eugenia 一起住在希腊,并担任高中教师。此前,他曾担任 UNIX 系统管理员、Oracle DBA、UNIX 程序员和 PL/SQL 程序员。他拥有伦敦大学学院的数学学士学位和信息技术硕士学位。可以通过 mtsouk@freemail.gr 联系 Mihalis。






