251205更新

This commit is contained in:
liubiren 2025-12-05 15:20:51 +08:00
parent 00ff6a5577
commit 973ca8113c
4 changed files with 161 additions and 132 deletions

View File

@ -90,7 +90,7 @@ class MySQLClient:
class CacheClient: class CacheClient:
"""缓存客户端""" """缓存客户端"""
def __init__(self, cache_ttl: int = 360, database: str = "Caches.db"): def __init__(self, cache_ttl: int = 360, database: str = "SQLite.db"):
""" """
:param cache_ttl: 缓存生存时间单位为天 :param cache_ttl: 缓存生存时间单位为天
:param database: 缓存数据库名称 :param database: 缓存数据库名称
@ -154,20 +154,31 @@ class CacheClient:
:return: 缓存 :return: 缓存
""" """
with threading.Lock(): # 线程锁,保证并发安全 with threading.Lock(): # 线程锁,保证并发安全
with self.connection.cursor() as cursor:
# noinspection PyBroadException # noinspection PyBroadException
try: try:
# 创建游标
cursor = self.connection.cursor()
# 根据缓存唯一标识查询有效缓存 # 根据缓存唯一标识查询有效缓存
cursor.execute( cursor.execute(
"SELECT cache FROM caches WHERE guid = ? AND timestamp >= ?", "SELECT cache FROM caches WHERE guid = ? AND timestamp >= ?",
(guid, time.time() - self.cache_ttl * 86400), (guid, time.time() - self.cache_ttl * 86400),
) )
if result := cursor.fetchone(): if result := cursor.fetchone():
print()
return json.loads(result[0]) return json.loads(result[0])
return None return None
# 若发生异常则回滚事务并返回None
except Exception: except Exception:
self.connection.rollback() self.connection.rollback()
return None return None
finally:
# 确保游标关闭(关键:释放资源)
if cursor:
# noinspection PyBroadException
try:
cursor.close()
except Exception:
pass
def update(self, guid: str, cache: Dict) -> bool: def update(self, guid: str, cache: Dict) -> bool:
""" """
@ -177,9 +188,10 @@ class CacheClient:
:return: 成功返回True失败返回False :return: 成功返回True失败返回False
""" """
with threading.Lock(): # 线程锁,保证并发安全 with threading.Lock(): # 线程锁,保证并发安全
with self.connection.cursor() as cursor:
# noinspection PyBroadException # noinspection PyBroadException
try: try:
# 创建游标
cursor = self.connection.cursor()
# 新增或覆盖缓存 # 新增或覆盖缓存
cursor.execute( cursor.execute(
"INSERT OR REPLACE INTO caches (guid, cache, timestamp) VALUES (?, ?, ?)", "INSERT OR REPLACE INTO caches (guid, cache, timestamp) VALUES (?, ?, ?)",
@ -192,9 +204,18 @@ class CacheClient:
# 提交事务 # 提交事务
self.connection.commit() self.connection.commit()
return True return True
# 若发生异常则回滚事务并返回None
except Exception: except Exception:
self.connection.rollback() # 异常回滚事务 self.connection.rollback()
return False return False
finally:
# 确保游标关闭(关键:释放资源)
if cursor:
# noinspection PyBroadException
try:
cursor.close()
except Exception:
pass
""" """

Binary file not shown.

Binary file not shown.

View File

@ -5,24 +5,23 @@
功能清单 功能清单
https://liubiren.feishu.cn/docx/WFjTdBpzroUjQvxxrNIcKvGnneh?from=from_copylink https://liubiren.feishu.cn/docx/WFjTdBpzroUjQvxxrNIcKvGnneh?from=from_copylink
""" """
import hashlib
import json import json
import re import re
import uuid import uuid
from base64 import b64encode from base64 import b64encode
from datetime import datetime from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP from decimal import Decimal, ROUND_HALF_UP
from hashlib import md5
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple
import cv2 import cv2
import numpy import numpy
import pandas
from dateutil.parser import parse from dateutil.parser import parse
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from jionlp import parse_location
from zen import ZenDecision, ZenEngine from zen import ZenDecision, ZenEngine
from utils.client import Authenticator, HTTPClient from utils.client import Authenticator, HTTPClient, CacheClient
# from utils.ocr import fuzzy_match # from utils.ocr import fuzzy_match
@ -33,111 +32,120 @@ from utils.client import Authenticator, HTTPClient
# ------------------------- # -------------------------
def image_read():
"""本地读取影像件"""
# 影像件读取(默认转为单通道灰度图,实际需下载影像件)
image_ndarray = cv2.imread(globals()["image_path"].as_posix(), cv2.IMREAD_GRAYSCALE)
if image_ndarray is None:
return None
# 编码为图像字节流
# noinspection PyShadowingNames # noinspection PyShadowingNames
success, image_bytes = cv2.imencode(f".{globals()["image_format"]}", image_ndarray) def image_read(
if not success or image_bytes is None: image_path: Path,
return None ) -> Tuple[numpy.ndarray | None, str | None, bytes | None]:
"""
return image_bytes 本地打开并读取影像件
:param image_path: 影像件路径对象
:return: 影像件数组影像件格式和影像件字节流
def images_compression(**kwargs) -> tuple[str | None, str | None]: """
"""影像件压缩并BASE64编码""" # noinspection PyBroadException
try:
# 影像件打开并读取(默认转为单通道灰度图)
image_ndarray = cv2.imread(image_path.as_posix(), cv2.IMREAD_GRAYSCALE)
if image_ndarray is None:
raise RuntimeError("影像件打开并读取发生异常")
# 影像件格式 # 影像件格式
image_format = kwargs.get("image_format", globals()["image_format"]) image_format = image_path.suffix.lower()
if image_format is None:
return None, None
# 影像件 # 按照影像件格式将影像件数组编码
image = kwargs.get("image", globals()["image"]) success, image_ndarray_encoded = cv2.imencode(image_format, image_ndarray)
if image is None: if not success or image_ndarray_encoded is None:
return None, None raise RuntimeError("编码为图像字节数组发生异常")
# 将编码后图像数组转为字节流
image_bytes = image_ndarray_encoded.tobytes()
# 编码为图像字节流 return image_ndarray, image_format, image_bytes
success, image_bytes = cv2.imencode(ext=f".{image_format}", img=image) except Exception:
# 若发生异常则返回NONE return None, None, None
if not success or image_bytes is None:
return None, None
# 生成影像件唯一标识
image_guid = hashlib.md5(image_bytes.tobytes()).hexdigest().upper()
# BASE64编码 # noinspection PyShadowingNames
image_base64 = b64encode(image_bytes.tobytes()).decode("utf-8") def images_compress(
image_ndarray, image_format, image_bytes, image_size_specified=2
) -> Optional[str]:
"""
影像件压缩
:param image_ndarray: 影像件数组
:param image_format: 影像件格式
:param image_bytes: 影像件字节流
:param image_size_specified: 指定影像件大小单位为兆字节MB
:return: 压缩后影像件BASE64编码
"""
# 将指定影像件大小单位由兆字节转为字节
image_size_specified = image_size_specified * 1024 * 1024
# 将指定影像件大小单位由MB转为B # 影像件BASE64编码
image_size_specified = kwargs.get("image_size_specified", 2) * 1048576 image_base64 = b64encode(image_bytes).decode("utf-8")
# 若影像件BASE64编码后大小小于指定影像件大小则返回
if len(image_base64) <= image_size_specified: if len(image_base64) <= image_size_specified:
return image_guid, image_base64 return image_base64
# 双循环压缩影像件 # 通过调整影像件质量和尺寸达到压缩影像件目的
# 外循环压缩:通过降低影像件质量实现压缩影像件大小 # 外循环压缩:通过调整影像件质量实现压缩影像件大小
for quality in range(90, 0, -10): for quality in range(90, 50, -10):
image_ndarray_copy = image_ndarray.copy()
image_copy = image.copy() # 内循环压缩:通过调整影像件尺寸实现压缩影像件大小
# 内循环压缩:通过等比例调整影像件尺寸实现压缩影像件大小
for i in range(25): for i in range(25):
# 按照影像件格式和影像件质量将影像件数组编码
success, image_bytes = cv2.imencode( success, image_ndarray_encoded = cv2.imencode(
ext=f".{image_format}", image_format,
img=image_copy, image_ndarray_copy,
params=( params=(
[cv2.IMWRITE_PNG_COMPRESSION, 10 - quality // 10] [cv2.IMWRITE_PNG_COMPRESSION, 10 - quality // 10]
if image_format == "png" if image_format == "png"
else [cv2.IMWRITE_JPEG_QUALITY, quality] else [cv2.IMWRITE_JPEG_QUALITY, quality]
), ),
) )
if not success or image_bytes is None: if not success or image_ndarray_encoded is None:
break break
image_base64 = b64encode(image_bytes.tobytes()).decode("utf-8")
if len(image_base64) <= image_size_specified:
return image_guid, image_base64
# 调整影像件尺寸
image_copy = cv2.resize(
src=image_copy,
dsize=(int(image_copy.shape[0] * 0.9), int(image_copy.shape[1] * 0.9)),
interpolation=cv2.INTER_AREA,
)
# 若调整后影像件尺寸中长或宽小于200像素则停止调整影像件尺寸
if min(image_copy.shape[:2]) < 200:
break
# 若仍未压缩至指定影像件大小则返回NONE
return None, None
def images_classification(**kwargs) -> tuple[str | None, str | None]:
"""影像件分类"""
# 影像件全局唯一标识:优先使用关键词变量,其次使用全局变量,再次使用随机唯一标识
image_guid = kwargs.get(
"image_guid", globals().get("image_guid", uuid.uuid4().hex.upper())
)
# 影像件格式
image_format = kwargs.get("image_format", globals()["image_format"])
if image_format is None:
return None, None
# 影像件BASE64编码 # 影像件BASE64编码
image_base64 = kwargs.get("image_base64", globals()["image_base64"]) image_base64 = b64encode(image_ndarray_encoded.tobytes()).decode("utf-8")
if image_base64 is None: if len(image_base64) <= image_size_specified:
return None, None return image_base64
# 调整影像件尺寸
image_ndarray_copy = cv2.resize(
image_ndarray_copy,
(
int(image_ndarray_copy.shape[0] * 0.9),
int(image_ndarray_copy.shape[1] * 0.9),
),
interpolation=cv2.INTER_AREA,
)
# 若调整后影像件尺寸中长或宽小于350像素则停止调整影像件尺寸
if min(image_ndarray_copy.shape[:2]) < 350:
break
return None
def images_classify(
image, image_ndarray, image_format, image_bytes
) -> tuple[str | None, str | None]:
"""
影像件分类并旋正
:param image: 影像件数据
:param image_ndarray: 影像件数组
:param image_format: 影像件格式
:param image_bytes: 影像件字节流
:return: 压缩后影像件BASE64编码
"""
# 影像件唯一标识
image_uuid = image["影像件唯一标识"]
with CacheClient() as client:
# 根据作业环节和影像件唯一标识生成缓存唯一标识
cache_guid = md5(("初审" + image_uuid).encode("utf-8")).hexdigest().upper()
cache = client.query(cache_guid)
if cache is not None:
# 影像件类型
image_type = cache["image_type"]
# 影像件方向
image_o
# 请求深圳快瞳影像件分类接口 # 请求深圳快瞳影像件分类接口
response = globals()["http_client"].post( response = globals()["http_client"].post(
@ -1180,39 +1188,38 @@ if __name__ == "__main__":
for image_index, image_path in enumerate( for image_index, image_path in enumerate(
sorted( sorted(
[ [
image_path x
for image_path in case_path.glob(pattern="*") for x in case_path.glob(pattern="*")
if image_path.is_file() if x.is_file() and x.suffix.lower() in [".jpg", ".jpeg", ".png"]
and image_path.suffix.lower() in [".jpg", ".jpeg", ".png"]
], # 实际作业亦仅支持JPG、JPEG或PNG ], # 实际作业亦仅支持JPG、JPEG或PNG
key=lambda x: x.name, key=lambda x: x.stat().st_ctime, # 根据影像件创建时间顺序排序
), ),
1, 1,
): ):
# 初始化影像件数据 # 初始化影像件数据
image = { image = {
"原始影像件": { "原始影像件": {
"影像件地址": image_path.as_posix(), "影像件地址": image_path.as_posix(), # 将影像件路径对象转为字符串
"影像件名称": (image_name := image_path.stem), "影像件名称": (image_name := image_path.stem),
"影像件格式": (image_format := image_path.suffix.lower()),
}, },
} }
# 本地读取影像件(实际从影像件服务器下载) # 本地打开并读取影像件(实际作业为从作业系统的影像件服务器下载并读取影像件,因赔案签收时会转存影像件至影像件服务器,若下载并读取影像件发生异常则技术支持排查)
image_bytes = image_read() image_ndarray, image_format, image_bytes = image_read(image_path)
if image_bytes is None: # 若本地打开并读取影像件发生异常则跳过该影像件
# noinspection PyTypeChecker if image_format is None or image_bytes is None:
image["影像件唯一标识"] = None raise RuntimeError("本地打开并读取影像件发生异常")
continue
image["原始影像件"]["影像件格式"] = image_format
# 生成影像件唯一标识 # 生成影像件唯一标识
# noinspection PyTypeChecker # noinspection PyTypeChecker
image["影像件唯一标识"] = ( image["影像件唯一标识"] = md5(image_bytes).hexdigest().upper()
image_guid := hashlib.md5(image_bytes.tobytes()).hexdigest().upper()
) # 影像件分类
print(image) print(image)
exit() exit()
"""
# 影像件压缩输出BASE64编码 # 影像件压缩输出BASE64编码
image_guid, image_base64 = images_compression() image_guid, image_base64 = images_compression()
@ -1586,3 +1593,4 @@ if __name__ == "__main__":
} }
) )
) )
"""