ふにゃるんv2

もとは、http://d.hatena.ne.jp/Wacky/

IronPythonとGLEEライブラリを使って、GraphVizのdotもどきなツールを作ろう

はてブしていたURLを眺め返していたら、以下のブックマークを見つけました。


ふむふむ、IronPythonを使って、Microsoft Researchが提供している GLEE って言う グラフ作成ライブラリを使ってみよう。というBlogのようです。
このBlogを書いた方は、どうもpydot(GraphVizPythonから呼び出せる)ライブラリを使ってダイアグラムを描画したかったらしいのですが、pydotIronPythonから呼べないので、GLEEを使ってみたよ。って事みたいです。


で、暫く眺めていて、

  1. そういや、前に簡単なコールグラフを作成したいなぁと思ってたっけなぁ
  2. でも、Win版のGraphVizって、日本語扱おうとすると面倒だったよなぁ(今は直っているかも)
  3. GLEEって、結構簡単にグラフが描けるんだなぁ
  4. 一々API呼び出すより、GraphVizのdotコマンド風に仕立て上げた方が、自分的需要高そう
  5. ちょっと作ってみようかしらん

と、思いまして、適当ツールを仕立て上げてみました。


名前は、「GraphVizもどきなGLEEを使ったツール」って事で、gra_glee.py(グラグリー←グリズリー風に読んであげて下さい)って命名しました。

gra_glee が出来る事

簡単に言うと以下の機能を持ってます。

  • dot書式風のテキストファイルを読み込んで、グラフを作成します
  • 作成したグラフは、プレビューできます(下図参照)
  • 他に、直接 画像ファイルに保存も出来ます(バッチコマンド用に)
  • dot書式風のテキストファイルは、shift-jis形式をサポートしており、日本語大丈夫ぶぅ。です


例えば、以下のファイルを、

digraph "g" {
   "Python ファミリー" -> "Python 2.4" -> "Python 2.5";
   "Python ファミリー" -> "IronPython 1.0" -> "IronPython 1.1" -> "IronPython 2.0";
   "Python 2.4" -> "IronPython 1.0";
   "Python 2.5" -> "IronPython 2.0";
   "Python ファミリー" [shape=box];
}

以下のように呼び出すと、

$ ipy gra_glee.py -i dot1.txt

以下のプレビューワが起動します。
GLEE1
GLEE1 posted by (C)wacky

以下のように呼び出すと、直接画像ファイルにも落とせます。

$ ipy gra_glee.py -i dot1.txt -o hoge.png -t png

動かす為に必要なもの

動かすには、以下を用意します。


GLEEライブラリは、以下のURLから、Downloadん所から持ってきて下さい。


GLEEライブラリをインストールすると、"C:\Program Files\Microsoft Research\GLEE\bin"に、以下のアセンブリモジュールがあるので、gra_glee.pyを動かす場所にコピーして下さい。


次に、以下のコードを、"gra_glee.py"としてファイルに保存してください。

#!/usr/bin/env python
# coding: UTF-8
import sys
sys.path.append(r"c:\python24\lib")
from optparse import OptionParser
import shlex

import clr
clr.AddReferenceByPartialName("System.Drawing")
clr.AddReferenceByPartialName("System.Windows.Forms")
from System.Windows.Forms import *
from System.Drawing import *
from System.Drawing.Imaging import *
clr.AddReferenceByPartialName("Microsoft.GLEE")
clr.AddReferenceByPartialName("Microsoft.GLEE.Drawing")
clr.AddReferenceByPartialName("Microsoft.GLEE.GraphViewerGDI")
import System
import Microsoft
"""
名前:  GraphvizとGLEEを合わせて、gra_glee.py
機能:  要するに、Graphvizのdotユーティリティの真似をするユーティリティです。
メモ:
描画には、GLEEライブラリを使用しています。
dotファイルの構文解析(もどき)には、shlexを使っています。
本来なら、文法解析しながら行うべき(BNF例もあるしね)なんですけど、テキトーです。
なお、読み込む形式は、あくまでdot構文もどきです。

参考にしたもの:
Nauman Leghari's Blog : Fun with IronPython & GLEE
http://weblogs.asp.net/nleghari/archive/2007/04/02/fun-with-ironpython-glee.aspx
GLEE: Graph Layout Engine
http://research.microsoft.com/~levnach/GLEEWebPage.htm
dot ユーザーズガイド日本語訳
http://www.cbrc.jp/~tominaga/translations/graphviz/dotguide.pdf
The shlex module ::: www.effbot.org
http://effbot.org/librarybook/shlex.htm
"""

