Python 玩转 Excel 案例【第 9 期】
Python 玩转 Excel 案例【第 9 期】
📌 案例说明
原始数据表: 人员信息
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
目标任务:
在日常数据处理中,我们经常需要向已有的Excel表格中追加新数据。本期案例将跟大家一起学习如何利用Python,像操作数据库一样,简单、高效地将DataFrame数据追加到现有Excel文件的末尾:
核心操作:数据追加
-
• 关键函数: append_to_excel() -
• 核心逻辑:找到Excel中最后一条有数据的行,在其下方开始写入新数据。
📜 完整代码
"""
简单追加数据到Excel
"""
import pandas as pd
from openpyxl import load_workbook
from openpyxl.utils.dataframe import dataframe_to_rows
def find_last_data_row(ws):
"""找到最后一行的有数据的行号(跳过空白行)- 参考main.py的方法"""
# 从最后一行开始往前找
for row in range(ws.max_row, 0, -1):
# 检查这一行是否有数据
for col in range(1, ws.max_column + 1):
if ws.cell(row, col).value is not None:
return row
return 0 # 全空表
def append_to_excel(df_to_append, excel_file, sheet_name):
"""追加数据到Excel - 完全参考main.py的方式"""
if df_to_append is None or len(df_to_append) == 0:
print("没有数据需要追加")
return
print(f"📝 准备追加 {len(df_to_append)} 行数据...")
# 加载工作簿
wb = load_workbook(excel_file)
ws = wb[sheet_name]
# 找到真正的最后一行数据
last_data_row = find_last_data_row(ws)
start_row = last_data_row + 1
print(f"最后数据行: {last_data_row}, 将从第 {start_row} 行开始追加")
# 使用dataframe_to_rows批量写入
for r_idx, row in enumerate(dataframe_to_rows(df_to_append, index=False, header=False), start=start_row):
for c_idx, value in enumerate(row, start=1):
if pd.notna(value): # 只写入非空值
ws.cell(r_idx, c_idx, value)
# 保存文件
wb.save(excel_file)
print(f"✅ 成功追加到第 {start_row}-{start_row+len(df_to_append)-1} 行")
# ============= 直接运行 =============
if __name__ == "__main__":
# 要追加的数据
df_new = pd.DataFrame({
'姓名': ['赵六', '钱七', '孙八'],
'年龄': [32, 27, 29],
'城市': ['深圳', '杭州', '成都']
})
# 执行追加
append_to_excel(df_new, 'chapter-9.xlsx', 'Sheet1')
第一步:导入所需库
import pandas as pd
from openpyxl import load_workbook
from openpyxl.utils.dataframe import dataframe_to_rows
代码解释:
-
• import pandas as pd
导入 pandas 库,用于创建和管理我们想要追加的数据(DataFrame)。 -
• from openpyxl import load_workbook
从 openpyxl 库导入load_workbook函数,它是我们操作Excel文件的核心工具,可以加载一个现有的Excel工作簿。 -
• from openpyxl.utils.dataframe import dataframe_to_rows
导入一个实用工具,它能将pandas的DataFrame高效地转换成适合写入Excel的行数据格式。
第二步:查找最后一行数据
def find_last_data_row(ws):
"""找到最后一行的有数据的行号(跳过空白行)"""
# 从最后一行开始往前找
for row in range(ws.max_row, 0, -1):
# 检查这一行是否有数据
for col in range(1, ws.max_column + 1):
if ws.cell(row, col).value is not None:
return row
return 0 # 全空表
代码解释:
-
• 为什么要从后往前找?
避免把中间空行(如删除数据留下的空白)误认为最后一行,导致新数据与原有数据之间出现空白。从后往前找能准确定位真正有数据的最后一行,确保数据连续追加。 -
• ws.max_row
获取工作表的最大行号(包含空白行)。 -
• 双重循环
外层循环从最大行号倒序遍历每一行;内层循环遍历该行的每一列,检查单元格的值是否为None。 -
• 一旦发现非空值
说明找到了最后一行有数据的位置,立即返回该行号。
📌 关于行和列索引的说明
openpyxl索引规则(操作Excel文件):
-
• Excel的行号和列号都是从1开始计数 -
• ws.cell(row, col)中的row和col必须与Excel实际位置对应 -
• ws.cell(1, 1)表示第1行第1列(A1单元格)
代码执行过程:
for row in range(ws.max_row, 0, -1): # row从最大行号向下到1
for col in range(1, ws.max_column + 1): # col从1到最大列号
if ws.cell(row, col).value is not None:
return row
示例表格:
A B C
1 姓名 年龄 城市 ← 表头(第1行)
2 张三 20 北京 ← 数据行
3 李四 25 上海 ← 数据行
4 王五 30 广州 ← 数据行
5 (空) (空) (空) ← 空行
-
• ws.max_row = 5(共5行) -
• ws.max_column = 3(共3列) -
• 从row=5开始检查:第5行全空 → row=4有数据 → 返回4
💡 注意区分:pandas中DataFrame的行索引是从0开始的(如
df.iloc[0]表示第一行),而openpyxl操作Excel时行号从1开始,两者规则不同。
包含表头的设计:
-
• 表头也是有数据的行,所以计入最后一行的查找范围 -
• 返回4后, start_row = last_data_row + 1 = 5,新数据从第5行开始追加 -
• 这样能正确处理有空行的表格,确保数据连续追加
第三步:定义核心的追加函数
def append_to_excel(df_to_append, excel_file, sheet_name):
"""追加数据到Excel"""
if df_to_append is None or len(df_to_append) == 0:
print("没有数据需要追加")
return
# 加载工作簿和工作表
wb = load_workbook(excel_file)
ws = wb[sheet_name]
代码详解:
-
• def append_to_excel(...):
这是我们本次案例的核心函数,它接收三个参数: -
• 要追加的数据(DataFrame) -
• 目标Excel文件路径 -
• 目标工作表名称。 -
• 空数据检查
如果传入的DataFrame为空,则直接退出,避免无效操作。 -
• 加载文件
使用load_workbook打开Excel文件,并通过工作表名称sheet_name获取到具体的ws(worksheet) 对象。所有后续操作都将基于这个ws对象。
💡 小贴士:将核心逻辑封装成函数,不仅使代码结构清晰,也方便你在其他脚本或Jupyter Notebook中直接调用,实现代码复用。
运算符优先级:为什么有的需要括号,有的不需要?
问题复现
场景一:不需要括号的情况
if df_to_append is None or len(df_to_append) == 0:
# 这里不需要加括号也能正确执行
场景二:必须加括号的情况(昨天的文章)
# ❌ 错误写法(不加括号导致错误)
month_filter = sales_data['成交日期'].dt.year == current_year & \
sales_data['成交日期'].dt.month == current_month
# ✅ 正确写法(每个条件必须加括号)
month_filter = (sales_data['成交日期'].dt.year == current_year) & \
(sales_data['成交日期'].dt.month == current_month)
根本原因:不同的运算符,不同的优先级
1. and / or 是逻辑运算符(优先级低)
# 执行顺序(不需要括号):
if df_to_append is None or len(df_to_append) == 0:
# 第1步:先执行比较运算(优先级高)
df_to_append is None # 比较运算,先执行
len(df_to_append) == 0 # 比较运算,先执行
# 第2步:再执行逻辑运算(优先级低)
结果1 or 结果2 # or 最后执行
为什么不需要括号?
-
• is、==等比较运算符的优先级 高于or/and -
• 程序自然先执行比较,后执行逻辑判断
2. & 是位运算符(优先级高)
# 如果不加括号,实际执行顺序:
sales_data['成交日期'].dt.year == current_year & sales_data['成交日期'].dt.month == current_month
# 第1步:先执行位运算(优先级高) ← ❌ 错误的执行顺序
current_year & sales_data['成交日期'].dt.month # 位运算先执行!
# 第2步:再执行比较运算
sales_data['成交日期'].dt.year == (位运算结果) == current_month
为什么必须加括号?
-
• &的优先级 高于== -
• 不加括号时, &会先执行,导致逻辑错误
运算符优先级对照表
|
|
|
|
|
|---|---|---|---|
| 高 |
|
&
|
|
|
|
|
==!=><isin |
a == b |
| 低 |
|
andornot |
a and b |
验证示例
示例1:逻辑运算符(不需要括号)
# 普通Python逻辑判断
x = 5
if x > 3 and x < 10: # ✅ 可以正常执行
print("OK")
# 等价于(加了括号效果一样)
if (x > 3) and (x < 10): # ✅ 加了括号也行,但没必要
print("OK")
示例2:位运算符(必须加括号)
import pandas as pd
df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
# ❌ 错误:不加括号
filter = df['A'] > 1 & df['B'] < 6 # 报错或逻辑错误
# ✅ 正确:必须加括号
filter = (df['A'] > 1) & (df['B'] < 6) # 每个条件用括号包裹
为什么 pandas 中常用 & 而不是 and?
还是刚才的例子:
import pandas as pd
df = pd.DataFrame({'A': [1, 5, 3], 'B': [4, 2, 7]})
print(df)
输出:
A B
0 1 4
1 5 2
2 3 7
❌ 错误方式:使用 and
# 尝试筛选 A>1 且 B<6 的行
df[(df['A'] > 1) and (df['B'] < 6)] # ValueError
为什么会报错?
-
• (df['A'] > 1)返回的是一个布尔 Series:[False, True, True] -
• (df['B'] < 6)返回的是一个布尔 Series:[True, True, False] -
• and是Python关键字,只能对单个布尔值进行运算,无法处理两个Series -
• 相当于问:”这两个Series整体都为真吗?”——这没有意义
✅ 正确方式:使用 &
# 元素级别的布尔运算
condition = (df['A'] > 1) & (df['B'] < 6)
print(condition)
# 用条件筛选数据
result = df[condition]
print(result)
输出:
0 False # 第0行:1>1? False & 4<6? True = False
1 True # 第1行:5>1? True & 2<6? True = True
2 False # 第2行:3>1? True & 7<6? False = False
dtype: bool
A B
1 5 2 # 只有第1行同时满足两个条件
& 做了什么?
-
• 把两个布尔 Series 的对应位置逐一进行”与”运算 -
• 第0行: False & True = False -
• 第1行: True & True = True -
• 第2行: True & False = False -
• 返回一个新的布尔 Series
关键区别总结
|
|
|
|
|
|---|---|---|---|
and |
|
|
|
& |
|
|
|
打个比方:
-
• and像是问全班同学:”大家都考及格了吗?” → 只回答是或否 -
• &像是问每个同学:”你考及格了吗?” → 得到每个同学的是/否列表
快速判断法则
|
|
|
|
|
|---|---|---|---|
|
|
and
or |
|
and
or优先级低 |
|
|
&
| |
|
&
|
总结
# ✅ 普通逻辑判断 - 用 and/or,不需要括号
if df is None or len(df) == 0:
pass
# ✅ pandas布尔索引 - 用 &/|,必须加括号
filter = (df['列1'] > 10) & (df['列2'] < 20)
# ⚠️ 记住:加括号永远没错,但要知道为什么
# 加括号不仅解决优先级问题,代码也更清晰易读!
第四步:定位追加起点并写入数据
# 找到真正的最后一行数据
last_data_row = find_last_data_row(ws)
start_row = last_data_row + 1
print(f"最后数据行: {last_data_row}, 将从第 {start_row} 行开始追加")
# 使用dataframe_to_rows批量写入
for r_idx, row in enumerate(dataframe_to_rows(df_to_append, index=False, header=False), start=start_row):
for c_idx, value in enumerate(row, start=1):
if pd.notna(value): # 只写入非空值
ws.cell(r_idx, c_idx, value)
代码详解:
-
• 计算起始行
调用我们第二步定义的find_last_data_row函数,得到最后有数据的行号last_data_row。那么,新数据的起始行号就是last_data_row + 1。
逐层解析写入过程
第1层:dataframe_to_rows(df_to_append, index=False, header=False)
-
• dataframe_to_rows
openpyxl提供的函数,专门把pandas的DataFrame转换成一行一行的数据 -
• df_to_append
我们要追加的DataFrame数据 -
• index=False
不写入DataFrame的行号(0,1,2…) -
• header=False
不写入DataFrame的列名(姓名、年龄…)
举个栗子🌰:
假如 df_to_append 是这样的DataFrame:
姓名 年龄 城市
0 赵六 28 深圳
1 钱七 32 广州
dataframe_to_rows 处理后的样子:
第1行:['赵六', 28, '深圳']
第2行:['钱七', 32, '广州']
(去掉了行号0,1和列名”姓名”,”年龄”,”城市”)
第2层:enumerate(iterable, start=start_row)
enumerate(dataframe_to_rows(...), start=start_row)
-
• enumerate
Python内置函数,给可迭代对象中的每个元素加上编号 -
• iterable
要枚举的可迭代对象(这里是dataframe_to_rows生成的行数据) -
• start=n
编号从n开始(如start=4表示从4开始编号) -
• 返回值 (索引, 值)的配对,如(4, ['赵六',28,'深圳'])
举个栗子🌰:
假如start_row=4,数据有两行:
枚举结果:
第1个数据(赵六那行)→ 编号4
第2个数据(钱七那行)→ 编号5
enumerate的返回值是成对的:(编号, 数据行)
第3层:外层for循环
for r_idx, row in enumerate(...):
-
• for: Python的循环语句 -
• r_idx: 行号变量(如4,5,6…) -
• row: 行数据变量(如['赵六',28,'深圳']) -
• in: 从…中取数据 -
• enumerate(...): 枚举函数,给每行数据分配编号
循环过程:
第1次循环:r_idx=4, row=['赵六',28,'深圳']
第2次循环:r_idx=5, row=['钱七',32,'广州']
第4层:内层enumerate
enumerate(row, start=1)
这又是给每一行的每个单元格编号:
-
• row=['赵六',28,'深圳'] -
• enumerate(row, start=1)的结果:第1个单元格(赵六)→ 编号1
第2个单元格(28) → 编号2
第3个单元格(深圳)→ 编号3
第5层:内层for循环
for c_idx, value in enumerate(row, start=1):
-
• c_idx
列号,从1开始(A列=1,B列=2…) -
• value
单元格的值(如’赵六’、28、’深圳’)
循环过程:
第1次循环:c_idx=1, value='赵六'
第2次循环:c_idx=2, value=28
第3次循环:c_idx=3, value='深圳'
第6层:空值判断
if pd.notna(value):
-
• notna()
检查是否不是空值(NaN/None) -
• value
要检查的单元格的值
作用:
-
• 如果 value是正常的数值或文字 →pd.notna()返回True,执行写入 -
• 如果 value是None或NaN(空值)→pd.notna()返回False,跳过不写
为什么要有这个判断?
保持Excel格式!如果单元格是空的,就不去动它,保留它原来的格式(比如背景色、边框等)。
第7层:写入Excel
ws.cell(r_idx, c_idx, value)
-
• ws: 工作表对象(Sheet) -
• .cell(): 单元格操作方法 -
• r_idx: 行号(如4) -
• c_idx: 列号(如1) -
• value: 要写入的值(如’赵六’)
实际效果:
ws.cell(4, 1, '赵六') → 在A4单元格写入"赵六"
ws.cell(4, 2, 28) → 在B4单元格写入28
ws.cell(4, 3, '深圳') → 在C4单元格写入"深圳"
ws.cell(5, 1, '钱七') → 在A5单元格写入"钱七"
...
最终Excel效果:
A B C
1 姓名 年龄 城市 ← 原有的
2 张三 20 北京 ← 原有的
3 李四 25 上海 ← 原有的
4 赵六 28 深圳 ← 新追加
5 钱七 32 广州 ← 新追加
第五步:保存文件
# 保存文件
wb.save(excel_file)
print(f"✅ 成功追加到第 {start_row}-{start_row+len(df_to_append)-1} 行")
代码详解:
-
• wb.save(excel_file)load_workbook将Excel文件加载到内存中成为Workbook对象,我们对工作表的所有写入操作(ws.cell())都是在这个内存对象上进行的。最后必须调用save()方法,将内存中的整个工作簿重新写入磁盘文件(覆盖原文件),这样之前追加的数据才会被永久保存。 -
• 打印成功信息
清晰地告知用户操作结果,追加了多少行,从第几行到第几行,方便核对。
第六步:执行与测试
# ============= 直接运行 =============
if __name__ == "__main__":
# 要追加的数据
df_new = pd.DataFrame({
'姓名': ['赵六', '钱七', '孙八'],
'年龄': [32, 27, 29],
'城市': ['深圳', '杭州', '成都']
})
# 执行追加
append_to_excel(df_new, 'chapter-9.xlsx', 'Sheet1')
执行结果(假设 chapter-9.xlsx 的 Sheet1 中已有15行数据,包括表头):
📝 准备追加 3 行数据...
最后数据行: 15, 将从第 16 行开始追加
✅ 成功追加到第 16-18 行
此时,打开Excel文件,你会发现“赵六”、“钱七”、“孙八”三条数据被正确追加到了原有数据的末尾。
💡 小贴士:
if __name__ == "__main__":这个代码块确保了当我们直接运行这个Python脚本时,测试代码才会执行。如果这个文件被作为模块导入到其他地方,测试代码不会运行,保证了模块的可用性。
📚 本期核心知识点
📍 知识点 1:openpyxl 库的 load_workbook
用于加载一个已有的Excel工作簿文件(.xlsx),返回一个 Workbook 对象,这是所有后续操作的基础。
📍 知识点 2:worksheet.max_row
Worksheet 对象的一个属性,返回工作表中当前包含数据或格式的最大行号。注意,它可能包含空白行,因此不能直接用于定位数据末尾。
📍 知识点 3:遍历单元格查找最后数据行
通过从 max_row 开始逐行向上遍历,并检查每一行的每个单元格是否为 None,可以精确地找到数据的最后一行,避免被空白行干扰。
📍 知识点 4:dataframe_to_rows 函数
一个高效的工具,用于将pandas的DataFrame对象转换为一系列的行元组,便于批量写入Excel工作表。
📍 知识点 5:worksheet.cell() 方法
通过 ws.cell(row, column, value) 可以定位并操作指定单元格。row 和 column 都是从1开始的索引。
📍 知识点 6:pd.notna() 函数
pandas中用于检查数据是否为“非缺失值”的函数。返回 True 表示该值不是NaN或None,可以用来过滤空值,保护目标单元格的原始内容。
📍 知识点 7:Workbook.save() 方法
将对工作簿的所有修改(如添加数据)持久化到磁盘文件中。如果不调用此方法,所有更改都会在程序结束后丢失。
🔄 本案例核心流程
① 加载文件 → ② 定位末尾 → ③ 计算起点 → ④ 转换数据 → ⑤ 逐行写入 → ⑥ 保存文件
加载工作簿 (load_workbook)
↓
找到最后有数据的行 (find_last_data_row)
↓
计算追加起始行 = 最后数据行 + 1
↓
将DataFrame转换为行数据 (dataframe_to_rows)
↓
从起始行开始,循环写入每一行的每个单元格
↓
保存工作簿 (wb.save)
对应代码:
# ① 加载文件
wb = load_workbook(excel_file)
ws = wb[sheet_name]
# ② 定位末尾
last_data_row = find_last_data_row(ws)
# ③ 计算起点
start_row = last_data_row + 1
# ④ & ⑤ 转换并写入
for r_idx, row in enumerate(dataframe_to_rows(df_to_append, index=False, header=False), start=start_row):
for c_idx, value in enumerate(row, start=1):
if pd.notna(value):
ws.cell(r_idx, c_idx, value)
# ⑥ 保存文件
wb.save(excel_file)
📦 资源获取提示
关注「码农自习室」,后台回复关键词 PythonExcel案例,即可获取本文完整代码及配套练习数据集,一起动手掌握高效数据操作的核心技巧!
❤️ 支持我们
如果本文对你有帮助,欢迎点赞 + 关注 + 点亮小红心,你的支持就是我们持续创作的最大动力~
夜雨聆风