news 2026/10/23 13:53:23

Python Excel处理实战:稳读准写可调试的产线级工具包

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 文件,每个含 5~8 张 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 精确控制:

openpyxl==3.1.2 xlrd==1.2.0 python-dateutil==2.8.2

提示:xlrd>=2.0.0已移除对 .xls 格式的支持,必须锁死1.2.0;openpyxl==3.1.2是最后一个默认启用data_only=True读取公式的稳定版,避免公式值被误读为公式字符串。

安装命令(推荐新建虚拟环境):

python -m venv venv_excel source venv_excel/bin/activate # Windows 用 venv_excel\Scripts\activate pip install -r requirements.txt

2.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不是胶水代码,而是流程控制器。它做了三件关键事:

  1. 加载配置:用json.load()读config.json,校验必填字段;
  2. 批量读取:遍历data/input/下所有 Excel,对每个文件调用reader.read_excel_file();
  3. 逐 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_ok=True) 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 / f"cleaned_{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_info=True)

这段代码的关键在于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_only=False): # values_only=False 才能拿到 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_only=False)是关键,若设为True,则拿不到cell.row/cell.column,无法做坐标映射;
  • 此方法能 100% 还原合并单元格的显示效果,比如 A1:C1 合并后填“总分”,A1、B1、C1 读出来都是“总分”,而非 A1=“总分”、B1=C1=None。

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 读 .xls,openpyxl 读 .xlsx

read_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_info=True)必须加formatting_info=True,否则读不到字体、颜色;
  • sheet.cell_type(row, col)可判断单元格类型(0=empty, 1=text, 2=number...),比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(title=sheet_name) # 先写数据(逐单元格赋值) for r_idx, row in enumerate(sheet_data.data, start=1): for c_idx, value in enumerate(row, start=1): ws.cell(row=r_idx, column=c_idx, value=value) # 再还原合并单元格(顺序很重要!必须在写数据之后) 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/fill

SheetData在读取时已缓存每个单元格的font,border,fill属性(通过cell.font等获取)。写入时逐单元格复制:

# 在 write_sheet_to_file() 内部,写完数据后追加: for r_idx, row in enumerate(sheet_data.data, start=1): for c_idx, _ in enumerate(row, start=1): 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

原因:xlrd>=2.0.0彻底移除了对.xls格式的支持,但项目里requirements.txt写的是xlrd(无版本),pip 会装最新版2.0.3。
解决:立即修改requirements.txt为xlrd==1.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_cells(Cell 对象列表),但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.2~1.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(filename=BytesIO(excel_bytes), data_only=True) # 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_ok=True) 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(bind=True, max_retries=3) 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(exc=exc, countdown=60 * (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}), 202

6.3 错误隔离:为每个 Excel 文件创建独立日志上下文

Web 项目中多个用户并发上传,日志不能混在一起。utils.py的setup_logger_for_task(task_id: str)函数创建带 task_id 前缀的 logger:

def setup_logger_for_task(task_id: str): logger = logging.getLogger(f"excel_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_id,Kibana 或 ELK 里可直接筛选。

从那以后我每次给新项目加 Excel 处理功能,都强制走一遍这三步:先写process_excel_bytes()封装,再套 Celery 异步壳,最后用 task_id 隔离日志。省去一半 debug 时间,上线后监控告警也精准到具体哪个用户的哪个文件出了问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 13:49:50

R语言期末设计报告资源包:从数据清洗到报告生成的完整流程解析

简介&#xff1a;这份R语言期末设计报告资源包面向正在学习R语言、需要完成课程设计或希望系统掌握数据分析流程的高校学生与自学者。内容围绕真实项目展开&#xff0c;涵盖数据读取与清洗、缺失值与异常值处理、探索性数据分析、线性回归与随机森林等统计建模&#xff0c;以及…

作者头像 李华
网站建设 2026/10/23 13:39:36

DeepSeek API集成实战:三端兼容的生产级NLP工具链

简介&#xff1a;本资源是一份面向开发者与AI产品实践者的DeepSeek大模型实操指南&#xff0c;聚焦网页端交互、API集成开发及移动端部署三大核心场景&#xff0c;帮助具备Python基础的技术人员快速掌握自然语言处理工具在客服自动化、智能助手构建等业务中的落地方法。资源为单…

作者头像 李华
网站建设 2026/10/23 13:37:37

用C++编写ParaView插件读取CGNS:架构与实现

简介&#xff1a;CGNSReader_ParaView_Plugin 是一款面向 CFD 后处理场景的 ParaView 插件&#xff0c;用于直接读取结构化与多块非结构化 CGNS 文件。针对原生加载时容易出现的较大内存占用&#xff0c;插件基于底层 CGNS API 重新实现&#xff0c;思路参考了 VisIt 的 CGNS 阅…

作者头像 李华
网站建设 2026/10/23 13:20:41

多普勒效应MATLAB仿真全攻略:建模、参数调优与频移提取

简介&#xff1a;面向西电通信工程学院学生的信号与系统大作业&#xff08;多普勒效应&#xff09;完整实践资源&#xff0c;将课堂理论、MATLAB仿真与实验报告融为一体&#xff0c;解决了从公式推导到代码实现再到报告成文的系列问题。压缩包共6个文件&#xff0c;约1.03MB&am…

作者头像 李华
网站建设 2026/10/23 13:14:40

APITable内网穿透实战:5分钟用cpolar实现团队远程协作

1. 为什么团队协作表格需要内网穿透1.1 从一次真实的团队协作困境说起去年我参与了一个跨地域的产品研发项目&#xff0c;团队分布在三个不同的办公地点。项目初期&#xff0c;我们用过各种在线表格工具&#xff0c;但很快就遇到了瓶颈&#xff1a;数据量一大就卡顿&#xff0c…

作者头像 李华
网站建设 2026/10/23 13:04:36

C++手搓单纯形法:从建模到求解的完整计算程序

简介&#xff1a;这份资源是面向算法学习者与C开发者的单纯形法完整实现程序&#xff0c;用于求解线性规划问题的最优解。单纯形法由丹齐格于1947年提出&#xff0c;通过迭代在基本可行解之间转换&#xff0c;逐步逼近目标函数最优值&#xff0c;是运筹学与数值计算中的经典算法…

作者头像 李华