115 lines
3.9 KiB
Python
115 lines
3.9 KiB
Python
"""阿里云 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
|