您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   模型库  
会员   
   
DeepSeek大模型应用开发实践
6月12-13日 厦门
基于 UML 和EA进行分析设计
6月23-24日 北京+线上
人工智能、机器学习 TensorFlow+Keras
6月30日-7月1日 直播
     
   
 
 订阅
python环境解析任意编程语言 tree-sitter使用方法
 

  2866  次浏览      28 次
2023-9-28
 
编辑推荐:
本文主要介绍py-tree-sitter 功能特点、安装流程以及可能发生的安装错误,还介绍如何利用tree-sitter,编写query,定位语法树节点,希望对您的学习有所帮助 。
本文来自于CSDN-妙妙屋的mong男博客,由火龙果软件Alice编辑、推荐。

背景

我个人目前仍在研究代码有关的知识。目前基于深度学习表征代码的论文越来越卷了,用到的工具越来越高级了。目前有一个开源项目tree-sitter,专门用于解析具体语法树,声称:

足够通用,能用于任何编程语言

足够迅速,能在文本编辑器中响应每一个用户输入

足够鲁棒,即便语法错误也能解析语法树

无依赖性,能很好地嵌入于程序中

在官方提供的playground玩了玩,的确1、2、3点都很符合。

所以个人做 (水)了本篇文章。

安装

py-tree-sitter已经做了详细的描述,所以这里简短描述,顺便说个遇到的问题。

1.找个合适的python环境,install

pip3 install tree_sitter

2.对于要解析的编程语言,随便创建文件夹(比如vendor),该目录git clone指定语言的仓库,在tree-sitter官网这里找,比如我对Java、Python、C++、C#、JS感兴趣:

git clone https://github.com/tree-sitter/tree-sitter-java
git clone https://github.com/tree-sitter/tree-sitter-python
git clone https://github.com/tree-sitter/tree-sitter-cpp
git clone https://github.com/tree-sitter/tree-sitter-c-sharp
git clone https://github.com/tree-sitter/tree-sitter-javascript

需要注意的是,C++对应cpp,C#对应c-sharp,后面使用的时候需要认清楚官方定义的名称。

3.创建build文件夹,用于保存xxx.so文件,该文件相当于自定义的编译器,用于解析代码生成语法树。然后复制以下代码运行。

from tree_sitter import Language

Language.build_library(
# so文件保存位置
'build/my-languages.so',

# vendor文件下git clone的仓库
[
'vendor/tree-sitter-java',
'vendor/tree-sitter-python',
'vendor/tree-sitter-cpp',
'vendor/tree-sitter-c-sharp',
'vendor/tree-sitter-javascript',
]
)

这里有一个小插曲,个人用windows电脑,一开始运行这段代码直接报错,好像说缺少什么msvc文件,所以我还下载了visual studio才解决。现在看到tree-sitter的__init__.py文件下,有一条compiler = new_compiler()代码,发现以下代码:

if compiler is None:
# get_default_compiler 用于选择_default_compilers:
'''
_default_compilers = (
('cygwin.*', 'unix'),
('posix', 'unix'),
('nt', 'msvc'),
)
'''
compiler = get_default_compiler(plat) # windows是nt

'''
compiler_class = { 'unix': ('unixccompiler', 'UnixCCompiler',
"standard UNIX-style compiler"),
'msvc': ('_msvccompiler', 'MSVCCompiler',
"Microsoft Visual C++"),
'cygwin': ('cygwinccompiler', 'CygwinCCompiler',
"Cygwin port of GNU C Compiler for Win32"),
'mingw32': ('cygwinccompiler', 'Mingw32CCompiler',
"Mingw32 port of GNU C Compiler for Win32"),
'bcpp': ('bcppcompiler', 'BCPPCompiler',
"Borland C++ Compiler"),
}
'''
(module_name, class_name, long_description) = compiler_class[compiler]

看了代码后就清楚了,我之前电脑缺少Microsoft Visual C++,安装visual studio,配置C++后就好了。

解析

from tree_sitter import Language, Parser