# color名からカラーパターンを求める連想配列
name2color = {
    'red':      Microsoft.Glee.Drawing.Color.Red,
    'green':    Microsoft.Glee.Drawing.Color.Green,
    'blue':     Microsoft.Glee.Drawing.Color.Blue,
    'black':    Microsoft.Glee.Drawing.Color.Black,
}

# shape名からボックス形状を求める連想配列
name2shape = {
    'box':          Microsoft.Glee.Drawing.Shape.Box,
    'triangle':     Microsoft.Glee.Drawing.Shape.Triangle,
    'circle':       Microsoft.Glee.Drawing.Shape.Circle,
}

# format名から保存形式を求める連想配列
name2format = {
    'gif':          System.Drawing.Imaging.ImageFormat.Gif,
    'png':          System.Drawing.Imaging.ImageFormat.Png,
    'jpg':          System.Drawing.Imaging.ImageFormat.Jpeg,
    'bmp':          System.Drawing.Imaging.ImageFormat.Bmp
}

def graph_attr(graph, name, attr_name, val):
    """アトリビュートの処理"""
    o = graph.FindNode(name)
    
    if attr_name == 'color':
        o.Attr.Fillcolor = name2color[val]
    elif attr_name == 'shape':
        o.Attr.Shape = name2shape[val]
    elif attr_name == 'weight':
        o.Attr.LineWidth = int(val)
    elif attr_name == 'label':
        o.Attr.Label = val

def graph_stmt(graph, yylex):
    """描画処理本体
    """
    name = attr_name = ""
    attr_mode = False
    while 1:
        token = yylex.get_token().strip('"')
        if token == "}":    break   # 終了チェック
        if not token:       break   # 異常終了チェック
        print repr(token)
        
        if token == ";":            # ステートメントの終わり
            name = ""
        elif token == "-":          # "->"を見つけた
            assert yylex.get_token().strip('"') == '>'
            token = yylex.get_token().strip('"')
            graph.AddEdge(name, token)
            print name, token
        elif token == '[':          # '[' アトリビュート設定の開始
            assert attr_mode == False
            attr_mode = True
        elif token == ']':          # ']' アトリビュート設定の終了
            assert attr_mode == True
            attr_mode = False
        elif token == '=':          # '=' 1つのアトリビュート設定
            assert attr_mode == True
            assert attr_name != ""
            token = yylex.get_token().strip('"')
            graph_attr(graph, name, attr_name, token)
            attr_name = ""          # ',' 1つのアトリビュートの区切り
        elif token == ',':
            assert attr_mode == True
            assert attr_name != ""
            attr_name = ""
        
        if attr_mode == False:
            name = token
        else:
            attr_name = token
    assert token == "}", "'}'で終わってないですよ?"

def make_graph(graph, yylex):
    """構文解析しつつ、グラフを作成する
        - graph     Microsoft.Glee.Drawing.Graphが実体
        - yylex     shlexが実体
    """
    print dir(graph)
    
    # キーワードは、'digraph'かな?
    assert yylex.get_token() == "digraph", "最初のワードはdigraphを期待しています"
    # 次は、id名
    graph.Label = yylex.get_token().strip('"')
    print "id名:", graph.Label
    
    # 次は、'{'
    assert yylex.get_token() == "{"
    
    # 処理本体('}'を見つけると返ってくる)
    graph_stmt(graph, yylex)

