Graphviz入门

Graphviz入门

安装Graphviz

在官网上面下载相关文件,地址:http://www.graphviz.org/download/

graphviz简介

graphviz是贝尔实验室设计的一个开源的画图工具,它的强大主要体现在“所思即所得”(WYTIWYG,what you think is what you get),这是和office的“所见即所得“(WYSIWYG,what you see is what you get)完全不同的一种方式。它使用一个特定的DSL(领域特定语言): dot作为脚本语言,然后使用布局引擎来解析此脚本,并完成自动布局。它的输入是一个用dot语言 编写的绘图脚本,通过对输入脚本的解析,分析出其中的点,边以及子图,然后根据属性进行绘制。graphviz提供丰富的导出格式,如常用的图片格式,SVG,PDF格式等。用graphviz来绘图的时候,你的主要工作就是编写dot脚本,你只要关注图中各个点之间的关系就好了,你不需要考虑如何安排各个节点的位置,怎样布局能够使你所绘制的图看起来更美观一些。

graphviz中包含了众多的布局器:

  • dot 默认布局方式,渲染的图具有明确方向性,主要用于有向图
  • neato 渲染的图缺乏方向性,基于spring-model(又称force-based)算法
  • twopi 渲染的图用放射性布局,径向布局
  • circo 渲染的图用环型布局,圆环布局
  • fdp 渲染的图缺乏方向性,用于无向图
  • sfdp 渲染大型的图,图片缺乏方向性

graphviz的设计初衷是对有向图/无向图等进行自动布局,开发人员使用dot脚本定义图形元素,然后选择算法进行布局,最终导出结果。

首先,在dot脚本中定义图的顶点和边,顶点和边都具有各自的属性,比如形状,颜色,填充模式,字体,样式等。然后使用合适的布局算法进行布局。布局算法除了绘制各个顶点和边之外,需要尽可能的将顶点均匀的分布在画布上,并且尽可能的减少边的交叉(如果交叉过多,就很难看清楚顶点之间的关系了)。所以使用graphviz的一般流程为:

  • 定义一个图,并向图中添加需要的顶点和边
  • 为顶点和边添加样式
  • 使用布局引擎进行绘制

一旦熟悉这种开发模式,就可以快速的将你的想法绘制出来。
graph1

第一个graphviz图

比如,要绘制一个有向图,包含5个节点a,b,c,d,e。其中a和b指向c,c和d指向e。可以定义下列脚本:

digraph test{
    a;
    b;
    c;
    d;
    e;

    a->c;
    b->c;
    c->e;
    d->e;

}

这里写图片描述
使用dot布局方式,绘制出来的效果如下:
默认的顶点中的文字为定义顶点变量的名称,形状为椭圆。边的默认样式为黑色实线箭头,我们可以在脚本中做一下修改,将顶点改为方形,边改为虚线。

设置点和线的形状和颜色

在digraph的花括号内,添加顶点和边的新定义:

node [shape="record"];
edge [style="dashed"];

则绘制的效果如下:
这里写图片描述
+ 进一步修改顶点和边样式

进一步,我们将顶点a的颜色改为淡绿色,并将c到d的边改为红色,脚本如下:

digraph test{
    node [shape="record"];
    edge [style="dashed"];
    a [style="filled", color="black", fillcolor="skyblue"];
    b;
    c;
    d;
    e;

    a->c;
    b->c;
    c->e;
    d->e [color="red"];

}

绘制的结果如下:
graph1-2
应当注意到,顶点和边都接受属性的定义,形式为在顶点和边的定义之后加上一个由方括号括起来的key-value列表,每个key-value对由逗号隔开。如果图中顶点和边采用统一的风格,则可以在图定义的首部定义node, edge的属性。比如上图中,定义所有的顶点为方框,所有的边为虚线,在具体的顶点和边之后定义的属性将覆盖此全局属性。如特定与a的绿色,c到d的边的红色。

  • 以图片为节点

除了颜色,节点还可以使用图片。不过需要注意的是,在使用图片作为节点的时候,需要将本来的形状设置为none,并且将label置为空字符串,避免出现文字对图片的干扰。

