1. 项目背景与核心问题
这个Python脚本文件名直译为"处理PDF的脚本",从版本编号1902和修改标记0121-3来看,这应该是一个长期维护的企业级工具脚本。这类文件名中的版本号通常对应内部开发周期,而修改标记则指向特定需求或缺陷的追踪编号。
PDF处理脚本在企业环境中通常承担以下关键任务:
- 自动化批量处理扫描件/电子文档
- 实现格式转换(如PDF转Word/Excel)
- 提取关键字段数据(发票号、合同条款等)
- 添加水印/页眉页脚等标准化处理
需要特别注意:这类脚本的修改往往涉及企业文档安全策略,任何改动都必须经过严格测试。我曾参与过某金融机构的PDF处理系统升级,一个正则表达式错误就导致上千份合同编号提取失败。
2. 必须修改的代码模块分析
2.1 文本提取逻辑改造
原始代码可能使用了PyPDF2的基础文本提取:
python复制from PyPDF2 import PdfFileReader
def extract_text(path):
with open(path, 'rb') as f:
pdf = PdfFileReader(f)
text = [pdf.getPage(i).extractText() for i in range(pdf.numPages)]
return '\n'.join(text)
建议升级为pdfminer.six的精细化解析:
python复制from pdfminer.high_level import extract_text
def extract_text_v2(path):
text = extract_text(path,
codec='utf-8',
laparams={'line_overlap': 0.5,
'char_margin': 2.0})
return text.strip()
关键改进点:新版处理复杂版式PDF时准确率提升40%以上,特别是表格和分栏内容
2.2 内存泄漏修复方案
典型的内存泄漏场景出现在批量处理时:
python复制# 错误示范:每次循环都创建新reader对象
for pdf_file in file_list:
reader = PdfFileReader(open(pdf_file, 'rb')) # 文件句柄未关闭
process(reader)
应修改为上下文管理方式:
python复制from contextlib import contextmanager
@contextmanager
def pdf_reader(path):
try:
with open(path, 'rb') as f:
yield PdfFileReader(f)
finally:
pass # 确保资源释放
for pdf_file in file_list:
with pdf_reader(pdf_file) as reader:
process(reader)
2.3 加密PDF处理增强
旧版可能简单跳过加密文件:
python复制if pdf.isEncrypted:
continue # 直接跳过
建议增加解密尝试:
python复制def handle_encrypted(pdf, password_list=['']):
for pwd in password_list:
try:
if pdf.decrypt(pwd):
return True
except:
continue
return False
3. 关键参数配置优化
3.1 页面尺寸自适应处理
添加智能尺寸检测:
python复制from PyPDF2.pdf import PageObject
def get_page_size(page):
media_box = page.mediaBox
return (
float(media_box.getUpperRight_x()) - float(media_box.getLowerLeft_x()),
float(media_box.getUpperRight_y()) - float(media_box.getLowerLeft_y())
)
3.2 字体嵌入配置
确保生成PDF时保留字体:
python复制from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
pdfmetrics.registerFont(TTFont('SimSun', 'SimSun.ttf'))
c = canvas.Canvas("output.pdf")
c.setFont("SimSun", 12) # 显式指定中文字体
4. 异常处理机制完善
4.1 结构化错误分类
python复制class PDFProcessError(Exception):
ERR_CODE = {
1001: "文件损坏",
1002: "加密文档",
1003: "字体缺失",
2001: "权限不足"
}
def __init__(self, code):
self.code = code
super().__init__(f"[{code}] {self.ERR_CODE.get(code, '未知错误')}")
4.2 重试机制实现
python复制from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10))
def process_pdf_with_retry(path):
try:
return process_pdf(path)
except PDFProcessError as e:
if e.code == 1002: # 仅对加密文档重试
raise
log_error(f"处理失败: {path} - {str(e)}")
5. 性能优化关键点
5.1 多进程处理实现
python复制from multiprocessing import Pool
def batch_process(file_list, workers=4):
with Pool(workers) as p:
results = p.map(process_single_pdf, file_list)
return results
5.2 缓存机制设计
python复制import hashlib
from functools import lru_cache
def get_file_hash(path):
with open(path, 'rb') as f:
return hashlib.md5(f.read()).hexdigest()
@lru_cache(maxsize=100)
def get_pdf_metadata(path):
file_hash = get_file_hash(path)
# 缓存元数据获取结果
6. 安全合规注意事项
- 临时文件必须使用安全删除:
python复制import os
import tempfile
from shred import shred
def safe_temp_pdf(original_path):
fd, temp_path = tempfile.mkstemp(suffix='.pdf')
try:
with open(original_path, 'rb') as src, os.fdopen(fd, 'wb') as dst:
dst.write(src.read())
yield temp_path
finally:
shred(temp_path) # 物理删除
- 日志脱敏处理:
python复制import re
def sanitize_log(text):
patterns = [
r'\d{4}-\d{2}-\d{2}', # 日期
r'\d{16,19}', # 银行卡号
r'\d{17}[\dXx]' # 身份证号
]
for pat in patterns:
text = re.sub(pat, '[REDACTED]', text)
return text
7. 测试方案建议
7.1 单元测试重点
python复制import unittest
from unittest.mock import patch
class TestPDFProcess(unittest.TestCase):
@patch('PyPDF2.PdfFileReader')
def test_encrypted_pdf(self, mock_reader):
mock_reader.return_value.isEncrypted = True
with self.assertRaises(PDFProcessError) as ctx:
process_pdf('dummy.pdf')
self.assertEqual(ctx.exception.code, 1002)
7.2 性能基准测试
python复制import timeit
test_files = ['sample1.pdf', 'sample2.pdf', 'sample3.pdf']
def benchmark():
print("单线程:",
timeit.timeit(lambda: [process_pdf(f) for f in test_files], number=10))
print("多进程:",
timeit.timeit(lambda: batch_process(test_files), number=10))
8. 版本兼容性处理
8.1 库版本检测
python复制def check_dependencies():
requirements = {
'PyPDF2': '1.26.0',
'pdfminer.six': '20200517',
'reportlab': '3.5.34'
}
for pkg, ver in requirements.items():
try:
current = __import__(pkg).__version__
if current < ver:
raise ImportError(f"{pkg}需要{ver}+,当前是{current}")
except AttributeError:
continue
8.2 回退机制实现
python复制def safe_extract_text(path):
try:
from pdfminer.high_level import extract_text
return extract_text(path)
except ImportError:
from PyPDF2 import PdfFileReader
with open(path, 'rb') as f:
return PdfFileReader(f).getPage(0).extractText()
9. 文档字符串规范示例
python复制def merge_pdfs(output_path, input_paths, watermark=None):
"""
合并多个PDF文件并可选添加水印
Args:
output_path (str): 输出文件路径
input_paths (list): 待合并的PDF路径列表
watermark (str/None): 水印文本,None表示不添加
Returns:
int: 合并后的总页数
Raises:
PDFProcessError: 当输入文件损坏或加密时抛出
"""
# 实现代码...
10. 持续集成建议
.github/workflows/test.yml示例:
yaml复制name: PDF Processor CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest coverage
- name: Test with pytest
run: |
pytest --cov=./ --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v1
在实际企业环境中维护这类脚本时,建议建立PDF测试样本库,应包含以下典型case:
- 扫描件图片PDF
- 多层加密的合同文档
- 包含复杂表格的报表
- 混合排版的中英文文档
- 损坏的PDF文件样本
每次代码修改都应跑完整个测试样本集,我们团队的经验法则是:处理1000页PDF的峰值内存消耗不应超过物理内存的30%,平均单页处理时间在普通办公电脑上要控制在200ms以内。