Files
ancient_date_parser/client_app.py
T
sansenandClaude Code 4e68372518 日志改为摘要 + 有界预览:单次请求日志 6940KB → 2.8KB
/convert/start 原先 logger.info 直接 dump 整份结果(json.dumps(indent=2)):实测
1MB 输入 → 20188 条 → 6.9MB 日志,是原文的 15%,indent 还要再乘 1.4。转几份族谱
日志文件就会被撑爆。

改为:
- 一行摘要:输入字数、结果条数、类型分布、耗时(格式与 batch 接口的日志保持一致)
- 前 LOG_PREVIEW_ITEMS(=10) 条结果预览,体积有硬上界
- 新增 _result_summary() helper

类型分布一行能看出解析形态(如 {'干支纪年': 15624, '古代纪年': 147, ...}),
排查"整批变成未匹配"这类回归比翻全量结果还快。明细请用接口返回值。

验证:单次 1MB 请求日志 2.8KB(降 2428x);/convert/start、/convert/batch、
403 鉴权、空文本四条路径均正常;pytest 260 passed。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 17:42:42 +08:00

138 lines
5.2 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.
import json
from collections import Counter
from time import perf_counter
from fastapi import FastAPI, HTTPException, Header, Request
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from loguru import logger
from era_parser import *
app = FastAPI()
# 添加 CORS 支持
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 可以改成你的前端域名,例如 ["https://yourdomain.com"]
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# batch 接口单次请求的条目上限(防滥用)
BATCH_MAX_ITEMS = 1000
# 日志里最多预览多少条结果。完整结果序列化后体积可达数 MB
# (实测 1MB 输入 → 20188 条 → 6.9MB JSON,是原文的 15%,含缩进还要再乘 1.4),
# 全量落盘会迅速撑爆日志文件。
LOG_PREVIEW_ITEMS = 10
def _check_auth(Authorization: str):
if Authorization != "3FFA6A4B073CF065969630692331A873":
raise HTTPException(status_code=403, detail="未授权的操作")
def _result_summary(res: list) -> str:
"""一行摘要:结果条数 + 类型分布(如 {'古代纪年': 12, '干支纪年': 3})。
/convert/start 的日志只记摘要与有界预览,不记全量结果——看明细请用接口返回值。
"""
kinds = Counter(r["类型"] for r in res)
return f"{len(res)} 条, 类型分布 {dict(kinds)}"
def _parse_urlencoded_content(body: bytes) -> str:
"""手动解析 x-www-form-urlencoded 的 content 字段:绕过 starlette 1MB 表单限制"""
from urllib.parse import parse_qs
params = parse_qs(body.decode("utf-8"), keep_blank_values=True)
return params.get("content", [""])[0]
@app.post("/convert/start")
async def convert_str(
request: Request,
Authorization: str = Header(None)
):
"""
带调试信息的转换接口 (支持 x-www-form-urlencoded)
手动解析表单体:绕过 starlette 1MB 表单限制(族谱全文可达数 MB)
"""
_check_auth(Authorization)
try:
content = _parse_urlencoded_content(await request.body())
t0 = perf_counter()
res = convert_text(content)
dt = perf_counter() - t0
# 只记摘要 + 前 N 条预览(格式与 batch 接口的日志保持一致)
logger.info(f"转换完成: 输入 {len(content)} 字, {_result_summary(res)}, "
f"耗时 {dt * 1000:.0f}ms")
if res:
head = res[:LOG_PREVIEW_ITEMS]
logger.info(f"结果前 {len(head)} 条: {json.dumps(head, ensure_ascii=False)}")
return JSONResponse(content={"code": 0, "result": res})
except Exception as e:
logger.error(f"转换异常: {str(e)}")
return JSONResponse(
content={"code": 500, "result": f"转换服务异常: {str(e)}"},
status_code=500
)
@app.post("/convert/batch")
async def convert_batch(
request: Request,
Authorization: str = Header(None)
):
"""
批量转换接口:短文本多任务场景,一次请求处理 N 条 content。
请求: {"items": [{"id": "row1", "content": "..."}, ...]}(application/json)
响应: {"code": 0, "result": [{"id": "row1", "result": [...]}, ...]}
- id 透传用于前端回填,缺省按数组顺序编号
- 单条解析失败不影响其他条目(该条返回 error 字段)
- 每条 content 独立解析,位置语义与单接口一致(从 0 起)
"""
_check_auth(Authorization)
# 请求体解析与入参校验失败 → 400(客户端错误)
try:
payload = json.loads((await request.body()).decode("utf-8"))
except Exception as e:
logger.warning(f"batch 请求体解析失败: {str(e)}")
raise HTTPException(status_code=400, detail="请求体必须是合法 JSON")
items = payload.get("items") if isinstance(payload, dict) else None
if not isinstance(items, list) or not items:
raise HTTPException(status_code=400, detail="items 必须是非空数组")
if len(items) > BATCH_MAX_ITEMS:
raise HTTPException(status_code=400, detail=f"items 超过上限 {BATCH_MAX_ITEMS} 条")
try:
t0 = perf_counter()
results = []
for i, item in enumerate(items):
item_id = item.get("id") if isinstance(item, dict) else None
content = item.get("content", "") if isinstance(item, dict) else ""
if item_id is None:
item_id = str(i)
try:
results.append({"id": item_id, "result": convert_text(content)})
except Exception as e:
logger.error(f"batch 条目 {item_id} 解析异常: {str(e)}")
results.append({"id": item_id, "error": str(e)})
dt = perf_counter() - t0
logger.info(f"批量转换: {len(results)} 条, 耗时 {dt * 1000:.0f}ms")
return JSONResponse(content={"code": 0, "result": results})
except Exception as e:
logger.error(f"批量转换异常: {str(e)}")
return JSONResponse(
content={"code": 500, "result": f"批量转换服务异常: {str(e)}"},
status_code=500
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=16661, log_level="info")