PyPDF2与pdfplumber:Python自动化处理PDF的完整实战指南
1. 从手工整理PDF到自动化处理这一篇讲清楚核心脉络先说一个我自己的场景。前阵子接到一份工作需要把手头几百份电子合同全部做一遍信息登记提取每一份合同里的甲方、乙方、金额、日期还要把同年度的合同合并到一个PDF文件里最后统一加上仅限内部使用的水印再分发出去。如果靠PDF阅读器一份份打开、复制、粘贴、另存为这份活三天都干不完。最后我用Python把整个流程压到了半个小时以内靠的就是三个字自动化。PDF文档处理这件事在Python生态里已经有非常成熟的方案。常用的两个库是PyPDF2和pdfplumber。很多刚入门的朋友容易把它们搞混以为都是处理PDF的库功能差不多随便选一个就行。实际上它们的分工非常清楚PyPDF2负责编辑——合并、拆分、旋转、加密、加水印、改元数据pdfplumber负责读取——从页面里提取文字、表格、线条坐标。两个库配合使用基本就能覆盖日常工作里九成以上的PDF处理需求。这篇内容适合谁两类人。第一类是像我一样经常和PDF打交道的运维、数据、行政、财务岗位从业者需要批量处理合同、报告、账单第二类是刚接触Python办公自动化的学习者想搞清楚这两个库到底能做什么、怎么上手、有哪些坑。我不打算写成官方文档的翻译重点讲实际项目里怎么用、为什么这样用以及我踩过的那些坑。2. 工具选型背后的逻辑PyPDF2和pdfplumber到底怎么分工先说结论如果要读内容优先选pdfplumber如果要改结构优先选PyPDF2。这个结论不是拍脑袋而是两个库的设计目标决定的。PyPDF2的前身是PyPDF再往前追溯是pyPdf。它出身于PDF结构操作领域核心能力在PDF文件的骨架层面页面的合并、拆离、旋转、裁剪文档的加密解密书签目录的读写元数据信息标题、作者、创建时间的查看与修改。它也能提取文本但效果只是能用遇到复杂的排版、多栏布局、表格数据就会露馅。pdfplumber则完全不同。它建立在PDF解析库pdfminer.six的基础之上专门解决从PDF里拿数据这件事。它会把页面解析成一个个对象字符、线条、矩形、图片还能根据这些基础对象推断出表格结构、文本块的层级关系。所以它提取出来的文本顺序、坐标位置、表格行列都精确到像素级别特别适合做数据抓取和信息抽取。这两个库放在一起正好补齐了PDF处理的两块拼图——一个管外壳一个管内核。我的习惯是先用pdfplumber把需要的数据从PDF里抽出来再用PyPDF2对PDF文件本身做批量改造。两条线互不干扰配合起来非常顺手。对比维度PyPDF2pdfplumber核心定位文档结构编辑内容解析提取文本提取能力弱适合简单页面强保留排版和坐标表格提取不支持强支持复杂表格合并拆分旋转支持不支持加密解密支持不支持元数据读写支持不支持水印叠加支持不支持依赖复杂度低依赖pdfminer.six另外提醒一句PyPDF2目前官方的发展重心已经转移到新一代分支pypdf如果你是新建项目直接安装pypdf其实更合适API和PyPDF2基本兼容修复了不少旧问题。但市面上大量教程和存量代码还是PyPDF2所以我这篇还是以PyPDF2为主如果你用pypdf绝大部分代码可以直接照抄。3. 环境准备与第一个常用操作读取PDF基础信息和元数据3.1 安装环节的版本隐藏陷阱安装很简单一条命令搞定pip install PyPDF2 pdfplumber但这里有个必须注意的版本坑。PyPDF2在3.0.0版本做了重大重构很多旧教程里的写法比如PdfFileReader、getPage()、getNumPages()在新版本里全部改名了PdfFileReader变成了PdfReadergetNumPages()变成了len(reader.pages)getPage(n)变成了reader.pages[n]。如果你在网上搜到的是2021年之前的代码直接复制会报一堆AttributeError。我建议你在项目里固定一个版本别装最新版之后又看旧文档否则会很痛苦。这篇文章里的代码都按PyPDF2 3.x版本为准。确认版本可以用pip show PyPDF2如果你已经装的是2.x老版本建议升级到3.x因为3.x之后的API更简洁也更稳定。3.2 PDF元数据是什么为什么值得关注标题里那个热搜词pypdf2中元数据是什么我在这里好好展开讲一下。PDF元数据metadata简单说就是藏在PDF文件内部、描述这个文档自身属性的信息。就好比一张照片有EXIF信息拍摄时间、光圈、快门一个Word文档有属性面板里的作者、标题、关键词PDF也有类似的档案卡。它不属于页面内容但记录了一堆关键信息Title文档标题不是文件名是内部标题Author作者Subject主题描述Keywords关键词Creator生成该PDF的软件名比如Microsoft WordProducerPDF转换器信息CreationDate创建时间ModDate最后修改时间很多刚接触的朋友会问我打开PDF看到的标题不是文件名吗 不是的。PDF阅读器左上角显示的名称优先取的是内部元数据里的Title字段文件名只是次要来源。所以如果我们能批量修改元数据就能让一堆文件名乱糟糟的PDF在阅读器里显示成统一规范的标题——这个效果在整理电子档案的时候非常有用。查看元数据的代码很简单from PyPDF2 import PdfReader reader PdfReader(合同.pdf) metadata reader.metadata print(标题:, metadata.title) print(作者:, metadata.author) print(创建时间:, metadata.creation_date) print(是否加密:, reader.is_encrypted)3.3 修改元数据批量给PDF做身份登记如果一份PDF的元数据是空的、或者不正确我们要做的就是重写它。PyPDF2提供的方式是用PdfWriter创建一个新文件把原文件页面拷贝进去再写入新的元数据。听起来有点绕但逻辑其实很简单——PyPDF2不能直接在原文件上改必须读进来、写出去生成一个新的PDF文件。来一段完整的批量处理代码from PyPDF2 import PdfReader, PdfWriter from pathlib import Path def set_pdf_metadata(file_path, titleNone, authorNone, subjectNone): reader PdfReader(file_path) writer PdfWriter() # 把所有页面拷贝到writer中 for page in reader.pages: writer.add_page(page) # 构建新元数据原有信息保留 current_meta reader.metadata new_meta { /Title: title if title else (current_meta.title if current_meta else 未命名), /Author: author if author else (current_meta.author if current_meta else unknown), /Subject: subject if subject else (current_meta.subject if current_meta else ), } writer.add_metadata(new_meta) out_path file_path.with_stem(file_path.stem _已标记) with open(out_path, wb) as f: writer.write(f) return out_path注意这里用的键名是/Title这种带斜杠的PDF内部名称不是随便起的。PyPDF2会把这些键翻译成元数据对象。实际使用中我一般还会顺带把/CreationDate和/ModDate一起写上格式要遵循PDF规范像这样from PyPDF2.generic import NameObject, TextStringObject from datetime import datetime # 手动设置日期 now_str datetime.now().strftime(%Y%m%d%H%M%S) metadata reader.metadata if metadata: writer.add_metadata({ /Title: 2024年度合同汇总, /Author: 财务部, /CreationDate: TextStringObject(fD:{now_str}), })如果你直接复制这段代码可能会发现D:前缀有点突兀。这是PDF内部日期字符串的标准格式D:后面跟年月日时分秒时区可以再加0800。4. 文本提取实战pdfplumber从页面里抠出每一句话4.1 为什么pdfplumber提取文本更可靠PyPDF2也能提取文本但实际效果差别很大。我举个具体例子一份从Excel导出的报表PDF页面上有表头、页脚、多列数据。用PyPDF2的page.extract_text()提取出来的往往是一坨文字挤在一起列和列之间没空格表格数字全连成一排完全没法用。但同样的文件用pdfplumber提取因为pdfplumber能够感知每个字符在页面上的坐标位置所以输出顺序能忠实还原从左到右、从上到下的阅读顺序。看一下提取文本的基本用法import pdfplumber with pdfplumber.open(报表.pdf) as pdf: first_page pdf.pages[0] text first_page.extract_text() print(text)这里有个关键点extract_text()返回的是按行组织的字符串。pdfplumber在内部会把页面识别成一行一行的文本行每行内的字符又按x坐标从左到右排列。对于大多数Word或WPS导出的PDF这个顺序完全够用。但如果你是处理那种报纸式多栏布局的PDF直接调用extract_text()会发现问题它会先取完第一栏的所有内容再取第二栏而不是按视觉上的从左到右逐行来。因为第二栏第一行的y坐标和第一栏第一行相同但x坐标不同。解决办法是在提取时按列切割页面区域。pdfplumber支持用page.crop()裁剪区域# 左半边 left_bbox (0, 0, page.width / 2, page.height) left_part page.crop(left_bbox) left_text left_part.extract_text() # 右半边 right_bbox (page.width / 2, 0, page.width, page.height) right_part page.crop(right_bbox) right_text right_part.extract_text()然后根据自己的需求拼接。这个方法我在处理双栏排版论文时反复用过效果靠谱。4.2 文本提取的空结果之谜相信每个用过pdfplumber的人都遇到过这种情况extract_text()返回None代码看起来一点问题都没有但页面上明明有字。我第一次遇到这个问题时排查了很久。后来发现根本原因是——这个PDF不是真的文本PDF而是一张扫描图片存进去的图片PDF。比如用打印机扫描出来的合同、手机拍照成PDF的发票里面根本没有可提取的文本字符层只有一张背景图片。pdfplumber的extract_text()本质是提取字符对象页面上没有字符自然返回空。怎么判断页面里是不是有文本有一个很实用的探测方法with pdfplumber.open(扫描件.pdf) as pdf: page pdf.pages[0] chars page.chars # 字符对象列表 imgs page.images # 图片对象列表 print(字符数:, len(chars), 图片数:, len(imgs))如果chars是空列表而images有内容那基本可以断定是图片PDF。这种PDF要用OCR光学字符识别来处理比如配合pytesseract或者百度、腾讯的OCR接口。这些属于另一个话题了但至少你现在知道遇到提取不出文本时别先怀疑代码先怀疑PDF本身的类型。4.3 保留排版细节extract_words与坐标信息有时候我们不只是想拿到文字内容还想知道某个词在页面上的具体位置比如要按坐标定位盖章位置、要验证某个词是否在指定区域内。pdfplumber把每个单词的坐标信息也暴露出来了with pdfplumber.open(布局.pdf) as pdf: page pdf.pages[0] words page.extract_words() for w in words[:5]: print(w)每个words元素是一个dict包含text、x0、x1、top、bottom等字段。x0是单词左边缘的x坐标x1是右边缘top是上边缘bottom是下边缘。单位是PDF内部的点pt1英寸等于72pt。这套坐标系统的作用很大。我做过一个需求统计每份PDF页面上合同编号四个字出现在第几页、离页面左上角多少距离从而判断某个公司的合同模板有没有变动。直接遍历所有页面的extract_words()匹配关键词效率非常高。5. 表格提取与坐标定位pdfplumber的另一项硬功夫5.1 从表格到二维数据的完整流程PDF里的表格堪称办公自动化的头号难题。Excel转成PDF之后原来规整的行列结构被拍平成一堆线条和字符普通文本提取根本拿不到表格结构。pdfplumber对此提供了专门的extract_table()方法它利用页面上的纵线和横线来推测表格区域。基本用法with pdfplumber.open(工资表.pdf) as pdf: page pdf.pages[0] table page.extract_table() # table是一个二维列表table[0]是表头table[1:]是数据行 for row in table: print(row)返回的结构是一个list of lists第一行是表头之后是数据行。这和我们平时用pandas处理二维数据的习惯高度吻合可以直接转成DataFrameimport pandas as pd with pdfplumber.open(工资表.pdf) as pdf: page pdf.pages[0] table page.extract_table() df pd.DataFrame(table[1:], columnstable[0]) print(df.head())这一步做完PDF里的表格就变成结构化数据了后续做汇总、筛选、入数据库全都顺理成章。但现实世界没有这么美好。extract_table()有一个前提页面上的表格必须有明显、完整的线条。如果表格是那种无边框的隐性表格或者合并过单元格它的推测效果就会打折扣。针对这种情况可以在调用时传参调整table page.extract_table({ vertical_strategy: lines, # 竖线策略可选lines或text horizontal_strategy: lines, snap_tolerance: 3, # 线条吸附容忍度 })vertical_strategy有两个选项lines按线条识别text按文本对齐识别。遇到无边框表格时把策略设为text往往能发挥作用。5.2 多页表格的坑表头重复与页间断层处理超过一页的表格时会遇到两个讨厌问题。第一每页都有重复的表头直接拼接数据后表头混在数据行里第二跨页的位置上可能某一行被从中间截断导致下一列的数据对不上。我的处理方式是提取每一页的表格后先判断第一行是不是表头再决定是否丢弃。简单场景下可以用字符串匹配all_rows [] for page in pdf.pages: table page.extract_table() if table: # 如果第一行和表头模型一致则跳过表头 if table[0] and table[0][0] 姓名: table table[1:] all_rows.extend(table)更复杂的场景建议提取后人工抽查几页的边界情况写个断言校验每一行的字段数量是否一致。很多脏数据问题都是从这里冒出来的。5.3 不只有表格用线条坐标定位盖章区域pdfplumber的坐标能力还可以帮我们画出页面上所有线条的位置with pdfplumber.open(带线条.pdf) as pdf: page pdf.pages[0] lines page.lines # 普通线条 rects page.rects # 矩形框 curves page.curves # 曲线打印出来你会发现每条线都有x0, x1, top, bottom这样的坐标。这在处理在合同固定位置加印章、在指定区域填充文字之类的需求时非常有用——你可以先用pdfplumber读取目标位置的坐标然后用PyPDF2在该坐标点上叠加内容实现按坐标操作PDF。6. 自动化编辑用PyPDF2批量合并、拆分、旋转、加密与加水印6.1 页面合并与拆分从多个PDF合成新文件先讲合并。假设你手上有一批同类PDF希望按指定顺序合并成一个完整文件。用PyPDF2做这件事的核心是建一个空的PdfWriter然后依次把其他PDF的页面add进去。from PyPDF2 import PdfReader, PdfWriter def merge_pdfs(file_list, output_path): writer PdfWriter() for file in file_list: reader PdfReader(file) for page in reader.pages: writer.add_page(page) with open(output_path, wb) as f: writer.write(f)这段代码虽然简单但有一个经常被忽略的性能点add_page()是会持续占用内存的如果合并的是几十个上百页的大文件writer会变得很臃肿。实测下来合并超过500页的文件时建议分批合并再合并避免一次吃太多内存。拆分是反过来的操作把一个PDF按页范围切割成多个文件。比如我要把一本100页的标书拆成10个十页的小册子发给不同评审专家from PyPDF2 import PdfReader, PdfWriter def split_pdf(input_path, pages_per_file10): reader PdfReader(input_path) total_pages len(reader.pages) for start in range(0, total_pages, pages_per_file): end min(start pages_per_file, total_pages) writer PdfWriter() for i in range(start, end): writer.add_page(reader.pages[i]) output_path f{input_path.stem}_第{start//pages_per_file1}部分.pdf with open(output_path, wb) as f: writer.write(f)6.2 旋转与裁剪处理方向不对的扫描件扫描件最大的问题就是页面方向五花八门横的、竖的、倒着的。PyPDF2旋转页面的API非常简单from PyPDF2 import PdfReader, PdfWriter reader PdfReader(歪着的文件.pdf) writer PdfWriter() for i, page in enumerate(reader.pages): if i in [0, 2]: # 假设第1、3页是倒着的 page.rotate(180) # 顺时针旋转180度 writer.add_page(page) with open(转正后.pdf, wb) as f: writer.write(f)注意rotate()是在原页面对象上直接做修改所以如果你继续用同一个page对象旋转角度会累加。想要旋转90度还是180度按实际情况调整即可。裁剪功能类似用page.cropbox定义页面显示区域# 只保留页面上半部分 page.cropbox.upperleft (0, page.mediabox.height / 2) page.cropbox.lowerright (page.mediabox.width, 0)6.3 加密与解密给文档加把锁PyPDF2支持对PDF做密码保护也可以打开带密码的PDF并移除密码。加密码from PyPDF2 import PdfReader, PdfWriter reader PdfReader(原始文件.pdf) writer PdfWriter() for page in reader.pages: writer.add_page(page) # 设置密码user_password是打开密码owner_password是权限密码 writer.encrypt(user_password123456, owner_passwordadmin123, use_128bitTrue) with open(加密文件.pdf, wb) as f: writer.write(f)这里有个容易忽略的点encrypt()之后的writer对象已经带上了加密状态如果你再往里面add_page()新页面新页面不会自动被加密吗实测中我发现加密操作对整个writer生效之后再添加的页面也会被一并加密但为了保险起见我还是建议先加完所有页面再加密顺序别弄反。解除加密则更简单。如果知道密码reader PdfReader(加密文件.pdf) if reader.is_encrypted: reader.decrypt(123456) # 传入密码 writer PdfWriter() for page in reader.pages: writer.add_page(page) with open(解密文件.pdf, wb) as f: writer.write(f)这里有一个非常容易踩的坑在PyPDF2 3.x版本里PdfReader()读取加密文档时如果不先调用decrypt()就访问reader.pages会直接抛异常。异常提示可能不太直观让人以为是文件损坏。所以判断逻辑切记先is_encrypted判断再decrypt。6.4 水印叠加给每页盖上专属印记给PDF加水印是PyPDF2使用频率极高的功能之一。原理说起来很简单创建一个透明的水印PDF然后把每一页原页面和透明水印页合成为一个新页面。具体做法是先准备一个只有水印内容的PDF文件可以用Word画一行字导出成PDF然后在代码里加载它from PyPDF2 import PdfReader, PdfWriter def add_watermark(input_file, watermark_file, output_file): watermark_reader PdfReader(watermark_file) watermark_page watermark_reader.pages[0] reader PdfReader(input_file) writer PdfWriter() for page in reader.pages: page.merge_page(watermark_page) # 水印页叠加到当前页上 writer.add_page(page) with open(output_file, wb) as f: writer.write(f)merge_page()会把两个页面的内容重叠显示在同一张页面上。水印PDF本身最好是透明的只包含文字不要带背景色否则会覆盖原文件内容。如果找不到合适的水印PDF可以用reportlab现场生成一个。这个库是PDF生成界的瑞士军刀配合PyPDF2做水印非常好用from reportlab.pdfgen import canvas def create_watermark(text, outputwatermark.pdf): c canvas.Canvas(output) c.setFont(Helvetica, 48) c.setFillAlpha(0.2) # 透明度 c.setFillColorRGB(1, 0, 0) # 红色 c.translate(300, 400) c.rotate(45) # 旋转45度 c.drawString(-150, 0, text) c.save()这里setFillAlpha(0.2)是可选的设置后水印有一定透明度不遮挡正文。7. 批次处理实测把上述能力组合成一条自动化流水线7.1 一个完整的批处理脚本思路光有单个功能点还不够实际工作中最有价值的往往是把它们组合起来。下面我给出一个典型的自动化流水线设计思路这个思路我用了很多次每次都能省下大量人力。假设需求是批量处理某个文件夹下的所有PDF要求——提取每份PDF的标题和正文前200字生成一个索引清单把每份PDF的元数据Author统一改成办公室给所有PDF加上内部资料水印全部处理完后把小于三页的PDF合并成一个待审核材料.pdf伪代码逻辑如下import pdfplumber from pathlib import Path from PyPDF2 import PdfReader, PdfWriter src_dir Path(./待处理文件) watermark_file 水印.pdf output_index [] file_list list(src_dir.glob(*.pdf)) for file in sorted(file_list): # 1. 提取标题和首段文本 with pdfplumber.open(file) as pdf: first_page pdf.pages[0] text first_page.extract_text() title_raw text.split(\n)[0] if text else 无标题 excerpt text[:200].replace(\n, ) if text else 暂无内容 output_index.append((file.name, title_raw, excerpt)) # 23. 改元数据 加水印 for file in file_list: reader PdfReader(file) writer PdfWriter() watermark PdfReader(watermark_file).pages[0] for page in reader.pages: page.merge_page(watermark) writer.add_page(page) writer.add_metadata({/Author: 办公室}) out_path file.with_stem(file.stem _已处理) with open(out_path, wb) as f: writer.write(f) # 4. 合并小于3页的PDF small_files [f for f in file_list if len(PdfReader(f).pages) 3] if small_files: merge_pdfs(small_files, 待审核材料.pdf) # 1存数据 with open(索引清单.csv, w, encodingutf-8-sig) as f: for name, title, excerpt in output_index: f.write(f{name},{title},{excerpt}\n)这段逻辑看起来简单但拼的就是各种细节。特别注意with pdfplumber.open(file)打开文件后file这个Path对象是要保持存在的不能在循环里重复覆盖同名输出否则会报文件被占用。7.2 批处理中的文件命名与临时文件策略批量处理最大的隐患是把输出文件覆盖到输入文件上。想象一下你读取了a.pdf写出了一个a_已处理.pdf这个没问题。但如果你不小心把输出路径设为a.pdf处理到一半时文件被锁住或者覆盖轻则丢数据重则把原始文件搞坏。我的习惯做法是输出文件统一加后缀例如_已处理、_水印版中间产物放到单独的temp/目录全部处理完成后再统一整理归档调试阶段先在两个文件上跑通再全量处理8. 踩坑实录四个最常见的问题和排查方法8.1 AttributeError: PdfReader object has no attribute getNumPages几乎每天都会有人在群里问这个问题。原因就是版本新旧API不一致。旧版用reader.getNumPages()新版用len(reader.pages)。如果你拿到的报错是类似的属性缺失先检查自己的代码是不是混用了新旧写法。建议统一按新版API写。8.2 加密文档解密后仍然提取不出来我遇到过一种情况用PyPDF2的decrypt()成功接收到密码但再用pdfplumber打开同一个文件仍然提示需要密码。原因在于有些加密PDF只加密了文件的元数据和结构部分内容流并没有完全加密pdfplumber在打开时先判断is_encrypted为True就直接抛错。解决办法是先用PyPDF2解密一次并另存为新的非加密PDF再用pdfplumber提取reader PdfReader(加密.pdf) if reader.is_encrypted: reader.decrypt(123456) writer PdfWriter() for page in reader.pages: writer.add_page(page) with open(去掉加密.pdf, wb) as f: writer.write(f)这条先解密转存的临时方案能解决绝大多数双库配合的问题。8.3 提取出的中文乱码PDF文本提取出现乱码通常是字体编码映射的问题。pdfplumber对中文字体的支持已经很不错但某些国产软件生成的PDF会使用非标准字体编码导致提取出来的中文变成乱码或方框。我的排查思路分三步第一确认不是扫描件第二用page.chars检查单个字符的Unicode值看看是整个字体映射错误还是个别字符丢失第三如果确认是字体映射错误目前没有特别完美的纯Python方案建议改用OCR处理或者把PDF先转换为图片再做识别。8.4 表格有合并单元格时列错位这是表格提取里最折磨人的问题。合并单元格在PDF里表现为一个宽矩形盖住了多个逻辑单元格pdfplumber虽然能画出这个矩形但在推表格结构时可能把它当成一列。应对办法是提取后手动检查表头结构遇到合并单元格时写专门的规则清洗。比如下面这段处理逻辑def clean_table(table): # 移除全空行 table [row for row in table if any(cell and cell.strip() for cell in row)] # 如果某行的列数少于表头列数用上方单元格填充 header_len len(table[0]) for i in range(1, len(table)): if len(table[i]) header_len: table[i] table[i] [] * (header_len - len(table[i])) return table这不算通用解但对多数表头固定、数据行偶尔合并的场景够用。9. 实操中的几个补充技巧除了上面的主流程再分享几个我在实际项目里积累的小技巧不写会可惜。第一个是关于extract_text()的参数。这个方法有一个可选参数layout当你希望保留更多的排版信息时加上layoutTrue会有奇效text page.extract_text(layoutTrue)它能更好地保留空格和换行结构适合需要按原文格式阅读的场景。但缺点是有时会插入大量空白字符如果你要做关键词匹配反而建议关闭。第二个是关于按坐标裁剪。在处理票据、发票这类固定版式文档时我常把页面裁剪成固定区域对每个区域分别调用extract_text()这样能精确抽取发票号码金额小写开票日期等信息。这个思路做报销单自动录入非常管用。第三个是关于性能。pdfplumber逐页解析大文件时比较慢如果只是想知道页数或元数据用PyPDF2就够了别动不动就甩出pdfplumber的大炮。我处理过一个500页的文件PDF2几毫秒能拿到页数pdfplumber要花好几秒差距在这类高频调用上还挺明显的。10. 最后从工具组合到工作流程的思考用PyPDF2和pdfplumber处理PDF其实只是整个办公自动化链条里的一环。我个人的真实感受是工具本身学起来并不难难的是一开始就想清楚我要自动化的这个流程到底包含哪几步以及每一步应该由哪个库负责。很多朋友一上来就搜怎么提取PDF文本搜到代码能跑但遇到合并、加密、水印就又懵了就是因为缺少这张全局地图。以我自己的经验比较合理的成长路径是先用pdfplumber把文本和表格提取跑通解决读的问题再用PyPDF2把合并、拆分、加密、水印跑通解决写的问题最后再根据自己的业务场景把多个步骤串成批处理脚本。等你把手头的PDF处理需求都从前到后跑过一遍之后再去接触OCR识别、PDF表单填写、批量转图片之类的高级主题就会轻松很多。工具会迭代API会改名但读内容用pdfplumber、改结构用PyPDF2这条核心方法论是能长期帮你解决问题的。

