☰
xlsxwriter 实战:Python 生成 Excel 报表的样式、公式与避坑指南
2026/10/17 14:27:18 网站建设 项目流程

简介:这份PDF资料面向需要借助Python批量生成Excel报表的开发者与数据分析人员,系统讲解xlsxwriter库输出各类xlsx文件的用法。内容围绕Workbook、Worksheet与格式对象展开,覆盖创建文件、写入行列数据、添加求和公式、设置加粗等自定义样式,并延伸至图表、条件格式、数据验证等进阶功能,还给出将常用逻辑封装为函数或类的复用思路。资源包共1个PDF文件,约63KB,篇幅精炼,便于随时查阅与对照练习。目前已有270人学习下载,适合希望摆脱手工制表、用代码高效产出规范表格的读者,可作为日常开发与数据分析场景下的实用参考手册。

1. 从一份“看起来没问题”的报表说起:xlsxwriter 到底能干什么

有个做数据运营的朋友找我,说他们每周要出一份对账表,字段不多,但格式要求很死:首行合并、条件信息用富文本拼在一格里、表头加粗、数据行斑马纹、列宽固定。他一开始用别的库硬怼,结果合并单元格和富文本总是对不齐,公式写进去还变成了纯文本。后来换成 xlsxwriter,问题基本一次性解决。这就是这个库的定位:它不负责读 Excel,只专注把 Python 里的数据“写”成格式完整的 xlsx,而且写出来的东西在 Excel、WPS、LibreOffice 里打开都稳。

这份资源就是围绕 xlsxwriter 输出各种 xlsx 文件的示例集合,核心对象是Workbook,往下挂Worksheet,再通过add_format控制样式。它适合两类人:一类是刚接触 Python 写 Excel、想找一份能直接抄的模板;另一类是已经用过 openpyxl 或 pandas,但被合并单元格、富文本、公式写入坑过,想换一条更顺的路。下面我按“先跑通最小闭环,再上样式和公式,最后讲封装和排错”的顺序拆一遍,中间会给出可直接复现的代码和参数说明。

2. 最小闭环:Workbook、Worksheet 与 write 的三层关系

2.1 为什么是 Workbook 而不是直接操作文件

xlsxwriter 的设计是“先建工作簿,再建工作表,最后关闭保存”。这个顺序不能乱,因为Workbook()只是在内存里初始化一个 xlsx 结构,真正的磁盘写入发生在close()那一刻。如果你忘了调close(),文件要么不存在,要么是个 0 字节的空壳。常见做法是配合try/finally或者上下文管理器来保证关闭,但 xlsxwriter 本身没有实现__enter__和__exit__,所以得自己包一层。

import xlsxwriter filename = 'demo_basic.xlsx' workbook = xlsxwriter.Workbook(filename) worksheet = workbook.add_worksheet('what') workbook.close()

逻辑说明:Workbook(filename)里的 filename 可以是相对路径,也可以是绝对路径。如果目录不存在,xlsxwriter 不会自动创建,会直接抛FileNotFoundError。add_worksheet('what')里的参数是 sheet 名,不传就默认叫 Sheet1。参数说明:sheet 名不能超过 31 个字符,不能包含[]:*?/\这些字符,否则 Excel 打开时会报错。这一步跑通后,你得到一个空表,接下来才是写数据。

2.2 write 的行列索引与公式写入

write(row, col, data, format)是使用频率最高的方法。row 和 col 都是从 0 开始计数的整数,这一点和 Excel 界面上的 A1 表示法不同。资源里给了一个 expenses 的例子,我把它改成更贴近实际对账的场景,并修正了公式写法。

import xlsxwriter filename = 'demo_write.xlsx' workbook = xlsxwriter.Workbook(filename) worksheet = workbook.add_worksheet('对账') expenses = ( ['Rent', 1000], ['Gas', 100], ['Food', 300], ['Gym', 50], ) row = 0 col = 0 for item, cost in expenses: worksheet.write(row, col, item) worksheet.write(row, col + 1, cost) row += 1 # 写入求和公式,注意 Excel 行号从 1 开始,所以 B1:B4 对应第 1 到第 4 行 worksheet.write(row, col, '=SUM(B1:B4)') workbook.close()

