☰
Python表格修饰实战:openpyxl样式与pandas.style技巧
2026/10/8 3:26:58 网站建设 项目流程

用Python处理表格这件事,大家基本都会写两行pandas,读CSV、做筛选、算个透视表,操作很熟练。但一提到“表格修饰”,很多人就卡住了:要么是用openpyxl写十来行代码结果样式全丢,要么是DataFrame在Jupyter里渲染出来一片白底黑字,自己看着都嫌弃,更别提交给业务方了。我最近帮团队搭了一个月度销售数据的自动报表脚本,核心需求就是从数据库把数拉出来之后,输出一份能让业务同事直接打开看的Excel——带表头颜色、数据条、百分比格式、冻结窗格那种“别人家的表格”。折腾了几天之后,我决定把整个思路、代码、踩坑过程都整理出来,这篇就专门聊Python对表格的修饰。

先说说我的判断:这个主题看起来简单,其实牵扯到两条完全不同的技术线。一条是用openpyxl或xlsxwriter直接操作Excel文件本身,把单元格样式、条件格式、列宽这些属性写进去;另一条是用pandas自带的可视化能力,把DataFrame渲染成带渐变背景、色阶、条形填充的HTML表格,用于数据分析报告、邮件正文、网页展示。两条线的应用场景不同,但很多人不知道什么时候该用哪个,经常是拿openpyxl去调显示样式、拿DataFrame.style去做导出文件,结果两件事都做得别扭。这篇内容我会把两条线的取舍逻辑讲清楚,再给一份可以直接抄作业的完整案例。

1. 表格修饰到底在解决什么问题

1.1 三种最常见的需求场景

结合我自己的项目经验,Python对表格修饰这个需求,现实中通常是从三种场景里冒出来的。

第一种是程序导出Excel文件,但生成出来的表格“灰头土脸”。比如你用pandas的to_excel直接导出一份数据,表头是默认的黑体字,单元格没有边框,数字不带千分位,日期格式一会儿2024-01-15一会儿2024/1/15。这种表在自己调试时无所谓,但一旦发给客户、领导或者外部门同事,对方第一印象就是“这数据不专业”。这里修饰的重点是样式:字体、字号、颜色、边框、对齐、数字格式、列宽。

第二种场景是数据分析报告中的表格展示。你在Jupyter Notebook里做了统计,想直接把结果表贴到PPT或者内部Wiki里。默认的DataFrame样式丑到爆,而且一列数字密密麻麻根本看不清趋势。这种场景需要的是背景渐变、色阶、数据条、突出最大值最小值等视觉元素。这些不是Excel的范畴,而是HTML/CSS的渲染效果,pandas的Styler对象就是为这个而生的。

第三种常见需求是批量处理多张同等结构的工作表,比如每月都生成同样格式的周报、多门店同结构的销售表、分城市的日报表。人工在Excel里改格式,一组50个文件能改到人麻。这时候Python的价值不是“做一张好看的表”,而是“让50张表都长得一模一样”。这个场景下,样式逻辑一旦封装成函数,后续每个月的报表就只剩“喂数据、出文件”两步操作。

1.2 工具选型:openpyxl、xlsxwriter、pandas.style怎么选

既然有两条技术线,工具选择就很重要。我把它整理成一个简单对照逻辑,不一定严谨,但足够帮你做决策。

工具擅长的事不适合的事使用场景
openpyxl读写Excel文件,修改已有工作簿,支持样式、公式、图表大文件写入性能较差,不支持老式xls需要修改现有Excel模板、逐格控制样式
xlsxwriter创建新xlsx文件,写入性能好,条件格式丰富不能读取已有文件,不能处理xls从零生成报表,大数据量写入
pandas.DataFrame.style基于HTML/CSS的表格渲染,支持渐变、色阶、格式化不能直接改Excel文件的单元格样式,输出的HTML需要浏览器渲染Jupyter报告、导出HTML/图片用于展示

我个人的经验是:如果你要的是“文件级修饰”,也就是最终交付一个打开就能用的Excel文件,优先考虑openpyxl,因为它的读写能力和对已有文件的兼容性都更好,而且可以用模板文件作为底子,改起来更灵活。如果你每次都是全新生成一个报表,不涉及读取已有文件,xlsxwriter写入速度快很多,条件格式种类也更丰富。

而pandas.style那条线,实际上和Excel文件修饰是平行关系。它输出的是HTML字符串或者图片,适合嵌在Jupyter里看、贴到在线文档、或者做成报告素材。很多人误以为DataFrame.style能直接改Excel,其实它压根不碰Excel文件格式,这点先要分清。