# 注意C++对应cpp,C#对应c_sharp(!这里短横线变成了下划线)
# 看仓库名称
CPP_LANGUAGE = Language('build/my-languages.so', 'cpp')
CS_LANGUAGE = Language('build/my-languages.so', 'c_sharp')

# 举一个CPP例子
cpp_parser = Parser()
cpp_parser.set_language(CPP_LANGUAGE)

# 这是b站网友写的代码,解析看看
cpp_code_snippet = '''
int mian{
piantf("hell world");
remake O;
}
'''

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

最近,个人还发现了版本问题。tree-sitter 0.19.0版本运行 parser.set_language()出现:

ValueError: Incompatible Language version 14. Must be between 13 and 13

这有可能tree-sitter版本太旧所致,重装即可解决。

语法树属性(一部分)

通过debugger,可以查看语法树节点的属性(指root_node下的节点),可以发现:

# 孩子节点【节点数、节点列表】
root_node.child_count: int
root_node.children: list[Node]| None

# 该语法树节点对应代码字符串位置【左闭右开】
root_node.start_byte: int
root_node.end_byte: int

# 语法树节点对应代码 (行, 列) 位置元组
root_node.start_point: tuple[int, int]
root_node.end_point: tuple[int, int]

'''
以上的行、列以及字符串位置都是以0开始
'''

# 语法树命名节点、命名类型 以及 语法树对应的文本
# 因为具体语法树有代码所有的标记,所以一些符号可能没有类型
# 我猜测该属性可以用于区别具体语法树符号节点,构建抽象语法树
root_node.is_named: bool
root_node.type: str # 没有类型时,这里显示代码原始标记
root_node.text: bytes

# 语法树父节点
root_node.parent: Node| None

# 语法树左兄弟、左命名兄弟
root_node.prev_sibling: Node| None
root_node.prev_named_sibling: Node| None

# 语法树右兄弟、右命名兄弟
root_node.next_sibling: Node| None
root_node.next_named_sibling: Node| None

还有其他节点,不过我觉得有些trivial,这里不展开分析了。

解析小例子

我发现这个tree-sitter库是看到论文GraphCodeBert后才了解到,后来,很多研究比如UniXcoder,CodeT5,TreeBert和SynCoBert【不开源的论文】等等都用了该库。

【吐槽:深度学习表征代码越来越卷了,Money and Equipment Is All You Need 属于是了,各个下游任务刷榜。本来实验室刚从传统算法转机器学习,就一个GPU,留给我硕士菜鸡的毕业的机会都快弄没了。😅】

GraphCodeBert使用语法树分词的方法还是不错的,这里是原论文别人写的代码,GraphCodeBert的分词代码网址在这里,个人觉得很不错,供参考:

from tree_sitter import Language, Parser

def tree_to_token_index(root_node):
'''
定位代码token,返回token在代码中原始位置

从root_node开始,深度遍历其孩子节点:
1. 如果root_node没有孩子(root_node是叶节点)或者root_node是字符串或者注释,直接返回code_snippet对应的位置
个人猜想:
估计某些编程语言的string和comment类型的语法树只有单引号、双引号叶子节点,而该节点内容被忽略掉了
2. 如果有孩子节点,深度遍历,回溯时获取结果


从 tree_to_token_index 返回的token位置元组列表 以及 代码行 生成代码token
这里第二个参数,GraphCodeBert项目源代码写的是code,不是line_of_code

1. 如果token起止都在同一行
定位该代码行,定位改行的起止列,获取token
2. token跨行【比如Python三个单引号包围的注释、或者Javascript中的模板字符串等等】
1) 定位首行的token所在列
2) 循环遍历到目标行之前,所有内容
3) 定位末行的token所在列
以上内容拼接即可


定位代码token,返回token在代码中原始位置

从root_node开始,深度遍历其孩子节点:

1. 如果root_node没有孩子(root_node是叶节点)或者root_node是字符串或者注释,直接返回code_snippet对应的位置

个人猜想:

估计某些编程语言的string和comment类型的语法树只有单引号、双引号叶子节点,而该节点内容被忽略掉了

2. 如果有孩子节点,深度遍历,回溯时获取结果

使用的属性:


root_node.start_point: tuple[int, int]
root_node.end_point: tuple[int, int]

参数: root_node: Node

返回: code_tokens: list[tuple[tuple[int,int], tuple[int, int]]]
'''

