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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章