2. openpyxl:给Excel表格做一次“彻底整形”

2.1 字体、边框和填充色的设置细节

openpyxl的样式体系,核心是Font、PatternFill、Alignment、Border这四件套。初次接触的人最容易被几个小细节坑到,先说清楚。

from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side wb = Workbook() ws = wb.active # 常规套路:先定义好样式对象,再赋给单元格 header_font = Font(name="微软雅黑", bold=True, color="FFFFFF", size=11) header_fill = PatternFill(start_color="305496", end_color="305496", fill_type="solid") header_align = Alignment(horizontal="center", vertical="center") thin_side = Side(style="thin", color="BFBFBF") border_all = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side) for col in range(1, 6): cell = ws.cell(row=1, column=col) cell.font = header_font cell.fill = header_fill cell.alignment = header_align cell.border = border_all

这段代码里有几个关键点。PatternFill如果不写fill_type="solid",你会发现颜色根本没显示出来,这是出现频率极高的新手坑,很多帖子的示例代码都是PatternFill(start_color="", end_color=""),看着像没问题,实际跑起来颜色是空的。其次,Side必须使用style="thin"而不是自己设置粗细数值,见过有人用style=2然后报错,这里只认字符串形式。

字体方面,如果你最终文件要在Windows环境的Excel里打开,中文字体建议用微软雅黑,不要用什么花哨字体,否则对方电脑没安装这个字体,回退效果非常难看。数字方面,如果你设置了千分位格式但数据本身是字符串,Excel虽然能显示成数字样式,但排序和求和会出问题,所以最好在写入前把类型转好,别指望样式替你转换类型。

2.2 条件格式:用数据条说话

条件格式是我最喜欢用的修饰手段。原因很简单:手动画色只能表达“好”和“坏”,而数据条和色阶能表达“到底有多好、多坏”。openpyxl里数据条不算复杂,但范围填错会一点效果都看不到。

from openpyxl.formatting.rule import DataBarRule # 给D2到D30单元格加数据条 rule = DataBarRule( start_type="min", end_type="max", color="638EC5", showValue=True, ) ws.conditional_formatting.add("D2:D30", rule)

这段代码的逻辑是自动按范围内的最大最小值拉伸数据条长度,你不需要手动设定阈值,数据一变,条的长度自动跟着变,这就是比静态填充好在哪。色阶规则类似,用ColorScaleRule可以实现“低值红、中值黄、高值绿”的效果。

一个经验:条件格式的范围一定要和实际数据范围严格匹配,多一个空行或者少一个空行,效果就缺一块或者多一条空条。如果你不确定范围,可以用ws.max_row和ws.max_column动态计算,别手写死数字。

2.3 列宽、冻结窗格与打印布局

表格修饰不能只看“脸”,用户体验同样重要。列宽不调整,内容要么挤成一坨,要么宽松到看不懂;不冻结首行,滚动几屏后表头不见了,看数据的人得来回滚到头上去核对列名。

ws.column_dimensions["A"].width = 12 ws.column_dimensions["B"].width = 10 ws.freeze_panes = "A2" # 冻结第一行 ws.auto_filter.ref = ws.dimensions # 给全表加筛选按钮 ws.page_setup.orientation = "landscape" # 横向打印 ws.page_setup.fitToWidth = 1 # 按宽度缩放 ws.page_setup.fitToHeight = 0 # 不限页数

freeze_panes这个参数很容易理解错,它不是写要冻结的行数,而是写“冻结点”的坐标,A2表示从A2这个位置开始滚动,也就是第一行不滚。类似地,要冻结前两列就写C1。打印设置里fitToWidth=1是自动把表格缩成一页宽,这个对数据列很多的情况很管用,不然打印出来右边几列被截断,业务同事得拼纸看。

3. pandas.style:数据分析报告里的表格美化

3.1 链式方法快速生成彩色样式

如果你要修饰的不是Excel文件,而是数据分析展示用的表格,pandas自带的Styler才是正确工具。它和openpyxl完全不是一个思路:Styler生成的不是单元格样式属性,而是HTML的CSS内联样式,配合浏览器渲染出各种渐变效果。

import pandas as pd df = pd.DataFrame({ "城市": ["上海", "北京", "广州", "深圳", "杭州"], "销售额": [1200, 980, 750, 840, 660], "毛利率": [0.35, 0.28, 0.31, 0.22, 0.26], }) styled_df = ( df.style .format({"销售额": "¥{:,.0f}", "毛利率": "{:.2%}"}) .background_gradient(cmap="Greens", subset=["销售额"]) .bar(color="#5B9BD5", subset=["毛利率"]) .set_properties(**{"border": "1px solid #E0E0E0", "text-align": "center"}) )

这段代码的链式结构很清晰:format负责数字显示格式,background_gradient给销售额列加绿色渐变底色,值越大背景色越深,bar在毛利率列里画条形图,set_properties是统一设置边框和居中。

这里有个细节:background_gradient里的subset如果不写,渐变会作用于所有数值列,效果很容易花,所以明确指定列名是必须习惯。颜色映射选Greens、Blues这类单向色,效果干净;红绿双色映射通常用于涨跌对比。

3.2 导出HTML、图片和PDF的几种姿势

DataFrame.style的一大问题是:它只能在支持HTML渲染的环境里看到效果。如果你在Jupyter里看没问题,但想要PNG图片贴到文档里,就需要额外手段。我试过几种方式,各有适用范围。

最简单的导出是HTML字符串:

html_content = styled_df.to_html() with open("table.html", "w", encoding="utf-8") as f: f.write(html_content)

这样导出的HTML可以直接在线文档中转Word、贴进企业微信等。另外还有dataframe_image这个库可以把Styler渲染成PNG,但它在底层会调用无头浏览器,环境里没有安装对应组件会报错,不太适合纯脚本环境。

如果只是临时截图,我推荐一个土办法:在浏览器里打开to_html导出的文件,然后用截图工具截取。虽然手动一点,但效果稳定,不会遇到一堆依赖问题。还有一种做法是用matplotlib把表格画出来,虽然风格比较老,但胜在完全可控,适合做简单汇总表输出成PDF。

4. 完整实操:把月度销售报表打扮成“别人家的表格”

4.1 准备环境和模拟数据

下面进入实战环节。因为原始需求只是“Python对表格修饰”,没有具体数据,我这里造了一份模拟月度销售数据,场景是各城市分品类销售情况,比较接近真实业务里常见的数据结构。先装依赖,别装多了,pandas和openpyxl就够。

pip install pandas openpyxl

环境弄好之后,造数据。这里我用随机数生成一份,日期固定在当月,方便模拟月度报表演示。

import pandas as pd import numpy as np from datetime import date np.random.seed(42) cities = ["上海", "北京", "广州", "深圳", "杭州"] categories = ["数码", "家电", "服饰", "食品"] data = [] for city in cities: for cat in categories: data.append({ "城市": city, "品类": cat, "销售额": int(np.random.uniform(80000, 300000)), "订单量": int(np.random.uniform(300, 1500)), "毛利率": round(np.random.uniform(0.15, 0.45), 3), }) df = pd.DataFrame(data) df.insert(0, "日期", date.today().strftime("%Y-%m-%d"))

这份数据不需要清洗,但现实中的表大概率有缺失值、无意义空格、类型不一致的问题。建议先做一步类型检查,比如df.dtypes看是否都是数值型,别等到Excel样式加完了才发现销售额是字符串。

4.2 一步步给表格“上妆”

先用pandas把维度汇总一下,增加一行合计,这样最后Excel里的表在逻辑上是完整的。合计行这样加:先对数值列sum,再把城市列填成“合计”,品类列填空。

summary = df.groupby(["城市", "品类"])[["销售额", "订单量", "毛利率"]].sum().reset_index() summary["毛利率"] = summary["销售额"] / df.groupby(["城市", "品类"])["销售额"].sum() * 0 # 占位

毛利率这个字段直接求和是没意义的,正确做法是重新计算加权平均毛利率,但为了演示方便这里我先占位,后面步骤里会改成基于销售额加权。

接着交给openpyxl做修饰。核心思路是:先写数据,再统一调样式,最后配置列宽、冻结、打印。我不主张逐格边写边改样式,因为那样代码分散、后期难维护,还容易把逻辑绕晕。

from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.formatting.rule import DataBarRule from openpyxl.utils import get_column_letter wb = Workbook() ws = wb.active header_font = Font(name="微软雅黑", bold=True, color="FFFFFF", size=11) header_fill = PatternFill(start_color="305496", end_color="305496", fill_type="solid") center_align = Alignment(horizontal="center", vertical="center") left_align = Alignment(horizontal="left", vertical="center") thin_side = Side(style="thin", color="BFBFBF") border_all = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side) total_rows = summary.shape[0] + 1 # 加表头行 # 写表头 headers = ["日期", "城市", "品类", "销售额", "订单量", "毛利率"] for col, head in enumerate(headers, start=1): cell = ws.cell(row=1, column=col, value=head) cell.font = header_font cell.fill = header_fill cell.alignment = center_align cell.border = border_all # 写数据 for row_idx, record in enumerate(summary.itertuples(index=False), start=2): ws.cell(row=row_idx, column=1, value=record[0]).alignment = center_align ws.cell(row=row_idx, column=2, value=record[1]).alignment = center_align ws.cell(row=row_idx, column=3, value=record[2]).alignment = left_align sales_cell = ws.cell(row=row_idx, column=4, value=record[3]) sales_cell.number_format = "#,##0" sales_cell.alignment = right_align ...

