diff --git a/Article/PythonBasis/python29/1.md b/Article/PythonBasis/python29/1.md new file mode 100644 index 00000000..04242fe2 --- /dev/null +++ b/Article/PythonBasis/python29/1.md @@ -0,0 +1,617 @@ +# JSON:程序之间交换数据的「通用语」 + +各位老朋友,咱们一路从 `python0` 走到现在,变量、函数、类、异常、`pathlib`、`pytest`……该聊的都聊得差不多了。可是水哥想问一句:你的程序算出来的数据,**最后是怎么「出门」的**? + +想象一下——你写了个脚本,爬了一堆数据,或者算出了一份用户列表,现在要把它发给另一个程序。那个程序可能是别人用 Java 写的后端、可能是浏览器里的 JavaScript、可能是手机 App、也可能就是另一个 Python 脚本。你怎么发? + +发一段 `print` 出来的字符串?对方还得自己拆。发一个 `"name=两点水&age=28"` 这种自创格式?那得先跟对方开个会约定好。直接把内存里的 `dict` 对象丢过去?更不行——你内存里的对象,对方根本看不懂。 + +答案其实各位早就见过了,它叫 **JSON**。 + +各位在前面的章节里已经瞥见过它的影子——`python25` 写测试的时候用过 `json.dumps`,`python27` 的 `todo-cli` 用它把数据存进 `~/.todo-cli.json`。但那些都是「顺手用一下」。今天这一章,水哥把它正式请出来,从头到尾聊透。 + +学完这一章,各位应该能: + +1. 知道 JSON 是啥、它凭啥成了程序之间交换数据的「通用语」 +2. 记住 JSON 的六种数据类型,以及它和 Python 类型的对应关系 +3. 用 `json.loads` / `json.dumps` 在字符串和 Python 对象之间来回转换 +4. 用 `json.load` / `json.dump` 读写 JSON 文件 +5. 用 `ensure_ascii`、`indent`、`sort_keys` 处理中文、美化输出 +6. 用 `default` 序列化 `datetime` 和自定义对象,用 `object_hook` 反序列化回去 +7. 用 `JSONDecodeError` 优雅地处理坏掉的 JSON + +走起。 + + + +## JSON 是个啥 + +JSON 全称 **JavaScript Object Notation**,直译过来是「JavaScript 对象表示法」。名字里带 JavaScript,但它早就不归 JavaScript 一家所有了——现在是**几乎所有主流语言都支持**的数据交换格式。 + +它干的活儿特别朴素:把「内存里的数据」变成「一段字符串」发出去,对方收到字符串,再变回自己内存里的数据。这个「变来变去」的过程,专业说法叫: + +- **序列化(serialize)**:把内存里的对象 → 一段字符串,方便传输或存储 +- **反序列化(deserialize)**:把字符串 → 内存里的对象,方便程序使用 + +各位天天都在跟它打交道,只是没注意。你手机 App 调后端接口、网页加载数据、配置文件(比如 VSCode 的 `settings.json`)、甚至各种 AI 接口的请求和返回,背后几乎全是 JSON。说它是「程序世界的普通话」,一点不夸张。 + +那为什么偏偏是 JSON 赢了?水哥给三个理由: + +1. **人看得懂**。JSON 长得跟 Python 的 `dict`、`list` 几乎一模一样,一眼就能看明白里面是啥数据。这跟早期那些二进制格式一比,优势是碾压性的。 +2. **机器好解析**。它规则简单、没有歧义,任何语言写个解析器都不费劲,所以 Python、Java、Go、JavaScript、C++……全都有现成的库。 +3. **语言无关**。它只描述「数据长什么样」,不关心「你是哪个语言写的」。Java 的 `Map` 序列化成 JSON,Python 的 `dict` 能原样读回来。 + +下一节咱们先看看 JSON 到底长什么样、能装哪些数据。 + + + +## JSON 的六种数据类型 + +JSON 一共就六种数据类型,全部在这里了,各位记住这个清单: + +| JSON 类型 | 长什么样 | 说明 | +| --- | --- | --- | +| 对象(object) | `{"name": "两点水", "age": 28}` | 花括号包起来的键值对,键必须是字符串 | +| 数组(array) | `["写代码", "健身"]` | 方括号包起来的有序列表 | +| 字符串(string) | `"两点水"` | 必须用**双引号**,不能用单引号 | +| 数字(number) | `28`、`3.14`、`-5`、`1.2e10` | 整数和浮点数都算,没有单独的类型 | +| 布尔(boolean) | `true`、`false` | 注意是小写,不是 Python 的 `True` / `False` | +| 空值(null) | `null` | 表示「没有值」,对应 Python 的 `None` | + +一段完整的 JSON 长这样: + +```json +{ + "name": "两点水", + "age": 28, + "married": false, + "hobbies": ["写代码", "健身"], + "salary": null +} +``` + +各位看着是不是眼熟?对,它跟 Python 的 `dict` + `list` 混在一起特别像。但水哥必须提醒一句——**JSON 不是 Python**,两者有几处一眼就能看穿的差别: + +1. JSON 字符串必须双引号,Python 单双都行 +2. JSON 布尔是 `true` / `false`,Python 是 `True` / `False` +3. JSON 空值是 `null`,Python 是 `None` +4. JSON **没有注释**、**没有尾逗号**(最后一个元素后面不能再跟逗号) +5. JSON 的 key 必须是字符串,Python 的 dict key 可以是任何不可变对象 + +尤其是「没有尾逗号」这条,很多人写 Python 写顺手了,写 JSON 时会在最后一个元素后头加个逗号,结果解析直接报错。各位心里留个印象。 + + + +## Python 的 json 模块 + +好消息是,处理 JSON 在 Python 里不需要装任何第三方库——**标准库自带的 `json` 模块**就全包了。一行 `import json` 即可。 + +`json` 模块里最核心的是四个函数,它们两两一组: + +| 函数 | 方向 | 处理对象 | +| --- | --- | --- | +| `json.dumps(obj)` | Python 对象 → JSON 字符串 | 字符串 | +| `json.loads(s)` | JSON 字符串 → Python 对象 | 字符串 | +| `json.dump(obj, f)` | Python 对象 → JSON 文件 | 文件对象 | +| `json.load(f)` | JSON 文件 → Python 对象 | 文件对象 | + +记法也简单——**带 `s` 的(`dumps` / `loads`)处理字符串(string),不带 `s` 的(`dump` / `load`)处理文件(file)**。下一节先从最常用的两个「字符串」函数讲起。 + + + +## loads / dumps:字符串 ←→ 对象 + +先看「反序列化」——`json.loads`,把一段 JSON 字符串变成 Python 对象。`loads` 这个名字里的 `s` 就是 string 的意思。 + +```python +import json + +s = '{"name": "两点水", "age": 28, "hobbies": ["写代码", "健身"], "married": false, "salary": null}' +obj = json.loads(s) +print(obj) +print(type(obj)) +print(obj["name"]) +``` + +输出: + +``` +{'name': '两点水', 'age': 28, 'hobbies': ['写代码', '健身'], 'married': False, 'salary': None} + +两点水 +``` + +各位看,JSON 里的 `false` 变成了 Python 的 `False`,`null` 变成了 `None`,整个对象变成了一个 `dict`。反序列化之后,它就是一个地地道道的 Python 对象,你可以随便 `obj["name"]`、`obj["hobbies"][0]` 这样访问。 + +再看「序列化」——`json.dumps`,把 Python 对象变成 JSON 字符串: + +```python +import json + +d = {"name": "两点水", "age": 28, "hobbies": ["写代码", "健身"]} +s = json.dumps(d) +print(s) +print(type(s)) +``` + +输出: + +``` +{"name": "\u4e24\u70b9\u6c34", "age": 28, "hobbies": ["\u5199\u4ee3\u7801", "\u5065\u8eab"]} + +``` + +两个点各位注意: + +1. 返回的 `s` 类型是 `str`,不是 `dict`——它就是一段**字符串**,随时可以发到网上、写进文件 +2. 中文变成了 `\u4e24\u70b9\u6c34` 这种样子。这是 JSON 的默认行为,稍后专门讲怎么让中文原样显示(`ensure_ascii`) + +把两者串起来,就是一个完整的「往返」: + +```python +import json + +d = {"name": "两点水", "age": 28} +s = json.dumps(d, ensure_ascii=False) +print("序列化后的字符串:", s) + +d2 = json.loads(s) +print("反序列化回来:", d2) +print("和原来相等吗:", d == d2) +``` + +输出: + +``` +序列化后的字符串: {"name": "两点水", "age": 28} +反序列化回来: {'name': '两点水', 'age': 28} +和原来相等吗: True +``` + +`dict` 变成字符串,字符串又变回 `dict`,内容一模一样。这就是序列化 / 反序列化的完整闭环。 + + + +## 类型对照表 + +`loads` 和 `dumps` 之间,JSON 类型和 Python 类型是怎么一一对应的?这张表是重点,各位记牢: + +| JSON | Python(`loads` 之后) | +| --- | --- | +| 对象 `{}` | `dict` | +| 数组 `[]` | `list` | +| 字符串 | `str` | +| 整数 | `int` | +| 浮点数 | `float` | +| `true` | `True` | +| `false` | `False` | +| `null` | `None` | + +反过来,`dumps` 时 Python 对象能序列化成 JSON 的,基本也是这套:`dict` → 对象、`list`/`tuple` → 数组、`str` → 字符串、`int`/`float` → 数字、`True`/`False` → `true`/`false`、`None` → `null`。 + +这里有个隐藏小坑:Python 的 `tuple` 会被 `json.dumps` 当成数组处理,序列化成 `[...]`。等 `loads` 回来的时候,它已经变成 `list` 了——**元组一去不复返**: + +```python +import json + +t = (1, 2, 3) +s = json.dumps(t) +print("序列化后:", s) + +back = json.loads(s) +print("读回来:", back) +print("类型:", type(back)) +``` + +输出: + +``` +序列化后: [1, 2, 3] +(1, 2, 3) +类型: +``` + +各位以后处理「元组 → JSON → 读回」这种场景,记得读回来是 `list`,别还用元组的用法。 + + + +## load / dump:读写文件(那个 s 到底啥意思) + +前面说过,`loads` / `dumps` 处理的是字符串,而 `load` / `dump` 处理的是**文件**。写文件用 `json.dump`,读文件用 `json.load`: + +```python +import json +import tempfile +from pathlib import Path + +data = {"name": "两点水", "age": 28, "skills": ["Python", "写作"]} + +# 用临时目录演示,跑完自动清理,不污染你的项目 +# 实际写代码时,直接换成 with open("config.json", "w", encoding="utf-8") 就行 +with tempfile.TemporaryDirectory() as td: + path = Path(td) / "config.json" + + # 写:json.dump(对象, 文件对象) + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + # 读:json.load(文件对象) + with open(path, "r", encoding="utf-8") as f: + loaded = json.load(f) + + print(loaded) + print(type(loaded)) +``` + +输出: + +``` +{'name': '两点水', 'age': 28, 'skills': ['Python', '写作']} + +``` + +各位把这段代码跟前面的 `loads` / `dumps` 对比一下,会发现一个套路: + +- `json.dump(obj, f)` 等于 `f.write(json.dumps(obj))` +- `json.load(f)` 等于 `json.loads(f.read())` + +`dump` / `load` 其实就是帮你把「读文件」和「转换」两步并成一步,省得你自己 `read()` / `write()` 一次。 + +两个细节水哥再啰嗦一下: + +1. 文件一定要用 `encoding="utf-8"` 打开。JSON 的官方编码就是 UTF-8,不写这个参数,在 Windows 上中文大概率会乱码。 +2. 写文件时 `indent=2` 是可选的,它让 JSON 文件里自动带缩进换行,用编辑器打开的时候好看、好改。不加的话整段会挤成一行。 + + + +## 中文:ensure_ascii 的坑 + +各位还记得前面 `json.dumps` 时,中文变成了 `\u4e24\u70b9\u6c34` 这串东西吗?那就是 JSON 的默认行为——**把非 ASCII 字符转成 `\uXXXX` 的转义形式**。 + +这个行为由参数 `ensure_ascii` 控制,默认是 `True`(即「确保只有 ASCII 字符」)。对比一下: + +```python +import json + +d = {"name": "两点水"} +print(json.dumps(d)) +print(json.dumps(d, ensure_ascii=False)) +``` + +输出: + +``` +{"name": "\u4e24\u70b9\u6c34"} +{"name": "两点水"} +``` + +两种输出**都是合法的 JSON**,语义完全等价。`\u4e24\u70b9\u6c34` 是「两点水」三个字的 Unicode 码点转义,对方解析之后照样能还原成「两点水」。 + +那到底该用哪个?水哥给个结论: + +- **发给别的程序 / 存进文件**:`ensure_ascii=True`(默认)更安全,因为任何环境都能正确传输,不依赖编码设置 +- **要给人看 / 写进日志 / 存配置文件**:`ensure_ascii=False`,肉眼可读,调试方便 + +水哥自己的习惯是——**只要涉及中文,一律加 `ensure_ascii=False`**。尤其是写进 `config.json`、日志这种「要给人读」的场景,转义成一堆 `\uXXXX` 简直没法看。等各位真的碰到「接口要求必须 ASCII」这种极少数场景,再手动改回默认也不迟。 + + + +## indent 和 sort_keys:好看 + 稳定 + +`json.dumps` 还有两个常用参数,一个管「好不好看」,一个管「稳不稳定」。 + +### indent:缩进 + +默认 `json.dumps` 输出是一整行,数据一多就挤成一条「腊肠」。加 `indent=2`,它会自动换行、缩进: + +```python +import json + +d = {"name": "两点水", "age": 28, "hobbies": ["写代码", "健身"]} +print(json.dumps(d, ensure_ascii=False, indent=2)) +``` + +输出: + +``` +{ + "name": "两点水", + "age": 28, + "hobbies": [ + "写代码", + "健身" + ] +} +``` + +`indent` 的值就是缩进的空格数,`indent=2` 最常用,`indent=4` 也行,看你习惯。写配置文件、给人看的 JSON,基本都会加 `indent`。 + +### sort_keys:排序 + +默认情况下,`dumps` 会保持字典的插入顺序(Python 3.7+ 字典有序)。但有时候你希望 key 按字母排序,让输出稳定可预期——比如两份内容一样的配置,不管插入顺序怎么变,序列化出来的字符串都一样,方便 `diff` 对比: + +```python +import json + +d = {"age": 28, "name": "两点水", "id": 1001} +print(json.dumps(d, ensure_ascii=False, sort_keys=True)) +``` + +输出: + +``` +{"age": 28, "id": 1001, "name": "两点水"} +``` + +`sort_keys=True` 之后,`age`、`id`、`name` 按字母顺序排好了,跟你当初插入的顺序无关。 + +这俩参数经常一起出现,水哥写配置文件的固定姿势就是: + + +```python +json.dump(cfg, f, ensure_ascii=False, indent=2, sort_keys=True) +``` + +三个参数一上,中文可读、缩进好看、顺序稳定,齐活。 + + + +## default:序列化「不听话」的对象 + +前面那些 `dict`、`list`、`str`、`int` 都能直接序列化。但 Python 里有一堆对象是 `json` 模块**不认识**的——`datetime`、`date`、`set`、还有你自定义的类。 + +拿 `datetime` 试一下,直接 `dumps` 会炸: + +```python +import json +from datetime import datetime + +now = datetime(2026, 5, 8, 10, 30, 0) + +# 直接 dumps 一个 datetime,会炸 +try: + json.dumps({"time": now}) +except TypeError as e: + print(f"直接 dumps 报错:{e}") + +# 加上 default=str,遇到不认识的对象就让 str() 兜底 +s = json.dumps({"time": now}, default=str, ensure_ascii=False) +print(s) +``` + +输出: + +``` +直接 dumps 报错:Object of type datetime is not JSON serializable +{"time": "2026-05-08 10:30:00"} +``` + +`default` 参数的作用是:当 `json.dumps` 碰到一个它不认识的对象时,就调用你给的 `default` 函数,用它的返回值继续序列化。传 `default=str` 就是最简单粗暴的做法——「不认识?直接转成字符串」。 + +不过 `default=str` 是把 `datetime` 变成字符串 `"2026-05-08 10:30:00"`,读回来之后它是 `str`,不再是个 `datetime` 了。如果你想要更精细的控制,就自己写一个转换函数: + +```python +import json + + +class User: + def __init__(self, name, age): + self.name = name + self.age = age + + +def user_to_dict(obj): + if isinstance(obj, User): + return {"name": obj.name, "age": obj.age} + raise TypeError(f"不知道咋序列化 {type(obj)}") + + +u = User("两点水", 28) +print(json.dumps(u, default=user_to_dict, ensure_ascii=False)) +``` + +输出: + +``` +{"name": "两点水", "age": 28} +``` + +`user_to_dict` 里做判断:碰到 `User` 对象就返回一个 `dict`,碰到别的类型就抛 `TypeError`。`json.dumps` 会拿着这个 `dict` 继续正常序列化。这样你就把「自定义对象 → 可序列化的 dict」这条规则完全掌握在自己手里了。 + + + +## object_hook:反序列化成自定义对象 + +`default` 解决的是「序列化出去」,那「读回来」呢?默认 `json.loads` 会把 JSON 对象一律转成 `dict`。如果你想让它读回来变成你自己定义的类实例,就要用 `object_hook`。 + +`object_hook` 会在**每个 JSON 对象被解析成 dict 之后**被调用一次,你可以在里面把它换成任意对象: + +```python +import json + + +class User: + def __init__(self, name, age): + self.name = name + self.age = age + + def __repr__(self): + return f"User(name={self.name!r}, age={self.age!r})" + + +def dict_to_user(d): + return User(d["name"], d["age"]) + + +s = '{"name": "两点水", "age": 28}' +u = json.loads(s, object_hook=dict_to_user) +print(u) +print(type(u)) +print(u.name, u.age) +``` + +输出: + +``` +User(name='两点水', age=28) + +两点水 28 +``` + +读回来之后,`u` 不再是 `dict`,而是一个 `User` 实例,能直接 `u.name`、`u.age` 访问。 + +`default` 和 `object_hook` 正好是一对——**`default` 管序列化(对象出去),`object_hook` 管反序列化(对象回来)**。不过水哥得说句实话:`object_hook` 用到的场景远没有 `default` 多,大多数时候读回来是个 `dict` 就够用了。各位知道有这个东西、等真需要的时候会翻文档就行。 + + + +## JSONDecodeError:坏 JSON 不崩 + +天有不测风云,你从网上拿到的 JSON,不一定都是合法的。可能是接口抽风返回了半截、可能是配置文件被人手改坏了、可能是编码出了问题。这时候 `json.loads` 会抛一个 `json.JSONDecodeError`: + +```python +import json + +bad = '{"name": "两点水", "age": }' + +try: + json.loads(bad) +except json.JSONDecodeError as e: + print(f"解析失败:{e.msg}") + print(f"出错位置:第 {e.lineno} 行,第 {e.colno} 列") +``` + +输出: + +``` +解析失败:Expecting value +出错位置:第 1 行,第 24 列 +``` + +`JSONDecodeError` 是 `ValueError` 的子类,它带了几个很贴心的字段: + +- `e.msg`:错误的英文描述,比如 `Expecting value`(「这里本来该有个值」) +- `e.lineno` / `e.colno`:出错在第几行第几列 +- `e.pos`:出错字符在整个字符串里的位置(从 0 开始数) + +有了这三个字段,你就能在日志里精确指出「这份 JSON 哪里坏了」,排查起来省一大截。 + +所以各位处理外部 JSON 的正确姿势,永远是**包一层 `try/except`**: + +```python +import json + + +def safe_loads(s): + try: + return json.loads(s) + except json.JSONDecodeError as e: + print(f"JSON 解析失败:{e}") + return None + + +print(safe_loads('{"ok": true}')) +print(safe_loads('{"ok": }')) +``` + +输出: + +``` +{'ok': True} +JSON 解析失败:Expecting value: line 1 column 8 (char 7) +None +``` + +坏数据进来,返回个 `None` 或者用默认值兜底,而不是让整个程序崩掉。这一层防御,是写「跟外部世界打交道」的代码的基本功。 + + + +## 实战:配置读写小工具 + +理论讲得差不多了,水哥带各位写一个小工具收尾——一个「配置管理器」。它把前面学的 `json.dump` / `json.load`、`ensure_ascii`、`indent`、`sort_keys`、`JSONDecodeError` 全串起来。 + +需求很简单: + +- 保存一份配置到 JSON 文件(中文可读、带缩进、key 排序) +- 读配置,文件不存在给默认值,文件损坏了报错但不崩 + +```python +import json +import tempfile +from pathlib import Path + +CONFIG = { + "username": "两点水", + "theme": "dark", + "font_size": 14, + "plugins": ["ruff", "pytest"], +} + + +def save_config(path, cfg): + with open(path, "w", encoding="utf-8") as f: + json.dump(cfg, f, ensure_ascii=False, indent=2, sort_keys=True) + + +def load_config(path): + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except FileNotFoundError: + print("配置文件不存在,用默认配置") + return {} + except json.JSONDecodeError as e: + print(f"配置文件损坏:{e}") + return {} + + +with tempfile.TemporaryDirectory() as td: + path = Path(td) / "config.json" + save_config(path, CONFIG) + print(path.read_text(encoding="utf-8")) + print("---") + print(load_config(path)) + print("---") + print(load_config(Path(td) / "不存在.json")) +``` + +输出: + +``` +{ + "font_size": 14, + "plugins": [ + "ruff", + "pytest" + ], + "theme": "dark", + "username": "两点水" +} +--- +{'font_size': 14, 'plugins': ['ruff', 'pytest'], 'theme': 'dark', 'username': '两点水'} +--- +配置文件不存在,用默认配置 +{} +``` + +各位看,一个五十行不到的小工具,就把「写配置、读配置、容错」全做齐了。`save_config` 里三个参数一上,写出来的 `config.json` 中文能读、缩进整齐、key 排序稳定;`load_config` 里两个 `except`,把「文件不存在」和「文件损坏」两种情况分别接住。 + +这个模式水哥在真实项目里用了无数遍——小到命令行工具的配置,大到服务的启动配置,本质都是这么个「写出去、读回来、加容错」的路子。各位把它抄进自己的工具里,立刻就能用。 + + + +## 小结 + +一章下来,咱们把 JSON 从「见过」聊到了「会用」。回顾一下要点: + +1. **JSON 是程序之间交换数据的通用格式**,人看得懂、机器好解析、语言无关,几乎是所有接口和配置的事实标准 +2. **JSON 一共六种类型**:对象、数组、字符串、数字、布尔、空值,注意布尔是小写 `true`/`false`、空值是 `null`、字符串必须双引号、没有注释和尾逗号 +3. **`json.loads` / `json.dumps` 处理字符串**,`json.load` / `json.dump` 处理文件——带 `s` 的是 string +4. **类型对照**:对象↔`dict`、数组↔`list`、`true`/`false`↔`True`/`False`、`null`↔`None`;注意 `tuple` 序列化后读回来是 `list` +5. **中文加 `ensure_ascii=False`**,否则会被转义成 `\uXXXX`;给人看的 JSON 一律加上 +6. **`indent` 让输出好看,`sort_keys` 让 key 稳定**,写配置文件的固定三件套是 `ensure_ascii=False, indent=2, sort_keys=True` +7. **`default` 序列化不认识的对象**(如 `datetime`、自定义类),`object_hook` 反序列化成自定义对象 +8. **读外部 JSON 一定包 `try/except JSONDecodeError`**,它自带 `msg`、`lineno`、`colno` 帮你定位错误 + +JSON 这东西,语法五分钟就能学会,但它是「程序出门社交」的基本功——以后各位调 API、写配置、存数据、对接各种 AI 接口,都绕不开它。这一章聊的是「怎么读怎么写」,剩下的就靠各位把它用进自己的项目里,练成肌肉记忆。 + +走起。 diff --git a/README.md b/README.md index bb656710..2e57d0a4 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ AI 时代,我以为这个课程不会再有人看了,但没想到这个教 * [标准日志 logging](/Article/PythonBasis/python26/1.md) * [打包发布与 typer](/Article/PythonBasis/python27/1.md) * [学完之后做什么](/Article/PythonBasis/python28/1.md) +* [JSON](/Article/PythonBasis/python29/1.md) diff --git a/mkdocs.yml b/mkdocs.yml index 30a8c3e3..b45877d3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -171,3 +171,4 @@ nav: - 标准日志 logging: PythonBasis/python26/1.md - 打包发布与 typer: PythonBasis/python27/1.md - 学完之后做什么: PythonBasis/python28/1.md + - JSON: PythonBasis/python29/1.md