digraph test{
    node [shape="record"];
    edge [style="dashed"];
    a [style="filled", color="black", fillcolor="skyblue"];
    b;
    c;
    d [shape="none", image="C:UsersMarvinDesktop	img.jpg", label=""];
    e;

    a->c;
    b->c;
    c->e;
    d->e [color="red"];

}

graph1-3
digraph是有向图,graph是无向图,要注意,->用在有向图中,–用在无向图中表示一条边,不能混用。

//digraph是有向图,graph是无向图,要注意,->用在有向图中,--用在无向图中表示一条边,不能混用。
digraph G { //第一行给出了图的类型和名字
    main -> parse -> execute; //当一个点第一次出现,它就被创建了
    main -> init; //用->标示符创建一条边
    main -> cleanup;
    execute -> make_string;
    execute -> printf
    init -> make_string;
    main -> printf;
    execute -> compare;
}
//然后在cmd下用这个文件运行dot
//dot -Tps graph1.dot -o graph1.ps
//这是ps格式,你也可以改成jpg等格式。
//-Tps选择了postscript output,
//就画出了这个图。

graph1

来看下一个稍微复杂点的例子,我们开始手动的设置一下图的属性。可以给点设置属性,也可以给边设置属性。先来讲讲怎么设置边的属性,在每条边后面的双括号里设置边的属性。也可以在用edge设置边的默认值。而给点设置属性就必须给每个点单独的设置一个属性,node表示点的默认值。

//点的默认参数是shape=ellipse, width=.75, height=.5 and labeled by the node name.
//一些点的形状在appendix.h 中,一些常用的形状有bos,circle,record,plaintext。
digraph G {
    size ="4,4";// 把图的尺寸设为4 inch,4 inch
    main [shape=box];//把main点的形状设为方形
    main -> parse [weight=8]; //weight是设置了这条边的重要程度,默认是1。
    parse -> execute;
    main -> init [style=dotted]; //让这条线是点状的
    main -> cleanup;
    execute -> { make_string; printf} //这条语句一次连了两条线
    init -> make_string;
    edge [color=red]; // so is this 把边的默认颜色设为了red
    main -> printf [style=bold,label="100 times"]; //label就是在边上写了一行字
    make_string [label="make a
string"];// 让make_string变成了一个两行的字符串(注意那个
)。
    node [shape=box,style=filled,color=".7 .3 1.0"];// 设置了一下点的默认参数,蓝色,这个被用在了compare中。
    execute -> compare;
}

画出以下图形:

graph2

//可以设置每条边箭头的方向,用dir,有forward(default),back,both,none 四种。
digraph html {
    A -> B[dir = both];
    B -> C[dir = none];
    C -> D[dir = back];
    D -> A[dir = forward];
}

graph3

//点的shape 除了record 和Mrecord 这两种之外,其他的形状都是多边形,而我们可以对多边形进行一下属性上的设置,
//shape = polygon。Sides 用于设置它的边数,peripheries 用于设置多边形的外框的层数,
//regular = true 可以让你的多边形是一个规则的多边形,orientation =*,可以让你的多边形旋转一个角度,
//如orientation = 15 就是转了15 度。Skew 后面跟一个(-1.0~1.0)的小数,能让你的图形斜切一个角度,distortion 是让你的图形产生透视效果。
digraph G {
    a -> b -> c;
    b -> d;
    a [shape=polygon,sides=5,peripheries=3,color=lightblue,style=filled];
    c [shape=polygon,sides=4,skew=.4,label="hello world"]
    d [shape=invtriangle];
    e [shape=polygon,sides=4,distortion=.7];
}

graph4

digraph A{
    A -> B;
    A[orientation = 15, regular = true, shape = polygon, sides = 8, peripheries = 4, color= red style = filled];
    B[shape = polygon, sides = 4, skew = 0.5, color = blue];
}

graph5