逻辑说明:循环里row从 0 递增,写到第 4 行时循环结束,此时row等于 4,所以公式写在第五行。参数说明:write的第三个参数如果是字符串且以=开头,xlsxwriter 会把它当作公式写入,而不是纯文本。这里有个容易翻车的点:资源原文里写的是=sum(B0:B4),但 Excel 的行号是从 1 开始的,B0 不是合法单元格,打开后公式会变成#NAME?。正确写法是=SUM(B1:B4)。另外,公式里的函数名大小写不敏感,但为了可读性建议统一大写。

2.3 样式对象 add_format 的创建时机

样式必须在Workbook对象上创建,不能在Worksheet上创建。add_format()返回一个格式对象,可以反复传给不同的write调用。资源里用bold = workbook.add_format({'bold': True})做了一个加粗样式,这个思路是对的,但实际业务里往往需要组合多个属性。

import xlsxwriter filename = 'demo_format.xlsx' workbook = xlsxwriter.Workbook(filename) worksheet = workbook.add_worksheet('样式') bold = workbook.add_format({'bold': True}) money = workbook.add_format({'num_format': '#,##0.00'}) center = workbook.add_format({'align': 'center', 'valign': 'vcenter'}) worksheet.write(0, 0, '项目', bold) worksheet.write(0, 1, '金额', bold) worksheet.write(1, 0, 'Rent', center) worksheet.write(1, 1, 1000, money) workbook.close()

逻辑说明:add_format接收一个字典,键是属性名,值是属性值。bold控制加粗,num_format控制数字显示格式,align和valign控制水平和垂直对齐。参数说明:num_format的写法遵循 Excel 的自定义格式规则,#,##0.00表示千分位加两位小数。如果你不确定某个属性名,可以去翻 xlsxwriter 源码里的property字典,那里列出了所有支持的键。常见做法是把这些格式定义集中在一个函数里,避免散落在业务代码中。

3. 把样式、合并、富文本串起来:一份可复用的报表生成函数

3.1 用 Struct 和 dict_merge 管理格式字典

资源里给了一个Struct类,把字典变成点语法访问,再配合dict_merge做格式继承。这个思路在格式多的时候能减少重复代码。我把它整理成更清晰的版本,并补上注释。

class Struct(dict): """为字典加上点语法访问,例如 o.a 等价于 o['a']""" def __init__(self, dictobj=None): super().__init__() if dictobj: self.update(dictobj) def __getattr__(self, name): if name.startswith('__'): raise AttributeError return self.get(name) def __setattr__(self, name, val): self[name] = val def dict_merge(*args): """合并多个字典,后面的覆盖前面的""" result = {} for arg in args: if isinstance(arg, dict): result.update(arg) return result

逻辑说明:Struct继承自 dict,__getattr__让不存在的键返回 None 而不是抛异常,这样在拼格式时不会因为少写一个键就崩掉。dict_merge用来做格式继承,比如先定义基础格式,再在基础格式上叠加加粗或背景色。参数说明:dict_merge只接受字典参数,非字典会被跳过,这样调用时不用做类型判断。

3.2 合并单元格与富文本首行

资源里的场景是:第一行合并 A1:F1,然后在里面用富文本拼出“地区:xxx 姓名:xxx 手机:xxx 创建时间:xxx”。这个需求在报表里很常见,xlsxwriter 用merge_range做合并,用write_rich_string做富文本。

import xlsxwriter filename = 'demo_rich.xlsx' workbook = xlsxwriter.Workbook(filename) worksheet = workbook.add_worksheet('报表') base = {'font_name': '宋体', 'font_size': 11, 'align': 'center', 'valign': 'vcenter', 'text_wrap': True} bold = workbook.add_format(dict_merge(base, {'bold': True})) normal = workbook.add_format(base) worksheet.merge_range('A1:F1', '') conditions = [] province, city, county = '省', '市', '地区' name, phone, date = '姓名', '电话', '2018-6' if province or city or county: conditions.append(bold) conditions.append('地区:') conditions.append(normal) conditions.append('%s%s%s ' % (province, city, county)) if name: conditions.append(bold) conditions.append('姓名:') conditions.append(normal) conditions.append('%s ' % name) if phone: conditions.append(bold) conditions.append('手机:') conditions.append(normal) conditions.append('%s ' % phone) if date: year, month = date[0:4], date[5:7] conditions.append(bold) conditions.append('创建时间:') conditions.append(normal) conditions.append('%s/%s ' % (year, month)) if conditions: worksheet.write_rich_string('A1', *conditions) workbook.close()