相关新闻

AI Agent可信度验证体系:行为证据链与动态信任建模

AI Agent可信度验证体系:行为证据链与动态信任建模

1. 这不是健身App,而是一套AI行为可信度验证体系“Show HN: Strava for AI agents, they train for trust instead of fitness”——这个标题刚刷出来时,我正调试一个客户部署的智能客服系统。它连续三天在凌晨2点自动触发错误重试逻辑,但日志…

2026/10/9 4:07:33 阅读更多 →
H3C SecPath V5防火墙运维实战:安全域、会话与巡检维护指南

H3C SecPath V5防火墙运维实战:安全域、会话与巡检维护指南

简介:H3C SecPath系列防火墙(V5)日常维护指导手册是一份面向网络运维工程师和安全管理员的技术资料,围绕V5版本防火墙在真实场景中的维护与排障展开,能够帮助读者快速掌握设备巡检、定期保养、故障诊断等核心维护工作&…

2026/10/9 4:07:33 阅读更多 →
Cursor 0.45.x 设备指纹治理:声明式设备身份管理方案

Cursor 0.45.x 设备指纹治理:声明式设备身份管理方案

1. 项目概述:这不是“破解工具”,而是一套面向开发者的设备标识治理方案“cursor设备ID修改器 支持0.45.x版本”——看到这个标题,很多刚接触 Cursor 的开发者第一反应是:“又要改硬件指纹?是不是在绕过授权&#xff1…

