diff --git a/utils/client.py b/utils/client.py index e193b20..7521acb 100644 --- a/utils/client.py +++ b/utils/client.py @@ -90,7 +90,7 @@ class MySQLClient: 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 database: 缓存数据库名称 @@ -154,20 +154,31 @@ class CacheClient: :return: 缓存 """ with threading.Lock(): # 线程锁,保证并发安全 - with self.connection.cursor() as cursor: - # noinspection PyBroadException - try: - # 根据缓存唯一标识查询有效缓存 - cursor.execute( - "SELECT cache FROM caches WHERE guid = ? AND timestamp >= ?", - (guid, time.time() - self.cache_ttl * 86400), - ) - if result := cursor.fetchone(): - return json.loads(result[0]) - return None - except Exception: - self.connection.rollback() - return None + # noinspection PyBroadException + try: + # 创建游标 + cursor = self.connection.cursor() + # 根据缓存唯一标识查询有效缓存 + cursor.execute( + "SELECT cache FROM caches WHERE guid = ? AND timestamp >= ?", + (guid, time.time() - self.cache_ttl * 86400), + ) + if result := cursor.fetchone(): + print() + return json.loads(result[0]) + return None + # 若发生异常则回滚事务并返回None + except Exception: + self.connection.rollback() + return None + finally: + # 确保游标关闭(关键:释放资源) + if cursor: + # noinspection PyBroadException + try: + cursor.close() + except Exception: + pass def update(self, guid: str, cache: Dict) -> bool: """ @@ -177,24 +188,34 @@ class CacheClient: :return: 成功返回True,失败返回False """ with threading.Lock(): # 线程锁,保证并发安全 - with self.connection.cursor() as cursor: - # noinspection PyBroadException - try: - # 新增或覆盖缓存 - cursor.execute( - "INSERT OR REPLACE INTO caches (guid, cache, timestamp) VALUES (?, ?, ?)", - ( - guid, - json.dumps(cache, ensure_ascii=False), - time.time(), - ), - ) - # 提交事务 - self.connection.commit() - return True - except Exception: - self.connection.rollback() # 异常回滚事务 - return False + # noinspection PyBroadException + try: + # 创建游标 + cursor = self.connection.cursor() + # 新增或覆盖缓存 + cursor.execute( + "INSERT OR REPLACE INTO caches (guid, cache, timestamp) VALUES (?, ?, ?)", + ( + guid, + json.dumps(cache, ensure_ascii=False), + time.time(), + ), + ) + # 提交事务 + self.connection.commit() + return True + # 若发生异常则回滚事务并返回None + except Exception: + self.connection.rollback() + return False + finally: + # 确保游标关闭(关键:释放资源) + if cursor: + # noinspection PyBroadException + try: + cursor.close() + except Exception: + pass """ diff --git a/票据理赔自动化/SQLite.db b/票据理赔自动化/SQLite.db index 006b8ea..5b06124 100644 Binary files a/票据理赔自动化/SQLite.db and b/票据理赔自动化/SQLite.db differ diff --git a/票据理赔自动化/SQLite_old.db b/票据理赔自动化/SQLite_old.db new file mode 100644 index 0000000..006b8ea Binary files /dev/null and b/票据理赔自动化/SQLite_old.db differ diff --git a/票据理赔自动化/main.py b/票据理赔自动化/main.py index 430f639..7f8e3a3 100644 --- a/票据理赔自动化/main.py +++ b/票据理赔自动化/main.py @@ -5,24 +5,23 @@ 功能清单 https://liubiren.feishu.cn/docx/WFjTdBpzroUjQvxxrNIcKvGnneh?from=from_copylink """ -import hashlib import json import re import uuid from base64 import b64encode from datetime import datetime from decimal import Decimal, ROUND_HALF_UP +from hashlib import md5 from pathlib import Path +from typing import Optional, Tuple import cv2 import numpy -import pandas from dateutil.parser import parse from jinja2 import Environment, FileSystemLoader -from jionlp import parse_location from zen import ZenDecision, ZenEngine -from utils.client import Authenticator, HTTPClient +from utils.client import Authenticator, HTTPClient, CacheClient # 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 +def image_read( + image_path: Path, +) -> Tuple[numpy.ndarray | None, str | None, bytes | None]: + """ + 本地打开并读取影像件 + :param image_path: 影像件路径对象 + :return: 影像件数组、影像件格式和影像件字节流 + """ + # noinspection PyBroadException + try: + # 影像件打开并读取(默认转为单通道灰度图) + image_ndarray = cv2.imread(image_path.as_posix(), cv2.IMREAD_GRAYSCALE) + if image_ndarray is None: + raise RuntimeError("影像件打开并读取发生异常") - # 编码为图像字节流 - # noinspection PyShadowingNames - success, image_bytes = cv2.imencode(f".{globals()["image_format"]}", image_ndarray) - if not success or image_bytes is None: - return None + # 影像件格式 + image_format = image_path.suffix.lower() - return image_bytes + # 按照影像件格式将影像件数组编码 + success, image_ndarray_encoded = cv2.imencode(image_format, image_ndarray) + if not success or image_ndarray_encoded is None: + raise RuntimeError("编码为图像字节数组发生异常") + # 将编码后图像数组转为字节流 + image_bytes = image_ndarray_encoded.tobytes() + + return image_ndarray, image_format, image_bytes + except Exception: + return None, None, None -def images_compression(**kwargs) -> tuple[str | None, str | None]: - """影像件压缩并BASE64编码""" +# noinspection PyShadowingNames +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 - # 影像件格式 - image_format = kwargs.get("image_format", globals()["image_format"]) - if image_format is None: - return None, None - - # 影像件 - image = kwargs.get("image", globals()["image"]) - if image is None: - return None, None - - # 编码为图像字节流 - success, image_bytes = cv2.imencode(ext=f".{image_format}", img=image) - # 若发生异常则返回NONE - if not success or image_bytes is None: - return None, None - - # 生成影像件唯一标识 - image_guid = hashlib.md5(image_bytes.tobytes()).hexdigest().upper() - - # BASE64编码 - image_base64 = b64encode(image_bytes.tobytes()).decode("utf-8") - - # 将指定影像件大小单位由MB转为B - image_size_specified = kwargs.get("image_size_specified", 2) * 1048576 - - # 若影像件BASE64编码后大小小于指定影像件大小则返回 + # 影像件BASE64编码 + image_base64 = b64encode(image_bytes).decode("utf-8") if len(image_base64) <= image_size_specified: - return image_guid, image_base64 + return image_base64 - # 双循环压缩影像件 - # 外循环压缩:通过降低影像件质量实现压缩影像件大小 - for quality in range(90, 0, -10): - - image_copy = image.copy() - - # 内循环压缩:通过等比例调整影像件尺寸实现压缩影像件大小 + # 通过调整影像件质量和尺寸达到压缩影像件目的 + # 外循环压缩:通过调整影像件质量实现压缩影像件大小 + for quality in range(90, 50, -10): + image_ndarray_copy = image_ndarray.copy() + # 内循环压缩:通过调整影像件尺寸实现压缩影像件大小 for i in range(25): - - success, image_bytes = cv2.imencode( - ext=f".{image_format}", - img=image_copy, + # 按照影像件格式和影像件质量将影像件数组编码 + success, image_ndarray_encoded = cv2.imencode( + image_format, + image_ndarray_copy, params=( [cv2.IMWRITE_PNG_COMPRESSION, 10 - quality // 10] if image_format == "png" else [cv2.IMWRITE_JPEG_QUALITY, quality] ), ) - if not success or image_bytes is None: + if not success or image_ndarray_encoded is None: break - image_base64 = b64encode(image_bytes.tobytes()).decode("utf-8") + # 影像件BASE64编码 + image_base64 = b64encode(image_ndarray_encoded.tobytes()).decode("utf-8") if len(image_base64) <= image_size_specified: - return image_guid, image_base64 + return image_base64 # 调整影像件尺寸 - image_copy = cv2.resize( - src=image_copy, - dsize=(int(image_copy.shape[0] * 0.9), int(image_copy.shape[1] * 0.9)), + 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, ) - - # 若调整后影像件尺寸中长或宽小于200像素则停止调整影像件尺寸 - if min(image_copy.shape[:2]) < 200: + # 若调整后影像件尺寸中长或宽小于350像素则停止调整影像件尺寸 + if min(image_ndarray_copy.shape[:2]) < 350: break - # 若仍未压缩至指定影像件大小则返回NONE - return None, None + return None -def images_classification(**kwargs) -> tuple[str | None, str | 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) - # 影像件全局唯一标识:优先使用关键词变量,其次使用全局变量,再次使用随机唯一标识 - 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编码 - image_base64 = kwargs.get("image_base64", globals()["image_base64"]) - if image_base64 is None: - return None, None + if cache is not None: + # 影像件类型 + image_type = cache["image_type"] + # 影像件方向 + image_o # 请求深圳快瞳影像件分类接口 response = globals()["http_client"].post( @@ -1180,39 +1188,38 @@ if __name__ == "__main__": for image_index, image_path in enumerate( sorted( [ - image_path - for image_path in case_path.glob(pattern="*") - if image_path.is_file() - and image_path.suffix.lower() in [".jpg", ".jpeg", ".png"] + x + for x in case_path.glob(pattern="*") + if x.is_file() and x.suffix.lower() in [".jpg", ".jpeg", ".png"] ], # 实际作业亦仅支持JPG、JPEG或PNG - key=lambda x: x.name, + key=lambda x: x.stat().st_ctime, # 根据影像件创建时间顺序排序 ), 1, ): # 初始化影像件数据 image = { "原始影像件": { - "影像件地址": image_path.as_posix(), + "影像件地址": image_path.as_posix(), # 将影像件路径对象转为字符串 "影像件名称": (image_name := image_path.stem), - "影像件格式": (image_format := image_path.suffix.lower()), }, } - # 本地读取影像件(实际从影像件服务器下载) - image_bytes = image_read() - if image_bytes is None: - # noinspection PyTypeChecker - image["影像件唯一标识"] = None - continue + # 本地打开并读取影像件(实际作业为从作业系统的影像件服务器下载并读取影像件,因赔案签收时会转存影像件至影像件服务器,若下载并读取影像件发生异常则技术支持排查) + image_ndarray, image_format, image_bytes = image_read(image_path) + # 若本地打开并读取影像件发生异常则跳过该影像件 + if image_format is None or image_bytes is None: + raise RuntimeError("本地打开并读取影像件发生异常") + image["原始影像件"]["影像件格式"] = image_format # 生成影像件唯一标识 # noinspection PyTypeChecker - image["影像件唯一标识"] = ( - image_guid := hashlib.md5(image_bytes.tobytes()).hexdigest().upper() - ) + image["影像件唯一标识"] = md5(image_bytes).hexdigest().upper() + + # 影像件分类 print(image) exit() +""" # 影像件压缩(输出BASE64编码) image_guid, image_base64 = images_compression() @@ -1586,3 +1593,4 @@ if __name__ == "__main__": } ) ) +"""