实际写的时候首位对齐变量要提前定义,上面这段省文了,完整脚本我会把所有代码整理在一个地方。到这里,表格的基本框架就搭好了,剩下的就是上颜色、加数据条、加自动筛选。

数据条我加在销售额列,让最大的数值条最长,一眼能看出哪个城市哪个品类跑得最好。毛利率列我用黄色作为文字颜色,再配合百分比格式,达到一种“重点信息突出”的效果。最后设置列宽、冻结窗格、加筛选按钮,整个文件交付出去就是一张能直接看的业务报表。

4.3 这个过程中踩过的三个坑

第一个坑是合并单元格的边框问题。我用merge_cells做了一个大标题,结果发现合并区域的边框只显示在外面一圈,内部线条全断。原因是openpyxl的Border只作用于左上角的单元格,所以对合并区域里的每个单元格都要逐个设置Border,否则打印出来样式是花的。

第二个坑是openpyxl在大批量单元格上逐个设置样式,性能很差。一开始我循环20行没什么感觉,后来扩展到几千行数据,脚本从秒级变成几十秒。解决办法是批量操作,比如用for row in ws.iter_rows(min_row=2, max_row=total_rows, min_col=1, max_col=6)循环,而不是每个单元格单独ws.cell,循环次数一样但代码执行速度会好很多,尤其在数据量上万时差距明显。

第三个坑是数字格式的优先级。我发现设置number_format = "0.0%"以后,Excel里显示的是百分比,但排序和求和仍然站在原始数值基础上,这没问题;但如果原始数据是字符串,比如毛利率是“35%”这种文本,number_format根本不会生效。所以写入前我必须确保数据是float格式,实在不行就用pd.to_numeric(..., errors="coerce")转一遍。

5. 常见问题排查与性能心得

5.1 报错速查表

我把之前用过openpyxl和pandas.style时遇到的典型报错整理成一个速查表,都是实际能遇见的场景。

报错信息常见原因解决办法
AttributeError: 'str' object has no attribute 'font'把单元格的名字当成了单元格对象,比如对"A1"赋值样式用ws["A1"]或ws.cell(row, column)获取Cell对象
颜色填充不生效PatternFill没写fill_type="solid"补上fill_type="solid"
打开Excel提示文件损坏条件格式范围引用无效,或者合并单元格与样式不一致确认conditional_formatting.add范围存在,避免合并区交叉
ModuleNotFoundError: No module named 'openpyxl'环境未安装库pip install openpyxl
Styler的applymap报错pandas版本升级后applymap改名成map新版用df.style.map(...)
导出HTML后效果消失CSS内联样式被某些平台过滤用to_html()后用浏览器环境渲染后再截图

这些报错单拎出来都很简单,但混在一起会让人抓狂,我也曾经在一个样式死活不生效的问题上查了快一个小时,最后才发现是fill_type没写全,只能说踩过一次就记住了。

5.2 性能经验与代码复用建议

如果只是几十行的表,用openpyxl逐格设置样式完全没问题。但数据量上千后,特别是循环里套了样式对象的创建,性能下降肉眼可见。我的建议是:把所有样式对象在循环外创建好,循环里只做赋值,不要反复Font(...)新建对象。另外多用iter_rows而不是cell访问,少写点代码还能少点出错概率。

代码复用方面,我现在习惯把“表头样式”、“数据区样式”、“合计行样式”分别封装成函数,接收一个worksheet对象,在里面统一设置。这样以后任何一张新报表过来,只要调用这三个函数,再改改列名,格式就完美统一了,省掉每次从零开始的麻烦。更进一步,可以把样式字典化,比如列名到样式的映射,这样多列差异化样式管理起来特别清晰。

最后再分享一个小习惯:每次生成完报表,我会用openpyxl再重新打开它,检查一遍每个sheet的dimensions、freeze_panes、auto_filter.ref,确认这些属性没有因为写入顺序而丢失。这一步虽然多写几行代码,但能避免太多“明明脚本跑完了,交付文件却缺了筛选”这种尴尬场景。表格修饰这件事,说到底不是让表格变得多花哨,而是让读表的人用最低的理解成本拿到有效信息,所有的颜色、边框、数据条,都是为这个目标服务的。

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

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

立即咨询