Files
Mofox-Core/src/utils/json_parser.py
minecraft1024a 7f74fc473e style(log): 移除日志输出中的 emoji 符号
为了在不同终端和环境中保持日志输出的整洁与一致性,统一移除了日志信息中的 emoji 符号。

此举旨在避免潜在的渲染问题,并使日志更易于程序化解析和人工阅读。同时,对部分代码进行了微小的类型标注优化。
2025-11-23 14:26:44 +08:00

241 lines
7.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.

"""
统一的 JSON 解析工具模块
提供统一的 LLM 响应 JSON 解析功能,使用 json_repair 库进行修复,
简化代码并提高解析成功率。
"""
import re
from typing import Any
import orjson
from json_repair import repair_json
from src.common.logger import get_logger
logger = get_logger(__name__)
def extract_and_parse_json(response: str, *, strict: bool = False) -> dict[str, Any] | list | None:
"""
从 LLM 响应中提取并解析 JSON
处理策略:
1. 清理 Markdown 代码块标记(```json 和 ```
2. 提取 JSON 对象或数组
3. 使用 json_repair 修复格式问题
4. 解析为 Python 对象
Args:
response: LLM 响应字符串
strict: 严格模式,如果为 True 则解析失败时返回 None否则尝试容错处理
Returns:
解析后的 dict 或 list失败时返回 None
Examples:
>>> extract_and_parse_json('```json\\n{"key": "value"}\\n```')
{'key': 'value'}
>>> extract_and_parse_json('Some text {"key": "value"} more text')
{'key': 'value'}
>>> extract_and_parse_json('[{"a": 1}, {"b": 2}]')
[{'a': 1}, {'b': 2}]
"""
if not response:
logger.debug("空响应,无法解析 JSON")
return None
try:
# 步骤 1: 清理响应
cleaned = _clean_llm_response(response)
if not cleaned:
logger.warning("清理后的响应为空")
return None
# 步骤 2: 尝试直接解析
try:
result = orjson.loads(cleaned)
logger.debug(f" JSON 直接解析成功,类型: {type(result).__name__}")
return result
except Exception as direct_error:
logger.debug(f"直接解析失败: {type(direct_error).__name__}: {direct_error}")
# 步骤 3: 使用 json_repair 修复并解析
try:
repaired = repair_json(cleaned)
# repair_json 可能返回字符串或已解析的对象
if isinstance(repaired, str):
result = orjson.loads(repaired)
logger.debug(f" JSON 修复后解析成功(字符串模式),类型: {type(result).__name__}")
else:
result = repaired
logger.debug(f" JSON 修复后解析成功(对象模式),类型: {type(result).__name__}")
return result
except Exception as repair_error:
logger.warning(f"JSON 修复失败: {type(repair_error).__name__}: {repair_error}")
if strict:
logger.error(f"严格模式下解析失败,响应片段: {cleaned[:200]}")
return None
# 最后的容错尝试:返回空字典或空列表
if cleaned.strip().startswith("["):
logger.warning("返回空列表作为容错")
return []
else:
logger.warning("返回空字典作为容错")
return {}
except Exception as e:
logger.error(f" JSON 解析过程出现异常: {type(e).__name__}: {e}")
if strict:
return None
return {} if not response.strip().startswith("[") else []
def _clean_llm_response(response: str) -> str:
"""
清理 LLM 响应,提取 JSON 部分
处理步骤:
1. 移除 Markdown 代码块标记(```json 和 ```
2. 提取第一个完整的 JSON 对象 {...} 或数组 [...]
3. 清理多余的空格和换行
Args:
response: 原始 LLM 响应
Returns:
清理后的 JSON 字符串
"""
if not response:
return ""
cleaned = response.strip()
# 移除 Markdown 代码块标记
# 匹配 ```json ... ``` 或 ``` ... ```
code_block_patterns = [
r"```json\s*(.*?)```", # ```json ... ```
r"```\s*(.*?)```", # ``` ... ```
]
for pattern in code_block_patterns:
match = re.search(pattern, cleaned, re.IGNORECASE | re.DOTALL)
if match:
cleaned = match.group(1).strip()
logger.debug(f"从 Markdown 代码块中提取内容,长度: {len(cleaned)}")
break
# 提取 JSON 对象或数组
# 优先查找对象 {...},其次查找数组 [...]
for start_char, end_char in [("{", "}"), ("[", "]")]:
start_idx = cleaned.find(start_char)
if start_idx != -1:
# 使用栈匹配找到对应的结束符
extracted = _extract_balanced_json(cleaned, start_idx, start_char, end_char)
if extracted:
logger.debug(f"提取到 {start_char}...{end_char} 结构,长度: {len(extracted)}")
return extracted
# 如果没有找到明确的 JSON 结构,返回清理后的原始内容
logger.debug("未找到明确的 JSON 结构,返回清理后的原始内容")
return cleaned
def _extract_balanced_json(text: str, start_idx: int, start_char: str, end_char: str) -> str | None:
"""
从指定位置提取平衡的 JSON 结构
使用栈匹配算法找到对应的结束符,处理嵌套和字符串中的特殊字符
Args:
text: 源文本
start_idx: 起始字符的索引
start_char: 起始字符({ 或 [
end_char: 结束字符(} 或 ]
Returns:
提取的 JSON 字符串,失败时返回 None
"""
depth = 0
in_string = False
escape_next = False
for i in range(start_idx, len(text)):
char = text[i]
# 处理转义字符
if escape_next:
escape_next = False
continue
if char == "\\":
escape_next = True
continue
# 处理字符串
if char == '"':
in_string = not in_string
continue
# 只在非字符串内处理括号
if not in_string:
if char == start_char:
depth += 1
elif char == end_char:
depth -= 1
if depth == 0:
# 找到匹配的结束符
return text[start_idx : i + 1].strip()
# 没有找到匹配的结束符
logger.debug(f"未找到匹配的 {end_char},深度: {depth}")
return None
def safe_parse_json(json_str: str, default: Any = None) -> Any:
"""
安全解析 JSON失败时返回默认值
Args:
json_str: JSON 字符串
default: 解析失败时返回的默认值
Returns:
解析结果或默认值
"""
try:
result = extract_and_parse_json(json_str, strict=False)
return result if result is not None else default
except Exception as e:
logger.warning(f"安全解析 JSON 失败: {e}")
return default
def extract_json_field(response: str, field_name: str, default: Any = None) -> Any:
"""
从 LLM 响应中提取特定字段的值
Args:
response: LLM 响应
field_name: 字段名
default: 字段不存在时的默认值
Returns:
字段值或默认值
"""
parsed = extract_and_parse_json(response, strict=False)
if isinstance(parsed, dict):
return parsed.get(field_name, default)
logger.warning(f"解析结果不是字典,无法提取字段 '{field_name}'")
return default