Python解析Excel、Word、PDF:办公文件处理实战
发布时间:2026/9/15 0:55:42 锦皓数字建站

1. 项目背景与整体设计思路1.1 为什么办公文件解析绕不开Python做后端开发这些年Excel、Word、PDF这三类文件几乎是每个业务系统都躲不开的坎。客户那边一句帮我导个报表、把这份合同附件存一下、生成个PDF发给客户落到代码里就是一整套解析、处理、预览、下载的链路。我这次接到的小项目很典型需要把用户上传的 Excel、Word、PDF 文件做解析提取内容做结构化存储同时还要支持在浏览器里预览以及按需下载原文件或转换后的文件。说白了就是一个办公文件的中转站加工厂。整个需求不复杂但是细节非常多。Python 在这类场景里优势很明确生态里有专门处理每种文件格式的成熟库写起来快遇到问题也好搜团队里其他同事接手也容易读懂。先说结论Excel 用 openpyxlWord 用 python-docxPDF 用 pdfplumber扫描版再挂 OCR。这套组合在绝大多数业务场景下够用而且都是纯 Python 实现部署不需要额外装 Office 环境这就把很多运维层面的麻烦提前规避掉了。1.2 核心工具库选型对比很多初学者一上来先问哪个库最强其实选库的关键是看你要处理的是哪种文件、处理到什么程度。我把常见的候选库整理了一张对比表方便大家按图索骥。文件类型推荐库擅长场景不擅长的场景Excelopenpyxlxlsx 读写、样式处理、公式读取xls 老格式需 xlrd 配合Excelpandas批量数据处理、行列表格操作保留原有格式、写入样式Wordpython-docxdocx 段落、表格、样式操作doc 老格式、复杂排版精准还原PDFpdfplumber文字抽取、表格还原扫描版图片型 PDFPDFPyPDF2 / pypdf合并拆分、元数据读写中文文本抽取准确率一般PDFpdf2imagePDF 转图片做预览图片质量受原始清晰度影响大我的选择逻辑很简单不要用 pandas 去处理需要保留原样式的 Excel不要用 PyPDF2 去抽包含复杂表格的 PDF每种库都有它的边界认清边界能少踩很多坑。另外提一句如果项目只需要读取 Excel 里的数值做计算pandas 确实快但如果你要把结果写回 Excel还要保留原来的边框、配色、列宽pandas 就很吃力这时候 openpyxl 才是正解。Word 也一样python-docx 对段落的控制力远强于先转成 HTML 再改这种思路。2. Excel 解析与处理的完整实践2.1 openpyxl 读取 Excel 的正确姿势openpyxl 读 xlsx 文件最基础的是load_workbook但很多人第一步就走偏了。默认的load_workbook会把公式结果也带上这在某些场景下反而有问题。我实际开发中会这样处理from openpyxl import load_workbook # 只读模式大文件加载更快同时保留公式不计算结果 wb load_workbook(销售数据.xlsx, read_onlyTrue, data_onlyFalse) sheet wb[Sheet1] # 遍历所有行 for row in sheet.iter_rows(values_onlyFalse): for cell in row: if cell.value is not None: print(cell.coordinate, cell.value) wb.close()这里有两个关键选择值得说明。read_onlyTrue适合大文件。之前接过一个几千行的收入表正常模式加载要好几秒只读模式基本秒开。如果文件还要写回就不能用只读模式这点要记住。data_onlyFalse是保留公式原文如果你想拿公式计算后的结果要改成data_onlyTrue。但这里有个巨坑data_onlyTrue只有在文件被 Excel 或 LibreOffice 保存过一次之后缓存里才有计算值。如果用代码直接生成的 xlsx 再读取计算值可能是None。这个我踩过不止一次排查到最后才发现是缓存问题。实际开发里我还会顺手封装一个通用读取函数处理合并单元格、空行、表头映射这些琐碎问题def read_excel_2_dict(path, sheet_nameNone): wb load_workbook(path, read_onlyTrue, data_onlyTrue) ws wb[sheet_name] if sheet_name else wb.active rows ws.iter_rows(values_onlyTrue) headers [str(h).strip() if h else for h in next(rows)] result [] for row in rows: if all(cell is None for cell in row): continue result.append(dict(zip(headers, row))) wb.close() return result把每行转成字典后面处理逻辑写起来会舒服很多。注意空行过滤很多运营同事导出的数据里夹杂大量全空行不处理的话下游调用方会被搞疯。2.2 数据清洗与多表拼接解析完数据只是第一步真正花时间的是清洗。Excel 里的数据可以说是什么鬼样子都有数字存成了文本、日期格式五花八门、金额带上了千分位逗号、单元格里混着换行符。这个项目里我做了一个清洗函数专治这类问题def clean_cell(value): if value is None: return if isinstance(value, str): value value.replace(\n, ).replace(\r, ).strip() value value.replace(,, ).replace(元, ) if isinstance(value, (int, float)): # 处理 float 精度问题 return round(value, 2) return value多表拼接也是高频需求。比如用户上传了 12 个月的分表要合成年度数据。我会先用glob把这批文件全部读出来再用pandas.concat合并最后用 openpyxl 写回一个汇总表。这里有个经验读数据用 pandas 很高效但写回不要用 pandas 的to_excel因为它的格式控制能力很弱。正确的做法是先用 pandas 整理完数据再通过openpyxl逐个单元格写入这样可以精确控制表头样式、列宽和冻结窗格。import glob import pandas as pd from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment from openpyxl.utils import get_column_letter frames [] for f in glob.glob(data/*.xlsx): df pd.read_excel(f) frames.append(df) merged pd.concat(frames, ignore_indexTrue) # 用 openpyxl 写回保留样式控制 wb Workbook() ws wb.active ws.append(list(merged.columns)) # 表头样式 for cell in ws[1]: cell.font Font(boldTrue, colorFFFFFF) cell.fill PatternFill(start_color4F81BD, end_color4F81BD, fill_typesolid) cell.alignment Alignment(horizontalcenter) # 写数据行 for row in merged.itertuples(indexFalse): ws.append(list(row)) # 自适应列宽中文场景需要乘以系数 for col_idx, col_name in enumerate(merged.columns, 1): max_len max(merged[col_name].astype(str).map(len).max(), len(str(col_name))) * 1.2 ws.column_dimensions[get_column_letter(col_idx)].width min(max_len 2, 30) wb.save(年度汇总.xlsx)列宽处理是个容易忽略的细节。西文字符宽度和中文不一样直接按字符数设置列宽中文表头很容易被截断所以乘一个 1.2 的系数是我试出来的经验值。2.3 Excel 模板填充与批量生成报表实际项目里用户更常见的诉求不是让你从头画一个 Excel而是我有一个公司的模板你往里面填数据。这个需求本质上是模板渲染。我做的这个功能后台维护了几个模板文件每个模板里有固定的表头、公司 Logo、品牌色甚至还有一行行的提示文字。程序要做的就是把参数填充到指定位置。openpyxl 支持通过单元格坐标或者命名区域定位from openpyxl import load_workbook wb load_workbook(模板.xlsx) ws wb[销售报表] # 已知模板结构A1是标题B3是日期B4起是数据区 ws[A1] f{year}年度销售报表 ws[B3] report_date start_row 4 for i, item in enumerate(data_list): ws.cell(rowstart_row i, column1, valueitem[片区]) ws.cell(rowstart_row i, column2, valueitem[销售额]) ws.cell(rowstart_row i, column3, valueitem[同比]) wb.save(报表输出.xlsx)这种方式的优点是完全保留模板的样式生成的文件拿出去就可以直接给领导看。缺点也很明显模板一旦被调整了结构代码里的行列坐标就全乱套。所以模板必须锁定保护然后给所有需要填写的单元格设置好名称代码里通过名称定位这样即使别人在中间插了几行只要名称没乱程序不会崩。有个小细节如果单元格里本身带着公式比如合计行是SUM(B4:B10)你往里面填数据的时候要注意行号变化。更稳妥的方案是把合计行也做成代码计算后写入而不是依赖模板公式。因为 openpyxl 写入的公式不会自动计算出结果缓存用户用 Excel 打开时虽然能看到结果但程序自己读这个文件时data_onlyTrue会读出None。3. Word 文档的解析与生成3.1 用 python-docx 读懂 Word 的结构Word 的 docx 格式和 xlsx 一样本质上是一个 zip 压缩包里面是 XML 文件。python-docx 做的事情就是把这些 XML 暴露成段落、表格、样式等对象。读取段落比较直接from docx import Document doc Document(合同.docx) for para in doc.paragraphs: if para.text.strip(): print(f【{para.style.name}】 {para.text})但实际业务里Word 里的信息往往不止在段落里还遍布在表格、页眉页脚、批注里。尤其是合同或标书类文档正文用表格排版的情况非常多。python-docx 读表格的接口是doc.tables每个 table 对象有rows和columnsfor table in doc.tables: for row in table.rows: for cell in row.cells: # 注意合并单元格时 cell 会重复出现 print(cell.text)这里有个坑合并单元格会导致cell对象在多个行列位置上重复出现。如果你按坐标去取值同一个单元格会被打印多次。处理方式是去重或者干脆先把所有 cell 文本放到一个 list 里再转成集合。Word 自动化处理场景里还有一个很常见的问题用户发来一个 PDF说帮我把里面内容提取出来。这种我通常先用 pdf 库抽文本再组装成 docx。后面会展开讲。3.2 占位符替换与样式还原Word 模板填充是另一个高频需求。比如公司统一格式的说明函只有客户名称和日期不同。传统做法是把模板另存为一份新的再把变动的地方改掉。python-docx 对段落的文本替换可以直接操作run对象from docx import Document doc Document(函件模板.docx) def replace_placeholder(paragraph, old, new): for run in paragraph.runs: if old in run.text: run.text run.text.replace(old, new)但问题来了Word 的段落里文字可能被拆成多个run。比如【客户名称】这几个字可能第一个 run 是【客第二个是户名称】如果你直接遍历 run 找完整的占位符往往会失败。我在项目里用的方法是先把同段落的 run 文本合并起来替换再写回def replace_placeholder_merge_runs(paragraph, mapping): full_text .join(run.text for run in paragraph.runs) for old, new in mapping.items(): full_text full_text.replace(old, new) if len(paragraph.runs) 0: # 全部写进第一个 run其余清空 paragraph.runs[0].text full_text for run in paragraph.runs[1:]: run.text 这样操作简单粗暴但会丢失个别 run 上的特殊格式比如下划线、颜色。如果模板比较规整占位符本身格式统一这个方案完全够用。如果占位符是分散在多个 run 里的你也只能这样处理。另外强烈建议模板里的占位符不要用花括号{}因为 Word 的自动更正可能会把它改掉而且调试的时候难以区分。用【】这种全角括号在 Word 里体验最稳定。3.3 从结构化数据生成 Word 报告还有一个场景是从数据库或者其他系统拿到结构化数据要批量生成一份份 Word 报告。这个项目里我实现了一个月度经营分析报告的生成逻辑结构上分三个块封面信息、正文段落、数据表格。from docx import Document from docx.shared import Pt, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH doc Document() # 设置正文默认字体中文场景关键步骤 style doc.styles[Normal] style.font.name Calibri style.font.size Pt(11) # 中文字体必须通过元素设置否则默认为宋体且不生效于中文字符 style.element.rPr.rFonts.set(qn(w:eastAsia), 微软雅黑) # 标题 title doc.add_heading(level0) title.alignment WD_ALIGN_PARAGRAPH.CENTER run title.add_run(2024年度经营分析报告) run.font.name 微软雅黑 run.font.size Pt(18) # 添加表格 table doc.add_table(rows1, cols4) table.style Light Grid Accent 1 hdr table.rows[0].cells hdr[0].text 指标 hdr[1].text 一季度 hdr[2].text 二季度 hdr[3].text 同比变化这里不得不提醒一个几乎所有 python-docx 新手都会踩的坑中文字体设置。font.name 微软雅黑只设置了西文字体中文标题和正文仍然会用宋体。必须通过rFonts的w:eastAsia属性手动指定中文字体代码里的qn(w:eastAsia)就是干这个的。不设置的话生成的 docx 在自己电脑上打开是正常字体发给同事一打开就变成默认宋体格式一下就垮了。段落间距和行距也是报告观感的关键。Word 默认段落间距偏小我习惯统一设置from docx.shared import Pt for paragraph in doc.paragraphs: paragraph.paragraph_format.line_spacing 1.5 paragraph.paragraph_format.space_after Pt(6)这些细节不会影响功能但会影响别人对你代码产物的第一印象。给领导汇报的东西格式就是门面不能丢。4. PDF 解析与预览处理4.1 pdfplumber 抽取文本和表格PDF 在这个项目里三合一的角色有时候是数据源要解析里面的内容有时候是展示层网页预览直接上 PDF有时候是出口把数据导出成 PDF 给用户。解析 PDF 文本我用的是 pdfplumber。它的底层是 pdfminer.six但对外 API 友好很多尤其是表格抽取能力比直接用 pdfminer 省一大半力气。import pdfplumber with pdfplumber.open(年报.pdf) as pdf: page pdf.pages[5] # 取第6页索引从0开始 text page.extract_text() table page.extract_table()extract_text的性能在纯文本型 PDF 上表现很好中文没有明显乱码。但它的输出是按行组织的遇到多栏排版会错乱这是 pdfplumber 的固有缺陷。多栏 PDF 需要额外设置columns参数或者手动裁切页面区域。表格抽取同样有讲究。extract_table默认按页面上检测到的横线竖线来切表但很多 PDF 里的表格没有明显的边框线或者线是画出来的且颜色极浅。这种情况下我建议先看下表格是否可以用extract_text加正则去解析反而更稳。举例账单类 PDF 的行结构很规律文本抽取加正则比依赖表格线靠谱得多。PDF 里还有一类信息藏在表单字段里。比如政府网站下载的申请表格内容是 AcroForm 字段。pdfplumber 读不了字段值这时要用pypdf的get_fields()方法单独处理。4.2 扫描版 PDF 的 OCR 处理思路扫描版 PDF 本质是一堆图片没有文本层pdfplumberextract_text返回的是空字符串或零散乱码。这种文件要做解析只能走 OCR 路线。我的处理链路是PDF 转图片pdf2image再用 Tesseract 或者 PaddleOCR 识别文字。from pdf2image import convert_from_path images convert_from_path(扫描件.pdf, dpi300) for i, img in enumerate(images): img.save(fpage_{i}.png, PNG)300 DPI 是经验值低于这个准确率下降高于这个文件体积暴涨但识别提升有限。如果原扫描件文字本来就清晰200 DPI 就够还能快不少。OCR 我推荐 PaddleOCR中文识别准确率比 Tesseract 好很多尤其是印刷体和手写体混排的场景。它的调用方式也比较统一from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch) result ocr.ocr(page_0.png, clsTrue) for line in result: print(line[1][0])OCR 出来的结果是带坐标的这很关键。比如想提取发票号后面的数字不能只按文本顺序找还得结合坐标判断发票号和后面的数字是否在同一行。我通常的做法是拿到识别结果后按行分组再对每一行做正则匹配。需要说明的是扫描版 PDF 的解析永远做不到 100% 准确业务上要留人工复核的入口。如果上游能提供电子版坚决不要走 OCR这是在项目立项时就要跟需求方说清楚的边界。4.3 PDF 转图片做在线预览网页端预览 PDF最省事的方案是直接让浏览器渲染 PDF。Chrome、Edge、Firefox 都内置了 PDF 查看器一个iframe就搞定。但坑在于不同浏览器渲染效果不一致而且手机端体验很差缩放、翻页都不太跟手。这个项目我为了统一体验选择了后端转图片前端看图的方案。流程是用户上传 PDF → 后端用pdf2image把每页渲染成 PNG → 前端用图片懒加载实现类似电子书的翻页效果。def pdf_to_preview_images(pdf_path, output_dir, max_pages20): images convert_from_path(pdf_path, dpi120) result [] for i, img in enumerate(images[:max_pages]): out_path f{output_dir}/preview_{i:03d}.png img.save(out_path, PNG) result.append(out_path) return result120 DPI 对应的是 1440 像素宽左右在普通屏幕上放大到全屏也不会虚。超过 20 页的 PDF 我只转前 20 页避免后端内存爆掉。如果你的业务场景真的是长文档应该考虑流式加载只渲染当前页和前后两页用完就释放。还有一个小技巧生成预览图之前可以先用pypdf检查页数如果文件超过某个阈值就直接提示文件过大请下载后查看而不是硬着头皮转图。这种自我保护在真实项目里非常有用。5. 预览与下载的完整实现5.1 文件预览的三种方案对比文件预览是整个需求里最容易被低估的部分。很多人以为能打开就行实际上预览的流畅度直接决定了用户对这个系统的印象分。我做的这个项目里针对不同文件类型用了三种方案文件类型预览方案优势劣势PDFiframe 直接渲染大文件做图片替代实现简单支持缩放搜索手机端体验差Excel后端转成 HTML 表格前端渲染体积小、加载快公式、图表会丢失Word转 PDF 再预览 或 转 HTML相似度高转换耗时样式有偏差Excel 转 HTML 我用的还是 openpyxl读取每个单元格的值和样式直接生成table标签。这套方案的优点是数据量小几万行的 Excel 也能预览缺点是公式不会计算而且如果原表里有图表、数据透视表这些内容都会被丢掉只能作为数据预览不能作为完整预览。Word 转 PDF 我用过docx2pdf这个库本机用没问题但它依赖 Microsoft Word 或者 LibreOffice在服务器环境下跑需要额外安装系统组件。如果不想在服务器上装这么多东西可以退而求其次后台把 docx 转成 HTML 字符串前端渲染 HTML。python-docx 可以读取段落和表格的结构配合document_to_html逻辑能还原 80% 的视觉效果。5.2 文件下载与格式转换链路下载功能看起来简单就是返回一个文件流但实际做起来有几个细节不能马虎。第一个细节是文件下载的响应头尤其是中文文件名。Flask 或 FastAPI 里直接设置Content-Disposition中文字段可能会乱from urllib.parse import quote filename 销售报表2024.xlsx encoded quote(filename) headers {Content-Disposition: fattachment; filename*UTF-8{encoded}}第二个细节是下载格式要和上传格式不同。最常见的需求是Excel 转 PDF、Word 转 PDF。Excel 转 PDF 如果不想装 Office可以用LibreOffice的命令行libreoffice --headless --convert-to pdf --outdir /output/ 上传文件.xlsxLibreOffice 转换有几个坑中文字体缺失会导致生成的 PDF 出现豆腐块如果在容器里跑需要安装字体包转换大文件时务必加超时控制否则进程可能挂死。我的经验是转换前先调用fc-list检查中文字体是否可用没有就先安装fonts-noto-cjk。Word 转 PDF 也走同样的 LibreOffice 路线兼容性比docx2pdf好得多不依赖 Windows 和 Office。5.3 临时文件清理与权限控制文件类的应用最怕的是磁盘被传爆、临时文件没人清理。我在这个项目里做了三层防护第一层上传入口限制文件大小和类型超过 20MB 直接拒绝。PDF 扫描件动辄五六十 MB必须转图片的时候转完就删源文件。第二层生成预览文件时统一放到一个临时目录临时文件名带时间戳然后写一个后台定时任务清理超过 24 小时的临时文件。import os import time def clean_temp_dir(dir_path, expire_seconds86400): now time.time() for name in os.listdir(dir_path): file_path os.path.join(dir_path, name) if os.path.isfile(file_path): stat_mtime os.path.getmtime(file_path) if now - stat_mtime expire_seconds: os.remove(file_path)第三层下载接口要做权限校验不能拿个 URL 就随便下。常见的做法是生成一次性的 signed URL带过期时间。这个在 FastAPI 里就是加一个依赖注入的问题但重要性不能忽视——内部业务系统的文件一旦泄露麻烦不小。6. 常见问题与排查避坑实录6.1 中文乱码与编码问题这个项目里中文乱码出现了三次分别在不同的环节我记录一下排查思路。第一次是读旧版 xls 文件pandas 读取时指定了encodinggbk就解决了但要注意不是所有 xls 都是 GBK 编码的还有 UTF-8、GB2312。判断编码可以用chardet去检测虽然速度慢一点但准确率高。第二次是生成的 PDF 里中文变方块。原因就是前面说的 LibreOffice 容器里没有中文字体。解决方案是装fonts-noto-cjk并配置好字体别名。第三次是文件名乱码。在 Linux 服务器上文件系统字符集默认 UTF-8Windows 上传的文件名如果带了 GBK 编码的字符存到磁盘后名字就是乱码。处理方案是后端保存文件时用 UUID 重命名原始文件名存到数据库字段里展示的时候从数据库取。6.2 大文件与内存占用PDF 转图片是最吃内存的环节。一本几百页的书转成图片直接把 2GB 内存吃满不是开玩笑。我的解决方法是限制并发、分批处理、及时释放。pdf2image底层调用pdftoppm每页渲染都会占用内存所以千万不要一次性把所有页转成图片然后统一存 list。正确做法是逐页处理用完立即释放from pdf2image import convert_from_path images convert_from_path(large.pdf, dpi150) # 逐页处理并保存而不是先把所有页积累在内存里 for i, img in enumerate(images): img.save(fpage_{i}.png) # 这里还可以顺便做裁切、压缩 del imgExcel 也一样openpyxl 的read_only模式配合iter_rows可以极大降低内存占用处理十万行数据不会拖垮服务器。如果数据量再大就应该考虑上数据库或者分片处理而不是在 Python 进程里硬扛。6.3 环境安装与依赖冲突这个项目的依赖列表不长但paddleocr和pdf2image的依赖树拉到一起偶尔会碰到 protobuf 版本冲突。我的建议是项目依赖一定要用requirements.txt锁版本别用最新版三个字自欺欺人。上生产环境之前在干净的容器里从零跑一遍安装流程比省那几分钟时间划算得多。还有一个高频问题pdf2image装上之后报错找不到pdftoppm。这不是 Python 库的问题而是系统缺少 poppler-utils。在 Ubuntu 上执行apt-get install -y poppler-utils就解决了。Mac 上brew install poppler。遇到任何 command not found 性质的报错先冷静检查系统依赖再回头看 Python 代码。6.4 预览加载慢的优化技巧最后再说一个和调用方反馈最密切的问题预览打开慢。我排查过的一个例子是 10MB 的 PDF转图片要 8 秒用户等得直摔杯子。后来优化方案是上传完成后立刻异步转图片把结果存数据库用户点击预览时如果图片还没生成完先返回处理中状态或者先展示 PDF 原文件图片生成完以后更新状态。另一个优化是增加缓存。同一个文件被多次预览不应该每次都重新转图片。我基于文件 MD5 做了一层磁盘缓存命中就直接返回图片列表省去了大量重复计算。这些优化看似不起眼但对用户体感提升非常大。我这套做下来预览接口的平均响应时间从 8 秒级降到了 1 秒以内用户再也没有抱怨过打不开。做文件解析这类项目我最深的体会是真正有价值的不是某个库的 API 调用而是对整个链路的掌控——文件从哪来、到哪去、中间有哪些环节可能出错、出错之后怎么恢复。把这套链路想清楚了不管是 Excel、Word 还是 PDF都只是处理流程里的一个节点而已。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。