fix: make XY matrix JSON parsing more tolerant

Handle noisy text inputs by extracting the first JSON object, stripping line comments, fixing trailing/missing commas, and raising clearer errors.
This commit is contained in:
Nakano Kenji
2026-02-02 17:11:16 +08:00
parent f8c58786fd
commit 228f2a2249
+95 -11
View File
@@ -22,6 +22,7 @@ from typing import Any, Dict, List, Tuple
_CODE_FENCE_RE = re.compile(r"```(?:json)?\s*([\s\S]*?)\s*```", re.IGNORECASE)
_LINE_COMMENT_RE = re.compile(r"^\s*(//|#).*$", re.MULTILINE)
def _strip_code_fence(text: str) -> str:
@@ -31,19 +32,102 @@ def _strip_code_fence(text: str) -> str:
return m.group(1) if m else text
def _extract_first_json_object(text: str) -> str | None:
"""
Extract the first top-level JSON object {...} from a noisy text.
Uses brace counting and skips braces inside quoted strings.
"""
if not isinstance(text, str):
return None
s = text.lstrip("\ufeff").strip()
start = s.find("{")
if start == -1:
return None
depth = 0
in_str: str | None = None
esc = False
for i in range(start, len(s)):
ch = s[i]
if in_str is not None:
if esc:
esc = False
continue
if ch == "\\":
esc = True
continue
if ch == in_str:
in_str = None
continue
if ch == '"' or ch == "'":
in_str = ch
continue
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return s[start : i + 1]
return None
def _repair_missing_commas(text: str) -> str:
"""
Best-effort repair for a common hand-edited JSON mistake:
missing commas between top-level lines, e.g.
"a": "x"
"b": "y"
"""
if not isinstance(text, str):
return text
s = text
# Add comma before a new line starting with a quote when previous line looks like a value.
s = re.sub(
r'("|\d|\]|\}|true|false|null)\s*\n(\s*")',
r"\1,\n\2",
s,
flags=re.IGNORECASE,
)
return s
def _try_parse_json(text: str) -> Any:
try:
return json.loads(text)
except Exception:
cleaned = re.sub(r",\s*([}\]])", r"\1", text)
if not isinstance(text, str):
return text
s = text.lstrip("\ufeff")
# Strip full-line comments (common when users annotate JSON)
s = _LINE_COMMENT_RE.sub("", s).strip()
candidates: list[str] = [s]
extracted = _extract_first_json_object(s)
if extracted and extracted != s:
candidates.append(extracted)
last_err: Exception | None = None
for cand in candidates:
try:
return json.loads(cleaned)
except Exception:
a = cleaned.find("{")
b = cleaned.rfind("}")
if a != -1 and b != -1 and b > a:
return json.loads(cleaned[a : b + 1])
raise
return json.loads(cand)
except Exception as e:
last_err = e
# trailing commas
cleaned = re.sub(r",\s*([}\]])", r"\1", cand)
cleaned = _repair_missing_commas(cleaned)
cleaned = _LINE_COMMENT_RE.sub("", cleaned).strip()
try:
return json.loads(cleaned)
except Exception as e2:
last_err = e2
# Provide a more actionable message
if isinstance(last_err, json.JSONDecodeError):
msg = (
f"JSON 解析失败:{last_err.msg} (line {last_err.lineno}, col {last_err.colno}). "
f"常见原因:上一行结尾缺少逗号、或 JSON 里混入了说明文字。"
)
raise ValueError(msg) from last_err
raise last_err if last_err is not None else ValueError("JSON 解析失败:未知错误")
def _split_list(text: str) -> List[str]: