Windows下利用 Python OCR 识别电子发票(增值税专用发票)(使用 GhostScript 和 Tesseract )

早起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
#

image

一开始尝试发现图片背景变成了黑色,
而直接使用命令行,"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'

    # 转换为 JPEG
    image_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_FULL
    print(f"检测到完整页发票 (高度 {height} > 3000)")
elif height > 1500:
    page_type = 'HALF'
    coords = COORDS_HALF
    print(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 None
    
    try:
        # 裁剪并识别
        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 text
        else:
            print(f"  {field_name}: (识别为空)")
            return None
    except 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)

对于电子发票信息识别提取,上面的方案有些过时,但也不失为一种编程练习。

posted @ 2025-10-21 17:11  geyee  阅读(7)  评论(0)    收藏  举报