2026/10/9 4:07:33 阅读更多 →

最新新闻

最新IPA在线签名系统源码 全开源版本

最新IPA在线签名系统源码 全开源版本

源码下载:download.csdn.net/download/m0_66047725/93654842简介:最新IPA在线签名系统源码 全开源版本基于Thinkphp8.0VUE3开发 配合Zsign工具签名前台vue 后台使用art design pro管理框架后台带软件源自动拉取支持自助签名 支持在线签名测试环境&…

2026/10/9 5:36:41 阅读更多 →
2026最新版短视频去水印+视频号去水印小程序版本源码

2026最新版短视频去水印+视频号去水印小程序版本源码

源码下载:download.csdn.net/download/m0_66047725/93654835简介:2026最新版短视频去水印+视频号去水印小程序版本源码图片:安装教程:环境:PHP7.4MySQL5.7域名:必须备案,申请SSL证书…

2026/10/9 5:36:41 阅读更多 →
【ENSP】技巧

【ENSP】技巧

ENSP文章合集: https://wwaul.lanzout.com/b01gid75ah 密码:4r01 设备可视化操作 连线 通常,我们使用auto自动链接,但他没法选择端口及线的类型,比如交换机和路由器之前的链接,默认使用了交换机的e 0/0/3口交换机的g 0…

