Files
2024-06-05 02:44:41 +08:00

350 lines
12 KiB
Python

# coding=utf8
import json
from typing import Any, List, NamedTuple, Optional, Tuple
import json.decoder
from json.decoder import JSONDecodeError as PyJSONDecodeError, JSONDecoder, py_scanstring
from json.scanner import py_make_scanner
from typing import Any, Dict, NamedTuple, Optional, Tuple, Union
class FixResult(NamedTuple):
success: bool
line: str
origin: bool
class JSONFixer:
def __init__(self, max_try: int = 20, max_stack: int = 3, *, js_style: bool = False) -> None:
self._max_try = max_try
self._max_stack = max_stack
self._js_style = js_style
self.last_fix: Optional[bool] = None
self.fix_stack: List[str] = []
def fix(self, line: str, *, strict: bool = True) -> FixResult:
try:
json.loads(line, strict=strict)
return FixResult(success=True, line=line, origin=True)
except Exception:
pass
ok, new_line = self.fixwithtry(line, strict=strict)
return FixResult(success=ok, line=new_line, origin=False)
def fixwithtry(self, line: str, *, strict: bool = True) -> Tuple[bool, str]:
if self._max_try <= 0:
return False, line
self.fix_stack = []
self.last_fix = None
ok = False
for _ in range(self._max_try):
ok, new_line = self.patch_line(line, strict=strict)
if ok:
return ok, new_line
self.last_fix = line != new_line
if self.last_fix:
self.fix_stack.insert(0, new_line)
self.fix_stack = self.fix_stack[: self._max_stack]
line = new_line
return ok, line
def patch_line(self, line: str, *, strict: bool = True) -> Tuple[bool, str]:
result = decode_line(line, strict=strict)
if result.success:
return True, line
if isinstance(result.exception, ValueError):
return self.patch_value_error(line, result.err_info)
if isinstance(result.exception, StopIteration):
return self.patch_stop_iteration(line)
if result.exception is None:
return self.patch_half_parse(line, result.err_info)
return False, line
def patch_value_error(self, line: str, err_info: Any) -> Tuple[bool, str]:
if err_info["error"] is None:
return False, line
error = err_info["error"]
pos = err_info["pos"]
nextchar = line[pos: pos + 1]
lastchar = line[pos - 1: pos]
nextline = line[pos:]
lastline = line[:pos]
if error == errors.StringUnterminatedString:
return False, insert_line(line, '"', len(line))
if error == errors.ObjectExceptKey:
if nextchar == "":
return False, insert_line(line, "}", pos)
if nextchar == ":":
return False, insert_line(line, '""', pos)
if lastchar in "{," and nextchar == ",":
return False, remove_line(line, pos, pos + 1)
if lastchar == "," and nextchar == "}":
return False, remove_line(line, pos - 1, pos)
if nextchar in "[{":
return False, insert_line(line, '"":', pos)
if self._js_style:
# find 'abc'
if nextchar == "'":
nextline = remove_line(nextline, 0, 1)
idx = nextline.find(":")
if idx != -1 and idx != 0 and nextline[idx - 1] == "'":
nextline = remove_line(nextline, idx - 1, idx)
return False, lastline + nextline
# abc:1 --> "aabc":1
idx = nextline.find(":")
if idx != -1:
line = lastline + insert_line(nextline, '"', idx)
return False, insert_line(line, '"', pos)
# TODO process more case "
return False, insert_line(line, '"', pos)
if error == errors.ObjectExceptColon:
return False, insert_line(line, ":", pos)
if error == errors.ObjectExceptObject:
if nextchar == "":
if lastchar == "{":
return False, insert_line(line, "}", pos)
return False, insert_line(line, "null}", pos)
if nextchar == "}":
return False, insert_line(line, "null", pos)
# TODO guess more
return False, insert_line(line, '"', pos)
if error == errors.ObjectExceptComma:
if nextchar == "":
return False, insert_line(line, "}", pos)
return False, insert_line(line, ",", pos)
if error == errors.ArrayExceptObject:
if nextchar == "," and lastchar == "[":
return False, remove_line(line, pos, pos + 1)
if nextchar == ",":
return False, insert_line(line, "null", pos)
if nextchar == "]":
return False, remove_line(line, pos - 1, pos)
if nextchar == "":
if lastchar == "[":
return False, insert_line(line, "]", pos)
return False, insert_line(line, "null]", pos)
# TODO guess more?
return False, insert_line(line, "{", pos)
if error == errors.ArrayExceptComma:
if len(line) == pos:
return False, insert_line(line, "]", pos)
return False, insert_line(line, ",", pos)
# TODO unknonwn
return False, line
def patch_stop_iteration(self, line: str) -> Tuple[bool, str]:
# TODO clean
# TODO fix
# 1. }]
# 2. ]}
# 3. constans
# 4. -
# 先 patch 完 {[]}
# TODO: process number
if line.startswith("-."):
new_line = "-0." + line[2:]
return False, new_line
# patch
left = patch_lastest_left_object_and_array(line)
if left == "":
if not self.last_fix:
left = patch_guess_left(line)
new_line = left + line
return False, new_line
def patch_half_parse(self, line: str, err_info: Any) -> Tuple[bool, str]:
obj, end = err_info
nextline = line[end:].strip()
nextchar = nextline[:1]
left = patch_lastest_left_object_and_array(nextline)
# ??
if left == "":
if nextchar == ",":
left = "["
elif nextchar == ":" and isinstance(obj, str):
left = "{"
else:
if not self.last_fix:
left = patch_guess_left(nextline)
new_line = left + line[:end] + nextline
return False, new_line
# TODO better name
def patch_lastest_left_object_and_array(line: str) -> str:
# '}]{[' --> '[{}]{['
pairs = {"}": "{", "]": "["}
breaks = "{["
left = ""
for char in line:
if char in breaks:
break
if char in pairs:
left = pairs[char] + left
return left
# TODO better name
# TODO 改成 lastest
# TODO {}}]]]] --> { not [
def patch_guess_left(line: str) -> str:
miss_object = line.count("}") - line.count("{")
miss_array = line.count("]") - line.count("[")
if miss_object == miss_array == 0:
if line[-1:] == '"' and line.count('"') == 1:
return '"'
elif miss_object >= miss_array:
return "{"
else:
return "["
return ""
def insert_line(line: str, value: str, pos: int) -> str:
return line[:pos] + value + line[pos:]
def remove_line(line: str, start: int, end: int) -> str:
return line[:start] + line[end:]
class JSONDecodeError:
def __init__(self, parser, message):
self.message = message
self.parser = parser
def __eq__(self, err):
return err.parser == self.parser and self.message in err.message
class errors:
StringInvalidUXXXXEscape = JSONDecodeError(
"py_scanstring", "Invalid \\uXXXX escape")
# 2 different case
StringUnterminatedString = JSONDecodeError(
"py_scanstring", "Unterminated string starting at")
StringInvalidControlCharacter = JSONDecodeError(
"py_scanstring", "Invalid control character")
StringInvalidEscape = JSONDecodeError("py_scanstring", "Invalid \\escape")
ObjectExceptColon = JSONDecodeError(
"JSONObject", "Expecting ':' delimiter")
ObjectExceptObject = JSONDecodeError("JSONObject", "Expecting value")
# 2 different case
ObjectExceptKey = JSONDecodeError(
"JSONObject", "Expecting property name enclosed in double quotes")
ObjectExceptComma = JSONDecodeError(
"JSONObject", "Expecting ',' delimiter")
ArrayExceptObject = JSONDecodeError("JSONArray", "Expecting value")
ArrayExceptComma = JSONDecodeError("JSONArray", "Expecting ',' delimiter")
@classmethod
def get_decode_error(cls, parser, message):
err = JSONDecodeError(parser, message)
for _, value in cls.__dict__.items():
if isinstance(value, JSONDecodeError):
if err == value:
return value
return None
"""
01 先不看,不研究
02 badcase: " --> "" success
03 控制符 pass
04 unicode \\u 的 pass
05 同上
06 object 后面没有跟随 " , badcase: {abc":1} --> {"abc":1}
07 object key 后面没有 : , badcase: {"abc"1} --> {"abc":1}
08 object 开始检测 Value 收到 StopIteration
08.1 要么后面没有了
08.2 要么后面不是 "/{/[/n[ull]/t[rue]/f[alse]/number/NaN/Infinity/-Infinity 开头的东西
-- 08.1 后面补上 null}
-- 08.2 无脑补一个 "
09 object 解析完一个 pair 后,下一个不是}, 期待一个 ','
badcase {"k":1"s":2}
10 在 09 的基础上解析完{"k":1, 发现下一个不是 ", 这个后面再优化(暂时和 06 一致)
badcase {"k":1,x":2}
11 array 开始检测 Value 收到 StopIteration
11.1 要么后面没有了,补上]
11.2 同 08.2,无脑补一个{ 看看
12 array 解析完前一个 object, 需要一个 ,
这里 nextchar 既不是 ] 也不是, 代表这个 nextchar 的 end 也已经+1 了,所以减 2
"""
def errmsg_inv(e: ValueError) -> Dict[str, Any]:
assert isinstance(e, PyJSONDecodeError)
parser = e.__dict__.get("parser", "")
errmsg = e.msg
localerr = errors.get_decode_error(parser, errmsg)
return {
"parsers": e.__dict__.get("parsers", []),
"error": localerr,
"lineno": e.lineno,
"colno": e.colno,
"pos": e.pos,
}
def record_parser_name(parser: Any) -> Any:
def new_parser(*args: Any, **kwargs: Any) -> Any:
try:
return parser(*args, **kwargs)
except Exception as e:
if "parser" not in e.__dict__:
e.__dict__["parser"] = parser.__name__
if "parsers" not in e.__dict__:
e.__dict__["parsers"] = []
e.__dict__["parsers"].append(parser.__name__)
raise e
return new_parser
def make_decoder(*, strict: bool = True) -> JSONDecoder:
json.decoder.scanstring = record_parser_name(py_scanstring)
decoder = JSONDecoder(strict=strict)
decoder.parse_object = record_parser_name(decoder.parse_object)
decoder.parse_array = record_parser_name(decoder.parse_array)
decoder.parse_string = record_parser_name(py_scanstring)
decoder.parse_object = record_parser_name(decoder.parse_object)
decoder.scan_once = py_make_scanner(decoder)
return decoder
decoder = make_decoder()
decoder_unstrict = make_decoder(strict=False)
class DecodeResult(NamedTuple):
success: bool
exception: Optional[Exception]
err_info: Optional[Union[Dict[str, Any], Tuple[Any, Any]]]
def decode_line(line: str, *, strict: bool = True) -> DecodeResult:
try:
obj, end = (decoder if strict else decoder_unstrict).scan_once(line, 0)
ok = end == len(line)
return DecodeResult(success=ok, exception=None, err_info=(obj, end))
except StopIteration as e:
return DecodeResult(success=False, exception=e, err_info=None)
except ValueError as e:
err_info = errmsg_inv(e)
return DecodeResult(success=False, exception=e, err_info=err_info)