# 前言
哎,又受朋友所托找我帮忙搞机,她最近兼职了一个仓管职位,主要是负责仓库进出货验收的,每天要在系统上根据合同编号下载对应的验收单号文档,一个合同通常会有 N个
验收单号文档,她需要把这个合同的所有验收单号文档下载到本地,然后打印出来签名盖章,再扫描成图片,最后再将图片以验收单号命名,作为附件上传到系统
她希望我可以帮她写个程序自动识别这些图片的验收单号,并以该单号作为文件名,这样她就不用打开每个图片对验收单号了,以便减轻她的工作量
图片内容如下:
开始
# 脚本实现
了解了对方的需求,我就开始搜索对应的方案,暂时锁定了 pytesseract
和 easyocr
这两个开源的图片识别文字项目
经过测试发现 pytesseract
的识别效果并不理想,但是 easyocr
能够准确的识别所有扫描图片的验收单号,只是有时识别出来的验收单号开头或末尾会多了一个字母 "I"
或数字 “1”
,因为这个文档是以表格绘制的,所以单元格的 "|"
符号有时会识别成有效字符,并且有时还会识别出一些 特殊字符
,但是经过了解发现目前验收单号的开头关键字都是固定 CKCR
,而且都是字母和数字的组合,验收单号长度固定为 20位
我在本地测试没有问题后,就把相关的软件安装包和模块拷贝到优盘,然后就去我朋友单位帮她部署环境,需要的软件包和模块如下:
C:\Users\Administrator\Documents\offline_packages>tree /F
文件夹 PATH 列表
卷序列号为 3EB4-6A00
C:.
│ easyocr-1.7.2-py3-none-any.whl
│ filelock-3.19.1-py3-none-any.whl
│ fsspec-2025.7.0-py3-none-any.whl
│ imageio-2.37.0-py3-none-any.whl
│ jinja2-3.1.6-py3-none-any.whl
│ lazy_loader-0.4-py3-none-any.whl
│ MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl
│ mpmath-1.3.0-py3-none-any.whl
│ networkx-3.5-py3-none-any.whl
│ ninja-1.13.0-py3-none-win_amd64.whl
│ numpy-2.2.6-cp312-cp312-win_amd64.whl
│ opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl
│ opencv_python_headless-4.12.0.88-cp37-abi3-win_amd64.whl
│ packaging-25.0-py3-none-any.whl
│ pillow-11.3.0-cp312-cp312-win_amd64.whl
│ pyclipper-1.3.0.post6-cp312-cp312-win_amd64.whl
│ python-3.12.0-amd64.exe
│ python_bidi-0.6.6-cp312-cp312-win_amd64.whl
│ PyYAML-6.0.2-cp312-cp312-win_amd64.whl
│ scikit_image-0.25.2-cp312-cp312-win_amd64.whl
│ scipy-1.16.1-cp312-cp312-win_amd64.whl
│ setuptools-80.9.0-py3-none-any.whl
│ shapely-2.1.1-cp312-cp312-win_amd64.whl
│ sympy-1.14.0-py3-none-any.whl
│ tifffile-2025.6.11-py3-none-any.whl
│ torch-2.8.0-cp312-cp312-win_amd64.whl
│ torchvision-0.23.0-cp312-cp312-win_amd64.whl
│ typing_extensions-4.15.0-py3-none-any.whl
│ VC_redist.x64.exe
│
└─.EasyOCR
├─model
│ chinese_sim.pth
│ craft_mlt_25k.pth
│ english_g2.pth
│ zh_sim_g2.pth
│
└─user_network
脚本代码如下:
import easyocr | |
import os | |
import re | |
from PIL import Image | |
import cv2 | |
import numpy as np | |
import logging | |
# 禁用不必要的日志记录 | |
logging.disable(logging.WARNING) | |
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' | |
def preprocess_image(image_path): | |
"""图像预处理以提高OCR准确率""" | |
try: | |
# 使用 OpenCV 读取图像 | |
img = cv2.imread(image_path) | |
if img is None: | |
# 如果 OpenCV 无法读取,使用 PIL | |
pil_img = Image.open(image_path) | |
img = np.array(pil_img) | |
# 转换 BGR 格式(OpenCV 使用 BGR) | |
if len(img.shape) == 3: | |
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) | |
# 转换为灰度图 | |
if len(img.shape) == 3: | |
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
else: | |
gray = img | |
# 提高对比度 | |
gray = cv2.convertScaleAbs(gray, alpha=1.5, beta=0) | |
# 二值化处理 | |
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) | |
return binary | |
except Exception as e: | |
print(f"图像预处理失败: {e}") | |
# 如果预处理失败,返回原始图像 | |
return cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) | |
def clean_filename(text): | |
"""清理文本,使其适合作为文件名""" | |
# 移除 Windows 文件名中不允许的字符 | |
invalid_chars = '<>:"/\\|?*\'' | |
for char in invalid_chars: | |
text = text.replace(char, '') | |
# 移除换行符和制表符 | |
text = text.replace('\n', '').replace('\r', '').replace('\t', '') | |
# 移除首尾空格 | |
return text.strip() | |
def extract_keyword_text(text, keyword): | |
"""从文本中提取包含关键字在内的20个字符,忽略所有特殊字符""" | |
if not text or not keyword: | |
return None | |
# 查找关键字位置(不区分大小写) | |
pattern = re.compile(re.escape(keyword), re.IGNORECASE) | |
match = pattern.search(text) | |
if not match: | |
return None | |
# 获取关键字开始的位置 | |
start_pos = match.start() | |
# 从关键字开始的位置提取文本 | |
remaining_text = text[start_pos:] | |
# 移除所有非字母数字字符(只保留字母和数字) | |
# 使用正则表达式移除所有非字母数字字符 | |
cleaned_text = re.sub(r'[^A-Za-z0-9]', '', remaining_text) | |
# 取前 20 个字符 | |
return cleaned_text[:20] | |
def main(): | |
# 设置模型路径(确保您已经将模型文件复制到这个位置) | |
model_storage_directory = r'C:\Users\Administrator\.EasyOCR\model' | |
# 检查模型文件是否存在 | |
if not os.path.exists(model_storage_directory): | |
print(f"错误: 模型目录不存在: {model_storage_directory}") | |
print("请先将模型文件复制到该目录") | |
return | |
# 初始化 EasyOCR 阅读器,指定模型路径 | |
try: | |
print("正在初始化OCR引擎...") | |
reader = easyocr.Reader( | |
['ch_sim', 'en'], | |
gpu=False, | |
download_enabled=False, # 禁用下载 | |
model_storage_directory=model_storage_directory | |
) | |
print("OCR引擎初始化完成") | |
except Exception as e: | |
print(f"OCR引擎初始化失败: {e}") | |
print("请检查模型文件是否正确放置") | |
return | |
# 指定图片文件夹路径 | |
folder_path = input("请输入合同图片文件夹路径: ").strip().strip('"') | |
# 确保文件夹存在 | |
if not os.path.exists(folder_path): | |
print(f"错误: 文件夹不存在: {folder_path}") | |
return | |
# 获取用户输入的关键字 | |
keyword = input("请输入验收单号的关键字: ").strip() | |
print(f"开始处理文件夹: {folder_path}") | |
print(f"使用关键字: {keyword}") | |
# 支持的图片格式 | |
image_extensions = ('.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.tif') | |
image_files = [f for f in os.listdir(folder_path) if f.lower().endswith(image_extensions)] | |
if not image_files: | |
print("未找到图片文件") | |
return | |
print(f"找到 {len(image_files)} 个图片文件") | |
success_count = 0 | |
processed_count = 0 | |
for filename in image_files: | |
file_path = os.path.join(folder_path, filename) | |
print(f"\n正在处理 ({processed_count + 1}/{len(image_files)}): {filename}") | |
processed_count += 1 | |
try: | |
# 图像预处理 | |
processed_img = preprocess_image(file_path) | |
# 使用 EasyOCR 进行识别 | |
print("正在进行OCR识别...") | |
results = reader.readtext(processed_img, detail=0) | |
text = '\n'.join(results) | |
if text: | |
print(f"识别到的文本:\n{text[:25]}...") # 只显示前 25 个字符 | |
else: | |
print("未识别到文本") | |
continue | |
# 提取关键字后的文本 | |
extracted_text = extract_keyword_text(text, keyword) | |
if extracted_text: | |
print(f"找到提取的文本: {extracted_text}") | |
# 清理文本,确保它可以作为文件名 | |
clean_text = clean_filename(extracted_text) | |
# 构建新文件名 | |
file_extension = os.path.splitext(filename)[1] | |
new_filename = f"{clean_text}{file_extension}" | |
new_file_path = os.path.join(folder_path, new_filename) | |
# 检查是否已存在同名文件 | |
counter = 1 | |
while os.path.exists(new_file_path): | |
new_filename = f"{clean_text}_{counter}{file_extension}" | |
new_file_path = os.path.join(folder_path, new_filename) | |
counter += 1 | |
# 重命名文件 | |
os.rename(file_path, new_file_path) | |
success_count += 1 | |
print(f"成功重命名: {filename} -> {new_filename}") | |
else: | |
print(f"未找到包含关键字 '{keyword}' 的文本") | |
except Exception as e: | |
print(f"处理 {filename} 时出错: {str(e)}") | |
print(f"\n处理完成! 成功重命名 {success_count}/{processed_count} 个文件") | |
if __name__ == "__main__": | |
main() |
# 执行效果
脚本识别效果:
图片内容详情:
可以看到虽然脚本识别出来的验收单号是: ICKCRO3162025080026171
, 但是最终文件重命名的还是正确的验收单号: CKCRO316202508002617
另外,因为我朋友单位的电脑是没有独立显卡的,所以脚本中禁用了 GPU
, 处理起来速度会有点慢,如果有独立显卡的话,可以将代码中的 gpu=False
改成 gpu=True
试试,但是具体速度有多大提升我没有测试
有相同需求的小伙伴可以参考一下