2026/10/9 5:36:41 阅读更多 →
GitHub日榜高效刷法:从star陷阱到技术选型的避坑指南

GitHub日榜高效刷法:从star陷阱到技术选型的避坑指南

每天早上到工位,我第一件事不是查邮件,而是先打开GitHub的Trending页面,看一遍日期最接近的日榜。这个习惯保持了一两年,从中挖到过不少能直接落地到项目的库,也踩过不少看起来很美、实际中看不中用的坑。GitHub热榜说…

2026/10/9 5:36:41 阅读更多 →
随机化学算法在电网连锁故障N-k分析中的Matlab实现

随机化学算法在电网连锁故障N-k分析中的Matlab实现

电网连锁故障分析这件事,做过的人都知道有多头疼。系统规模一上来,想判断哪几种故障组合最容易把电网拖入大停电,暴力枚举几乎不可行,蒙特卡洛又慢得让人失去耐心。去年我在做 N-k 安全分析时接触到了“随机化学”这个思路&#x…

2026/10/9 5:36:41 阅读更多 →
学Simulink——基于反电动势过零检测的直流无刷电机(BLDC)无感控制仿真

学Simulink——基于反电动势过零检测的直流无刷电机(BLDC)无感控制仿真

目录 手把手教你学Simulink——基于反电动势过零检测的直流无刷电机(BLDC)无感控制仿真 一、 引言:当“霍尔传感器”成为过去式——反电动势过零检测如何成就真正的“无感”BLDC? 二、 问题本质:反电动势过零的“物理机制”与“协同逻辑” 1. 核心物理机制 2. 协同逻辑…

2026/10/9 5:35:40 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →