专业的编程技术博客社区

网站首页 > 博客文章 正文

pandas + xlwt制作格式化报表遇到的两个问题

baijin 2024-10-24 08:44:52 博客文章 6 ℃ 0 评论

一、两个问题的出现场景

近日使用pandas和xlwt编写了一段脚本,基于期末成绩数据制作成绩表,打印后分发给每个老师。成绩数据包含上课教师、课序号、学生学号、学生姓名和总分等字段,发给老师的成绩表要求每个班(以课序号区分)的成绩单独排成一个纵列,每页最多3列,每个老师的成绩单独分页。编写脚本如下:

import xlrd
import xlwt
import pandas as pd
import numpy as np
from xpinyin import Pinyin

#############################################################
##每次运行程序需要修改成绩数据和成绩单名称

BASE_FILE='成绩数据.xls'
OUT_FILE='成绩单.xls'
#############################################################

p=Pinyin()
def py(v):
    return p.get_pinyin(v,'')

def getWidth(columnWidth):
    return round(columnWidth*256+182.8571)

def c(v):
    return v.replace('*', '')[:-1]

#读取成绩数据并进行预处理,成绩数据包含SKJS, KXH, XH, XM, 总分等字段。
wb = xlrd.open_workbook(IN_FILE,encoding_override='cp936')
df = pd.read_excel(wb, dtype={'KXH':'string', 'XH':'string'})
df.SKJS=df.SKJS.apply(c)  #删除任课教师字段里的*号
df['JSPY']=df.SKJS.apply(py) #添加任课教师拼音字段,方便排序
df['KXHINT']=df.KXH.astype(int) #添加课序号数字字段,方便排序
df.sort_values(['JSPY', 'SKJS', 'KXHINT', 'XH'], inplace=True)  #值排序

#新建workbook
book = xlwt.Workbook(encoding='utf-8')
sheet=book.add_sheet('成绩单')
sheet.set_header_str(''.encode())  #设置页眉为空
sheet.set_footer_str('202301 FINAL   page &P, &N pages'.encode())   #设置页脚

#设置列宽
cc=[13, 7.71, 4, 2, 13, 7.71, 4, 2, 13, 7.71, 4]
i=0
for c in cc:
    sheet.col(i).width=getWidth(c)
    i+=1

#设置行高
for i in range(5000):  
    sheet.row(i).height_mismatch = True
    sheet.row(i).height = 18*20  #行高18磅,每页44行。

#设置样式
s1=xlwt.easyxf('font:name 微软雅黑, height 240, bold 1; align: horz center, vert center;')  #字号12磅
s2=xlwt.easyxf('font:name 宋体, height 220; align: vert center;')  #字号11磅

hs=44  #行高18磅时, 每页行数44
teacher=''
total_cols=0

for k, group in df.groupby(['JSPY', 'SKJS', 'KXHINT', 'KXH']):
    if k[1]!=teacher:
        teacher=k[1]
        if total_cols % 3!=0:
            total_cols+=3-total_cols % 3

    col_id=total_cols
    
    xh_count=group.XH.count()+1
    cols=xh_count // hs if xh_count % hs==0 else xh_count // hs+1  #需要列数
    total_cols+=cols

    start_row=col_id//3*hs
    start_col=(col_id % 3)*3+col_id % 3

    sheet.write(start_row, start_col, k[1], style=s1)
    sheet.write(start_row, start_col+1, k[3], style=s1)
    i=1
    for c in group.itertuples():
        n=(col_id+i//hs) %3
        col_offset=n*3+n
        row_offset=start_row+i % hs+(i+col_id %3*hs)//(hs*3)*hs
        
        sheet.write(row_offset, col_offset, c.XH, style=s2)
        sheet.write(row_offset, col_offset+1, c.XM, style=s2)
        sheet.write(row_offset, col_offset+2, c.总分, style=s2)
        i+=1

book.save(OUT_FILE)

脚本运行过程中,遇到问题1(注释设置行高代码的情况下):

折腾很久终于解决这个问题(稍后详述),在Excel中打开成绩单文件时,遇到问题2:

问题1让脚本不能正常运行并生成成绩单.xls文件,问题2更让自己惴惴不安:万一真丢了数据,如何从上万条成绩记录中检查丢了哪些数据?!

下面描述探索和解决这两个问题的过程,一方面便于自己加深认识,另一方面希望他人不要继续踩坑。

二、问题1的探索和解决过程

根据错误提示给出的线索,打开xlwt包的Row.py文件,发现这样一段代码:

def __init__(self, rowx, parent_sheet):
    if not (isinstance(rowx, int_types) and 0 <= rowx <= 65535):
        raise ValueError("row index was %r, not allowed by .xls format" % rowx)

这段代码说明,如果行索引不是整数,或者行索引大于65535,xlwt就会报错。在图1所示的错误信息中,行索引是44,显然报错不是因为行索引越界引发。那么只能是:xlwt判断当前的行索引不是整数。于是赶紧检查start_row和start_col两个变量的类型,下面的发现很意外:

type(start_row)
<class 'numpy.int64'>
type(start_col)
<class 'numpy.int64'>

编写脚本时,我一直以为start_row和start_col就是整数(<class 'int'>),完全没有想到这两个变量居然是numpy.int64。虽然numpy.int64也是整数类型,但是xlwt有时不能准确理解和处理。那么变量start_row如何变成numpy.int64的呢?我注意到这行代码:

xh_count=group.XH.count()+1

xh_count变量用于保存当前班上学号个数+1,脚本使用该变量计算当前班的成绩需要排成几列。查看xh_count的变量类型,发现就是numpy.int64,从而导致其它相关变量全部成为这个整数类型,这就是错误的根源所在。为了阻断该变量“污染”其它变量,添加一行代码:

xh_count=int(xh_count)

经过测试,xlwt不再报错,问题得到解决。当然还有其它解决办法,原则是使用sheet.write方法时,确保传入的行变量和列变量类型必须是python的int,而不是pandas的numpy.int64。

三、问题2的思考和解决过程

问题2不是在运行脚本生成成绩单文件过程中出现的,使得脚本调试更加困难。最初遇到这个问题时,我怀疑可能是xlwt和python/pandas不兼容引起的,毕竟xlwt包从2018年起就没有更新了。于是我编写了一段验证脚本,没有使用pandas dataframe数据,直接用xlwt生成xls文件。但打开这个文件时Excel没有弹出提示框,说明我的怀疑不成立。由此判断,问题还是出在pandas传入的数据上。经过反复调试,发现如果把dataframe的float数据写入xls文件,Excel打开文件时就可能弹出提示。解决这个问题的办法也很简单:把原代码中的float变量使用str函数或float函数进行强制转换即可。如下列代码所示:

sheet.write(row_offset, col_offset+2, c.总分, style=s2)

修改为

sheet.write(row_offset, col_offset+2, float(c.总分), style=s2)

或者修改为

sheet.write(row_offset, col_offset+2, str(c.总分), style=s2)

四、总结

xlwt配合pandas制作格式化报表过程中,使用sheet.write方法写入数据时,必须保证:

  1. 行变量和列变量类型必须是python的整数(<class 'int'>),而不是pandas的numpy.int64(<class 'numpy.int64'>);
  2. 写入值是pandas float32类型时,使用python内置的float函数或str函数进行强制转换,这样保证数据安全写入xls文件。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表