图形化工具进行效能分析
此篇博客主要谈谈用图形化工具分析与优化python代码,虽然我们的工程不是很大,但符合比较大吧,功能有字母频率统计、词频统计、支持stopword、动词时态归一化、动介短语频率统计。我以 还是全说吧step0-输出某个英文文本文件中 26 字母出现的频率,由高到低排列,并显示字母出现的百分比,精确到小数点后面两位
为例来说明吧。
- step0-输出某个英文文本文件中 26 字母出现的频率,由高到低排列,并显示字母出现的百分比,精确到小数点后面两位
- step1:输出单个文件中的前 N 个最常出现的英语单词,支持stopword,包含动词归一化。
- step2: 2个或两个以上短语的频率,支持stopword,包含动词归一化。
- step3: 统计动介短语出现的频率 , 支持stopword,包含动词归一化
有人会问,效能分析不是对整个工程进行优化,你为什么割裂开了呢。因为一方面为了好分析,更重的是,写代码时功能是独立实现的,每个功能我都封装成了一个模块,所以一步一步来分析和效能分析的初衷并不想违背。
cprofile分析工具
在使用图形化工具之前之前先插一句话,那就是为什么要使用可视化工具呢?原因很简单直观,你可以去使用cProfile分析Python程序性能看一下使用cprofile
的基本用法,不想看的话,可以接着往下看我的分析。因为我会简要说一下,当然你要是特别熟悉产cprofile
你可以直接跳过这部分。直接去搭建图形化工具所需的环境。
举个例子,大致看下,不用那么认真。
#!/usr/bin/env python
#-*- coding:utf-8 -*-
#author: albert time:2018/10/22 0022
import time
import re
import operator
from string import punctuation #所有标点
start = time.clock()
# 对文本的每一行计算字母频率的函数
def ProcessLine(line, counts):
# 用去掉除了字母以外的其他数字
line=ReplacePunctuations(line)
for ch in line:
counts[ch] = counts.get(ch, 0) + 1
return counts
# 用去掉除了字母以外的其他数字
def ReplacePunctuations(line):
tags = [',', '.', '?', '"', '“', '”', '—']
for ch in line :
#这里直接用了string的标点符号库。将标点符号替换成空格
if ch in tags:
line=line.replace(ch,"")
return line
def main():
file = open('gone_with_the_wind.txt')
wordsCount = 0
# 建立用于计算26个字母的空字典
alphabetCounts = {}
for line in file:
alphabetCounts = ProcessLine(line.lower(), alphabetCounts) # 这里line.lower()的作用是将大写替换成小写,方便统计词频
file.close()
if __name__ == "__main__":
import cProfile
end = time.clock()
print(end - start)
# 直接把分析结果打印到控制台
cProfile.run("main()")
# 把分析结果保存到文件中,不过内容可读性差...需要调用pstats模块分析结果
cProfile.run("main()", "result")
# 还可以直接使用命令行进行操作
# >python -m cProfile myscript.py -o result
此程序执行的功能是统计英文文本文件中 26 字母出现的频率,程序有main,Processline,ReplacePunctuations
三个模块。我想用cprofile
分析性能进而提升性能, 于是,先运行一下,结果为
顺序很乱,如果程序在庞大一点,就很难分析啦,我们可以根据消耗的时间tottime
排个顺序。(ps tottime:表示指定函数的总的运行时间,除掉函数中调用子函数的运行时间)
#接着上面的代码写
import pstats
#创建Stats对象
p = pstats.Stats("result")
# 先按time排序,再按cumulative时间排序,然后打倒出前50%中含有函数信息
p.sort_stats('time', 'cumulative').print_stats()
我们大致知道main,Processline,ReplacePunctuations
三个模块耗时,最多是ProcessLine
,我们就需要看preocessLine()
模块里调用了哪些函数,花费了多长时间,我们还是默认时间按时间排序。
#接上面的程序,查看ProcessLine函数中调用了哪些函数
p.print_callees("ProcessLine")
可以看到花费时间比较多的一个是调用ReplacePunctuations
时花费所花费的时间。
就这样一步步的分析,当然,在程序较小可以理清函数之间的调用关系,对程序很熟悉的时候,用cprofile
分析工具是很明智的选择,但是程序很大,而且并不是所有代码都是自己写的时候,图形化工具就很有用。
比如:
#Count.py
def CountLetters(file_name,n,stopName,verbName):
统计字母频率,很多代码
def CountWords(file_name,n,stopName,verbName):
统计单个单词频率,很多代码
def CountPhrases(file_name,n,stopName,verbName,k):
统计指定长度的词组频率,很多代码
def CountVerbPre(file_name,n,stopName,verbName,preName):
统计动介短语的频率,很多代码
用cProfile分析的结果
太多了我就不粘图片了,所以这时候如果用图形化工具
好像有点看不清,可以放大开,也可以点进链接里面看:link
简单的分析一下这幅图,由于函数名是count.py,所以第二层占用时间最长(100%)的便是count(count.py)
这个模块,我有四个主要模块CounterLetters、CounterWords、CountPhrases、CountVerbPre
但是之间是否有调用关系我不知道,所以第三层显示了四个函数模块各自的运行(不包含调用其他模块的)的时间以及之间的调用关系,再往下就是这个函数模块里的某个函数,或者某个调用的函数所占时间的多少,我们可以找到占用时间最长的模块进行优化。但是,前提是保证绝对时间在减小,因为这张图体现的是相对时间,我们的目的还是让程序运行的绝对时间减少。所以下来说一下图形化工具
图像化工具的环境搭建
-
win10系统
-
安装graphviz
pip install graphviz
-
下载转换 dot 的 python 代码gprof2dot 官方下载,下载完了,解压缩,将
gprof2dot.py
link[https://github.com/jrfonseca/gprof2dot/blob/master/gprof2dot.py] copy 到当前分析文件的路径,或者你系统 PATH 环境变量设置过的路径。
使用
python -m cProfile -o result.out -s cumulative step.py //性能分析, 分析结果保存到 result.out 文件;
python gprof2dot.py -f pstats result.out | dot -Tpng -o result.png //gprof2dot 将 result.out 转换为 dot 格式;再由 graphvix 转换为 png 图形格式。
结果
好像有点看不清,可以放大开,也可以点进链接里面看:link
简单的分析一下这幅图:
由于我的程序结构是:
#Count.py
def CountLetters(file_name,n,stopName,verbName):
统计字母频率
def CountWords(file_name,n,stopName,verbName):
统计单个单词频率
def CountPhrases(file_name,n,stopName,verbName,k):
统计指定长度的词组频率
def CountVerbPre(file_name,n,stopName,verbName,preName):
统计动介短语的频率
所以第二层占用时间最长(100%)的便是count(count.py)
这个模块,第三层显示了CounterLetters、CounterWords、CountPhrases、CountVerbPre
四个函数模块占用的时间,再往下就是这个函数模块里的某个函数,或者某个调用的函数所占时间的多少,我们可以找到占用时间最长的模块进行优化。但是,(敲黑板),前提是保证绝对时间在减小,因为这张图体现的是相对时间,我们的目的还是让程序运行的绝对时间减少。
STEP 0 输出某个英文文本文件中 26 字母出现的频率
以step0-输出某个英文文本文件中 26 字母出现的频率,由高到低排列,并显示字母出现的百分比,精确到小数点后面两位
为例来说明吧。
1.优化前
代码
#!/usr/bin/env python
#-*- coding:utf-8 -*-
#author: Enoch time:2018/10/22 0031
import time
import re
import operator
from string import punctuation
start = time.clock()
'''function:Calculate the word frequency of each line
input: line : a list contains a string for a row
counts: an empty dictionary
ouput: counts: a dictionary , keys are words and values are frequencies
data:2018/10/22
'''
def ProcessLine(line,counts):
#Replace the punctuation mark with a space
line = re.sub('[^a-z]', '', line)
for ch in line:
counts[ch] = counts.get(ch, 0) + 1
return counts
def main():
file = open("../Gone With The Wind.txt", 'r')
wordsCount = 0
alphabetCounts = {}
for line in file:
alphabetCounts = ProcessLine(line.lower(), alphabetCounts)
wordsCount = sum(alphabetCounts.values())
alphabetCounts = sorted(alphabetCounts.items(), key=lambda k: k[0])
alphabetCounts = sorted(alphabetCounts, key=lambda k: k[1], reverse=True)
for letter, fre in alphabetCounts:
print("|\t{:15}|{:<11.2%}|".format(letter, fre / wordsCount))
file.close()
if __name__ == '__main__':
main()
end = time.clock()
print (end-start)
编程思路
我们打开文本,每次读取一行程序,用函数ProcessLine()
来统计一行中出现的字母个数,在函数中ProcessLine()
用正则表达式re.sub('[^a-z]', '', line)
除去非数字的所有部分。有关正则表达式参考正则表达式
输出的结果应该是正确的,先看下最后一行,用时1s多,是该优化了。
我们运行以下两行代码
python -m cProfile -o result.out -s cumulative step0.py
python gprof2dot.py -f pstats result.out | dot -Tpng -o result.png
得到的结果如下图, 觉得图别扭的可以点击更加舒服一点的图片查看
可以看到文本有9千多行,low函数和re.sub被调用了9023次,每个字母每个字母的统计get也被调用了1765982次,反正就是一行一行处理太慢了,函数次数太多了。我们可以直接统计26个字母出现了几次,这样对于大的文本,我们是需要遍历26次。
2. 优化后
既然是处理一次处理整个文本,就可以直接封装成一个函数,用count
来统计26个字母出现的频率,而不去处理那么非字母的字符
#!/usr/bin/env python
#-*- coding:utf-8 -*-
#author: Enoch time:2018/10/22 0031
import re
import time
import os
import string
import sys
letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']
def CountLetters(file_name):
wordsCount = 0
alphabetCounts = {}
t0 = time.clock()
with open(file_name) as f:
txt = f.read().lower()
for letter in string.ascii_lowercase:
alphabetCounts[letter] = txt.count(letter) #here count is faster than re
wordsCount += alphabetCounts[letter]
t1 = time.clock()
for letter in string.ascii_lowercase:
alphabetCounts[letter] = alphabetCounts[letter]/wordsCount
alphabetCounts = sorted(alphabetCounts.items(), key=lambda k: k[0])
alphabetCounts = sorted(alphabetCounts, key = lambda k: k[1],reverse=True)
t2 = time.clock()
for letter,fre in alphabetCounts:
print("|\t{:15}|{:<11.2%}|".format(letter, fre))
print(t2-t1)
if __name__ == '__main__':
CountLetters('../gone_with_the_wind.txt')
运行结果
以后只关心运行的时间,功能正不正确单元测试,回归测试已经保证了,这里在不细讲啦
直接下降了好多级,当然这也是最终版本,中途改了几改。最重要的是,这些大部分也不是我想出来的,是队友太强,从最初我的最慢的replace
替换掉非字符,到杨涛用正则表达式 re
来处理字符问题,到后来张贺直接统计字母出现的频率,牺牲空间换时间,一路被带飞。此处只是说了优化前和优化后,中间的优化细节太细啦,就不细讲啦。忘了给优化后的图啦,我还是建议你们网页开,点这里看图片,
看来这个工具统计的东西太多了,分析大工程很好用,小工程还是用cprofile
吧!!!
#### 3. 待优化 试了变慢了
如果对结果还不满意,上图用淡紫色已经圈出,你可以继续优化str.lower
和str.count
两个函数,不对整个文本用str.lower
,而是在统计后,在str.lower
是不是可以优化,可以试试。
STEP 1 输出单个文件中的前 N 个最常出现的英语单词。
step1的结尾我们在分析一下支持stopword
这个功能
编程思路
起初我们使用正则表达式将多余的符号用空格代替,然后遍历所有的单词,用字典来存放单词及其出现的频率。但是由于遍历的次数太多,频繁的改动字典,耗时很大,于是我们改用collections中的Counter,可以统计相同单词出现的频率。源代码
结果
时间降到了0.4s左右
效能分析图
待优化
待优化的部分便是re.findall
,能不能用split()来划分呢?
支持stopword的编程思路
我们可以循环遍历字典的键值,将在```stopwords.txt``里的单词删除掉即可
源代码
结果
效能分析的结果
原图link
由于stopword.txt
太小,处理它根本占不了多少时间,而且实际情况下,stopword.txt
也不是很大。
step2: 2个或两个以上短语的频率 ,包含动词归一化
编程思路
起初我们按行处理,统计一行中短语出现频率,效率较低,之后我们改变思路,一次性统计,将文本连成一句话,由于re.findall
在滑动的时候没有overlap
,因此可以没统计一次,删掉开头一次词,在统计一遍。k个词的短语我们只需要遍历k-1次。
源代码
结果
用时1.6s左右
效能分析图
待优化
待优化的部分是
* re.findal()l
,能不能用split()来划分呢?
* sotred()
函数
* re.sub()
换成了其他并没有提升,等待某天恍然大悟。
step3: 统计动介短语出现的频率、支持动词归一化
这里我们将动词归一化与动介短语的统计放到一起分析,因为比起统计动词归一化的动介短语,统计没有归一化的单词就没有太大意义。
代码
编程思路
将动词表存到字典里,key为动词各种时态包括原形,value为动词原形。
找到两个词语的短语,存入Counter里(类似于字典),遍历如果短语中第一个单词为动词,第二个单词为介词,则此短语为动介短语,存入字典的同时将动词归一化。
结果
效能分析
可以看到在判断两个词语的短语是不是动介短语时,我们执行了13万次,但是其占用的时间很少,也就是说我们可以先不急于优化这个。
待优化
待优化的部分是待优化的部分依然是
* re.findal()l
,能不能用split()来划分呢?
* sotred()
函数
* re.sub()
换成了其他并没有提升,等待某天恍然大悟。哈哈。
总结
此次效能分析,打破我们自动化原先的程序为我所用的思想,同时也觉得,为用户服务并不是一件小事。速度要快,性能要好。