在日常办公中,经常需要批量处理Excel文件——调整列宽、字号、自动换行、行高等格式,然后导出为A4横向PDF。最近遇到一个实际需求:处理63个.xls格式的个人所得税扣缴申报表,每个表有51列,包含三级合并表头、大量数据行和合计行,必须将所有列缩放到一页宽,并保持原有边框和背景色。如果手动操作,工作量巨大且容易出错。本文将介绍三种技术方案的对比,重点分享基于win32com(Excel COM接口)的成功方案,以及开发中遇到的五个关键问题与解决方法。
需求梳理
原始表格结构:Row 1-4为标题/公司信息;Row 5-7为三级合并表头(跨行跨列合并);Row 8为列序号行;Row 9起为数据行(每文件1~47行不等);Row N为合计行;Row N+1起为声明/签章等。格式要求包括:固定列宽(如序号2.25、姓名4.5等,单位字符)、动态列宽(根据合计行值的字符长度映射,例如字符数4对应宽度3,字符数5对应4.18等,合计值为“-”时取数据行最长值)、字号9号加自动换行(姓名、身份证件类型等列,仅数据行)、行高数据行33.8磅、合计行21磅,导出PDF要求A4横向、所有列缩放至一页宽、左右边距极限最小。
技术选型三方案对比
方案一:xlrd + xlwt(失败)
这是处理.xls老格式的经典组合,但遇到两个致命问题。首先,xlrd读取的Font对象与xlwt的Font对象属性不兼容,例如xlrd的Font有charset属性,xlwt的Font没有;underline_type与underline命名不同等。更严重的是,样式复制极其困难——合并单元格的边框、背景色、对齐方式等极易丢失,报错如“AttributeError: 'Font' object has no attribute 'charset'”。对于带有三级合并表头和大量边框的报表,样式丢失不可接受,因此放弃。
方案二:openpyxl(不可行)
openpyxl仅支持.xlsx格式,而源文件是.xls,需先转换为.xlsx。转换过程本身可能丢失格式,且引入额外步骤,不符合零损失保真的要求。
方案三:win32com(Excel COM接口)(成功)
最终采用pywin32调用Excel应用程序的COM接口,本质上是用Python操控Excel应用程序本身,等于手动操作Excel。原生支持.xls,格式零丢失,可以直接控制列宽、字号、自动换行等属性,并调用ExportAsFixedFormat导出PDF。核心代码极简:
- import win32com.client as win32
- excel = win32.Dispatch('Excel.Application')
- excel.Visible = False
- excel.DisplayAlerts = False
- wb = excel.Workbooks.Open(r'D:\path\to\file.xls')
- ws = wb.Worksheets(1)
- # 设置列宽
- ws.Cells(1, col).EntireColumn.ColumnWidth = 4.5
- # 设置字号和自动换行
- cell.Font.Size = 9
- cell.WrapText = True
- # 导出PDF
- ws.ExportAsFixedFormat(Type=0, Filename=pdf_path)
复制代码
开发过程中遇到的关键问题
问题1:UsedRange在合并单元格上翻车
使用ws.UsedRange.Rows.Count获取行数时,合并单元格导致返回值不准甚至报错。解决方案:采用Cells(65536, 1).End(-4162).Row(相当于Ctrl+↑)或直接遍历查找合计行(列A含“合”和“计”)。函数示例:
- def get_total_row(ws):
- for row in range(1, 100):
- val = ws.Cells(row, 1).Value
- if val is not None and '合' in str(val) and '计' in str(val):
- return row
- return None
复制代码
问题2:DisplayAlerts在某些Excel版本上报错
设置excel.DisplayAlerts = False部分环境下抛异常。解决:用try/except包裹,不影响核心功能。
问题3:PDF只导出了第一页(最隐蔽的Bug)
多页Excel导出PDF后发现只有第一页内容。原因是在ExportAsFixedFormat中错误地指定了From=1和To=1参数。正确做法是去掉这两个参数,默认导出全部页面:
- ws.ExportAsFixedFormat(
- Type=0,
- Filename=pdf_path,
- IgnorePrintAreas=False,
- OpenAfterPublish=False
- )
复制代码
问题4:.bat启动脚本的中文乱码
为方便双击运行,编写.bat文件指定已安装pywin32的Python路径。但.bat文件若包含中文注释或echo,Windows cmd默认GBK编码下UTF-8中文会变成乱码当作命令执行。解决方案:.bat文件只使用纯英文/ASCII字符,彻底避免编码问题。
问题5:合计行值的类型不统一
同样数字列,有的合计值为float(如10754.93),有的是str(如'10754.93'),还有“-”字符串。需统一处理才能正确计算字符长度。自定义函数compute_width:对数字格式化两位小数,对空值或“-”则查数据行最长值。
- def compute_width(total_val, ws, col_1based, data_rows):
- s = str(total_val).strip()
- if s == '' or s == '-':
- max_len = max(len(str(ws.Cells(r, col_1based).Value or ''))
- for r in data_rows)
- else:
- formatted = f'{float(total_val):.2f}'
- char_count = len(formatted)
- return WIDTH_MAP.get(char_count, 3)
复制代码
经验总结
场景推荐方案:简单读写xlsx不需保留样式用openpyxl;简单读写xls不需保留样式用xlrd+xlwt;需精确保留样式+修改格式或导出PDF,首选win32com(COM接口)。核心心得:选对工具比写对代码更重要,COM接口本质是Excel自己在操作,保真度高;.bat文件慎用中文;PDF导出必须验证多页场景,避免From/To参数的误解。 |