逻辑说明:merge_range('A1:F1', '')先把 A1 到 F1 合并成一个单元格,第二个参数传空字符串占位。write_rich_string的第一个参数是单元格地址,后面跟的是“格式对象 + 文本片段”交替出现的序列。参数说明:富文本里每个文本片段前面必须跟一个格式对象,否则会报错。write_rich_string不支持公式,也不支持在合并单元格的非左上角单元格上调用。常见坑是:如果conditions列表为空,write_rich_string会抛异常,所以外面要加if conditions判断。

3.3 表头、斑马纹与列宽设置

表头加粗、数据行斑马纹、列宽固定,这三件事几乎每个报表都要做。资源里用row_index % 2 != 0来判断奇偶行,然后套不同的格式。我把它整理成更直观的写法。

import xlsxwriter filename = 'demo_table.xlsx' workbook = xlsxwriter.Workbook(filename) worksheet = workbook.add_worksheet('表格') base = {'font_name': '宋体', 'font_size': 11, 'align': 'center', 'valign': 'vcenter', 'border': 1} first_row = workbook.add_format(dict_merge(base, {'bold': True})) normal_row = workbook.add_format(base) even_row = workbook.add_format(dict_merge(base, {'bg_color': '#dddddd'})) cols = ['姓名', '电话', '地区'] for col_index, col in enumerate(cols): worksheet.write(0, col_index, col, first_row) data_list = [ {'name': 'Spencer', 'tel': '13888888888', 'reg': '中国'}, {'name': 'Jeff', 'tel': '139999999999', 'reg': '台湾省'}, ] for row_index, item in enumerate(data_list, start=1): fmt = even_row if row_index % 2 == 0 else normal_row worksheet.write(row_index, 0, item['name'], fmt) worksheet.write(row_index, 1, item['tel'], fmt) worksheet.write(row_index, 2, item['reg'], fmt) worksheet.set_column(0, 0, 20) worksheet.set_column(1, 1, 15) worksheet.set_column(2, 5, 20) workbook.close()

逻辑说明:enumerate(data_list, start=1)让数据行从第 1 行开始(第 0 行是表头),这样row_index % 2 == 0就是偶数行,套灰色背景。set_column(first_col, last_col, width)设置列宽,第一个参数是起始列索引,第二个是结束列索引,第三个是宽度。参数说明:列宽的单位是字符数,20 表示大约能显示 20 个半角字符。如果某列内容特别长,可以配合text_wrap自动换行,但行高不会自动调整,需要手动set_row。

4. 避坑与排查:xlsxwriter 用错会翻车的五个地方

4.1 公式写进去变成文本或报 #NAME?

现象:打开生成的 xlsx,公式单元格显示的是=SUM(B0:B4)这串字符本身,或者显示#NAME?。原因:一是write的第三个参数虽然以=开头,但如果你用了write_string而不是write,它会被当作文本;二是公式里的单元格引用不合法,比如 B0。解决:统一用write写公式,并且检查行号从 1 开始。如果公式里引用了中文 sheet 名,要用单引号包起来,例如='对账'!B1。

4.2 忘记 close 导致文件为空

现象:脚本跑完没报错,但目标文件不存在,或者存在但大小为 0。原因:Workbook在内存里缓存所有写入操作,只有close()才会真正落盘。解决:把close()放在finally块里,或者用contextlib包一个上下文管理器。常见做法是写一个with workbook_context(filename) as (wb, ws):的辅助函数,但 xlsxwriter 本身不提供,需要自己实现。

4.3 合并单元格后写入非左上角单元格报错

现象:调用了merge_range('A1:F1', ''),然后又调write('B1', 'x'),程序抛OverlappingRange或类似异常。原因:合并后的区域只有左上角单元格可写,其他单元格被锁定。解决:所有内容都写到左上角,或者用write_rich_string在左上角拼富文本。如果确实需要往合并区域的其他位置写,只能取消合并,改用普通单元格加边框模拟。

4.4 富文本片段缺少格式对象

现象:write_rich_string报TypeError或生成的单元格内容错乱。原因:富文本的参数序列必须是“格式对象、文本、格式对象、文本……”交替出现,如果连续两个文本片段之间没有格式对象,就会解析失败。解决:每个文本片段前面都显式传一个格式对象,哪怕这个格式是空的workbook.add_format({})。另外,富文本不支持write的公式参数,也不支持在同一个单元格上重复调用。

4.5 列宽设置不生效或覆盖