从 tree_to_token_index 返回的token位置元组列表 以及 代码行 生成代码token

这里第二个参数,GraphCodeBert项目源代码写的是code,不是line_of_code

1. 如果token起止都在同一行

定位该代码行,定位改行的起止列,获取token

2. token跨行【比如Python三个单引号包围的注释、或者Javascript中的模板字符串等等】

1) 定位首行的token所在列

2) 循环遍历到目标行之前,所有内容

3) 定位末行的token所在列

以上内容拼接即可

from tree_sitter import Language, Parser

def tree_to_token_index(root_node):
'''
定位代码token,返回token在代码中原始位置

从root_node开始,深度遍历其孩子节点:
1. 如果root_node没有孩子(root_node是叶节点)或者root_node是字符串或者注释,直接返回code_snippet对应的位置
个人猜想:
估计某些编程语言的string和comment类型的语法树只有单引号、双引号叶子节点,而该节点内容被忽略掉了
2. 如果有孩子节点,深度遍历,回溯时获取结果

使用的属性:
root_node.start_point: tuple[int, int]
root_node.end_point: tuple[int, int]

参数: root_node: Node

返回: code_tokens: list[tuple[tuple[int,int], tuple[int, int]]]
'''

# 我突然发现该代码没有检测到cpp的string(也就是"hell world"),所以我改了第一行的第二个条件
# 其他编程语言可能会有改变,所以需要小心谨慎
# 原代码行:
# if (len(root_node.children) == 0 or root_node.type == 'string') and root_node.type != 'comment':

if (len(root_node.children) == 0 or root_node.type.find('string') != -1) and root_node.type != 'comment':
return [(root_node.start_point, root_node.end_point)]
else:
code_tokens = []
for child in root_node.children:
code_tokens += tree_to_token_index(child)
return code_tokens

def index_to_code_token(index, code):
'''
从 tree_to_token_index 返回的token位置元组列表 以及 代码行 生成代码token
这里第二个参数,GraphCodeBert项目源代码写的是code,不是line_of_code

1. 如果token起止都在同一行
定位该代码行,定位改行的起止列,获取token
2. token跨行【比如Python三个单引号包围的注释、或者Javascript中的模板字符串等等】
1) 定位首行的token所在列
2) 循环遍历到目标行之前,所有内容
3) 定位末行的token所在列
以上内容拼接即可

参数: index: list[tuple[tuple[int,int], tuple[int, int]]]
参数: code: list[str]

返回: s: str
'''
start_point = index[0]
end_point = index[1]
if start_point[0] == end_point[0]:
s = code[start_point[0]][start_point[1]:end_point[1]]
else:
s = ""
s += code[start_point[0]][start_point[1]:]
for i in range(start_point[0]+1, end_point[0]):
s += code[i]
s += code[end_point[0]][:end_point[1]]
return s

if __name__ == '__main__':
# 声明CPP代码解析器
CPP_LANGUAGE = Language('build/my-languages.so', 'cpp')
cpp_parser = Parser()
cpp_parser.set_language(CPP_LANGUAGE)

# 这c语言不是我写的
cpp_code_snippet = '''
int mian{
piantf("hell world");
remake O;
}
'''

# 完成解析,获取根节点
tree = cpp_parser.parse(bytes(cpp_code_snippet, "utf8"))
root_node = tree.root_node

# 获取token对应的位置
tokens_index = tree_to_token_index(root_node)
# 获取代码行
cpp_loc = cpp_code_snippet.split('\n')
# 获取对应每个位置下的token
code_tokens = [index_to_code_token(x, cpp_loc) for x in tokens_index]
# ['int', 'mian', '{', 'piantf', '(', '"hell world"', ')', ';', 'remake', 'O', ';', '}']
print(code_tokens)

提取具体节点

其实tree-sitter还可以手动配置想要的语法树节点,通过定义query,便于直接提取特定语法树节点。我看有代码为了定位语法树节点,dfs语法树,手动写判断,一大堆代码,还要回溯判断父节点,太困难了。

