Pandas:如何将 cProfile 输出存储在 pandas DataFrame 中?

Pandas: How to store cProfile output in a pandas DataFrame?

已经有一些帖子讨论 python 使用 cProfile 进行分析,以及分析输出的挑战,因为输出文件 restats 来自下面的示例代码不是纯文本文件。下面的代码片段只是来自 docs.python.org/2/library/profile 的示例,不能直接复制。

import cProfile
import re
cProfile.run('re.compile("foo|bar")', 'restats')

这里有一个讨论:Profile a python script using cProfile into an external file, and on the docs.python.org有更多关于如何使用pstats.Stats分析输出的细节(仍然只是一个示例,而不是可重现):

import pstats
p = pstats.Stats('restats')
p.strip_dirs().sort_stats(-1).print_stats()

我可能在这里遗漏了一些非常重要的细节,但我真的很想将输出存储在 pandas DataFrame 中并从那里做进一步的分析。

我认为这会很简单,因为 运行 cProfile.run() 的 iPython 中的输出看起来相当整洁:

In[]:
cProfile.run('re.compile("foo|bar")'

Out[]:

关于如何将其放入相同格式的 pandas DataFrame 有什么建议吗?

看起来 https://github.com/ssanderson/pstats-view 可能会做你想做的事(尽管有与数据可视化和交互相关的不必要的依赖):

>>> from pstatsviewer import StatsViewer
>>> sv = StatsViewer("/path/to/profile.stats")
>>> sv.timings.columns
Index(['lineno', 'ccalls', 'ncalls', 'tottime', 'cumtime'], dtype='object')

我知道这已经有了答案,但对于不想麻烦下载另一个模块的人来说,这里有一个粗略的现成脚本,应该接近了:

%%capture profile_results    ## uses %%capture magic to send stdout to variable
cProfile.run("your_function( **run_parms )")

运行 首先,用 stout 的内容填充 profile_results,其中包含 cProfile.

的通常打印输出
## Parse the stdout text and split it into a table
data=[]
started=False

for l in profile_results.stdout.split("\n"):
    if not started:
        if l=="   ncalls  tottime  percall  cumtime  percall filename:lineno(function)":
            started=True
            data.append(l)
    else:
        data.append(l)
content=[]
for l in data:
    fs = l.find(" ",8)
    content.append(tuple([l[0:fs] , l[fs:fs+9], l[fs+9:fs+18], l[fs+18:fs+27], l[fs+27:fs+36], l[fs+36:]]))
prof_df = pd.DataFrame(content[1:], columns=content[0])

它不会因优雅或令人愉快的风格而赢得任何奖项,但它确实将 table 结果强制转换为可过滤的数据帧格式。

prof_df 

如果您在 cmd 中使用 python -m cProfile your_script.py

您可以将输出推送到 csv 文件,然后使用 pandas 进行解析 python -m cProfile your_script.py >> output.txt

然后用pandas

解析输出
df = pd.read_csv('output.txt', skiprows=5, sep='    ', names=['ncalls','tottime','percall','cumti    me','percall','filename:lineno(function)'])
df[['percall.1', 'filename']] = df['percall.1'].str.split(' ', expand=True, n=1)
df = df.drop('filename:lineno(function)', axis=1)

你可以使用这个函数来完成这个任务

def convert_to_df(path, offset=6):
    """
    path: path to file
    offset: line number from where the columns start
    """
    with open(path, "r") as f:
        core_profile = f.readlines()
    core_profile = core_profile[offset:]
    cols = core_profile[0].split()
    n = len(cols[:-1])
    data = [_.split() for _ in core_profile[1:]]
    data = [_ if len(_)==n+1 else _[:n]+[" ".join(_[n+1:])] for _ in data]
    data_ = pd.DataFrame(data, columns=cols)
    return data_

如果人们不想使用 %%capture 或通过 CSV,下面是一个拼凑的解决方案,在这种情况下比较同一文件夹中的多个 cProfile,方法是 (1) 按累积时间对每个 cProfile 进行排序(2) 仅将每个 .prof 的最高结果 (pstats.Stats(f, stream = p_output).sort_stats("cumulative").print_stats(1)) 添加到数据框(连同 .prof 文件名的一部分,以确定测量来自哪个配置文件)。

请参阅 here 了解一些原始代码(确实使用 CSV 作为中介)。

import io
import pstats
import pandas as pd
import glob

all_files = glob.glob(profiledir + "/*.prof")

li = []

for f in all_files:
    
    p_output = io.StringIO()

    prof_stats = pstats.Stats(f, stream = p_output).sort_stats("cumulative").print_stats(1)

    p_output = p_output.getvalue()
    p_output = 'ncalls' + p_output.split('ncalls')[-1]
    result = '\n'.join([','.join(line.rstrip().split(None,5)) for line in p_output.split('\n')])

    df = pd.read_csv(io.StringIO(result), sep=",", header=0)
    df['module name'] = f.split(' ')[0].split('\')[1] # differs depending on your file naming convention
    li.append(df) 

df = pd.concat(li, axis=0, ignore_index=True)

我知道这个问题有点老了,但我找到了一个简单的方法来解决它。

import cProfile
import pandas as pd

with cProfile.Profile() as pr:
    # run something

df = pd.DataFrame(
    pr.getstats(),
    columns=['func', 'ncalls', 'ccalls', 'tottime', 'cumtime', 'callers']
)