Python怎么用openpyxl逐行读取Excel构建字典
详细讲解如何使用Python的openpyxl库逐行读取Excel文件,将每一行数据转换为字典,涵盖安装、核心代码、多种构建方式及常见问题处理。
用openpyxl逐行读取Excel并构建字典,核心思路只有一句话:先把第一行当作字典的键,然后遍历剩余行,把每行的单元格值和键一一对应,组成一个字典。整个过程不超过十行代码,但实际使用时有几个细节值得留意——空行处理、数据类型保持、大文件性能,这篇文章会逐一说清楚。
安装openpyxl
openpyxl是专门处理.xlsx格式的第三方库,不支持旧的.xls格式。如果你的环境里还没装过,用pip安装即可:
pip install openpyxl
安装完成后在Python交互环境中执行import openpyxl,不报错就说明可以用了。如果你使用的是Anaconda环境,openpyxl通常已经预装,可以跳过这一步。
理解Excel在openpyxl中的数据结构
在写代码之前,有必要弄清楚openpyxl怎么看待一个Excel文件。openpyxl把一个.xlsx文件加载为Workbook对象,Workbook包含若干Worksheet,每个Worksheet是一张表。调用ws.iter_rows()可以按行遍历,每行返回一个由Cell对象组成的元组。Cell对象的.value属性就是单元格里的实际值,openpyxl会自动将数字、日期等保持为Python对应的类型,而不是统一变成字符串。
理解了这个层级关系,后面的代码就很好理解了:我们只需要在行这一层操作,用第一行的值做键,后续行的值做值。
最基本的写法:逐行构建字典列表
假设有一个Excel文件students.xlsx,第一行是表头姓名、年龄、城市、分数,从第二行开始是数据。我们要把每行数据变成一个字典,最终得到一个字典列表。
from openpyxl import load_workbook
wb = load_workbook('students.xlsx')
ws = wb.active
rows = ws.iter_rows(values_only=True)
headers = next(rows) # 取出第一行作为键
result = []
for row in rows:
record = dict(zip(headers, row))
result.append(record)
print(result)
假设Excel内容如下:
| 姓名 | 年龄 | 城市 | 分数 |
|--------|------|--------|------|
| 张三 | 28 | 北京 | 85.5 |
| 李四 | 24 | 上海 | 92.0 |
| 王五 | 31 | 广州 | 78.3 |
运行上面的代码,输出结果(示例):
[
{'姓名': '张三', '年龄': 28, '城市': '北京', '分数': 85.5},
{'姓名': '李四', '年龄': 24, '城市': '上海', '分数': 92.0},
{'姓名': '王五', '年龄': 31, '城市': '广州', '分数': 78.3}
]
几个关键点值得注意:
values_only=True让iter_rows()直接返回值的元组,而不是Cell对象的元组,代码更简洁。next(rows)从迭代器中取出第一行。由于iter_rows()返回的是生成器,取出第一行后,后续的for循环自然从第二行开始。zip(headers, row)把表头和数据一一配对,再用dict()转成字典,这是Python里非常地道的写法。
处理空行和缺失值
实际的Excel文件经常不那么干净。有时候中间会夹杂空行,有时候某些单元格没有填值。如果不做处理,空行会产生所有值都是None的字典,混在结果里会给后续处理添麻烦。
一个简单的办法是在循环中加一个判断——如果一整行的值全是None,就跳过:
result = []
for row in rows:
if all(v is None for v in row):
continue
record = dict(zip(headers, row))
result.append(record)
对于个别单元格为空的情况,构建出的字典里对应的值就是None。这在大多数场景下是合理的,因为None在Python中明确表示"没有值"。如果你需要把它替换为空字符串或默认值,可以用字典推导式:
record = {k: (v if v is not None else '') for k, v in zip(headers, row)}
用某一列的值做字典的键
上面的方式得到的是一个列表,用索引访问特定记录。但有时候我们更希望用某个字段——比如姓名——直接定位到对应的数据行。这时候可以构建一个以某列值为键、以整行字典为值的嵌套字典:
wb = load_workbook('students.xlsx')
ws = wb.active
rows = ws.iter_rows(values_only=True)
headers = next(rows)
data = {}
for row in rows:
if all(v is None for v in row):
continue
record = dict(zip(headers, row))
key = record['姓名']
data[key] = record
# 直接通过姓名访问
print(data['李四'])
# 输出(示例): {'姓名': '李四', '年龄': 24, '城市': '上海', '分数': 92.0}
这里有一个前提:作为键的那一列不能有重复值,否则后出现的行会覆盖前面的。如果确实存在重复,可以改成列表收集:
from collections import defaultdict
data = defaultdict(list)
for row in rows:
if all(v is None for v in row):
continue
record = dict(zip(headers, row))
data[record['城市']].append(record)
# data['北京'] 是一个列表,包含所有城市为北京的记录
只读取特定列或特定行范围
iter_rows()接受min_row、max_row、min_col、max_col参数,可以精确控制读取范围。比如表格有十几列但你只关心前四列,或者前几行是说明文字、真正的表头从第三行开始,都可以通过这些参数调整。
# 只读取第3行到第100行,第1到第4列
rows = ws.iter_rows(min_row=3, max_row=100,
min_col=1, max_col=4,
values_only=True)
headers = next(rows) # 第3行作为表头
需要注意的是,min_row和max_row用的是Excel中的行号(从1开始),不是Python索引。如果指定了max_row但实际数据没有那么多行,多出来的行会返回全None的元组,所以前面提到的空行过滤在这种场景下尤其有用。
处理大文件时的性能考虑
openpyxl默认会把整个工作簿加载到内存。对于几千行的文件这完全没问题,但如果文件有几十万行、几百MB,内存占用会很大。openpyxl为此提供了read_only模式:
wb = load_workbook('big_file.xlsx', read_only=True)
ws = wb.active
rows = ws.iter_rows(values_only=True)
headers = next(rows)
for row in rows:
record = dict(zip(headers, row))
# 逐行处理,不必一次性存入列表
process(record)
wb.close() # read_only模式下用完需要手动关闭
read_only=True使openpyxl以流式方式读取,内存占用大幅降低。但这个模式有两个限制:不能修改工作表,也不能随机访问单元格(比如ws['A5']会报错)。另外用完后必须调用wb.close()释放文件句柄,或者用with语句:
# 也可以用 contextmanager 的方式(openpyxl 3.0+ 支持)
# 但更稳妥的做法是显式关闭
.xls格式的旧文件,openpyxl无法打开,需要改用xlrd库。两者的API不同,不能直接替换。
把构建过程封装成函数
如果这个操作要反复用到,封装成函数是自然的选择。下面这个函数支持指定Sheet名称、选择用哪一列做字典键、以及是否启用只读模式:
from openpyxl import load_workbook
def excel_to_dicts(filepath, sheet_name=None, key_column=None, read_only=False):
"""
读取Excel文件,将每行数据转为字典。
参数:
filepath: xlsx文件路径
sheet_name: 工作表名称,默认读取活动工作表
key_column: 如果指定,返回以该列值为键的嵌套字典;否则返回列表
read_only: 是否使用只读模式(适合大文件)
返回:
list[dict] 或 dict[str, dict]
"""
wb = load_workbook(filepath, read_only=read_only)
ws = wb[sheet_name] if sheet_name else wb.active
rows = ws.iter_rows(values_only=True)
headers = next(rows)
if key_column:
result = {}
for row in rows:
if all(v is None for v in row):
continue
record = dict(zip(headers, row))
result[record[key_column]] = record
else:
result = [
dict(zip(headers, row))
for row in rows
if not all(v is None for v in row)
]
if read_only:
wb.close()
return result
调用示例:
# 返回字典列表
students = excel_to_dicts('students.xlsx')
# 以姓名为键
students_by_name = excel_to_dicts('students.xlsx', key_column='姓名')
# 读取指定Sheet,启用只读模式
data = excel_to_dicts('big_data.xlsx', sheet_name='2024年数据', read_only=True)
和其他方案的比较
逐行读取Excel构建字典这件事,除了openpyxl手写之外还有其他选择。pandas的read_excel()可以一步到位地把Excel读成DataFrame,然后用.to_dict(orient='records')转成字典列表。代码更短,但pandas是一个很重的依赖,如果你的项目只需要读取Excel构建字典,单独引入pandas有点浪费。
另一个选择是csv模块的DictReader——如果你的数据源可以先另存为CSV的话。DictReader天然就是用第一行做键、逐行返回字典,但它处理不了Excel的多Sheet、数据类型(CSV里所有值都是字符串)和格式信息。
openpyxl手写的优势在于:依赖轻、控制精细、保留数据类型。当你明确只需要把Excel行变成字典,并且希望精确控制读取范围和异常处理时,这种方式最合适。
容易踩的几个坑
在实际项目中用这套方法,有几个问题值得提前知道:
表头有合并单元格。openpyxl读取合并单元格时,只有左上角的单元格有值,其余为None。如果你的表头存在合并,zip出来的字典键会出现None。解决方法是在拿到headers后检查并补全,或者在Excel端取消合并后再处理。
日期列被读成datetime对象。openpyxl会把Excel中日期格式的单元格自动解析为Python的datetime对象。如果你需要字符串格式,在构建字典时可以手动转换:
from datetime import datetime
value = cell_value
if isinstance(value, datetime):
value = value.strftime('%Y-%m-%d')
数字列被读成浮点数。Excel中整数在openpyxl里有时会被读为int,有时会被读为float(比如"28"可能变成28.0),这取决于Excel内部的存储方式。如果你需要确保是整数,可以在构建字典时判断并转换。
文件被其他程序占用。如果Excel文件正在被WPS或Microsoft Excel打开,openpyxl可能无法读取或读取到不完整的数据。处理前确保文件没有被其他程序锁定。
完整可运行示例
最后,把前面讲的内容整合成一个完整的、可以直接运行的脚本。这个脚本先创建一个示例Excel文件,然后演示三种读取方式:
from openpyxl import Workbook, load_workbook
# ---- 第一步:创建示例文件 ----
wb_new = Workbook()
ws_new = wb_new.active
ws_new.append(['姓名', '年龄', '城市', '分数'])
ws_new.append(['张三', 28, '北京', 85.5])
ws_new.append(['李四', 24, '上海', 92.0])
ws_new.append(['王五', 31, '广州', 78.3])
ws_new.append(['赵六', 27, '深圳', 88.1])
wb_new.save('demo_students.xlsx')
print('示例文件已创建: demo_students.xlsx')
# ---- 第二步:读取为字典列表 ----
wb = load_workbook('demo_students.xlsx')
ws = wb.active
rows = ws.iter_rows(values_only=True)
headers = next(rows)
student_list = [
dict(zip(headers, row))
for row in rows
if not all(v is None for v in row)
]
print('\n字典列表:')
for s in student_list:
print(f" {s}")
# ---- 第三步:以姓名为键的字典 ----
wb2 = load_workbook('demo_students.xlsx')
ws2 = wb2.active
rows2 = ws2.iter_rows(values_only=True)
headers2 = next(rows2)
student_dict = {}
for row in rows2:
if all(v is None for v in row):
continue
record = dict(zip(headers2, row))
student_dict[record['姓名']] = record
print('\n通过姓名查找:')
print(f" 李四 → {student_dict['李四']}")
# ---- 第四步:只读取特定列 ----
wb3 = load_workbook('demo_students.xlsx')
ws3 = wb3.active
rows3 = ws3.iter_rows(min_col=1, max_col=2, values_only=True)
headers3 = next(rows3)
name_age = [dict(zip(headers3, row)) for row in rows3]
print('\n只取姓名和年龄:')
for item in name_age:
print(f" {item}")
运行后你会看到三种方式的输出结果(示例),确认整个流程是通的之后,就可以根据自己的实际文件路径和列名去修改了。
总结一下:openpyxl逐行读取Excel构建字典,本质上就是iter_rows取行、next取表头、zip配对、dict收尾。在这个基础上,根据实际需要加上空行过滤、列范围控制、只读模式和键列选择,就能应对绝大多数场景。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













