如何提取 Python 源代码中使用的所有函数和 API 调用?
How to extract all functions and API calls used in a Python source code?
让我们考虑以下Python源代码;
def package_data(pkg, roots):
data = []
for root in roots:
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
return {pkg: data}
我想从此源代码中提取所有函数和 API 调用。我找到了a similar question and solution。我 运行 这里给出的解决方案,它生成输出 [os.walk, data.append]
。但我正在寻找以下输出 [os.walk, os.path.join, data.append, os.path.relpath, os.path.join]
。
我分析了下面的解决方案代码后了解到,这可以访问第一个括号之前的每个节点并删除其余的东西。
import ast
class CallCollector(ast.NodeVisitor):
def __init__(self):
self.calls = []
self.current = None
def visit_Call(self, node):
# new call, trace the function expression
self.current = ''
self.visit(node.func)
self.calls.append(self.current)
self.current = None
def generic_visit(self, node):
if self.current is not None:
print("warning: {} node in function expression not supported".format(
node.__class__.__name__))
super(CallCollector, self).generic_visit(node)
# record the func expression
def visit_Name(self, node):
if self.current is None:
return
self.current += node.id
def visit_Attribute(self, node):
if self.current is None:
self.generic_visit(node)
self.visit(node.value)
self.current += '.' + node.attr
tree = ast.parse(yoursource)
cc = CallCollector()
cc.visit(tree)
print(cc.calls)
任何人都可以帮我修改这段代码,以便这段代码可以遍历括号内的 API 调用吗?
N.B:这可以在 python 中使用正则表达式来完成。但是需要大量的手工劳动才能找到合适的 API 调用。所以,我在抽象语法树的帮助下寻找一些东西。
不确定这是否是最佳或最简单的解决方案,但至少它确实能按预期为您的案例工作:
import ast
class CallCollector(ast.NodeVisitor):
def __init__(self):
self.calls = []
self._current = []
self._in_call = False
def visit_Call(self, node):
self._current = []
self._in_call = True
self.generic_visit(node)
def visit_Attribute(self, node):
if self._in_call:
self._current.append(node.attr)
self.generic_visit(node)
def visit_Name(self, node):
if self._in_call:
self._current.append(node.id)
self.calls.append('.'.join(self._current[::-1]))
# Reset the state
self._current = []
self._in_call = False
self.generic_visit(node)
举个例子:
['os.walk', 'os.path.join', 'data.append', 'os.path.relpath', 'os.path.join']
问题是您必须在所有 visit
中执行 generic_visit
以确保您 正确 遍历树。我还使用列表作为 current
加入 (reversed) 之后。
我发现这种方法不起作用的一个案例是链式操作,例如:d.setdefault(10, []).append(10)
.
以防万一您对我如何得出该解决方案感兴趣:
假设一个非常简单的节点访问者实现:
import ast
class CallCollector(ast.NodeVisitor):
def generic_visit(self, node):
try:
print(node, node.id)
except AttributeError:
try:
print(node, node.attr)
except AttributeError:
print(node)
return super().generic_visit(node)
这会打印很多东西,但是如果您查看结果,您会看到一些模式,例如:
...
<_ast.Call object at 0x000001AAEE8FFA58>
<_ast.Attribute object at 0x000001AAEE8FFBE0> walk
<_ast.Name object at 0x000001AAEE8FF518> os
...
和
...
<_ast.Call object at 0x000001AAEE8FF160>
<_ast.Attribute object at 0x000001AAEE8FF588> join
<_ast.Attribute object at 0x000001AAEE8FFC50> path
<_ast.Name object at 0x000001AAEE8FF5C0> os
...
所以首先访问调用节点,然后是属性(如果有的话),最后是名称。所以你必须在访问调用节点时重置状态,将所有属性附加到它并在你访问名称节点时停止。
可以在 generic_visit
中完成,但最好在方法 visit_Call
中完成,...然后从这些方法中调用 generic_visit
。
可能需要注意一点:这对于简单的情况非常有效,但一旦变得不平凡,它就无法可靠地工作。例如,如果您导入一个子包怎么办?如果将函数绑定到局部变量会怎样?如果调用 getattr
的结果怎么办?在 Python 中列出静态分析调用的函数可能是不可能的,因为除了普通问题之外,还有框架黑客和动态分配(例如,如果某些导入或调用的函数重新分配名称 os
在你的模块中)。
让我们考虑以下Python源代码;
def package_data(pkg, roots):
data = []
for root in roots:
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
return {pkg: data}
我想从此源代码中提取所有函数和 API 调用。我找到了a similar question and solution。我 运行 这里给出的解决方案,它生成输出 [os.walk, data.append]
。但我正在寻找以下输出 [os.walk, os.path.join, data.append, os.path.relpath, os.path.join]
。
我分析了下面的解决方案代码后了解到,这可以访问第一个括号之前的每个节点并删除其余的东西。
import ast
class CallCollector(ast.NodeVisitor):
def __init__(self):
self.calls = []
self.current = None
def visit_Call(self, node):
# new call, trace the function expression
self.current = ''
self.visit(node.func)
self.calls.append(self.current)
self.current = None
def generic_visit(self, node):
if self.current is not None:
print("warning: {} node in function expression not supported".format(
node.__class__.__name__))
super(CallCollector, self).generic_visit(node)
# record the func expression
def visit_Name(self, node):
if self.current is None:
return
self.current += node.id
def visit_Attribute(self, node):
if self.current is None:
self.generic_visit(node)
self.visit(node.value)
self.current += '.' + node.attr
tree = ast.parse(yoursource)
cc = CallCollector()
cc.visit(tree)
print(cc.calls)
任何人都可以帮我修改这段代码,以便这段代码可以遍历括号内的 API 调用吗?
N.B:这可以在 python 中使用正则表达式来完成。但是需要大量的手工劳动才能找到合适的 API 调用。所以,我在抽象语法树的帮助下寻找一些东西。
不确定这是否是最佳或最简单的解决方案,但至少它确实能按预期为您的案例工作:
import ast
class CallCollector(ast.NodeVisitor):
def __init__(self):
self.calls = []
self._current = []
self._in_call = False
def visit_Call(self, node):
self._current = []
self._in_call = True
self.generic_visit(node)
def visit_Attribute(self, node):
if self._in_call:
self._current.append(node.attr)
self.generic_visit(node)
def visit_Name(self, node):
if self._in_call:
self._current.append(node.id)
self.calls.append('.'.join(self._current[::-1]))
# Reset the state
self._current = []
self._in_call = False
self.generic_visit(node)
举个例子:
['os.walk', 'os.path.join', 'data.append', 'os.path.relpath', 'os.path.join']
问题是您必须在所有 visit
中执行 generic_visit
以确保您 正确 遍历树。我还使用列表作为 current
加入 (reversed) 之后。
我发现这种方法不起作用的一个案例是链式操作,例如:d.setdefault(10, []).append(10)
.
以防万一您对我如何得出该解决方案感兴趣:
假设一个非常简单的节点访问者实现:
import ast
class CallCollector(ast.NodeVisitor):
def generic_visit(self, node):
try:
print(node, node.id)
except AttributeError:
try:
print(node, node.attr)
except AttributeError:
print(node)
return super().generic_visit(node)
这会打印很多东西,但是如果您查看结果,您会看到一些模式,例如:
...
<_ast.Call object at 0x000001AAEE8FFA58>
<_ast.Attribute object at 0x000001AAEE8FFBE0> walk
<_ast.Name object at 0x000001AAEE8FF518> os
...
和
...
<_ast.Call object at 0x000001AAEE8FF160>
<_ast.Attribute object at 0x000001AAEE8FF588> join
<_ast.Attribute object at 0x000001AAEE8FFC50> path
<_ast.Name object at 0x000001AAEE8FF5C0> os
...
所以首先访问调用节点,然后是属性(如果有的话),最后是名称。所以你必须在访问调用节点时重置状态,将所有属性附加到它并在你访问名称节点时停止。
可以在 generic_visit
中完成,但最好在方法 visit_Call
中完成,...然后从这些方法中调用 generic_visit
。
可能需要注意一点:这对于简单的情况非常有效,但一旦变得不平凡,它就无法可靠地工作。例如,如果您导入一个子包怎么办?如果将函数绑定到局部变量会怎样?如果调用 getattr
的结果怎么办?在 Python 中列出静态分析调用的函数可能是不可能的,因为除了普通问题之外,还有框架黑客和动态分配(例如,如果某些导入或调用的函数重新分配名称 os
在你的模块中)。