"""阿里云 OCR 客户端封装(ocr-api 2021-07-07)。 只关注当前需要的能力——RecognizePassport(护照识别)。 其它证件接口(身份证、行驶证…)按需扩展即可,签名/客户端复用 _build_client。 配置项 (app.config.settings): - aliyun_ocr_access_key_id - aliyun_ocr_access_key_secret - aliyun_ocr_endpoint (默认 ocr-api.cn-hangzhou.aliyuncs.com) """ from __future__ import annotations import json from functools import lru_cache from io import BytesIO from typing import Any from app.config import settings class OcrConfigError(RuntimeError): """AK/SK 未配置等可恢复错误,路由层应返回 503。""" class OcrCallError(RuntimeError): """阿里云返回非 200 或网络异常等运行期错误。""" @lru_cache(maxsize=1) def _build_client(): # 延迟导入 SDK,缺包时给出明确提示 try: from alibabacloud_ocr_api20210707.client import Client from alibabacloud_tea_openapi.models import Config except ImportError as exc: # noqa: BLE001 raise OcrConfigError( "未安装阿里云 OCR SDK,请执行: pip install alibabacloud_ocr_api20210707" ) from exc ak = settings.aliyun_ocr_access_key_id.strip() sk = settings.aliyun_ocr_access_key_secret.strip() endpoint = settings.aliyun_ocr_endpoint.strip() or "ocr-api.cn-hangzhou.aliyuncs.com" if not ak or not sk: raise OcrConfigError( "阿里云 OCR 未配置:请在 .env 设置 ALIYUN_OCR_ACCESS_KEY_ID / ALIYUN_OCR_ACCESS_KEY_SECRET" ) config = Config(access_key_id=ak, access_key_secret=sk) config.endpoint = endpoint return Client(config) def _parse_data(body: Any) -> dict[str, Any]: """RecognizePassport 返回 body.data 是一段 JSON 字符串,统一解析为 dict。""" raw = getattr(body, "data", None) if raw is None and isinstance(body, dict): raw = body.get("Data") or body.get("data") if raw is None: return {} if isinstance(raw, (dict, list)): return raw # type: ignore[return-value] try: return json.loads(raw) except (TypeError, ValueError): return {"raw": str(raw)} def recognize_passport( *, image_bytes: bytes | None = None, image_url: str | None = None, ) -> dict[str, Any]: """识别护照。image_bytes / image_url 至少传一个。 返回结构(与控制台字段一致,常见字段示例): { "name": "...", # 姓名 "sex": "M", "country": "CHN", "passport_no": "E12345678", "date_of_birth": "1990-01-01", "date_of_expiry": "2030-01-01", "place_of_birth": "...", ...其它字段透传... "_request_id": "...", # 便于在阿里云后台溯源 } """ if not image_bytes and not image_url: raise ValueError("recognize_passport 需要 image_bytes 或 image_url") from alibabacloud_ocr_api20210707.models import RecognizePassportRequest from alibabacloud_tea_util.models import RuntimeOptions client = _build_client() req_kwargs: dict[str, Any] = {} if image_bytes is not None: req_kwargs["body"] = BytesIO(image_bytes) if image_url: req_kwargs["url"] = image_url request = RecognizePassportRequest(**req_kwargs) try: response = client.recognize_passport_with_options(request, RuntimeOptions()) except OcrConfigError: raise except Exception as exc: # noqa: BLE001 raise OcrCallError(f"调用阿里云 OCR 失败: {exc}") from exc body = getattr(response, "body", response) data = _parse_data(body) request_id = getattr(body, "request_id", None) or ( body.get("RequestId") if isinstance(body, dict) else None ) if request_id and isinstance(data, dict): data.setdefault("_request_id", request_id) return data