乐于分享
好东西不私藏

Python 玩转 Excel 案例【第 9 期】

Python 玩转 Excel 案例【第 9 期】

Python 玩转 Excel 案例【第 9 期】

📌 案例说明

原始数据表: 人员信息

姓名
年龄
城市
张三
25
北京
李四
30
上海

目标任务:

在日常数据处理中,我们经常需要向已有的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
整体判断
单个布尔值
一个True/False
&
逐行判断
pandas布尔索引
布尔Series

打个比方:

  • • and 像是问全班同学:”大家都考及格了吗?” → 只回答是或否
  • • & 像是问每个同学:”你考及格了吗?” → 得到每个同学的是/否列表

快速判断法则

场景
使用运算符
是否需要括号
原因
普通Python条件判断
and

 / or
❌ 不需要
and

/or优先级低
pandas布尔索引
&

 / |
✅ 必须加
&

优先级高

总结

# ✅ 普通逻辑判断 - 用 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,执行写入
  • • 如果valueNoneNaN(空值)→ 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案例,即可获取本文完整代码及配套练习数据集,一起动手掌握高效数据操作的核心技巧!


❤️ 支持我们

如果本文对你有帮助,欢迎点赞 + 关注 + 点亮小红心,你的支持就是我们持续创作的最大动力~

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Python 玩转 Excel 案例【第 9 期】

猜你喜欢

  • 暂无文章