上一期我们实现了数据追加功能,但有一个局限:模板中的公式不会被复制到新行。
比如模板中有一条示例数据,F列有公式 =D2*E2。追加新数据后,第3行、第4行...的F列是空的,需要手动拖拽填充,非常麻烦。
本案例解决这个问题:自动识别模板中最后一条数据行的公式,根据行号偏移量动态调整,填充到每个新数据行。
模板文件(模板.xlsx):
数据文件(销售数据.xlsx):
(共44行数据,此处省略中间部分)
需求:
=D3*E3,第4行为 =D4*E4...)核心操作:
(目标行号 - 公式来源行号)关键库:pandas、openpyxl、re
"""
Excel模板填充工具 - 进阶版(自动填充公式)
功能:追加数据,并自动填充调整行号后的公式
"""
import pandas as pd
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
import re
# 配置
TEMPLATE = "模板.xlsx"
DATA_FILE = "销售数据.xlsx"
OUTPUT = "结果_进阶版.xlsx"
def adjust_formula(formula, row_offset):
"""调整公式中的行号(如 D2*E2 → D(2+offset)*E(2+offset))"""
def replace(match):
col = match.group(1)
row = int(match.group(2))
return f"{col}{row + row_offset}"
return re.sub(r'([A-Z]+)(\d+)', replace, formula)
# 1. 读取数据
df = pd.read_excel(DATA_FILE)
rows = len(df)
print(f"读取数据: {rows}行")
# 2. 加载模板
wb = load_workbook(TEMPLATE)
ws = wb.active
# 3. 从下往上找最后一个有数据的行
START_ROW = 2
for row in range(ws.max_row, START_ROW - 1, -1):
if any(cell.value is not None for cell in ws[row]):
last_row = row
break
else:
last_row = START_ROW - 1
print(f"模板现有数据: 最后一行是第{last_row}行")
# 4. 确定数据写入的起始行
write_start_row = last_row + 1
print(f"新数据将从第{write_start_row}行开始写入")
# 5. 获取列映射(根据表头找到对应的列)
headers = [cell.value for cell in ws[1]]
col_map = {}
for idx, header in enumerate(headers, start=1):
if header in df.columns:
col_map[header] = idx
print(f"列映射: {col_map}")
# 6. 保存模板行的公式(从最后一条数据行获取)
template_formulas = {}
if last_row >= START_ROW:
for j in range(1, len(headers) + 1):
cell = ws.cell(row=last_row, column=j)
if isinstance(cell.value, str) and cell.value.startswith('='):
template_formulas[j] = cell.value
if template_formulas:
print(f"找到公式列: {[get_column_letter(col) for col in template_formulas.keys()]}")
# 7. 追加写入新数据
print("追加写入新数据...")
for i, row in df.iterrows():
target_row = write_start_row + i
for col_name, col_idx in col_map.items():
value = row[col_name]
if not pd.isna(value):
ws.cell(row=target_row, column=col_idx, value=value)
print(f"数据写入完成,共 {rows} 行")
# 8. 填充公式
if template_formulas:
print("填充公式...")
for i in range(rows):
target_row = write_start_row + i
row_offset = target_row - last_row
for col_idx, base_formula in template_formulas.items():
target_cell = ws.cell(row=target_row, column=col_idx)
if target_cell.value is None:
adjusted = adjust_formula(base_formula, row_offset)
target_cell.value = adjusted
print(f"公式填充完成")
# 9. 保存
wb.save(OUTPUT)
print(f"完成!→ {OUTPUT}")
print(f"数据范围: 第{write_start_row}行 到 第{write_start_row + rows - 1}行")运行结果(结果_进阶版.xlsx):
(共45行数据:原有1行 + 新追加44行,每行F列都有正确的公式)
import pandas as pd
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
import repandas:读取数据文件openpyxl:操作 Excel 文件get_column_letter:将列号转为字母(如 6→"F"),用于打印调试信息re:正则表达式,用于匹配和替换公式中的行号def adjust_formula(formula, row_offset):
def replace(match):
col = match.group(1)
row = int(match.group(2))
return f"{col}{row + row_offset}"
return re.sub(r'([A-Z]+)(\d+)', replace, formula)作用:将公式中所有的行号增加 row_offset。
执行示例:
adjust_formula("=D2*E2", 1) # 返回 "=D3*E3"
adjust_formula("=D2*E2", 8) # 返回 "=D10*E10"第1行:def adjust_formula(formula, row_offset):
formula:原始公式字符串,如 "=D2*E2"row_offset:行号偏移量,整数,如 1第2行:def replace(match):
replacere.sub 调用,处理每个匹配到的结果第3行:col = match.group(1)
match 是正则匹配到的对象group(1) 返回第一个捕获组的内容,即 ([A-Z]+) 匹配到的列字母"D2" 时,col = "D"第4行:row = int(match.group(2))
group(2) 返回第二个捕获组的内容,即 (\d+) 匹配到的行号"D2" 时,match.group(2) = "2"int() 将字符串转为整数,方便做加法运算第5行:return f"{col}{row + row_offset}"
col="D", row=2, row_offset=1 → 返回 "D3"第6行:return re.sub(r'([A-Z]+)(\d+)', replace, formula)
re.sub 在公式字符串 formula 中查找所有匹配 r'([A-Z]+)(\d+)' 的子串match 对象match 对象作为参数,调用 replace 函数re.sub 的作用:在一个字符串中查找匹配正则表达式的所有子串,并用指定的内容替换它们。
基本语法:
re.sub(pattern, repl, string)pattern | |
repl | |
string |
用法1:字符串替换(固定替换)
import re
result = re.sub(r'\d+', '99', '=D2*E2')
print(result) # "=D99*E99"所有数字都被替换成 "99"。
用法2:函数替换(动态计算)
def add_one(match):
return str(int(match.group(0)) + 1)
result = re.sub(r'\d+', add_one, '=D2*E2')
print(result) # "=D3*E3"每匹配到一个数字,就调用 add_one 函数,用返回值替换原内容。
疑问:def replace(match): 中的 match 参数是怎么传进去的?
答案:re.sub 内部自动创建并传入,你不需要手动传参。
执行流程:
re.sub 在 formula 中查找所有匹配 ([A-Z]+)(\d+) 的子串match 对象replace(match),将 match 对象作为参数传入replace 的返回值替换原匹配内容示意图:
re.sub 扫描 "=D2*E2"
↓
找到 "D2" → 创建 match 对象 → 调用 replace(match) → 返回 "D3"
↓
找到 "E2" → 创建 match 对象 → 调用 replace(match) → 返回 "E3"
↓
返回 "=D3*E3"re.sub(r'([A-Z]+)(\d+)', replace, formula)正则表达式模式:r'([A-Z]+)(\d+)'
r'':原始字符串前缀,告诉 Python 不要转义字符串中的反斜杠。没有这个前缀,\d 中的反斜杠会被 Python 解释为转义字符。():捕获组(Capturing Group)。括号内的匹配结果会被单独保存,可以通过 match.group(1)、match.group(2) 等获取。[A-Z]:字符集,匹配一个大写字母(A 到 Z 之间的任意一个)。+:量词,表示前面的元素出现一次或多次。[A-Z]+ 表示匹配一个或多个连续的大写字母。\d:元字符,匹配任意一个数字(0-9),等价于 [0-9]。\d+:匹配一个或多个连续的数字。捕获组是什么?
捕获组是用小括号 () 包裹的部分,正则引擎会将括号内匹配到的内容单独保存下来,方便后续提取或使用。
如何区分捕获组?
从左到右数左括号 ( 的位置,第1个 ( 就是第1组,第2个 ( 就是第2组,以此类推。
在 ([A-Z]+)(\d+) 中:
( 对应 ([A-Z]+) → 第1捕获组,匹配列字母( 对应 (\d+) → 第2捕获组,匹配行号通过 match.group(n) 获取捕获组内容:
match.group(0) | "D2" | |
match.group(1) | "D" | |
match.group(2) | "2" |
示例:当匹配到 "D2" 时
match.group(0) # "D2"
match.group(1) # "D"
match.group(2) # "2"r'([A-Z]+)(\d+)':正则表达式模式,用于匹配公式中的单元格引用replace:替换函数,接收 match 对象,返回替换后的字符串formula:原始公式字符串,如 "=D2*E2"正则表达式: ([A-Z]+)(\d+)
目标字符串: "=D2*E2"
匹配过程:
第1次扫描 → 找到 "D2"
group(1) = "D" (大写字母)
group(2) = "2" (数字)
第2次扫描 → 找到 "E2"
group(1) = "E"
group(2) = "2"调用: adjust_formula("=D2*E2", 1)
↓
进入 adjust_formula,定义 replace 函数(此时还未执行)
↓
执行 re.sub(r'([A-Z]+)(\d+)', replace, "=D2*E2")
↓
re.sub 扫描字符串 "=D2*E2"
↓
找到 "D2" → 创建 match 对象(包含 "D2", "D", "2")
↓
调用 replace(match) ← match 在这里被传入
↓
replace 返回 "D3"
↓
re.sub 将 "D2" 替换为 "D3",得到 "=D3*E2"
↓
继续扫描,找到 "E2" → 创建 match 对象(包含 "E2", "E", "2")
↓
调用 replace(match) ← 再次传入
↓
replace 返回 "E3"
↓
re.sub 将 "E2" 替换为 "E3",得到 "=D3*E3"
↓
返回最终结果一句话总结:你只需要定义 replace(match) 函数接收参数,re.sub 会在每次匹配时自动创建 match 对象并传给它。
df = pd.read_excel(DATA_FILE)
rows = len(df)
wb = load_workbook(TEMPLATE)
ws = wb.active数据文件结构(44行):
模板文件结构:
START_ROW = 2
for row in range(ws.max_row, START_ROW - 1, -1):
if any(cell.value is not None for cell in ws[row]):
last_row = row
break
else:
last_row = START_ROW - 1
write_start_row = last_row + 1逻辑说明:
last_row = 2(模板第2行有数据)write_start_row = 3(新数据从第3行开始写入)输出:
模板现有数据: 最后一行是第2行
新数据将从第3行开始写入headers = [cell.value for cell in ws[1]]
col_map = {}
for idx, header in enumerate(headers, start=1):
if header in df.columns:
col_map[header] = idx模板表头:["日期", "产品名称", "销售员", "单价", "数量", "总价"]
数据文件列名:["产品名称", "日期", "单价", "销售员", "数量"]
生成的 col_map:
{
"日期": 1, # 模板A列
"产品名称": 2, # 模板B列
"销售员": 3, # 模板C列
"单价": 4, # 模板D列
"数量": 5 # 模板E列
}注意:"总价"列不在数据文件中,不会出现在映射中,该列由公式填充。
输出:
列映射: {'日期': 1, '产品名称': 2, '销售员': 3, '单价': 4, '数量': 5}template_formulas = {}
if last_row >= START_ROW:
for j in range(1, len(headers) + 1):
cell = ws.cell(row=last_row, column=j)
if isinstance(cell.value, str) and cell.value.startswith('='):
template_formulas[j] = cell.value第1行:template_formulas = {}
"=D2*E2")第2行:if last_row >= START_ROW:
last_row:模板中最后一条数据的行号START_ROW = 2:数据起始行(第1行是表头)last_row >= 2 时才执行,即模板至少有一条数据last_row = 1),条件为 False,跳过整个代|码|块第3行:for j in range(1, len(headers) + 1):
j 从 1 到总列数len(headers):表头列表的长度,即列数(本例为 6)range(1, 7) 生成 [1, 2, 3, 4, 5, 6]第4行:cell = ws.cell(row=last_row, column=j)
row=last_row:最后一条数据行(本例第2行)column=j:当前遍历到的列号第5行:if isinstance(cell.value, str) and cell.value.startswith('='):
isinstance(cell.value, str):先检查值是否为字符串类型False"=D2*E2")是字符串,返回 Truecell.value.startswith('='):再检查字符串是否以 = 开头(公式一定以 = 开头)第6行:template_formulas[j] = cell.value
j(如 6)"=D2*E2")2025/1/1 | ||||||
手机 | ||||||
张三 | ||||||
5000 | ||||||
5 | ||||||
=D2*E2 |
{
6: "=D2*E2"
}if template_formulas:
print(f"找到公式列: {[get_column_letter(col) for col in template_formulas.keys()]}")输出:
找到公式列: ['F']for i, row in df.iterrows():
target_row = write_start_row + i
for col_name, col_idx in col_map.items():
value = row[col_name]
if not pd.isna(value):
ws.cell(row=target_row, column=col_idx, value=value)执行过程:
target_row=3 → 写入 A3~E3target_row=4 → 写入 A4~E4target_row=46 → 写入 A46~E46注意:F列(公式列)不在 col_map 中,不会被写入,保持空白。
输出:
追加写入新数据...
数据写入完成,共 44 行if template_formulas:
for i in range(rows):
target_row = write_start_row + i
row_offset = target_row - last_row
for col_idx, base_formula in template_formulas.items():
target_cell = ws.cell(row=target_row, column=col_idx)
if target_cell.value is None:
adjusted = adjust_formula(base_formula, row_offset)
target_cell.value = adjusted第1行:if template_formulas:
template_formulas 为空字典 {}False,跳过整个公式填充步骤第2行:for i in range(rows):
rows 是数据文件的行数(本例为 44)range(44) 生成 0, 1, 2, ..., 43i 表示当前处理的是第几条新数据(从0开始)第3行:target_row = write_start_row + i
write_start_row 是新数据写入的起始行(本例为 3)第4行:row_offset = target_row - last_row
last_row 是模板中最后一条数据行的行号(本例为 2)target_row=3 → 3 - 2 = 1target_row=4 → 4 - 2 = 2target_row=46 → 46 - 2 = 44第5行:for col_idx, base_formula in template_formulas.items():
template_formulas 是一个字典,如 {6: "=D2*E2"}.items() 返回键值对,每次循环得到 (键, 值)col_idx=6,base_formula="=D2*E2"第6行:target_cell = ws.cell(row=target_row, column=col_idx)
target_row=3,col_idx=6 → 定位到 F3 单元格第7行:if target_cell.value is None:
is None 判断值是否为 None(空单元格)第8行:adjusted = adjust_formula(base_formula, row_offset)
base_formula 是原始公式,如 "=D2*E2"row_offset 是计算好的偏移量"=D3*E3"第9行:target_cell.value = adjusted
"=D3*E3"=D2*E2 | =D3*E3 | |||||
=D2*E2 | =D4*E4 | |||||
=D2*E2 | =D5*E5 |
wb.save(OUTPUT)
print(f"完成!→ {OUTPUT}")
print(f"数据范围: 第{write_start_row}行 到 第{write_start_row + rows - 1}行")输出:
完成!→ 结果_进阶版.xlsx
数据范围: 第3行 到 第46行1. 正则表达式调整公式行号
re.sub(r'([A-Z]+)(\d+)', replace, formula)([A-Z]+) 匹配列字母(\d+) 匹配行号2. 偏移量计算公式
row_offset = target_row - last_rowlast_row 是公式来源行(模板中最后一条数据行)3. 公式来源行的选择
if last_row >= START_ROW:
# 从 last_row 获取公式4. 只填充空单元格
if target_cell.value is None:
target_cell.value = adjusted读取数据文件 → df, rows
加载模板文件 → wb, ws
查找最后数据行 → last_row
计算写入起始行 → write_start_row = last_row + 1
读取模板表头,建立列映射 → col_map
从 last_row 获取公式 → template_formulas
↓
┌─────────────────────────────────────┐
│ 追加写入新数据 │
│ for 每条数据: │
│ 根据 col_map 写入各列数值 │
│ (公式列保持空白) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 填充公式 │
│ for 每个新行: │
│ offset = 当前行号 - last_row │
│ 将原公式中的行号 + offset │
│ 写入公式列 │
└─────────────────────────────────────┘
↓
保存文件 → OUTPUT️ 点单时间到! ️
下期写什么?
你来定,我来写。
评论区见!
关注「|码|农自习室」,后台回复关键词 PythonExcel案例,即可获取本文完整代|码|及配套练习数据集,一起动手掌握高效数据操作的核心技巧!
如果本文对你有帮助,欢迎点赞 + 关注 + 点亮小红心,你的支持就是我们持续创作的最大动力~
# Python
# 玩转
# Excel
# 案例【第
# 25
# 期】:Excel模板自动填充神器!公式自动调整
# 软件教程
# 行号
# 关键词
# 单元格
# 偏移量
# 正则表达式
# 本例
# 进阶
# 遍历
# 王五
# 多个
# 创建一个
# 跳过
# 为空
# 第一个
# 返回值
# 行数
# 调整后
# 加载
# 到新
相关栏目:
【
设计资讯7769 】
【
行业资讯35393 】
相关文章:
专升本平面广告设计临摹
平面广告设计师连衣裙法式
平面广告设计感穿搭小个子
杂货铺老板的平面广告设计
品牌平面广告设计分类
八零年代的平面广告设计
平面广告设计累不累
平面广告设计实训室
平面广告设计的具体分类
平面广告设计的名词解释
重要的平面广告设计制作
练小妃平面广告设计
节奏感平面广告设计案例
苹果平面广告设计风格
平面广告设计开发工程师:美化商业世界的艺术家
一生一世平面广告设计师
平面广告设计实践感想
网络公益平面广告设计
经典平面广告设计鉴赏
视觉文字平面广告设计
好想你平面广告设计行业文章
关于志愿者的平面广告设计
平面广告设计费报价单
平面广告设计西游记:颠覆传统的创意之旅
创意平面广告设计案例分析
工艺品平面广告设计单价
城市形象平面广告设计欣赏
平面广告设计软件安装方法
幼儿拉丁舞平面广告设计:让孩子们享受舞动的乐趣
红点平面广告设计奖
著名平面广告设计分享
文明城市平面广告设计
食物平面广告设计优点
博日平面广告设计
启佳平面广告设计:展现品牌魅力的艺术
黑豆平面广告设计
平面广告设计画册设计
色彩对平面广告设计的作用
平面广告设计师等级
平面广告设计电脑工作站
平面广告设计与理论
平面广告设计的前景范围
平面广告设计技巧摘要
达元平面广告设计
平面广告设计字
房地产鬼城平面广告设计
鹌鹑蛋平面广告设计
找平面广告设计
平面广告设计脚本
平面广告设计霸王洗发露