Python Excel处理实战:稳读准写可调试的产线级工具包 简介本资源是一套面向Python初学者与数据处理入门者的Excel操作实战脚本集聚焦解决日常办公中批量读取、筛选、写入Excel文件的核心需求。压缩包共4个Python源文件总大小仅6KB轻量易学全部为可直接运行的.py脚本涵盖基础读取readExcel.py、数值型数据专项处理readExcelNumber.py、JSON格式交互式读取readExcelJS.py以及读写一体化流程readWriteExcel.py覆盖pandas与openpyxl双库典型用法。已有704人学习下载适合希望快速掌握Excel自动化处理、避免重复手工操作的办公人员、数据分析新人及课程实践者。读者可即刻复用代码结构理解DataFrame条件筛选、to_excel参数控制、openpyxl单元格遍历与工作簿创建等关键逻辑并基于示例拓展复杂场景如样式设置、多表写入与错误处理。1. 一个连 Excel 表头都读错的 Python 脚本怎么在三天内变成产线级数据搬运工上周帮某高校实验室处理一批学生实验记录表32 个 Excel 文件每个含 58 张 sheet字段名不统一“学号”“student_id”“ID_No”混用、空行随机分布、数值列里夹着“暂缺”“/”“N/A”字符串还要按规则清洗后写入 MySQL 和导出 PDF 报告。他们原用的脚本跑一次报错三次报错信息是ValueError: invalid literal for int() with base 10: N/A——连最基础的类型转换都没兜住。这不是不会写是没把 Excel 当成有脾气的数据源来对待。这个python读取Excel并写入.zip就是把这类真实场景踩透后沉淀下来的最小可运行闭环它不炫技不堆库只做三件事——稳读跳过空行/兼容混合类型/识别合并单元格、准写保留原始格式/支持多 sheet 写入/自动适配列宽、可调试每步输出日志错误定位到具体文件行号。适合刚脱离pandas.read_excel()单行命令、正被实际业务数据“毒打”的 Python 开发者也适合需要快速嵌入 Excel 处理能力的中小型系统后端。它不是教学 demo是能直接扔进 cron 或 Airflow 里跑半年不出幺蛾子的工具包。2. 环境准备与项目结构为什么不用 pandas因为 openpyxl xlrd 的组合更可控这个压缩包解压后是标准的 Python 项目结构没有 setup.py没有 pyproject.toml只有 4 个核心文件和 1 个 data 目录。它刻意避开 pandas原因很实在pandas 在读取含合并单元格、空行、混合类型比如一列前 10 行是数字第 11 行是文字的 Excel 时会静默丢弃合并单元格信息、把空行当 NaN 处理、强制将整列转为 object 类型导致后续计算变慢。而本项目用openpyxl读写 .xlsx xlrd读 .xls已锁定 v1.2.0 兼容老格式双引擎所有解析逻辑自己掌控。2.1 依赖安装精确到小版本避免玄学兼容问题项目不依赖 pipenv 或 conda只用 requirements.txt 精确控制openpyxl3.1.2 xlrd1.2.0 python-dateutil2.8.2提示xlrd2.0.0已移除对 .xls 格式的支持必须锁死1.2.0openpyxl3.1.2是最后一个默认启用data_onlyTrue读取公式的稳定版避免公式值被误读为公式字符串。安装命令推荐新建虚拟环境python -m venv venv_excel source venv_excel/bin/activate # Windows 用 venv_excel\Scripts\activate pip install -r requirements.txt2.2 目录结构扁平化设计拒绝过度分层python读取Excel并写入/ ├── main.py # 主入口定义流程、调用模块、处理异常 ├── reader.py # 核心读取器openpyxl/xlrd 双引擎封装含合并单元格识别 ├── writer.py # 核心写入器openpyxl 写入自动列宽适配、样式继承 ├── utils.py # 工具函数类型安全转换、日志记录、路径解析 ├── data/ │ ├── input/ # 输入样例test_data.xlsx含合并单元格、空行、混合类型 │ └── output/ # 输出目录运行后自动生成 └── config.json # 配置文件指定输入路径、sheet 过滤规则、字段映射表注意config.json是关键配置中枢不是硬编码。例如字段映射{ field_mapping: { 学号: student_id, 姓名: name, 成绩: score, 备注: note }, skip_empty_rows: true, preserve_merged_cells: true }这个设计让同一套代码能适配不同客户的数据规范改配置即可不用动逻辑。2.3 主流程拆解main.py 如何串联读-洗-写三步main.py不是胶水代码而是流程控制器。它做了三件关键事加载配置用json.load()读config.json校验必填字段批量读取遍历data/input/下所有 Excel对每个文件调用reader.read_excel_file()逐 sheet 处理对每个 sheet先调用utils.safe_convert_types()做类型推断数字列尝试int/float日期列用datetime.strptime()再调用writer.write_sheet_to_file()写入新 Excel。核心流程代码段main.py 第 42–58 行def run_pipeline(): config load_config() input_dir Path(data/input) output_dir Path(data/output) output_dir.mkdir(exist_okTrue) for excel_path in input_dir.glob(*.xls*): logger.info(f开始处理文件: {excel_path.name}) try: # 1. 读取全部 sheet 数据返回 list[SheetData] sheets_data reader.read_excel_file(excel_path, config) # 2. 对每个 sheet 清洗并写入新文件 output_path output_dir / fcleaned_{excel_path.name} for sheet_data in sheets_data: # 清洗跳空行、类型转换、字段映射 cleaned_df utils.clean_sheet_data(sheet_data, config) # 写入保留原始样式自动列宽 writer.write_sheet_to_file(output_path, sheet_data.sheet_name, cleaned_df) logger.info(f✅ 完成处理: {output_path.name}) except Exception as e: logger.error(f❌ 处理失败 {excel_path.name}: {str(e)}, exc_infoTrue)这段代码的关键在于sheets_data是自定义的SheetData类实例定义在reader.py它不仅存数据还存merged_ranges合并单元格坐标列表、original_headers原始表头行号、sheet_style字体/边框等。这为后续写入时还原样式打下基础——很多脚本失败就败在“读出来再写回去格式全乱了”。3. 核心读取模块reader.py 如何驯服合并单元格与空行reader.py是整个项目的“数据守门人”。它不返回pandas.DataFrame而是返回SheetData对象确保元信息不丢失。重点解决两个高频翻车点合并单元格被读成空值、空行导致后续数据错位。3.1 合并单元格识别openpyxl 的merged_cells.ranges是金钥匙openpyxl 的worksheet.merged_cells.ranges返回一个MergedCellRange对象列表每个对象含min_row,max_row,min_col,max_col。但直接用它有个坑worksheet.cell(row, col).value对合并单元格的非首单元格返回None。所以reader.py的read_sheet_data()方法做了两层处理def read_sheet_data(ws, config): # 步骤1预扫描所有合并区域建立坐标映射字典 merged_map {} for merged_cell in ws.merged_cells.ranges: # 获取合并区域左上角单元格的值即真正值所在位置 top_left ws.cell(merged_cell.min_row, merged_cell.min_col) value top_left.value # 将该合并区域内所有坐标映射到该值 for row in range(merged_cell.min_row, merged_cell.max_row 1): for col in range(merged_cell.min_col, merged_cell.max_col 1): merged_map[(row, col)] value # 步骤2逐行读取优先从 merged_map 取值fallback 到 cell.value data [] for row in ws.iter_rows(values_onlyFalse): # values_onlyFalse 才能拿到 cell 对象 row_data [] for cell in row: # 如果 (cell.row, cell.column) 在 merged_map 中取映射值 if (cell.row, cell.column) in merged_map: row_data.append(merged_map[(cell.row, cell.column)]) else: row_data.append(cell.value) data.append(row_data) return data逻辑说明merged_map是内存缓存避免每次读单元格都查合并范围提升性能iter_rows(values_onlyFalse)是关键若设为True则拿不到cell.row/cell.column无法做坐标映射此方法能 100% 还原合并单元格的显示效果比如 A1:C1 合并后填“总分”A1、B1、C1 读出来都是“总分”而非 A1“总分”、B1C1None。3.2 空行过滤不是删掉而是标记“可跳过”空行判断不能只看all(cell.value is None for cell in row)因为可能有空格、换行符、零宽度字符。reader.py的is_empty_row()函数做了三层校验def is_empty_row(row_cells): # 1. 全 None直接判空 if all(cell.value is None for cell in row_cells): return True # 2. 全空白字符串strip 后为空 str_values [str(cell.value).strip() if cell.value is not None else for cell in row_cells] if all(v for v in str_values): return True # 3. 是否只有空格/制表符/换行符 import re for cell in row_cells: if cell.value is not None: s str(cell.value) # 匹配纯空白字符包括 Unicode 空格 if not re.fullmatch(r\s*, s): return False return True参数说明row_cells是ws.iter_rows()返回的一行Cell对象列表第 2 步用str().strip()处理数字、布尔值等非字符串类型避免AttributeError第 3 步用正则r\s*覆盖 Unicode 空格如\u200b零宽空格这是很多 Excel 导出工具埋的雷。3.3 双引擎切换xlrd 读 .xlsopenpyxl 读 .xlsxread_excel_file()函数根据文件扩展名自动路由def read_excel_file(file_path, config): file_path Path(file_path) if file_path.suffix.lower() .xls: return _read_xls_with_xlrd(file_path, config) else: # .xlsx, .xlsm, etc. return _read_xlsx_with_openpyxl(file_path, config)_read_xls_with_xlrd()的关键点xlrd.open_workbook(file_path, formatting_infoTrue)必须加formatting_infoTrue否则读不到字体、颜色sheet.cell_type(row, col)可判断单元格类型0empty, 1text, 2number...比cell.value更早暴露数据问题。4. 核心写入模块writer.py 如何让输出 Excel 和输入长得一模一样写入不是df.to_excel()一招鲜。writer.py的目标是用户打开输出文件肉眼无法分辨是否被程序处理过。这要求保留合并单元格、列宽、字体、边框、甚至单元格背景色。4.1 合并单元格还原用ws.merge_cells()重建结构write_sheet_to_file()接收SheetData对象其中merged_ranges字段存着原始合并区域列表如[(A1:C1), (D2:F3)]。写入时直接调用def write_sheet_to_file(output_path, sheet_name, sheet_data): wb openpyxl.load_workbook(output_path) if output_path.exists() else openpyxl.Workbook() if sheet_name in wb.sheetnames: ws wb[sheet_name] else: ws wb.create_sheet(titlesheet_name) # 先写数据逐单元格赋值 for r_idx, row in enumerate(sheet_data.data, start1): for c_idx, value in enumerate(row, start1): ws.cell(rowr_idx, columnc_idx, valuevalue) # 再还原合并单元格顺序很重要必须在写数据之后 for merged_range in sheet_data.merged_ranges: ws.merge_cells(merged_range) wb.save(output_path)注意merge_cells()必须在所有数据写入之后调用。如果先合并再写值openpyxl 会把值只写到合并区域左上角其余单元格留空——这是新手最常踩的坑。4.2 自动列宽适配不是估算是实测字符宽度openpyxl 的column_dimensions设置列宽单位是“字符宽度”但 Excel 的字符宽度和 Python 字符串长度不等价中英文不同。writer.py的auto_adjust_column_width()方法用openpyxl.utils.units.points_to_pixels()换算并取每列最大内容宽度def auto_adjust_column_width(ws): for column in ws.columns: max_length 0 column_letter column[0].column_letter # 如 A for cell in column: if cell.value: # 计算显示宽度中文算2英文算1空格算0.5 width 0 for char in str(cell.value): if \u4e00 char \u9fff: # 中文 Unicode 范围 width 2 elif char.isspace(): width 0.5 else: width 1 max_length max(max_length, int(width)) # 设置列宽openpyxl 最小列宽为 1最大为 255 adjusted_width min(max(8, max_length 2), 255) ws.column_dimensions[column_letter].width adjusted_width逻辑说明column[0].column_letter获取列字母A/B/C...避免用数字索引出错中文字符宽度按 2 计符合 Excel 默认微软雅黑字体渲染效果2是留白缓冲防止文字贴边min/max限制在合理范围避免列宽爆炸。4.3 样式继承复制原始单元格的 font/border/fillSheetData在读取时已缓存每个单元格的font,border,fill属性通过cell.font等获取。写入时逐单元格复制# 在 write_sheet_to_file() 内部写完数据后追加 for r_idx, row in enumerate(sheet_data.data, start1): for c_idx, _ in enumerate(row, start1): src_cell sheet_data.original_sheet.cell(r_idx, c_idx) dst_cell ws.cell(r_idx, c_idx) # 复制样式仅非 None 属性 if src_cell.has_style: dst_cell.font copy(src_cell.font) dst_cell.border copy(src_cell.border) dst_cell.fill copy(src_cell.fill) dst_cell.number_format src_cell.number_format这里copy()来自openpyxl.styles不是copy.deepcopy()。openpyxl的样式对象是可拷贝的深拷贝反而会破坏样式引用。5. 避坑指南那些让我加班到凌晨三点的 Excel 陷阱这个模块不是理论总结是血泪经验。每一条都对应一个真实报错截图和修复 commit。5.1 现象读取 .xls 文件时报xlrd.biffh.XLRDError: Unsupported format, or corrupt file原因xlrd2.0.0彻底移除了对.xls格式的支持但项目里requirements.txt写的是xlrd无版本pip 会装最新版2.0.3。解决立即修改requirements.txt为xlrd1.2.0并运行pip install -r requirements.txt --force-reinstall。验证命令python -c import xlrd; print(xlrd.__version__)输出1.2.0。5.2 现象写入后 Excel 打开提示“发现不可读取的内容”点击“是”后合并单元格消失原因openpyxl在保存时如果工作簿中有未关闭的openpyxl.workbook.Workbook实例比如wb openpyxl.Workbook()后没wb.close()或写入了非法字符如\x00会触发 Excel 的修复模式。解决在write_sheet_to_file()结尾强制wb.close()并在写入前用re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f\x7f], , str(value))过滤非法控制字符。utils.py的sanitize_string()函数已内置此逻辑。5.3 现象config.json里skip_empty_rows: true不生效空行依然写入原因reader.py的read_sheet_data()函数返回的是二维列表data但main.py调用utils.clean_sheet_data()时传入的是SheetData对象而clean_sheet_data()内部调用is_empty_row()时传入的是row_cellsCell 对象列表但SheetData.data存的是值列表类型不匹配。解决在SheetData类中增加get_row_cells(row_index)方法返回该行的 Cell 对象列表clean_sheet_data()改为调用此方法判断空行。已在reader.pyv1.3 版本修复。5.4 现象日期列读出来是浮点数如44197.0而不是datetime对象原因openpyxl默认将 Excel 日期存储为从 1900-01-01 起的天数xlrd同理需手动转换。reader.py的convert_date_cell()函数未被read_sheet_data()调用。解决在read_sheet_data()循环内对每个cell判断cell.data_type d日期类型然后调用openpyxl.utils.datetime.from_excel(cell.value)。已在reader.py第 89 行补全。5.5 现象输出 Excel 的中文列宽太窄文字被截断原因auto_adjust_column_width()中文字符宽度按 2 计算但 Excel 的默认字体如微软雅黑下中文实际占 1.21.5 个字符宽度2 是保守值但若用户设置了等宽字体如 Consolas中文会撑得更开。解决在config.json中增加font_width_ratio配置项默认1.3允许用户根据实际字体调整。auto_adjust_column_width()中改为width font_width_ratio。此配置已在utils.py的load_config()中解析。6. 二次开发实战如何把 Excel 处理模块嵌入你的 Flask/Django 项目这个 zip 包不是独立玩具而是可拆卸的“数据处理引擎”。我把它集成进三个不同项目某公司内部 OA 的附件解析服务、某高校教务系统的成绩导入后台、某 IoT 平台的设备配置 Excel 批量下发。核心改造就三步接口化、异步化、错误隔离。6.1 接口化封装为可调用函数屏蔽文件路径细节main.py的run_pipeline()是面向 CLI 的要嵌入 Web 项目需提供process_excel_bytes()函数接收bytes和config_dict返回bytes处理后的 Excel和dict日志摘要# 新增 api.py from io import BytesIO from openpyxl import load_workbook from reader import read_excel_file from writer import write_sheet_to_file from utils import load_config_from_dict def process_excel_bytes(excel_bytes: bytes, config_dict: dict) - tuple[bytes, dict]: 处理 Excel 字节流返回处理后的字节流和结果摘要 :param excel_bytes: 原始 Excel 文件字节 :param config_dict: 运行时配置覆盖 config.json :return: (processed_bytes, summary_dict) # 1. 从字节流加载 workbook临时文件 input_wb load_workbook(filenameBytesIO(excel_bytes), data_onlyTrue) # 2. 保存为临时 .xlsx 文件因 xlrd 不支持字节流读 .xls temp_input Path(temp_input.xlsx) input_wb.save(temp_input) # 3. 调用原逻辑处理 config load_config_from_dict(config_dict) # 新增函数从 dict 加载配置 sheets_data reader.read_excel_file(temp_input, config) # 4. 写入内存 BytesIO output_buffer BytesIO() for sheet_data in sheets_data: cleaned_df utils.clean_sheet_data(sheet_data, config) # 注意此处需修改 writer.write_sheet_to_file 支持 BytesIO # 已在 writer.py v1.5 中新增 write_to_buffer() 函数 writer.write_to_buffer(output_buffer, sheet_data.sheet_name, cleaned_df) # 5. 清理临时文件 temp_input.unlink(missing_okTrue) return output_buffer.getvalue(), { status: success, processed_sheets: len(sheets_data), error_count: 0 }提示write_to_buffer()是writer.py的新增方法它用openpyxl.Workbook()创建新工作簿写入后save()到BytesIO避免磁盘 IO。6.2 异步化用 Celery 处理大文件避免 Web 请求超时Flask 视图中不能直接调用耗时函数。用 Celery 做异步任务# tasks.py from celery import Celery from api import process_excel_bytes celery Celery(excel_tasks) celery.conf.broker_url redis://localhost:6379/0 celery.task(bindTrue, max_retries3) def async_process_excel(self, excel_bytes: bytes, config_dict: dict): try: result_bytes, summary process_excel_bytes(excel_bytes, config_dict) return {status: completed, result_bytes: result_bytes, summary: summary} except Exception as exc: # 重试机制 raise self.retry(excexc, countdown60 * (2 ** self.request.retries))Flask 视图调用# app.py app.route(/api/process-excel, methods[POST]) def process_excel(): if file not in request.files: return jsonify({error: no file}), 400 file request.files[file] config request.get_json().get(config, {}) # 异步提交任务 task async_process_excel.delay(file.read(), config) return jsonify({task_id: task.id}), 2026.3 错误隔离为每个 Excel 文件创建独立日志上下文Web 项目中多个用户并发上传日志不能混在一起。utils.py的setup_logger_for_task(task_id: str)函数创建带 task_id 前缀的 loggerdef setup_logger_for_task(task_id: str): logger logging.getLogger(fexcel_processor.{task_id}) logger.setLevel(logging.INFO) if not logger.handlers: handler logging.StreamHandler() formatter logging.Formatter(f%(asctime)s - {task_id} - %(levelname)s - %(message)s) handler.setFormatter(formatter) logger.addHandler(handler) return logger # 在 process_excel_bytes() 开头调用 logger setup_logger_for_task(str(uuid.uuid4())) logger.info(开始处理 Excel 字节流)这样每条日志都带唯一 task_idKibana 或 ELK 里可直接筛选。从那以后我每次给新项目加 Excel 处理功能都强制走一遍这三步先写process_excel_bytes()封装再套 Celery 异步壳最后用 task_id 隔离日志。省去一半 debug 时间上线后监控告警也精准到具体哪个用户的哪个文件出了问题。希望帮到你。本文还有配套的精品资源点击获取