飞道的博客

Python处理Excel比Vba快100倍,媳妇连连夸赞今晚不用再跪搓衣板----python实战

366人阅读  评论(0)

最近经历了一次把vb脚本改造成python脚本,并获得性能提升数倍的过程,当然,这个过程也不是一帆风顺,中间也经历了一些波折,但是,也收获了一波新的认知。正好最近有时间,姑且写下来记录一下。

什么是VB

话说现在的年轻人,听说过这个编程语言的应该不多了。VB是一种由微软公司开发的包含协助开发环境的事件驱动编程语言。从任何标准来说,VB都是世界上使用人数最多的语言,它源自于BASIC编程语言,也属于高级语言的一种了。只是现在各大应用场景以及被Java、Go、Python等编程语言瓜分一空,VB基本很少人知道了。

什么是VBA

而VBA和VB又有点差别,Visual Basic for Applications(VBA)是Visual Basic的一种宏语言,是微软开发出来在其桌面应用程序中执行通用的自动化(OLE)任务的编程语言。主要能用来扩展Windows的应用程式功能,特别是Microsoft Office软件,比如excel、powerpoint、word等。

故事的开端

而本次故事的场景,就是在excel中编写vba宏脚本,而这个场景的需求,则来源于笔者的媳妇。笔者的媳妇平时的工作大部分时间都是跟excel打交道,也就是很多人口中的“表姐”,因此excel的各种高级操作比如vlookup、数据透视等,也算是应用的炉火纯青了。

可偏偏事不如人愿,企业中的业务总是会越来越复杂,老板的要求也会越来越高,渐渐地,有一些需求我媳妇用她炉火纯青的技巧也搞不动了。于是她把希望寄托在了我这个廉价劳动力身上,毕竟传说中的搞IT的,可是什么都能干的。

于是大概从几年前,我开始陆陆续续用vba写宏,帮助媳妇处理类似复杂的数据计算问题,说到这里,我翻了翻我的朋友圈,竟然有据可查:2017年就开始了!有图为证:

也就是从那个时候开始,媳妇搞不定的复杂数据处理问题,就扔给我用vba来搞。要知道,对于一个写惯Java语言的人来说,对vba这种语言真的是一百种不习惯,尤其是那个土得掉渣的开发环境,话不多说,上图:

有没有一种年代复古风的感觉!这还是最新版本的,老版本的连调试功能都没有,任何问题都得默念加各种打日志排查,更不用说高级点IDE都具备的自动补全、提示、重构等功能了,所以,用这个玩意写代码的效率那真是一言难尽。

就这样被媳妇的需求折磨了几年,好在这几年的需求也没复杂到哪里去,一路也就忍过来了。可最近一次媳妇扔过来的需求,可着实把我可累了一把。

详细的需求就不说了,大概就是对一个excel的两个sheet进行计算,其中一个sheet将近1万行,两外一个sheet数据量倒不多300多行,但是格式比较复杂,各种合并和拆分单元格(见下图),而要计算的需求复杂度相比之前也上升了一个台阶。

拿到需求后,我还是按照惯例用vba来写,大概耗费了一个周末的时间搞定了,虽然交了差。但是面对未来可能越来越复杂的需求,我的心里打了鼓,vba的开发效率和复杂数据处理需求的矛盾越来越突出,而且这次写的脚本,性能上也问题很大,整个处理过程耗时10分钟之巨,如下图所示:

作为一个自认优秀且有良心的搞IT的,怎么能够忍受这种开发效率和运行效率,二话不说,我要优化它!

怎么优化呢?话说在大数据处理领域,Python可算是TIOBE排行榜上,数一数二的利器了,尤其是在AI大热的背景下,Python在TIOBE排行榜上的地位是逐渐蹿升,除了大数据领域,Python在web开发、Excel办公、科学计算和数据可视化等方面也表现优秀。好了,就用Python搞!

Python优化过程

大概的优化思路是这样的:用Python的xlwings库来处理excel数据的读写,但数据的计算就不用它直接搞了,效率会比较低,而是用Pandas库在内存中进行数据的复杂计算,然后将计算后的结果写回excel

思路其实很简单,但实操的过程却不是完全一帆风顺,接下来就是整个优化的过程

第一版优化

因为用Pandas把数据读到内存后,是一个DataFrame,我们可以很容易的拿到这个DataFrame的行数和列数,类似一个数组一样可以方便的遍历,因此第一版的实现,使用的是标准的遍历的方法来实现,核心代码如下:

读取excel


  
  1. import pandas as pd
  2. import xlwings as xw
  3. #要处理的文件路径
  4. fpath = "datas/joyce/DS_format_bak.xlsm"
  5. #把CP和DS两个sheet的数据分别读入pandas的dataframe
  6. cp_df = pd.read_excel(fpath,sheet_name= "CP",header=[ 0])
  7. ds_df = pd.read_excel(fpath,sheet_name= "DS",header=[ 0, 1])
  8. ......

标准遍历方法


  
  1. for j in range( len(cp_df)):
  2. cp_measure = cp_df.loc[j, 'Measure']
  3. cp_item_group = cp_df.loc[j, 'Item Group']
  4. if cp_measure == "Total Publish Demand":
  5. for i in range( len(ds_df)):
  6. #如果cp和ds的item_group值相同
  7. if cp_item_group == ds_df.loc[i,( 'Total', 'Capabity')]:
  8. ......

