diff --git a/README.md b/README.md index 8ffcc5f010622e08b1fb5989b6939995cb842f22..587288d6d741ccdd5063b7a9415fbfecf16a8299 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,137 @@ # skill_tree_opencv -Open CV 技能树 \ No newline at end of file +## 目录结构说明 + +* 技能树`骨架文件`: + * 位置:`data/tree.json` + * 说明:该文件是执行 `python main.py` 生成的,请勿人工编辑 +* 技能树`根节点`配置文件: + * 位置:`data/config.json` + * 说明:可编辑配置关键词等字段,其中 `node_id` 字段是生成的,请勿编辑 +* 技能树`难度节点`: + * 位置:`data/xxx`,例如: `data/1.Java初阶` + * 说明: + * 每个技能树有 3 个等级,目录前的序号是必要的,用来保持文件夹目录的顺序 + * 每个目录下有一个 `config.json` 可配置关键词信息,其中 `node_id` 字段是生成的,请勿编辑 +* 技能树`章节点`: + * 位置:`data/xxx/xxx`,例如:`data/1.Java初阶/1.Java概述` + * 说明: + * 每个技能树的每个难度等级有 n 个章节,目录前的序号是必要的,用来保持文件夹目录的顺序 + * 每个目录下有一个 `config.json` 可配置关键词信息,其中 `node_id` 字段是生成的,请勿编辑 +* 技能树`知识节点`: + * 位置:`data/xxx/xxx/xxx`,例如:`data/1.Java初阶/1.Java概述/1.什么是Java` + * 说明: + * 每个技能树的每章有 `n` 个知识节点,目录前的序号是必要的,用来保持文件夹目录的顺序 + * 每个目录下有一个 `config.json` + * 其中 `node_id` 字段是生成的,请勿编辑 + * 其中 `keywords` 可配置关键字字段 + * 其中 `children` 可配置该`知识节点`下的子树结构信息,参考后面描述 + * 其中 `export` 可配置该`知识节点`下的导出习题信息,参考后面描述 + + +## `知识节点` 子树信息结构 + +例如 `data/1.Java初阶/1.Java概述/1.什么是Java/config.json` 里配置对该知识节点子树信息结构: +```json +{ + // ... + + "children": [ + // TODO ... + ], +} +``` + + + +## `知识节点` 的导出习题编辑 + +例如 `data/1.Java初阶/1.Java概述/1.什么是Java/config.json` 里配置对该知识节点导出的习题 + +```json +{ + // ... + "export": [ + "HellowWorld.json" + ] +} +``` + +在 export 字段中,我们列出习题定义 json ,下面我们了解如何编写习题。 + +## `知识节点` 的导出习题选项配置编辑 + +目前我们支持使用 markdown 语法直接编辑习题和各选项。 + +如前文内容,我们在知识节点下增加习题 `HelloWord`的定义文件,即在`data/1.Java初阶/1.Java概述/1.什么是Java` 目录增加一个`HelloWorld.json`文件: + +```json +{ + "type": "code_options", + "author": "刘鑫", + "source": "HelloWorld.md", + "notebook_enable": true +} +``` +其中 type 字段目前都固定是 `code_options`,notebook_enable 字段决定这个习题是否生成对应的 notebook 。根据具体情况写好其它字段,注意这里 source 的文件名,我们指定了一个 markdwon 文件。现在我们新建一个 HelloWorld.md 并编辑为: + +````markdown +# Hello World + +以下 `Hello World` 程序中,能够正确输出内容的是: + +## 答案 + +```java + +public class App { + public static void main(String[] args){ + System.out.println("Hello World"); + } +} +``` + +## 选项 + +### 不必要的返回值 + +```java + +public class App { + public int main(){ + System.out.printf("Hello World"); + return 0; + } +} +``` + +### 没有引用 System.out + +```java + +public class App { + public static void main(String[] args){ + println("Hello World"); + } +} +``` + +### 混合了 c 代码 + +```java +import stdout + +public class App { + public int main(){ + print("Hello World\n"); + return 0; + } +} + +``` + +```` + +这是一个最基本的习题结构,它包含标题、答案、选项,注意这几个一级和二级标题必须填写正确,解释器会读取这几个标题。而选项的标题会被直接忽略掉,在 +最终生成的习题中不包含选项的三级标题,所以这个标题可以用来标注一些编辑信息,例如“此选项没有关闭文件连接”,“类型错误”等等。 + diff --git "a/data/1.OpenCV\345\210\235\351\230\266/config.json" "b/data/1.OpenCV\345\210\235\351\230\266/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..f87da78d03fb174dd5d2b45d6398846d301b32fb --- /dev/null +++ "b/data/1.OpenCV\345\210\235\351\230\266/config.json" @@ -0,0 +1,6 @@ +{ + "export": [], + "node_id": "opencv-b943de85e3ad494885f0b4b529053c5a", + "title": "OpenCV初阶", + "keywords":[] +} \ No newline at end of file diff --git "a/data/2.OpenCV\344\270\255\351\230\266/config.json" "b/data/2.OpenCV\344\270\255\351\230\266/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..c16af783d6e0dc52fde4660938ffdddf3f25fa57 --- /dev/null +++ "b/data/2.OpenCV\344\270\255\351\230\266/config.json" @@ -0,0 +1,6 @@ +{ + "export": [], + "keywords": [], + "node_id": "opencv-e92c03e7b84c4c4ea7d23a2c32b88932", + "title": "OpenCV中阶" +} \ No newline at end of file diff --git "a/data/3.OpenCV\351\253\230\351\230\266/config.json" "b/data/3.OpenCV\351\253\230\351\230\266/config.json" new file mode 100644 index 0000000000000000000000000000000000000000..315932bf8d42200c171f3272f75a1b0c261c4ab0 --- /dev/null +++ "b/data/3.OpenCV\351\253\230\351\230\266/config.json" @@ -0,0 +1,6 @@ +{ + "export": [], + "keywords": [], + "node_id": "opencv-f27da6fd72924d1fbd05c6aff0fed4b7", + "title": "OpenCV高阶" +} \ No newline at end of file diff --git a/data/config.json b/data/config.json new file mode 100644 index 0000000000000000000000000000000000000000..be1e62f16daba41e81e29594bf0c19a1c3f7c070 --- /dev/null +++ b/data/config.json @@ -0,0 +1,6 @@ +{ + "tree_name": "opencv", + "keywords": [], + "title": "OpenCV", + "node_id": "opencv-22ad85b4166044c897cd32f625d21001" +} \ No newline at end of file diff --git a/data/tree.json b/data/tree.json new file mode 100644 index 0000000000000000000000000000000000000000..f58c6cba0b40a1ba04b8f6dfc733cad5eb1fa2d1 --- /dev/null +++ b/data/tree.json @@ -0,0 +1,29 @@ +{ + "opencv": { + "node_id": "opencv-22ad85b4166044c897cd32f625d21001", + "keywords": [], + "children": [ + { + "OpenCV初阶": { + "node_id": "opencv-b943de85e3ad494885f0b4b529053c5a", + "keywords": [], + "children": [] + } + }, + { + "OpenCV中阶": { + "node_id": "opencv-e92c03e7b84c4c4ea7d23a2c32b88932", + "keywords": [], + "children": [] + } + }, + { + "OpenCV高阶": { + "node_id": "opencv-f27da6fd72924d1fbd05c6aff0fed4b7", + "keywords": [], + "children": [] + } + } + ] + } +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..08e822cbc0ffe687103d2ba02386e5a5c9714609 --- /dev/null +++ b/main.py @@ -0,0 +1,4 @@ +from src.tree import gen_tree + +if __name__ == '__main__': + gen_tree('data') diff --git a/src/tree.py b/src/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..3b72d84a30c6abc9dcfefaed2db1785e75b5e9c8 --- /dev/null +++ b/src/tree.py @@ -0,0 +1,200 @@ +from genericpath import exists +import json +import os +import uuid +import sys +import re + +id_set = set() + + +def load_json(p): + with open(p, 'r') as f: + return json.loads(f.read()) + + +def dump_json(p, j, exist_ok=False, override=False): + if os.path.exists(p): + if exist_ok: + if not override: + return + else: + print(f"{p} already exist") + sys.exit(0) + + with open(p, 'w+') as f: + f.write(json.dumps(j, indent=2, ensure_ascii=False)) + + +def parse_no_name(d): + p = r'(\d+)\.(.*)' + m = re.search(p, d) + + try: + no = int(m.group(1)) + dir_name = m.group(2) + except: + sys.exit(0) + + return no, dir_name + + +def check_export(base, cfg): + flag = False + exports = [] + for export in cfg.get('export', []): + ecfg_path = os.path.join(base, export) + if os.path.exists(ecfg_path): + exports.append(export) + else: + flag = True + if flag: + cfg["export"] = exports + return flag + + +def gen_tree(data_path): + root = {} + + def gen_node_id(): + # return ''.join(str(uuid.uuid5(uuid.NAMESPACE_URL, 'skill_tree')).split('-')) + return "opencv-" + uuid.uuid4().hex + + def list_dir(p): + v = os.listdir(p) + v.sort() + for no_name in v: + no_dir = os.path.join(p, no_name) + if os.path.isdir(no_dir): + yield no_dir, no_name + + def ensure_id_helper(node): + flag = False + + if (node.get('node_id') is None) or node.get('node_id') in id_set: + node['node_id'] = gen_node_id() + flag = True + + id_set.add(node['node_id']) + + if 'children' in node: + for c in node["children"]: + flag = flag or ensure_id_helper(list(c.values())[0]) + + return flag + + def ensure_node_id(cfg): + return ensure_id_helper(cfg) + + def ensure_title_helper(node, cfg_path, title=""): + flag = False + + if node.get('title') is None: + if cfg_path: + node['title'] = re.sub( + "^[0-9]{1,3}\.", "", os.path.split(os.path.dirname(cfg_path))[-1]) + else: + node['title'] = title + flag = True + + if 'children' in node: + for c in node["children"]: + flag = flag or ensure_title_helper( + list(c.values())[0], None, list(c.keys())[0]) + + return flag + + def ensure_title(cfg, cfg_path): + return ensure_title_helper(cfg, cfg_path) + + def make_node(name, node_id, keywords, children=None): + node = {} + node_children = children or [] + node[name] = { + 'node_id': node_id, + 'keywords': keywords, + 'children': node_children + } + return node, node_children + + # 根节点 + cfg_path = os.path.join(data_path, 'config.json') + cfg = load_json(cfg_path) + if ensure_node_id(cfg): + dump_json(cfg_path, cfg, exist_ok=True, override=True) + + if ensure_title(cfg, cfg_path): + cfg["title"] = "C" + dump_json(cfg_path, cfg, exist_ok=True, override=True) + tree_node = { + "node_id": cfg['node_id'], + "keywords": cfg['keywords'], + "children": [] + } + root[cfg['tree_name']] = tree_node + + # 难度节点 + for level_no_dir, level_no_name in list_dir(data_path): + print(level_no_dir) + no, level_name = parse_no_name(level_no_name) + level_path = os.path.join(level_no_dir, 'config.json') + level_cfg = load_json(level_path) + if ensure_node_id(level_cfg) or check_export(level_no_dir, level_cfg): + dump_json(level_path, level_cfg, exist_ok=True, override=True) + if ensure_title(level_cfg, level_path): + dump_json(level_path, level_cfg, exist_ok=True, override=True) + + level_node, level_node_children = make_node( + level_name, level_cfg['node_id'], level_cfg['keywords']) + tree_node['children'].append(level_node) + + # 章节点 + for chapter_no_dir, chapter_no_name in list_dir(level_no_dir): + no, chapter_name = parse_no_name(chapter_no_name) + chapter_path = os.path.join(chapter_no_dir, 'config.json') + chapter_cfg = load_json(chapter_path) + if ensure_node_id(chapter_cfg) or check_export(chapter_no_dir, chapter_cfg): + dump_json(chapter_path, chapter_cfg, + exist_ok=True, override=True) + if ensure_title(chapter_cfg, chapter_path): + dump_json(chapter_path, chapter_cfg, + exist_ok=True, override=True) + + chapter_node, chapter_node_children = make_node( + chapter_name, chapter_cfg['node_id'], chapter_cfg['keywords']) + level_node_children.append(chapter_node) + + # 知识点 + for section_no_dir, section_no_name in list_dir(chapter_no_dir): + no, section_name = parse_no_name(section_no_name) + sec_path = os.path.join(section_no_dir, 'config.json') + sec_cfg = load_json(sec_path) + flag = ensure_node_id(sec_cfg) or check_export( + section_no_dir, sec_cfg) + + section_node, section_node_children = make_node( + section_name, sec_cfg['node_id'], sec_cfg['keywords'], sec_cfg.get('children', [])) + chapter_node_children.append(section_node) + + # 确保习题分配了习题ID + + for export in sec_cfg.get("export", []): + ecfg_path = os.path.join(section_no_dir, export) + ecfg = load_json(ecfg_path) + + if (ecfg.get('exercise_id') is None) or (ecfg.get('exercise_id') in id_set): + ecfg['exercise_id'] = uuid.uuid4().hex + dump_json(ecfg_path, ecfg, + exist_ok=True, override=True) + + id_set.add(ecfg['exercise_id']) + + if flag: + dump_json(sec_path, sec_cfg, exist_ok=True, override=True) + + if ensure_title(sec_cfg, sec_path): + dump_json(sec_path, sec_cfg, exist_ok=True, override=True) + + # 保存技能树骨架 + tree_path = os.path.join(data_path, 'tree.json') + dump_json(tree_path, root, exist_ok=True, override=True)