//record 和Mrecord 的区别就是Mrecord 的角是圆的。Record 就是由衡的和竖的矩形组成的图形。
digraph structs {
    node [shape=record];
    struct1 [shape=record,label="<f0> left|<f1> mid dle|<f2> right"];
    struct2 [shape=record,label="<f0> one|<f1> two"];
    struct3 [shape=record,label="hello
world |{ b |{c|<here> d|e}| f}| g | h"];
    struct1 -> struct2;
    struct1 -> struct3;
}

graph6

当你的线和线label 比较多时,可以给线的属性decorate = true,使得每条线的label 与所属线之间连线。还可以给每条线加上headlabel 和taillabel,给每条线的起始点和终点加上label,他们的颜色由labelfontcolor 来决定,而label 的颜色由fontcolor 来决定。

//
graph A{
    label = "I love you"; //给这幅图设置,名字
    labelloc = b; //图名字的位置在bottom,也可以是t
    labeljust = l; //图名字的位置在left,也可以是r
    edge[decorate = true];
    C -- D[label = "s1"];
    C -- E[label = "s2"];
    C -- F[label = "s3"];
    D -- E[label = "s4"];
    D -- F[label = "s5"];
    edge[decorate = false, labelfontcolor = blue, fontcolor = red];
    C1 -- D1[headlabel = "c1", taillabel = "d1", label = "c1 - d1"];
}

graph7

在dot 中我们可以用html 语言写一个table。在label 后用< >而不是”“就能引入html 语言。

//在dot 中我们可以用html 语言写一个table。在label 后用< >而不是""就能引入html 语言。
digraph html {
    abc [shape=none, margin=0, label=<
    <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">
    <TR><TD ROWSPAN="3"><FONT COLOR="red">hello</FONT><BR/>world</TD>
    <TD COLSPAN="3">b</TD>
    <TD ROWSPAN="3" BGCOLOR="lightgrey">g</TD>
    <TD ROWSPAN="3">h</TD>
    </TR>
    <TR><TD>c</TD>
    <TD PORT="here">d</TD>
    <TD>e</TD>
    </TR>
    <TR><TD COLSPAN="3">f</TD>
    </TR>
    </TABLE>>];
}

graph8

//这样创造了一个5 行5 列的表格,我们可以在表格中打字。
digraph html {
    abc [shape=none, margin=0, label=<
    <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">
    <TR><TD>0</TD><TD>1</TD><TD>2</TD><TD>3</TD><TD>4</TD>
    </TR>
    <TR><TD>1</TD><TD></TD><TD></TD><TD></TD><TD></TD>
    </TR>
    <TR><TD>2</TD><TD></TD><TD></TD><TD></TD><TD></TD>
    </TR>
    <TR><TD>3</TD><TD></TD><TD></TD><TD></TD><TD></TD>
    </TR>
    <TR><TD>4</TD><TD></TD><TD></TD><TD></TD><TD></TD>
    </TR>
    </TABLE>>];
}

graph9

设置点和线的位置,子图的概念

默认时图中的线都是从上到下的,我们可以将其改为从左到右,在文件的最上层打入rankdir=LR 就是从左到右,默认是TB(top -> bottom),也可以是RL,BT。当图中时间表之类的东西时,我们会需要点能排在一行(列),这时要用到rank,用花括号把rank=same,然后把需要并排的点一次输入。

//
digraph html {
    rankdir = LR;
    {
        node[shape = plaintext];
        1995 -> 1996 -> 1997 -> 1998 -> 1999 -> 2000 -> 2001;
    }
    {
        node[shape = box, style = filled];
        WAR3 -> Xhero -> Footman -> DOTA;
        WAR3 -> Battleship;
    }
    {rank = same; 1996; WAR3;}
    {rank = same; 1998; Xhero; Battleship;}
    {rank = same; 1999; Footman;}
    {rank = same; 2001; DOTA;}
}

graph10

设立一条边时,我们可以制定这条边从起点的那个位置射出和从哪个位置结束。控制符有”n”,”ne”,”e”, “se”, “s”, “sw”, “w” 和 “nw”,具体效果见下:

digraph html {
    node[shape = box];
    c:n -> d[label = n];
    c1:ne -> d1[label = ne];
    c2:e -> d2[label = e];
    b:se -> a[label = se];
    c3:s -> d3[label = s];
    c4:sw -> d4[label = sw];
    c5:w -> d5[label = w];
    c6:nw -> d6[label = nw];
}

graph11

我们也可以在record 中给点定义一些port,因为record 类型中都是一个个格子。

digraph html {
    label = "Binary search tree";
    node[shape = record];
    A[label = "<f0> | <f1> A |<f2> "];
    B[label = "<f0> | <f1> B |<f2> "];
    C[label = "<f0> | <f1> C |<f2> "];
    D[label = "<f0> | <f1> D |<f2> "];
    E[label = "<f0> | <f1> E |<f2> "];
    A:f0:sw -> B:f1;
    A:f2:se -> C:f1;
    B:f0:sw -> D:f1;
    B:f2:se -> E:f1;
}

graph12

//构造一个HASH 表
digraph G {
    nodesep=.05;
    rankdir=LR;
    node [shape=record,width=.1,height=.1];

    node0 [label = "<f0> |<f1> |<f2> |<f3> |<f4> |<f5> |<f6> | ",height=2.5];
    node [width = 1.5];
    node1 [label = "{<n> n14 | 719 |<p> }"];
    node2 [label = "{<n> a1 | 805 |<p> }"];
    node3 [label = "{<n> i9 | 718 |<p> }"];
    node4 [label = "{<n> e5 | 989 |<p> }"];
    node5 [label = "{<n> t20 | 959 |<p> }"] ;
    node6 [label = "{<n> o15 | 794 |<p> }"] ;
    node7 [label = "{<n> s19 | 659 |<p> }"] ;

    node0:f0 -> node1:n;
    node0:f1 -> node2:n;
    node0:f2 -> node3:n;
    node0:f5 -> node4:n;
    node0:f6 -> node5:n;
    node2:p -> node6:n;
    node4:p -> node7:n;
}

graph13

子图的绘制

graphviz支持子图,即图中的部分节点和边相对对立(软件的模块划分经常如此)。比如,我们可以将顶点c和d归为一个子图:

digraph test{
    node [shape="record"];
    edge [style="dashed"];
    a [style="filled", color="black", fillcolor="skyblue"];
    b;
    c;

    subgraph cluster_de{
        label="d and e";
        bgcolor="mintcream";
        d [shape="none", image="C:UsersMarvinDesktop	img.jpg", label=""];
        e;
        }
    a->c;
    b->c;
    c->e;
    d->e [color="red"];

}

将d和e划分到cluster_de这个子图中,标签为d and e,并添加背景色,以方便与主图区分开,绘制结果如下:

graph1-4

画一个子图就是subgraph cluster#,必须有cluster 前缀。

digraph g {
    subgraph cluster0 {
        //我是一个子图,subgraph定义了我,
        node[style = filled, color = white];
        //我之内的节点都是这种样式
        style = filled;
        //我的样式是填充
        color = lightgrey;
        //我的颜色
        a0->a1->a2->a3;
        label = "prcess #1"
        //我的标题
    }

    subgraph cluster1 {
        //我也是一个子图
        node[style = filled];
        b0->b1->b2->b3;
        label = "process #2";
        color = blue;
    }

    //定义完毕之后,下面还是连接了
    start->a0;
    start->b0;
    a1->b3;
    b2->a3;
    a3->end;
    b3->end;

    start[shape=Mdiamond];
    end[shape=Msquare];
}

graph14
当你想把一条边连到一个子图的边界上,先输入compound = true,然后就能用lhead 和ltail来设置连接的子图了。

digraph G{
    compound=true;
    subgraph cluster0{
        a->b;
        a->c;
        b->d;
        c->d;
    }
    subgraph cluster1{
        e->g;
        e->f;
    }
    b->f[lhead=cluster1];
    d->e;
    c->g[ltail=cluster0,lhead=cluster1];
    c->e[ltail=cluster0];
    d->h;
}

graph15
多边形结点(http://www.graphviz.org/doc/info/shapes.html)

下面显示了可能的多边形形状。

img img img img
box polygon ellipse oval
img img img img
circle point egg triangle
img img img img
plaintext plain diamond trapezium
img img img img
parallelogram house pentagon hexagon
img img img img
septagon octagon doublecircle doubleoctagon
img img img img
tripleoctagon invtriangle invtrapezium invhouse
img img img img
Mdiamond Msquare Mcircle rect
img img img img
rectangle square star none
img img img img
underline cylinder note tab
img img img img
folder box3d component promoter
img img img img
cds terminator utr primersite
img img img img
restrictionsite fivepoverhang threepoverhang noverhang
img img img img
assembly signature insulator ribosite
img img img img
rnastab proteasesite proteinstab rpromoter
img img img
rarrow larrow lpromoter

数据结构的可视化

实际开发中,经常要用到的是对复杂数据结构的描述,graphviz提供完善的机制来绘制此类图形。

一个hash表的数据结构

比如一个hash表的内容,可能具有下列结构:

struct st_hash_type {
    int (*compare) ();
    int (*hash) ();
};

struct st_table_entry {
    unsigned int hash;
    char *key;
    char *record;
    st_table_entry *next;
};

struct st_table {
    struct st_hash_type *type;
    int num_bins; /* slot count */
    int num_entries; /* total number of entries */
    struct st_table_entry **bins; /* slot */
};

绘制hash表的数据结构

从代码上看,由于结构体存在引用关系,不够清晰,如果层次较多,则很难以记住各个结构之间的关系,我们可以通过下图来更清楚的展示:
graph1-5

脚本如下:

digraph st2{
  fontname = "Verdana";
  fontsize = 10;
  rankdir=TB;

  node [fontname = "Verdana", fontsize = 10, color="skyblue", shape="record"];

  edge [fontname = "Verdana", fontsize = 10, color="crimson", style="solid"];

  st_hash_type [label="{<head>st_hash_type|(*compare)|(*hash)}"];
  st_table_entry [label="{<head>st_table_entry|hash|key|record|<next>next}"];
  st_table [label="{st_table|<type>type|num_bins|num_entries|<bins>bins}"];

  st_table:bins -> st_table_entry:head;
  st_table:type -> st_hash_type:head;
  st_table_entry:next -> st_table_entry:head [style="dashed", color="forestgreen"];
}

状态图

有限自动机示意图
graph1-6

上图是一个简易有限自动机,接受a及a结尾的任意长度的串。其脚本定义如下:

digraph automata_0 {
  size = "8.5, 11";
  fontname = "Microsoft YaHei";
  fontsize = 10;

  node [shape = circle, fontname = "Microsoft YaHei", fontsize = 10];
  edge [fontname = "Microsoft YaHei", fontsize = 10];

  0 [ style = filled, color=lightgrey ];
  2 [ shape = doublecircle ];

  0 -> 2 [ label = "a " ];
  0 -> 1 [ label = "other " ];
  1 -> 2 [ label = "a " ];
  1 -> 1 [ label = "other " ];
  2 -> 2 [ label = "a " ];
  2 -> 1 [ label = "other " ];

  "Machine: a" [ shape = plaintext ];
}

形状值为plaintext的表示不用绘制边框,仅展示纯文本内容,这个在绘图中,绘制指示性的文本时很有用,如上图中的Machine: a。

其他实例

一棵简单的抽象语法树(AST)

表达式 (3+4)*5 在编译时期,会形成一棵语法树,一边在计算时,先计算3+4的值,最后与5相乘。
graph1-7

对应的脚本如下:

digraph ast{
  fontname = "Microsoft YaHei";
  fontsize = 10;

  node [shape = circle, fontname = "Microsoft YaHei", fontsize = 10];
  edge [fontname = "Microsoft YaHei", fontsize = 10];
  node [shape="plaintext"];

  mul [label="mul(*)"];
  add [label="add(+)"];

  add -> 3
  add -> 4;
  mul -> add;
  mul -> 5;
}

简单的UML类图

下面是一简单的UML类图,Dog和Cat都是Animal的子类,Dog和Cat同属一个包,且有可能有联系(0..n)。
graph1-8
脚本如下:

digraph G{

  fontname = "Courier New"
  fontsize = 10

  node [ fontname = "Courier New", fontsize = 10, shape = "record" ];
  edge [ fontname = "Courier New", fontsize = 10 ];

  Animal [ label = "{Animal |+ name : Stringl+ age : intl|+ die() : voidl}" ];

      subgraph clusterAnimalImpl{
          bgcolor="yellow"
          Dog [ label = "{Dog||+ bark() : voidl}" ];
          Cat [ label = "{Cat||+ meow() : voidl}" ];
      };

  edge [ arrowhead = "empty" ];

  Dog->Animal;
  Cat->Animal;
  Dog->Cat [arrowhead="none", label="0..*"];
}

状态图

graph1-9

脚本:

digraph finite_state_machine {
  rankdir = LR;
  size = "8,5"

  node [shape = doublecircle];

  LR_0 LR_3 LR_4 LR_8;

  node [shape = circle];

  LR_0 -> LR_2 [ label = "SS(B)" ];
  LR_0 -> LR_1 [ label = "SS(S)" ];
  LR_1 -> LR_3 [ label = "S($end)" ];
  LR_2 -> LR_6 [ label = "SS(b)" ];
  LR_2 -> LR_5 [ label = "SS(a)" ];
  LR_2 -> LR_4 [ label = "S(A)" ];
  LR_5 -> LR_7 [ label = "S(b)" ];
  LR_5 -> LR_5 [ label = "S(a)" ];
  LR_6 -> LR_6 [ label = "S(b)" ];
  LR_6 -> LR_5 [ label = "S(a)" ];
  LR_7 -> LR_8 [ label = "S(b)" ];
  LR_7 -> LR_5 [ label = "S(a)" ];
  LR_8 -> LR_6 [ label = "S(b)" ];
  LR_8 -> LR_5 [ label = "S(a)" ];
}

时序图

““
digraph G {
rankdir=”LR”;
node[shape=”point”, width=0, height=0];
edge[arrowhead=”none”, style=”dashed”]

{
    rank="same";
    edge[style="solided"];
    LC[shape="plaintext"];
    LC -> step00 -> step01 -> step02 -> step03 -> step04 -> step05;
}

{
    rank="same";
    edge[style="solided"];
    Agency[shape="plaintext"];
    Agency -> step10 -> step11 -> step12 -> step13 -> step14 -> step15;
}

{
    rank="same";
    edge[style="solided"];
    Agent[shape="plaintext"];
    Agent -> step20 -> step21 -> step22 -> step23 -> step24 -> step25;
}

step00 -> step10 [label="sends email new custumer", arrowhead="normal"];
step11 -> step01 [label="declines", arrowhead="normal"];
step12 -> step02 [label="accepts", arrowhead="normal"];
step13 -> step23 [label="forward to", arrowhead="normal"];
step24 -> step14;
step14 -> step04 [arrowhead="normal"];

}
““
graph1-10

复杂实例

graph1-11
脚本如下

digraph G {
     rankdir=LR
     node [shape=plaintext]
05      a [
         label=<
         <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
         <TR>
             <TD ROWSPAN="3" BGCOLOR="yellow">class</TD>
         </TR>
         <TR>
             <TD PORT="here" BGCOLOR="lightblue">qualifier</TD>
         </TR>
         </TABLE>>
     ]
     b [shape=ellipse style=filled
    label=<
    <TABLE BGCOLOR="bisque">
    <TR>
        <TD COLSPAN="3">elephant</TD> 
        <TD ROWSPAN="2" BGCOLOR="chartreuse" 
            VALIGN="bottom" ALIGN="right">two</TD> 
    </TR>
    <TR>
        <TD COLSPAN="2" ROWSPAN="2">
            <TABLE BGCOLOR="grey">
            <TR> 
                <TD>corn</TD> 
            </TR> 
            <TR> 
                <TD BGCOLOR="yellow">c</TD> 
            </TR> 
            <TR> 
                <TD>f</TD> 
            </TR> 
            </TABLE> 
        </TD>
        <TD BGCOLOR="white">penguin</TD> 
     </TR> 
     <TR> 
         <TD COLSPAN="2" BORDER="4" ALIGN="right" PORT="there">4</TD> 
     </TR>
     </TABLE>>
     ]
     c [ 
         label=<
             long line 1<BR/>line 2<BR ALIGN="LEFT"/>line 3<BR ALIGN="RIGHT"/>>
     ]

     subgraph { rank=same b c }
     a:here -> b:there [dir=both arrowtail = diamond]
     c -> b
     d [shape=triangle]
     d -> c [label=<
     <TABLE>
     <TR>
         <TD BGCOLOR="red" WIDTH="10"> </TD>
         <TD>Edge labels<BR/>also</TD>
         <TD BGCOLOR="blue" WIDTH="10"> </TD>
     </TR>
     </TABLE>>
     ]
 }

Reference

Graphviz从入门到不精通

Graphviz - Graph Visualization Software

Graphviz-Documentation

使用 Graphviz 画拓扑图

Graphviz

原文地址:https://www.cnblogs.com/born2run/p/9581386.html