写入excel


  
  1. #保存结果到excel
  2. app = xw.App(visible= False,add_book= False)
  3. ds_format_workbook = app.books. open(fpath)
  4. ds_format_workbook.sheets[ "DS"]. range( "A3").expand().options(index= False).value = ds_df
  5. ds_format_workbook.save()
  6. ds_format_workbook.close()
  7. app.quit()
  8. ......

说到这里插一句,大家还记得我前面提到的那个各种拆分和合并单元格的复杂格式吗,这种格式在Pandas里又叫多层索引(MultiIndex),这种结构下数据的查询和操作,比普通的表格要复杂,大概处理代码类似下面:


  
  1. #用元组的方式来定位某一列
  2. ds_total_capabity1 = ds_df.loc[k,( 'Total', 'Capabity.1')]
  3. #
  4. #获取多层索引某一层数据的方法
  5. ds_month = ds_df.columns.get_level_values( 0)[k]
  6. ds_datatime = ds_df.columns.get_level_values( 1)[k]
  7. ......

因为这个话题跟本文章无关,这里就不展开了,有兴趣大家自己去学习了解。

这一版写完后,信心满满地执行脚本,但是立马被现实浇了一盆冷水,执行时间竟然要555秒,也就是9分多钟,并没有比vba快多少,如下图:

为什么会这样!Python不是号称数据处理利器吗。我们仔细看一下打印的日志输出,可以看到主要的瓶颈在循环计算这块,耗时469+42 = 517秒,基本所有时间都用在这里。当然,从日志也可以看到,读写excel的性能也一般,但并不是性能瓶颈。对于性能优化的一般准则是:数据驱动+二八原则,也即通过数据分析发现瓶颈,即占用80%耗时的地方,然后有针对性地优化该瓶颈。

内存中的循环计算为什么这么慢呢?遇事不决问度娘,通过一番搜索,终于让我找到一个官方解释,原来DataFrame(数据帧)是具有行和列的Pandas对象(objects),如果使用循环,则将遍历整个对象,Python无法利用任何内置函数,而且速度非常慢,建议用Pandas内置函数:iterrows(),iterrows()为每行返回一个Series,因此将DataFrame迭代为一对索引,将感兴趣的列作为Series进行迭代,这使其比标准循环更快。

既然官方这么说,那我们还怀疑什么,那就试试呗。

第二版优化

有了解决方案,那就好办了,无非就是把代码里所有用到标准循环的地方,改成用iterrows(),改动的地方代码如下:


  
  1. #根据CP和DS表的Item_group值做lookup,计算DS表的Delta值
  2. for index_i,cp_row in cp_df.iterrows():
  3. #获取CP表的Item_group和siteid值
  4. cp_item_group = cp_row[ 'Item Group']
  5. siteid = cp_row[ 'SITEID']
  6. key = cp_item_group + "-" + siteid
  7. for index_j,ds_row in ds_df.iterrows():
  8. #获取DS表的Item_group值
  9. ds_item_group = ds_row[( 'Total', 'Capabity')]
  10. if ds_item_group != "" and cp_item_group == ds_item_group :
  11. iner_iter_df = ds_df.loc[ index_j:index_j+ 5]
  12. ......

改完后执行,果然,效率提升了一些,见下图:

整体耗时337秒,也就是5分多钟,比前一版提升40%,看起来还不错。但是,作为一名优秀的IT人,不能满足于既有的成绩,要不断追求极致。于是,就有了第三版优化。

第三版优化

其实第三版优化的思路,还是追求更快地遍历效率,Pandas除了iterrows()之外,据说还有一个更快的apply()方法,能够对DataFrame的每一行逐行应用自定义函数,且遍历性能更好。于是,第三版的核心代码如下:


  
  1. def Cal_Delta_Loi_Iter_In_Cp( data):
  2. global cal_delta_loi_cp_row
  3. cal_delta_loi_cp_row = data
  4. #获取CP表的Item_group和siteid值
  5. global cp_item_group
  6. cp_item_group = cal_delta_loi_cp_row[ 'Item Group']
  7. siteid = cal_delta_loi_cp_row[ 'SITEID']
  8. global key
  9. key = cp_item_group + "-" + siteid
  10. ds_df.apply(Cal_Delta_Loi_Iter_In_Ds,axis= 1)
  11. #开始计算Delta和LOI值
  12. cp_df.apply(Cal_Delta_Loi_Iter_In_Cp,axis= 1)
  13. ......

按apply()改完代码再次执行,这次执行效率果然又上了一个台阶,如下图:

整体耗时147秒,也即2分多钟,相比上一版再次提升56%,Very Done!

小小总结一下

优化到这里,我们可以看到,使用Python的Pandas类库,并且使用较高性能的内置函数,能够很大程度提升数据处理的性能。但是,我们从前面打印出的日志也能看到,Python提供的xlwings库,在读写excel方面的性能缺很难说优秀,相比vba来说更是差了一大截。

VBA虽然数据结构少,数据计算速度慢,但访问自己Excel的Sheet,Range,Cell等对象却速度飞快,这就是一体化产品的优势。VBA读取Excel的Range,Cell等操作是通过底层的API直接读取数据的,而不是通过微软统一的外部开发接口。所以Python的各种开源和商用的Excel处理类库如果和VBA来比较读写Excel格子里面的数据,都是处于劣势的(至少是不占优势的)。

因此,Python处理Excel的时候,就要把Excel一次性地读取数据到Python的数据结构中,而不是大量调用Excel里的对象,不要说频繁地写入Excel,就是频繁地读取Excel里面的某些单元格也是效率较低的。


转载:https://blog.csdn.net/weixin_69999177/article/details/128359373
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场