python使用tree-sitter解析cpp代码绘制出树形图可视化

1 准备环境

首先安装好两个库

tree-sitter :解析Cpp源代码的解析器,性能好,能快速生成 AST / CST树(具体语法树)。安装步骤具体安装参考这篇博客👉python tree-sitter 使用方法

graphviz :可视化AST树的绘图库。通过 Graphviz官网,先下载Graphviz工具(根据自己的操作系统以及位数来选择,这里展示的是Windows 64位要下载的程序exe),然后完成本地安装。具体安装过程可以参考这篇博客👉 graphviz 可视化软件安装教程
Graphviz官网下载示范

然后在代码中引入使用

创建python环境,shell中使用conda install 或者 pip install 进行三方库的安装:

pip install tree_sitter
pip install graphviz

完成后,我们在项目当中导入两个库使用:

from tree_sitter import Language, Parser  # 解析器库
from graphviz import Digraph  # 绘图库
# ...

2 核心代码

随便用一段代码做例子,首先构造 tree-sitter 解析器,并获取到root_node节点。注意,root_node 是我们开始遍历的根节点

# 这是测试用的cpp代码片段, 斐波那契数列
cpp_code_snippet = '''
int fib(int n){
    long long f1 = 1, f2 = 1;
    int time = (n + 1) / 2;
    for (int i = 1; i < time; i++){
        f1 = f1 + f2;
        f2 = f2 + f1;
    }
    if (n % 2 == 0) return f2;
    else return f1;
}
'''

# tree-sitter CPP解析器
CPP_LANGUAGE = Language('build/my-languages.so', 'cpp')
cpp_parser = Parser()
cpp_parser.set_language(CPP_LANGUAGE)

tree = cpp_parser.parse(bytes(cpp_code_snippet, "utf8"))
root_node = tree.root_node  # 注意,root_node 才是可遍历的树节点

然后我们写一段遍历用的代码,考虑到 AST 树的层次性,这里用队列实现一个简单的广度优先遍历(BFS) ,方便后续知晓每个层次的结点数目和状态。
当然,你也可以替换成深度优先遍历(DFS)

def ast_bfs(root):
    """ traversal AST Tree stack By BFS
    """
    node_list = []  # node_list: [(node_id, node_str), ...]
    edge_list = []  # edge_list: [(parent_id, child_id), ...]

    # queue for BFS
    root_tuple = (root, None)  # 根结点 root
    queue = []
    queue.append(root_tuple)  # tuple (Node<> node, int parent_id)

    # BFS
    while queue:
      	# 出队
        (node, parent_id) = queue.pop(0)
		
		# 结点的类型
        node_type = str(node.type).strip() 
        
        # node_text 是代码内容,可以用于显示
        node_text = str(node.text.decode())
        
        # node_id 结点的ID,便与后续绘图 
        node_id = str(len(node_list))
		
		#  node_str,是你在图中希望显示的内容,这里默认“类型+代码”
        node_str = node_type + "\n" + node_text
        node_item = (node_id, node_str)  # tuple(node_id, node_string) 
        node_list.append(node_item)

        # 如果有父节点,添加一条从父节点到当前节点的边
        if parent_id is not None:
            edge_list.append((parent_id, node_id))

        # 遍历子节点入队
        for child in node.children:
            queue.append((child, node_id))

    return node_list, edge_list

最后,我们再写一段根据得到的 node_listedge_list 来绘制AST树的代码

def draw_tree(graph_name: str, node_list: list[tuple], edge_list: list[tuple]):
    """ draw a tree
    """
    # 创建一个Digraph对象
    dot = Digraph(comment=graph_name)
    # 绘制结点
    for node in node_list:
        dot.node(name=node[0], label=node[1], shape="box")
    # 绘制连线
    for edge in edge_list:
        dot.edge(tail_name=edge[0], head_name=edge[1], lable="")
        
    return dot

那么得到的图dot 可以进行保存,也可以直接预览

dot.view()  # 预览结果图

最终的AST结果图如下所示:

在这里插入图片描述

从下图我们可以直观看到,tree-sitter 生成的是 CST(具体语法树),也就是远大买的所有细粒度符号在树中都有它的具体结点位置,包括操作符、符号、括号、分号等。

如果要得到AST(抽象语法树),最直接的一种办法就是过滤掉这些符号即可,使用正则表达式匹配也好,或者通过规律筛去也好,例如判断node.typenode.text是否完全一致,据个人观察这是绝大多数符号的特性。

在这里插入图片描述

3 全部代码

最后放一个整体的代码供参考学习。

from tree_sitter import Language, Parser  # 解析器库
from graphviz import Digraph  # 绘图库

# tree-sitter CPP解析器
CPP_LANGUAGE = Language('build/my-languages.so', 'cpp')
cpp_parser = Parser()
cpp_parser.set_language(CPP_LANGUAGE)

# 测试用的cpp代码片段, 斐波那契数列
cpp_code_snippet = '''
int fib(int n){
    long long f1 = 1, f2 = 1;
    int time = (n + 1) / 2;
    for (int i = 1; i < time; i++){
        f1 = f1 + f2;
        f2 = f2 + f1;
    }
    if (n % 2 == 0) return f2;
    else return f1;
}
'''

def ast_bfs(root):
    """ traversal AST Tree stack By BFS"""
    node_list = []  # node_list: [(node_id, node_type), ...]
    edge_list = []  # edge_list: [(parent_id, child_id), ...]

    # queue for BFS
    root_tuple = (root, None)  # 根节点
    queue = []
    queue.append(root_tuple)  # tuple (Node<> node, int parent_id)

    # BFS
    while queue:
        # 出队
        (node, parent_id) = queue.pop(0)

        node_type = str(node.type).strip()  # 结点的类型
        node_text = str(node.text.decode())  # 代码的内容,可以用于显示(目前没有用)
        node_id = str(len(node_list))  # ID

        node_str = node_type + "\n" + node_text
        node_item = (node_id, node_str)  # tuple(node_id, node_string)  node_str是你想展示的内容
        node_list.append(node_item)

        # 如果有父节点,添加一条从父节点到当前节点的边
        if parent_id is not None:
            edge_list.append((parent_id, node_id))

        # 遍历子节点入队
        for child in node.children:
            queue.append((child, node_id))

    return node_list, edge_list


def draw_tree(graph_name: str, node_list: list[tuple], edge_list: list[tuple]):
    """ draw a tree
    """
    # 创建一个Digraph对象
    dot = Digraph(comment=graph_name)
    # 绘制结点
    for node in node_list:
        dot.node(name=node[0], label=node[1], shape="box")
    # 绘制连线
    for edge in edge_list:
        dot.edge(tail_name=edge[0], head_name=edge[1], lable="")
    return dot


print(cpp_code_snippet)
tree = cpp_parser.parse(bytes(cpp_code_snippet, "utf8"))
root_node = tree.root_node  # 注意,root_node 才是可遍历的树节点
node_list, edge_list = ast_bfs(root=root_node)  # BFS 遍历树
dot = draw_tree("test_graph", node_list, edge_list)
dot.view()

print('tree-sitter done.')

希望本篇文章对你有帮助,创作不易,感谢支持!

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值