在早起Python
公众号下,作者陈熹的解放双手|Python 批量自动提取、整理 PDF 发票!文章中,看到根据坐标识别图片的方法,觉得代码不是太详细。
试着在windows下重现,如下。
所需 requirements.txt 可以是
# Wand - ImageMagick 的 Python 绑定,用于 PDF 转图片
# 需要先安装 ImageMagick: https://imagemagick.org/script/download.php
Wand>=0.6.10# Pillow - Python 图像处理库,用于图片裁剪和处理
Pillow>=9.0.0# pyocr - Tesseract OCR 的 Python 包装器
# 需要先安装 Tesseract OCR: https://github.com/tesseract-ocr/tesseract
pyocr>=0.8.0# openpyxl - 用于读写 Excel 文件
openpyxl>=3.0.0# numpy - 数值计算库(图像处理可能需要)
numpy>=1.21.0# ========== 系统依赖(需要手动安装)==========
#
# 1. Ghostscript (必需 - 用于 PDF 处理)
# Windows: https://www.ghostscript.com/download/gsdnld.html
# 推荐版本: 9.27 或更高
# 安装路径: C:\Program Files\gs\gs9.27\bin
#
# 2. ImageMagick (必需 - 用于 PDF 转图片)
# Windows: https://imagemagick.org/script/download.php
# 推荐版本: 7.1.2-Q16-HDRI 或更高
# 安装路径: C:\Program Files\ImageMagick-7.1.2-Q16-HDRI
#
# 3. Tesseract OCR (必需 - 用于文字识别)
# Windows: https://github.com/UB-Mannheim/tesseract/wiki
# 推荐版本: 5.0 或更高
# 安装路径: C:\Program Files\Tesseract-OCR
#
# 重要: 需要下载中文语言包 chi_sim.traineddata
# 语言包下载: https://github.com/tesseract-ocr/tessdata
# 语言包路径: C:\Program Files\Tesseract-OCR\tessdata\chi_sim.traineddata
#
一开始尝试发现图片背景变成了黑色,
而直接使用命令行,"C:\Program Files\gs\gs9.27\bin\gswin64c.exe" -dNOPAUSE -dBATCH -sDEVICE=png16m -r300 -sOutputFile=output_%d.png dzfp_1.pdf 则是正常的。
可以合并图像到白色背景色进行更正或者使用之后的代码。chat.z.ai 给的解释是 PDF 文件本身可能包含透明图层或未定义背景色的区域。
with Image(filename=path, resolution=300) as img:# 创建一个白色背景图层with Image(width=img.width, height=img.height, background=Color('white')) as bg:# 将原图像合并到白色背景上bg.composite(img, 0, 0)bg.convert('jpeg')bg.save(filename='output_white.jpg')
参考原文,借助 AI,得到代码如下
import os
import sys
import io
from PIL import ImageDraw# 设置 Ghostscript 和 Tesseract 路径(在导入 wand 之前)
os.environ['MAGICK_HOME'] = r'C:\Program Files\ImageMagick-7.1.2-Q16-HDRI'
os.environ['PATH'] = (r'C:\Program Files\gs\gs9.27\bin;' +r'C:\Program Files\Tesseract-OCR;' +os.environ.get('PATH', ''))from wand.image import Image
from wand.color import Color
from PIL import Image as PI
import pyocr
from openpyxl import Workbook# 获取桌面路径包装成一个函数
def GetDesktopPath():return os.path.join(os.path.expanduser("~"), 'Desktop')# 获取当前文件所在文件夹路径
def GetCurrentDirectoryPath():return os.path.dirname(os.path.abspath(__file__))# ========== 配置区域 ==========
# 选择要处理的 PDF(取消注释其中一个)
# path = GetCurrentDirectoryPath() + r"/dzfp_1.pdf"
path = GetCurrentDirectoryPath() + r"/pdf/dzfp_2.pdf"# ========== 坐标配置 ==========
# 完整页坐标 (2480 x 3508)
COORDS_FULL = {'total_amount': (1642, 3100, 2000, 3270),'tax_id': (640, 510, 1180, 570),'issuer': (370, 3380, 480, 3450),
}# 半页坐标 (2480 x 1654)
# 注意:这些坐标需要根据实际的 pdf2_grid.jpg 来调整
COORDS_HALF = {'total_amount': (1860, 1100, 2200, 1200), # 需要根据实际位置调整'tax_id': (640, 510, 1180, 570), # 'issuer': (360, 1500, 480, 1600), # 需要根据实际位置调整
}# ========== 主程序 ==========
# 获取配置好的 tesseract
tools = pyocr.get_available_tools()
if len(tools) == 0:print("错误: 找不到 OCR 工具,请确认 Tesseract 已安装")sys.exit(1)tool = tools[0]
print(f"使用 OCR 工具: {tool.get_name()}")# 设置语言为简体中文
lang = 'chi_sim'
print(f"使用语言: {lang}")
print(f"处理文件: {os.path.basename(path)}\n")# 通过 wand 模块将 PDF 文件转化为分辨率为 300 的 jpeg 图片形式
with Image(filename=path, resolution=300) as image_pdf:# 设置白色背景,移除透明通道image_pdf.background_color = Color('white')image_pdf.alpha_channel = 'remove'# 转换为 JPEGimage_jpeg = image_pdf.convert('jpeg')# 如果是多个图片,则会保存多个,output-0.jpg, output-1.jpg, ...image_jpeg.save(filename="output.jpg")# 将图片解析为二进制矩阵image_lst = []for img in image_jpeg.sequence:img_page = Image(image=img)image_lst.append(img_page.make_blob('jpeg'))# 用 io 模块的 BytesIO 方法读取二进制内容列表的第一个为图片形式
new_img = PI.open(io.BytesIO(image_lst[0]))# 获取图片尺寸
width, height = new_img.size
print(f"PDF 转换成功,图片尺寸: {width} x {height}")
print(f"图片模式: {new_img.mode}\n")# ========== 新增功能:生成带网格的图片 ==========
# 创建一个带网格的图片,方便查看坐标
grid_img = new_img.copy()
draw = ImageDraw.Draw(grid_img)# 绘制网格线(每 50 像素一条)
grid_step = 50
for x in range(0, width, grid_step):draw.line([(x, 0), (x, height)], fill='red', width=2)# 标注 x 坐标draw.text((x + 5, 10), str(x), fill='red')for y in range(0, height, grid_step):draw.line([(0, y), (width, y)], fill='red', width=2)# 标注 y 坐标draw.text((10, y + 5), str(y), fill='red')# 保存网格图片
grid_img.save('coordinates_grid.jpg')
print(f"已生成网格图片: coordinates_grid.jpg\n")# ========== 自动检测页面类型 ==========
if height > 3000:page_type = 'FULL'coords = COORDS_FULLprint(f"检测到完整页发票 (高度 {height} > 3000)")
elif height > 1500:page_type = 'HALF'coords = COORDS_HALFprint(f"检测到半页发票 (高度 {height} 在 1500-3000 之间)")
else:print(f"未知的页面尺寸: {width} x {height}")sys.exit(1)print(f"使用 {page_type} 页坐标配置\n")# ========== 安全的 OCR 识别函数 ==========
def safe_ocr(img, coords, field_name):"""安全的 OCR 识别,检查坐标是否在范围内"""left, top, right, bottom = coords# 检查坐标是否在图片范围内if left < 0 or top < 0 or right > img.width or bottom > img.height:print(f" {field_name}: 坐标超出范围 {coords},图片尺寸 {img.width} x {img.height}")return Nonetry:# 裁剪并识别cropped = img.crop(coords)# 保存裁剪图片用于调试debug_filename = f"debug_{field_name}.jpg"cropped.save(debug_filename)# OCR 识别text = tool.image_to_string(cropped, lang=lang)text = text.strip()if text:print(f" {field_name}: {text}")return textelse:print(f" {field_name}: (识别为空)")return Noneexcept Exception as e:print(f" {field_name}: 识别失败 - {str(e)}")return None# ========== 识别各个字段 ==========
print("开始识别字段:")
print("="*60)# 总金额
txt1 = safe_ocr(new_img, coords['total_amount'], '总金额')# 纳税人识别号
txt2 = safe_ocr(new_img, coords['tax_id'], '纳税人识别号')# 开票人
txt3 = safe_ocr(new_img, coords['issuer'], '开票人')print("="*60)# ========== 写入 Excel ==========
workbook = Workbook()
sheet = workbook.active
header = ['字段', '值', '页面类型', '坐标']
sheet.append(header)sheet.append(['总金额', txt1 or '', page_type, str(coords['total_amount'])])
sheet.append(['纳税人识别号', txt2 or '', page_type, str(coords['tax_id'])])
sheet.append(['开票人', txt3 or '', page_type, str(coords['issuer'])])excel_path = GetDesktopPath() + r'\发票信息_自适应.xlsx'
workbook.save(excel_path)
print(f'\n数据已保存到: {excel_path}')# ========== 提示信息 ==========
if page_type == 'HALF':print("\n" + "="*60)print("注意:检测到半页发票")print("="*60)print("如果识别结果不正确,请:")print("1. 打开 coordinates_grid.jpg 查看网格")print("2. 找到字段的实际位置")print("3. 更新本文件中的 COORDS_HALF 配置")print("="*60)
对于电子发票信息识别提取,上面的方案有些过时,但也不失为一种编程练习。