现象:调了set_column,但打开后列宽还是默认值。原因:set_column的调用时机必须在close()之前,而且如果同一列被多次设置,后面的会覆盖前面的。另外,如果对合并单元格所在的列设置列宽,合并区域的列宽以左上角所在列的设置为准。解决:把所有set_column集中放在写数据之后、close()之前,并且检查是否有重复设置同一列。常见做法是先用一个列表记录每列需要的宽度,最后统一设置。

5. 进阶技巧:把 xlsxwriter 包成业务友好的导出器

5.1 用配置字典驱动格式,避免硬编码

当报表字段超过十个,格式定义散落在代码里会很难维护。我一般会定义一个FORMAT_CONFIG字典,把每种格式的属性写清楚,然后用一个函数批量创建格式对象。

import xlsxwriter FORMAT_CONFIG = { 'base': {'font_name': '宋体', 'font_size': 11, 'align': 'center', 'valign': 'vcenter'}, 'header': {'bold': True, 'border': 1, 'bg_color': '#f0f0f0'}, 'normal': {'border': 1}, 'even': {'border': 1, 'bg_color': '#fafafa'}, 'money': {'num_format': '#,##0.00', 'border': 1}, } def build_formats(workbook): formats = {} base = FORMAT_CONFIG['base'] for name, cfg in FORMAT_CONFIG.items(): merged = dict(base) merged.update(cfg) formats[name] = workbook.add_format(merged) return formats

逻辑说明:build_formats先取 base 配置作为底,再把每个具体格式的配置合并上去,这样所有格式都继承了字体和对齐。参数说明:FORMAT_CONFIG的键就是格式名,值是要覆盖或新增的属性。如果某个格式不需要继承 base,可以在配置里加一个_no_base标记,然后在循环里跳过合并,但大多数场景下继承是更省事的做法。

5.2 用 set_column 和 set_row 控制版面

列宽和行高是报表观感的关键。set_column可以一次设置多列,set_row可以设置单行高度。下面这个片段演示了如何根据内容长度动态估算列宽。

def auto_width(worksheet, data, start_col=0, min_width=8, max_width=40): """根据数据内容估算列宽,data 是二维列表""" if not data: return col_count = len(data[0]) for col in range(col_count): max_len = 0 for row in data: if col < len(row): text = str(row[col]) # 中文字符按两个宽度计算 length = sum(2 if ord(ch) > 127 else 1 for ch in text) max_len = max(max_len, length) width = min(max(max_len + 2, min_width), max_width) worksheet.set_column(start_col + col, start_col + col, width)

逻辑说明:遍历每一列的所有数据,计算最大显示宽度,中文字符按两个半角宽度估算,最后加上 2 个字符的余量,并限制在min_width和max_width之间。参数说明:start_col是起始列索引,min_width防止空列太窄,max_width防止某列内容过长把版面撑爆。这个函数在数据量不大时够用,数据量上万时建议先采样再估算。

5.3 验证生成结果:用 zipfile 检查内部结构

xlsx 本质上是一个 zip 包,里面是一堆 XML。如果你怀疑生成的文件有问题,可以用 Python 标准库zipfile打开看看内部结构,确认 sheet 和 sharedStrings 是否存在。

import zipfile def inspect_xlsx(path): with zipfile.ZipFile(path, 'r') as z: names = z.namelist() print('文件列表:', names) if 'xl/worksheets/sheet1.xml' in names: with z.open('xl/worksheets/sheet1.xml') as f: content = f.read(500) print('sheet1 前 500 字节:', content)

逻辑说明:zipfile.ZipFile以只读方式打开 xlsx,namelist()返回内部所有文件路径。正常的 xlsx 至少包含[Content_Types].xml、xl/workbook.xml、xl/worksheets/sheet1.xml。参数说明:z.open返回的是二进制流,读取后需要自行解码。这个技巧在排查“文件打不开”或“公式不计算”时特别有用,因为你可以直接看到 XML 里公式节点是否存在。

5.4 一个我踩过的坑:日期格式与 num_format

有一次我往单元格写datetime.date对象,同时套了num_format: 'yyyy/mm/dd',结果 Excel 里显示的是数字而不是日期。原因是 xlsxwriter 对 Python 的 date 和 datetime 有专门的write_datetime方法,如果你用write写日期对象,它会被当作数字处理,而num_format只负责显示格式,不负责类型转换。正确做法是:要么用write_datetime(row, col, date_obj, fmt),要么先把日期转成字符串再写。从那以后我每次写日期字段都强制走一遍write_datetime,并且用num_format控制显示样式,再也没翻过车。希望这些细节能帮到你,少走一点弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询