我的目的是辅助代码阅读(也方便写文档),因此不需要太详细的信息,只需要看用户定义的函数的函数调用关系。
文章目录
1.开源项目staticfg的安装和直接使用
一开始我试图寻找现成的轮子,在github上得偿所愿,https://github.com/coetaur0/staticfg,需要详细函数调用图的朋友可以一试,我总结了staticfg安装使用中的问题:
我的电脑上装了anaconda和pycharm,pycharm的解释器可以设置成anaconda中的python,这样就可以在pycharm中使用anaconda装的包。
在anaconda navigator中打开cmd.exe,可以直接使用pip命令来安装staticfg。pip会先安装astor库,如果超时的话就等它耗尽超时次数,然后重试。
pip成功安装之后,尝试readme中的demo,会报错,大意是说要将graphviz的可执行文件添加到环境变量。需要到http://www.graphviz.org/download/去下载,安装时可以选择自动添加到环境变量。然后readme中的斐波那契demo就可以正常分析了。
如果分析的py文件中有中文注释,会报unicode decode error,说gbk无法解码某一个byte。据说是因为python默认使用gbk。其中一个解决方法是在代码开头增加如下代码:
import _locale
_locale._getdefaultlocale = (lambda *args: ['zh_CN', 'utf8'])
不用修改任何代码的另一个临时解决方法是,python -X utf8 build_cfg.py [input] [output]。其中,build_cfg.py在clone下来的example目录下。
之后还可能会有另一个错误,在builder.py里,大意是+=的运算数不能是str和nonetype。这个问题貌似是输入文件中有strip函数引起的。如果可以顺利执行的话,生成的结果是一字排开的,如果函数很多,观感很差。函数内的很多语句都会被放在图里,真要读图感觉还没直接看代码快。
2.绘制python的简单调用关系图(支持多文件)
首先,我总结一下staticfg中不尽人意的地方:
- 代码中有中文注释时,可能会报错。(上面提到了解决方法,我在自己的实现中已经内置了,不会有问题。)
- 可能会出现上文提到的nonetype错误。(我改为了输出warnings,避免报错中止。)
- 生成的结果中,有一些一字排开的孤立结点(staticfg中对graphviz的使用比较粗糙,解决见下文)
- 结果中有大量函数内语句(比如return语句)和系统内置函数(我不需要这些信息)
为了解决这些问题,我借鉴了staticfg,做了自己的实现。
2.1 python ast前置知识(可酌情跳过)
了解前置知识之后,大致可以读懂staticfg的源码,并进行自己的改写。
python官方文档推荐的 https://greentreesnakes.readthedocs.io/
但这个我上不了,所以只好找了一些国内博客,也能解决问题:https://blog.csdn.net/ma89481508/article/details/56017697
print type(node).__name__
注意上面这篇博客中的这句话,可以在没有文档的情况下获取你想知道的node类型(比如你想知道a=10属于哪种类型,就输入一个只有a=10的py文件),从而知道想要override的官方函数的名字。(更笨的办法是慢慢翻ide的代码提示)
此外,这篇博客也很有帮助 https://www.cnblogs.com/yssjun/p/10069199.html
2.2 代码实现
import os
import ast
import sys
from graphviz import Digraph
import _locale
_locale._getdefaultlocale = (lambda *args: ['zh_CN', 'utf8'])
def simplecfg(*args):
visitor = CodeVisitor()
for infile in args[0]:
f = open(infile, "r")
r_node = ast.parse(f.read())
f.close()
visitor.filename = os.path.basename(infile).split('.')[0]
visitor.visit(r_node)
fpos = {}#fpos存放函数basename所在的py文件
for func in visitor.userfunc:
fr = func.split('.')[0]
bk = func.split('.')[-1]
fpos[bk] = fr
dest = {}#dest存放每个userfunc下调用了哪些userfunc
for line in visitor.info:
if line.startswith('User Function Name'):
defnow = line.split(':')[1]
dest[defnow] = []
continue
for func in visitor.userfunc:
basename = func.split('.')[-1]
line_tail = line.split(':')[-1]
line_tail = line_tail.split('.')[-1]
if basename == line_tail:
dest[defnow].append(basename)
break
dot = Digraph(comment='The Round Table')
ctr = 0
alias = {}
#在dot语法中,结点有自己的名字,这个名字跟结点在图片上显示的函数名字不同。alias存储两者的映射。
for func in visitor.userfunc:
ctr += 1
alias[func] = 'A'+str(ctr)#跟dot语法的命名规则有关,也可以用其它命名,不必纠结
dot.node(alias[func], func)
for key in dest.keys():
for dst in dest[key]:
fullname = fpos[dst] + '.' + dst
dot.edge(alias[key], alias[fullname])
dot.render('test-output/round-table.gv')
# print(ast.dump(r_node))
class CodeVisitor(ast.NodeVisitor):
userfunc = []
info = []
filename = ''
def generic_visit(self, node):
# print(type(node).__name__)
ast.NodeVisitor.generic_visit(self, node)
def visit_FunctionDef(self, node):
# print('User Function Name:%s' % node.name)
self.info.append('User Function Name:'+self.filename+'.'+node.name)
self.userfunc.append(self.filename+'.'+node.name)
ast.NodeVisitor.generic_visit(self, node)
def visit_Call(self, node):
# print(node._fields)
def recur_visit(node):
if type(node) == ast.Name:
return node.id
elif type(node) == ast.Attribute:
# Recursion on series of calls to attributes.
# print(node.attr)
func_name = recur_visit(node.value)
if type(node.attr) == str and type(func_name) == str:
func_name += '.' + node.attr
# else:
# print('attention!!!', type(node.attr), type(func_name))
return func_name
elif type(node) == ast.Str:
return node.s
elif type(node) == ast.Subscript:
return node.value.id
func = node.func
# print(type(func), func._fields)
func_name = recur_visit(func)
if(type(func_name)==str):
self.info.append('\tUser function Call:'+self.filename+'.'+func_name)
ast.NodeVisitor.generic_visit(self, node)
simplecfg(sys.argv[1:])
2.3 代码的使用
在使用上面的代码之前,需要正确安装graphviz,可参考第一部分。
如果用pycharm的话,可以在下方的terminal中用命令行运行上面的py文件,命令如下:
python 上面的py文件.py 输入文件1.py 输入文件2.py 输入文件n.py
执行过后,在上面的py文件的存储位置找test-output目录,可以看见生成了gv文件和pdf文件。
可以先直接打开pdf文件看一下,但我估计你不会满意,因为作的图太宽,根本看不清。
在stackoverflow上获得了如下解决方法:
unflatten -c 5 round-table.gv | dot -Gratio="fill" -Gsize="20,5" -Tpdf -o round-table.gv.pdf
在cmd或者powershell中先进入gv文件的目录,然后执行这条命令(前提是graphviz已经正确安装,参考第一部分),生成的pdf即为最终结果。
unflatten是graphviz/bin下的工具,作用就是让图更加紧凑。命令的解释:
-c会把孤立结点摆成一列,5为最大列长;
-Gratio设为fill后,对-Gsize的修改才有意义,图像会填充你指定大小的画布;
-T指定输出格式,-o指定输出文件。
结果示意:
虽然内容还是很多、图片还是很宽,但这是因为分析了5个文件,代码量将近3000行。
这个代码我只用过一次,对graphviz的使用也完全是照搬,欢迎评论指正。
3.附录:dot语言中结点的命名规则
graphviz的结点可以超过26个,只要能正确命名就可以,不一定要用单个的大写字母。