普贤教程 › 基础教程
JSON 处理
JSON 在 PuXian 里不是"序列化格式",而是字典与列表的直接映射。两个函数就够:json_stringify 与 json_parse_opt。
序列化
px
var user = { "id": 1024, "name": "晨曦", "tags": ["qa", "px"], "active": true, "score": 9.5, "note": null } print(json_stringify(user))
text
{"id":1024,"name":"晨曦","tags":["qa","px"],"active":true,"score":9.5,"note":null}输出是紧凑格式(无空格、无换行),键顺序不保证与书写顺序一致。要好看的缩进格式,用 std.go_json_indent。
解析
px
var text = "{\"name\":\"晨曦\",\"tags\":[\"a\",\"b\"],\"n\":3}" var r = json_parse_opt(text) if r.is_err(): print("解析失败:" + r.err()) else: var v = r.ok() print(v["name"]) print(str(len(v["tags"]))) print(str(v["n"]))
text
晨曦 2 3
永远用 json_parse_opt
json_parse 遇到非法输入抛的是不可捕获的运行时错误(0.2.382 仍没有 try/catch),本意返回 400 的接口会变成 500。json_parse_opt 把失败变成 Err,能正常处理。px
print(str(json_parse_opt("{oops").is_err()))text
true类型映射
| JSON | PuXian |
|---|---|
对象 {...} | dict |
数组 [...] | list |
| 整数 | int |
| 小数 | float |
| 字符串 | string |
| true / false | bool |
| null | null |
顶层是数组时解析得到的就是 list:
px
var arr = json_parse_opt("[1,2,3]").ok() print(type(arr)) print(str(arr[1]))
text
list
2安全取值
外部 JSON 结构不可信。缺键会直接报 R1008,所以取值前逐层判断:
px
def pick(d, path, fallback): # path 形如 "meta.region" var cur = d for seg in split(path, "."): if type(cur) != "dict": return fallback if not cur.has(seg): return fallback cur = cur[seg] if cur == null: return fallback return cur var resp = {"meta": {"region": "cn-north"}, "items": []} print(str(pick(resp, "meta.region", "-"))) print(str(pick(resp, "meta.zone", "-"))) print(str(pick(resp, "missing.deep.key", "-")))
text
cn-north - -
构造响应
px
def ok_resp(data): return json_stringify({"ok": true, "data": data, "ts": now_ms()}) def err_resp(code, msg): return json_stringify({"ok": false, "error": msg, "code": code}) print(ok_resp({"user": "晨曦"})) print(err_resp(400, "参数错误"))
text
{"ok":true,"data":{"user":"晨曦"},"ts":1793869xxxxx}
{"ok":false,"error":"参数错误","code":400}约定优于技巧接口统一用
{"ok":bool,...} 外壳,前端只需要判一个字段。错误里带 code 与 message,日志里能直接 grep 出来。数组遍历与转换
px
var rows = json_parse_opt("[{\"n\":1},{\"n\":2},{\"n\":3}]").ok() var total = 0 for r in rows: if r.has("n"): total += r["n"] print(str(total)) print(str(len(rows)))
text
6 3
写进文件与读回来
px
write_file("/tmp/px_data.json", json_stringify({"a": 1, "b": [1, 2]})) var back = json_parse_opt(read_file("/tmp/px_data.json")).ok() print(str(back["a"])) print(str(len(back["b"])))
text
1 2
常见问题
| 问题 | 原因与处理 |
|---|---|
| 解析中文乱码 | 源不是 UTF-8;先转码再解析 |
| 数字精度丢失 | 大整数以 float 存;需要精确值时用字符串承载 |
| 键顺序不对 | 字典无序,需要顺序就自己排序后拼 |
| 嵌套太深取值易错 | 用上面的 pick() 辅助函数逐层判空 |
与 Go 的差异
要让输出与 Go encoding/json 逐字节对拍,下面几处必须自己补,标准库不负责。
| 项 | Go | PuXian 0.2.382 |
|---|---|---|
\b \f | 输出 \b \f | json_stringify_go 输出 \u0008 \u000c |
| 非法 UTF-8 | 原始 U+FFFD | 输出 \ufffd 转义 |
| 解析合法性 | 严格 | json_parse_opt 明显更宽松(见下) |
json_parse_opt 比 Go 宽松下面这些 Go 全判非法,px 都返回
Ok:尾随垃圾 {"a":1}x、前导零 01、{"a":+1}、未知转义 {"a":"\q"}。从 Go 迁过来的输入校验会静默放过非法报文。(未闭合字符串、尾随逗号 [1,] 会返回 Err,这两类与 Go 一致。)要 Go json.Valid 同语义,用 std.go_json_scan(Go scanner 状态机的逐字节复刻)自己包一层:
px
import std.go_json_scan def go_json_valid(src): var s = gjs_new() var n = len(src) var i = 0 while i < n: gjs_step(s, src[i], i) i = i + 1 return gjs_eof(s, n) == GJS_END print(str(go_json_valid("{\"a\":1}"))) # true print(str(go_json_valid("{\"a\":1}x"))) # false print(str(go_json_valid("01"))) # false
text
true false false