同时还介绍了一些使用tree-sitter科研相关的论文(GraphCodeBert、UnixCoder、CodeT5、TreeBert以及SynCoBert【没开源】),顺带着给出了一个tree-sitter用于Tokenizer分词器样例(来自GraphCodeBert)。

这一篇文章将会介绍如何利用tree-sitter,编写query,定位语法树节点。以节省书写dfs+回溯等复杂python代码的时间。

我想这有可能对以下研究提供些许帮助:

分析代码功能【深度学习分析局部节点特征】

提取代码节点类型【代码token表征】

获取特定标识符

检索数据流【可定位到代码中不同的位置】

研究控制依赖【if、while、dowhile和switch语法树节点解析】

当然,这个query还是需要自己来写的(就像写sql)一样。这里给出了一些简单的样例作为参考。

准备工作

合适python环境下安装tree-sitter

没有安装看我写的这里(推荐),或者看官网py-tree-sitter这里(也推荐)。

打开官网给出的playground

建议打开,因为官网的playground做的语法树可视化界面直观,方便代码语法树调试。

个人尽管科研一年多的时间,也用过不少语法树工具:

pycparser(c语言)

javalang(java语言)

antlr(10种语言)

python环境ast(python语言)

SPT generator(C++、Python、Java)

srcML(C++、Python、Java、JS、Go)

这些语法树解析工具都很好,但是以上大多工具无法解析语法错误代码【难以接受】。更何况这些工具基本都没有在线的可视化调试工具。

所以建议打开playground,在Code、Query以及Tree的可视化界面中调试代码!

准备写在playground中写query

官方语法介绍在这里,先搬运过来:Pattern Matching with Queries

query格式

query是一个S-表达式,该表达式由一对嵌套结构的阔号组成,阔号内包含两部分:本节点类型 和 0个或多个子节点的S-表达式。

(节点类型
(子节点类型_1)
字段: (字节点类型_2)
)

节点类型在Tree中以蓝色突出显示,后面紧跟[行, 列]-[行, 列]起止位置。

比如样例代码中:

// 来自b站
int mian{
piantf("hell world");
remake O;
}

根节点是translation_unit节点类型,可写作:

( translation_unit ) @1

样例代码行"piantf("hell world");",如果选择实参列表,在Tree中对应的是一个arguments: argument_list【前面是字段,后面网页标出蓝色的argument_list才是节点类型】,可写作:

( argument_list ) @2

其中@1 @2是节点选择表达式【@自定义符号】,写在成对的括号外面。

网页中,选择的节点在Code界面高亮显示。

后面在写代码时,会通过自定义符号获取节点。

带有字段的query

前两个例子只选择了某个节点,但是没有区分其上下文,可能选择意想不到的部分。

int mian{
long main;
remake O;
}

现在,要选择函数定义语句的int的节点,可以发现只写( primitive_type )是不行的,会选择main的long,所以,加入字段以后,可以发现选中了正确的本部分:

( function_definition
type:(primitive_type) @3
)

// 或者写仔细点
( function_definition
type: (primitive_type) @3
declarator: (identifier)
)

// 字段是有先后顺序的,不可以颠倒type和decalarator顺序。
// 官方界面可以很直观地显示出错误
// 这是错误的表达式:
( function_definition
declarator: (identifier)
type: (primitive_type) @3
)


像这样,写得越细致,越能避免选择错误的节点。同时也避免了手写dfs+回溯代码的困难场景。

注意:选择哪个节点,就在哪个节点括号对后加@自定义符号,如果:

( function_definition
type:(primitive_type) @3
) @4

就会选择代码声明以及原始类型两种节点。

匿名节点

该匿名节点指的是代码中,像+, -, *, /, [, ],=等等符号,tree-sitter没有标志出他们的具体名称。【可能有些语法树节点就蕴含了语法构成吧,所以作者认为这些符号很琐碎,没有取名字】

a++;

双引号包含他们即可:

( update_expression
"++" @6
)

通配符节点

类似正则表达式,在节点类型阔号后使用+或*:

+ 表示一个或多个