def preview(in_file):
    """プレビューウィンドウを開いて確認できる"""
    # フォームの作成
    f = Form()
    
    view = Microsoft.Glee.GraphViewerGdi.GViewer()
    graph = Microsoft.Glee.Drawing.Graph("graph")
    
    sys.setdefaultencoding("cp932")
    lex = shlex.shlex(file(in_file, "r"))
    make_graph(graph, lex)
    view.Graph = graph;
    
    f.SuspendLayout()
    view.Dock = DockStyle.Fill
    f.Controls.Add(view)
    f.ResumeLayout()
    
    f.ShowDialog()      # ダイアログを開く

def direct_save(in_file, out_file, out_file_type):
    """直接画像ファイルに保存してしまう"""
    graph = Microsoft.Glee.Drawing.Graph("graph")
    
    sys.setdefaultencoding("cp932")
    lex = shlex.shlex(file(in_file, "r"))
    make_graph(graph, lex)
    
    renderer = Microsoft.Glee.GraphViewerGdi.GraphRenderer(graph)
    renderer.CalculateLayout()
    bmp = Bitmap(graph.Width, graph.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)
    renderer.Render(bmp)
    bmp.Save(out_file, name2format[out_file_type])

def main():
    # コマンド解析
    usage = "GLEEを使ったユーティリティ(Graphvizのdotコマンド風味)\n\nusage: %prog [options]"
    parser = OptionParser(usage)
    parser.add_option("-i", "--in", dest="in_file", help="input dot style file(need sjis encode)", metavar="FILENAME", action="store", type="string")
    parser.add_option("-o", "--out", dest="out_file", help="output graphic file", metavar="FILENAME", action="store", type="string")
    parser.add_option("-t", "--type", dest="save_type", help="output file format", action="store", type="string", default="png")
    (options, args) = parser.parse_args()
    print options, args
    
    if options.out_file == None:    # チェック用のウィンドウを開く
        preview(options.in_file)
    else:                           # 直接画像ファイルに落とす
        direct_save(options.in_file, options.out_file, options.save_type)

if __name__=="__main__":
    main()

これで準備完了です。

使い方

コマンドラインヘルプは、"-h"をコマンドラインオプションに渡せばOKです。
以下のようなメッセージが出ます。

$ ipy gra_glee.py -h
usage: GLEEを使ったユーティリティ(Graphvizのdotコマンド風味)

usage: gra_glee.py [options]

options:
  -h, --help            show this help message and exit
  -i FILENAME, --in=FILENAME
                        input dot style file(need sjis encode)
  -o FILENAME, --out=FILENAME
                        output graphic file
  -t SAVE_TYPE, --type=SAVE_TYPE
                        output file format


入力ファイルに指定できるのは、あくまで GraphVizのdotファイル風味です。
コードを見れば、どこまで対応できるのか一目瞭然ですので、そこいら辺は割愛させてもらいます。


幾つかサンプルを示します。

digraph "g" {
   "Python ファミリー" -> "Python 2.4" -> "Python 2.5";
   "Python ファミリー" -> "IronPython 1.0" -> "IronPython 1.1" -> "IronPython 2.0";
   "Python 2.4" -> "IronPython 1.0";
   "Python 2.5" -> "IronPython 2.0";
   "Python ファミリー" [shape=box];
}

GLEE1
GLEE1 posted by (C)wacky

digraph "g" {
   "A" -> "B" ;
   "B" -> "C" [weight=2,label="ふが"] ;
   "C" -> "A" [color=red,shape=box] ;
   "A" -> "ほげ" -> "E";
}

GLEE2
GLEE2 posted by (C)wacky

最後に反省点

ちょっくら反省点。

  • テキトーに作ったので、あくまで構文解析もどきです。
    (まぁ、当面の自分的需要は満たせるかな…みたいな)
  • dotファイル風なので、対応しているコマンドもテキトーです
  • GLEEのサンプルを見ていると、ノード選択とか出来るらしいですが、全く対応していません


まぁ、180行程度のコードでも、このぐらい出来るんだよ。って事で、勘弁して下さい。はい。