Files
Shudao/app.py
T

343 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Web backend for the OCR table -> online spreadsheet pipeline.
The OCR call mirrors `api.py` as closely as possible:
* same module-level imports (alibabacloud_ocr_api20210707, credentials,
tea_openapi, darabonba_stream, tea_util, etc.)
* same `Sample` class with `create_client` (only difference: credentials
are parameterized instead of hardcoded)
* same `main` shape: build client -> read body stream -> build
`RecognizeTableOcrRequest` -> call `recognize_table_ocr_with_options`
* response: we use `to_map()` (the proper TeaModel way) instead of
`print(json.dumps(resp, default=str))`, which produces invalid JSON
(Python repr, not JSON) in the original.
Endpoints:
GET / -> index.html
POST /api/convert -> image + aliyun AK -> full pipeline -> {sheet_data, ocr_result}
POST /api/load_xlsx -> .xlsx file -> {sheet_data}
POST /api/load_json -> OCR-style .json file -> {sheet_data}
GET /api/load_demo -> 111.html / 111.xlsx -> {sheet_data}
GET /sheet -> sheet.html (legacy standalone template)
GET /api/health -> health check
"""
import json
import os
import re
import sys
import tempfile
import traceback
from flask import Flask, jsonify, request, send_from_directory
# Make sibling modules importable
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
# ---------- Aliyun SDK imports (mirror api.py) ----------
from alibabacloud_credentials.client import Client as CredentialClient
from alibabacloud_credentials.models import Config as CredentialConfig
from alibabacloud_darabonba_stream.client import Client as StreamClient
from alibabacloud_ocr_api20210707 import models as ocr_api_20210707_models
from alibabacloud_ocr_api20210707.client import Client as ocr_api20210707Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_tea_util import models as util_models
from generate_sheet import xlsx_to_spreadsheet_data # noqa: E402
# ---------- Sample class: same shape as api.py ----------
class Sample:
def __init__(self):
pass
@staticmethod
def create_client(access_key_id: str, access_key_secret: str) -> ocr_api20210707Client:
"""
Mirrors api.py's create_client, but credentials are parameters
instead of hardcoded. Everything else (config shape, endpoint)
is identical to api.py.
"""
credentialsConfig = CredentialConfig(
type='access_key',
access_key_id=access_key_id,
access_key_secret=access_key_secret,
)
credentialsClient = CredentialClient(credentialsConfig)
config = open_api_models.Config(
credential=credentialsClient
)
config.endpoint = f'ocr-api.cn-hangzhou.aliyuncs.com'
return ocr_api20210707Client(config)
@staticmethod
def recognize_table(image_path: str, access_key_id: str, access_key_secret: str) -> dict:
"""
Mirrors api.py's main(), but:
* image path is a parameter
* credentials are parameters
* returns the structured response dict (via to_map) instead of
printing invalid-JSON string
"""
client = Sample.create_client(access_key_id, access_key_secret)
body_stream = StreamClient.read_from_file_path(image_path)
recognize_table_ocr_request = ocr_api_20210707_models.RecognizeTableOcrRequest(
body=body_stream
)
runtime = util_models.RuntimeOptions()
resp = client.recognize_table_ocr_with_options(recognize_table_ocr_request, runtime)
# `to_map()` is the documented way to get a JSON-serializable dict
# from a TeaModel response. This is what api.py's `default=str`
# hack was trying to do (and failing at, since str(resp) yields
# a Python repr, not JSON).
return resp.to_map() if hasattr(resp, 'to_map') else _tea_fallback(resp)
def _tea_fallback(resp):
"""Last-resort: serialize via str(). May produce Python repr, not JSON."""
try:
return json.loads(json.dumps(resp, default=str))
except Exception:
return {"_raw": str(resp)}
# ---------- Flask app ----------
app = Flask(__name__, static_folder=None)
def run_subprocess(cmd: list) -> tuple[int, str, str]:
import subprocess
p = subprocess.run(cmd, capture_output=True, text=True, cwd=HERE)
return p.returncode, p.stdout, p.stderr
@app.route("/")
def index():
return send_from_directory(HERE, "index.html")
@app.route("/sheet")
@app.route("/sheet.html")
def sheet():
return send_from_directory(HERE, "sheet.html")
@app.route("/111.html")
def sheet_111():
return send_from_directory(HERE, "111.html")
@app.route("/api/health")
def health():
return jsonify({"ok": True, "service": "shudao"})
@app.route("/api/convert", methods=["POST"])
def api_convert():
if "image" not in request.files:
return jsonify({"ok": False, "stage": "input", "error": "缺少图片(字段名 image"}), 400
access_key_id = (request.form.get("access_key_id") or "").strip()
access_key_secret = (request.form.get("access_key_secret") or "").strip()
if not access_key_id or not access_key_secret:
return jsonify({"ok": False, "stage": "input", "error": "缺少阿里云 AccessKey ID / Secret"}), 400
image = request.files["image"]
if not image.filename:
return jsonify({"ok": False, "stage": "input", "error": "图片文件为空"}), 400
workdir = tempfile.mkdtemp(prefix="shudao_")
img_path = os.path.join(workdir, "input" + os.path.splitext(image.filename)[1].lower() or ".png")
json_path = os.path.join(workdir, "ocr.json")
xlsx_path = os.path.join(workdir, "ocr.xlsx")
try:
image.save(img_path)
# ---- Step 1: OCR (mirrors api.py exactly) ----
try:
ocr_raw = Sample.recognize_table(img_path, access_key_id, access_key_secret)
except Exception as e:
return jsonify({
"ok": False,
"stage": "ocr",
"error": f"OCR 调用失败:{e}",
"trace": traceback.format_exc(),
}), 500
# The Aliyun SDK returns {body: {Data: "<json string>", RequestId}, headers, statusCode}
# json_to_excel.py expects the body shape: {Data: "<json string>", RequestId}
# (it then json.loads the Data string to get prism_tablesInfo).
ocr_body = ocr_raw.get("body") if isinstance(ocr_raw, dict) else None
if not isinstance(ocr_body, dict):
return jsonify({
"ok": False,
"stage": "ocr",
"error": "OCR 响应格式异常:缺少 body 字段",
"raw_type": type(ocr_raw).__name__,
}), 500
with open(json_path, "w", encoding="utf-8") as f:
json.dump(ocr_body, f, ensure_ascii=False, indent=2)
# ---- Step 2: json -> xlsx ----
rc, out, err = run_subprocess(
[sys.executable, os.path.join(HERE, "json_to_excel.py"), json_path, xlsx_path]
)
if rc != 0:
return jsonify({
"ok": False,
"stage": "json_to_excel",
"error": err or out or "json_to_excel 失败",
}), 500
# ---- Step 3: xlsx -> sheet data ----
try:
sheet_data = xlsx_to_spreadsheet_data(xlsx_path)
except Exception as e:
return jsonify({
"ok": False,
"stage": "xlsx_to_sheet",
"error": f"xlsx 解析失败:{e}",
"trace": traceback.format_exc(),
}), 500
return jsonify({
"ok": True,
"sheet_data": sheet_data,
"ocr_result": ocr_raw,
})
except Exception as e:
return jsonify({
"ok": False,
"stage": "unhandled",
"error": f"未处理异常:{e}",
"trace": traceback.format_exc(),
}), 500
# -------------------- Direct file imports (skip OCR) --------------------
@app.route("/api/load_xlsx", methods=["POST"])
def api_load_xlsx():
"""Directly parse an uploaded .xlsx file into sheet data."""
if "file" not in request.files:
return jsonify({"ok": False, "error": "缺少文件(字段名 file"}), 400
f = request.files["file"]
if not f.filename:
return jsonify({"ok": False, "error": "文件为空"}), 400
if not f.filename.lower().endswith(".xlsx"):
return jsonify({"ok": False, "error": "请上传 .xlsx 文件"}), 400
workdir = tempfile.mkdtemp(prefix="shudao_xlsx_")
xlsx_path = os.path.join(workdir, f.filename)
try:
f.save(xlsx_path)
sheet_data = xlsx_to_spreadsheet_data(xlsx_path)
return jsonify({"ok": True, "sheet_data": sheet_data})
except Exception as e:
return jsonify({
"ok": False,
"stage": "xlsx_parse",
"error": f"xlsx 解析失败:{e}",
"trace": traceback.format_exc(),
}), 500
@app.route("/api/load_json", methods=["POST"])
def api_load_json():
"""Parse an uploaded Aliyun-OCR-style JSON file -> xlsx -> sheet data."""
if "file" not in request.files:
return jsonify({"ok": False, "error": "缺少文件(字段名 file"}), 400
f = request.files["file"]
if not f.filename:
return jsonify({"ok": False, "error": "文件为空"}), 400
workdir = tempfile.mkdtemp(prefix="shudao_json_")
json_path = os.path.join(workdir, "input.json")
xlsx_path = os.path.join(workdir, "out.xlsx")
try:
f.save(json_path)
# Validate the JSON structure
with open(json_path, "r", encoding="utf-8") as fp:
data = json.load(fp)
# Accept either the body shape {Data: "<json string>", RequestId: ...}
# or the already-parsed OCR shape with prism_tablesInfo at the top level
if isinstance(data, dict) and "Data" in data and isinstance(data["Data"], str):
ocr_body = data # already in body shape
elif isinstance(data, dict) and "prism_tablesInfo" in data:
# Wrap into body shape so json_to_excel can handle it
ocr_body = {"Data": json.dumps(data, ensure_ascii=False)}
else:
return jsonify({
"ok": False,
"stage": "json_validate",
"error": "JSON 格式不符合预期(既不是 Aliyun OCR body 也没有 prism_tablesInfo",
}), 400
with open(json_path, "w", encoding="utf-8") as fp:
json.dump(ocr_body, fp, ensure_ascii=False, indent=2)
rc, out, err = run_subprocess(
[sys.executable, os.path.join(HERE, "json_to_excel.py"), json_path, xlsx_path]
)
if rc != 0:
return jsonify({
"ok": False,
"stage": "json_to_excel",
"error": err or out or "json_to_excel 失败",
}), 500
sheet_data = xlsx_to_spreadsheet_data(xlsx_path)
return jsonify({"ok": True, "sheet_data": sheet_data})
except Exception as e:
return jsonify({
"ok": False,
"stage": "json_load",
"error": f"JSON 加载失败:{e}",
"trace": traceback.format_exc(),
}), 500
@app.route("/api/load_demo", methods=["GET"])
def api_load_demo():
"""Load the project-local 111.html's embedded sheet data (if any),
or fall back to re-running generate_sheet on 111.xlsx."""
# Approach 1: extract {{DATA}} from 111.html (cleanest, no re-conversion)
candidate_html = os.path.join(HERE, "111.html")
if os.path.exists(candidate_html):
try:
with open(candidate_html, "r", encoding="utf-8") as fp:
content = fp.read()
# Find the first { ... } that contains the "name" key
# The data is in a `const rawData = {...};` block
m = re.search(r"const\s+rawData\s*=\s*(\{.*?\});\s*//\s*Normalize", content, re.DOTALL)
if m:
sheet_data = json.loads(m.group(1))
return jsonify({"ok": True, "sheet_data": sheet_data, "source": "111.html"})
except Exception:
pass # fall through
# Approach 2: re-run generate_sheet on 111.xlsx
candidate_xlsx = os.path.join(HERE, "111.xlsx")
if not os.path.exists(candidate_xlsx):
return jsonify({
"ok": False,
"error": "示例数据不可用(既没有 111.html 也没有 111.xlsx",
}), 404
try:
sheet_data = xlsx_to_spreadsheet_data(candidate_xlsx)
return jsonify({"ok": True, "sheet_data": sheet_data, "source": "111.xlsx"})
except Exception as e:
return jsonify({
"ok": False,
"stage": "xlsx_parse",
"error": f"111.xlsx 解析失败:{e}",
"trace": traceback.format_exc(),
}), 500
if __name__ == "__main__":
port = int(os.environ.get("PORT", "5000"))
print(f" * Serving Shudao on http://127.0.0.1:{port}")
app.run(host="127.0.0.1", port=port, debug=False)