* 表示0个或多个

节点如果没有限制,可以通配符_

( _ ) @all

还有其他使用方法,官方介绍得很仔细,我估计个人还要继续做相关介绍😅。

在Python代码中获取节点

cpp代码:

#include <stdio.h>

int cmp(a, b){
return a > b? 1: 0;
}

int main(){
int arr[] = {5, 2, 1, 3, 0};
char s[] = {'h', 'e', 'l', 'l', 'o'};
int n = 5;
int i, j, tmp;

for(i = 0; i < n; i++){
for(j = n - i - 1; j > 0; i--) {
if(cmp(arr[j - 1], arr[j])){
tmp = arr[j-1];
arr[j-1] = arr[j];
arr[j] = tmp;
}
}
}
printf("%s\n", s);
return 0;
希望有人能看到这里,🤗这是代码中错误的片段。
}


希望有人能看到这里,🤗这是代码中错误的片段。

需求:

获取函数名称

获取数组初始化列表

获取函数调用语句

获取赋值语句的右侧

获取错误(error)

(function_declarator declarator: (identifier)@1 )
(initializer_list) @2
( call_expression ) @3
(assignment_expression right:(_) @4)
(ERROR) @error

需要使用的python代码语句:

# 需要使用的语句
from tree_sitter import Language, Parser

# 实例化
CPP_LANGUAGE = Language('build/my-languages.so', 'cpp')
# 书写query
query_text = '( translation_unit ) @1'
# 构建query
query = CPP_LANGUAGE.query(cpp_query_text)

# 获取节点【root_node来源与Parser解析的代码文本】
root_node = xxx_parser.parse(bytes(code_snippet, 'utf8)).root_node
# capture: list[Node, str]
capture = query.captures(root_node)
for node, alias in capture: # node为代码节点,alias为自定义符号
print(node.type, alias)

所以可以这么写:

from tree_sitter import Language, Parser

# 声明CPP代码解析器
CPP_LANGUAGE = Language('build/my-languages.so', 'cpp')
cpp_parser = Parser()
cpp_parser.set_language(CPP_LANGUAGE)

cpp_code_snippet = '''
// 把上面cpp代码赋值粘贴到这里
'''

# 定义query
cpp_query_text = '''
(function_declarator declarator: (identifier)@1 )
(initializer_list) @2
( call_expression ) @3
(assignment_expression right:(_) @4)
(ERROR) @error
'''
query = CPP_LANGUAGE.query(cpp_query_text)

# 获取具体语法树
tree = cpp_parser.parse(bytes(cpp_code_snippet, "utf8"))
root_node = tree.root_node

# 获取节点
# capture: list[Node, str]
capture = query.captures(root_node)
for node, alias in capture:
print(node.type, alias

后序

希望对大家有帮助。

 

   
2866 次浏览       28
相关文章

编译原理--C语言C文件和头文件的关系
用 C 语言开发一门编程语言 — 抽象语法树
C语言 | 嵌入式C语言编程规范
详解C语言数组越界及其避免方法
 
相关文档

C语言-指针讲解
详解C语言中的回调函数
ARM下C语言编程解析
设计模式的C语言实现
相关课程

C++高级编程
C++并行编程与操作
C++ 11,14,17,20新特性
C/C++开发基础

最新活动计划
DeepSeek大模型应用开发 6-12[厦门]
人工智能.机器学习TensorFlow 6-30[直播]
基于 UML 和EA进行分析设计 6-23[北京]
嵌入式软件架构-高级实践 7-9[北京]
用户体验、易用性测试与评估 7-25[西安]
图数据库与知识图谱 8-23[北京]
 
 
最新文章
编译原理--C语言C文件和头文件的关系
用 C 语言开发一门编程语言 — 抽象语法树
C语言 | 嵌入式C语言编程规范
详解C语言数组越界及其避免方法
最新课程
C++高级编程
C++并行编程与操作
C++ 11,14,17,20新特性
C/C++开发基础
成功案例
某航天科工单位 C++新特性与开发进阶
北京 C#高级开发技术
四方电气集团 嵌入式高级C语言编程
北大方正 C语言单元测试实践