From 711cc9a5fe75f392a3e7a857b4aca4305d32ec9e Mon Sep 17 00:00:00 2001 From: Mars Liu Date: Wed, 3 Nov 2021 11:45:45 +0800 Subject: [PATCH] reflections for new pipeline --- README.md | 316 +++++++++++++----- .../config.json" | 5 +- .../config.json" | 5 +- data/config.json | 3 +- src/__pycache__/tree.cpython-38.pyc | Bin 3108 -> 4708 bytes src/tree.py | 126 +++++-- 6 files changed, 335 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 6967b1aa..25ab28d5 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,7 @@ pip install -r requirement.txt ```json { // ... - "export": [ - { - "file": "solution.c", - "variants": null, - "depends": [] - }, - // ... - ] + "export": ["solution.json"] } ``` @@ -74,117 +67,258 @@ pip install -r requirement.txt ## `知识节点` 的导出习题选项配置编辑 -首先,在知识节点下增加一个习题代码,例如在 `data/1.算法初阶/1.蓝桥杯/7段码` 下增加一个`solution.c`代码: +首先,我们根据前文,在 `data/1.算法初阶/1.蓝桥杯/7段码` 目录增加一个`solution.json`文件: -```c -#include -int main(int argc, char** argv){ - printf("Hello,Wrold!"); - return 0; +```json +{ + "type": "code_options", + "author": "卢昕", + "source": "solution.md" } ``` -其次,增加一个同名的选项配置文件`solution.json`,目前有两种配置规则 +然后在 `data/1.算法初阶/1.蓝桥杯/7段码` 下增加一个`solution.md`文件: -**单行替换规则**: +````markdown +# 7段码 +#### 题目描述 +小蓝要用七段码数码管来表示一种特殊的文字。 +![七段码](https://img-blog.csdnimg.cn/2020110916441977.png#pic_left) +上图给出了七段码数码管的一个图示,数码管中一共有 7 段可以发光的二极管,分别标记为 a, b, c, d, e, f, g。 -* 配置由`one_line`字段指定的单行替换字典 -* 格式是:`"<源字符串>"`: [`"<替换字符串A>"`, `<替换字符串B>`,...], - * 其中每个 `"<源字符串>"` `/` `"<替换字符串A>"` 被生成为是一个替换选项 - * 指定的配置应该能至少生成 `3+` 个替换选项 +小蓝要选择一部分二极管(至少要有一个)发光来表达字符。在设计字符的表达时,要求所有发光的二极管是连成一片的。 -```json -{ - "one_line": { - "printf": ["print"], - "return 0;": ["return 0"], - "(\"Hello,Wrold!\")": [" \"Hello,Wrold!\""] - } -} -``` +* 例如:b 发光,其他二极管不发光可以用来表达一种字符。 -上面的替换规则会将代码替换成 3 个变种的代码: - -```c -// 变种代码1 -#include -int main(int argc, char** argv){ - print("Hello,Wrold!"); - return 0; -} -``` +* 例如:c 发光,其他二极管不发光可以用来表达一种字符。 -```c -// 变种代码2 -#include -int main(int argc, char** argv){ - print("Hello,Wrold!"); - return 0 -} -``` +这种方案与上一行的方案可以用来表示不同的字符,尽管看上去比较相似。 -```c -// 变种代码3 -#include -int main(int argc, char** argv){ - print "Hello,Wrold!"; - return 0 -} -``` +* 例如:a, b, c, d, e 发光,f, g 不发光可以用来表达一种字符。 -这些变种代码将会作为技能树该知识点该代码选择题的选项。 +* 例如:b, f 发光,其他二极管不发光则不能用来表达一种字符,因为发光的二极管没有连成一片。 -**多行替换规则**: +请问,小蓝可以用七段码数码管表达多少种不同的字符? -* 配置由`multiline`字段指定的多行替换数组 -* 数组的每个元素是一组替换规则,会整组被替换 +## aop +### before +```cpp +#include +using namespace std; +int use[10]; +int ans, e[10][10], father[10]; +void init() +{ -例如: + e[1][2] = e[1][6] = 1; + e[2][1] = e[2][7] = e[2][3] = 1; + e[3][2] = e[3][4] = e[3][7] = 1; + e[4][3] = e[4][5] = 1; + e[5][4] = e[5][6] = e[5][7] = 1; + e[6][1] = e[6][5] = e[6][7] = 1; +} -```json +int find(int a) { - "multiline": [ - { - "printf": "print" - }, - { - "int main(int argc, char** argv){" : "int main(char** argv){", - "return 0;" : "return 0", - }, - { - "#include ": "" - } - ] + if (father[a] == a) + return a; + father[a] = find(father[a]); + return father[a]; } ``` +### after +```cpp +int main() +{ + init(); + dfs(1); + cout << ans; + return 0; +} -同样,该配置将支持将源代码生成3个变种代码 +``` -```c -// 变种代码1 -#include -int main(int argc, char** argv){ - print("Hello,Wrold!"); - return 0; +## 答案 +```cpp +void dfs(int d) +{ + if (d > 7) + { + for (int i = 1; i <= 7; i++) + { + father[i] = i; + } + + for (int i = 1; i < 8; i++) + { + for (int j = 1; j < 8; j++) + { + if (e[i][j] == 1 && use[i] && use[j]) + { + int fx = find(i); + int fy = find(j); + if (fx != fy) + { + father[fx] = fy; + } + } + } + } + int k = 0; + for (int i = 1; i < 8; i++) + { + if (use[i] && father[i] == i) + { + k++; + } + } + if (k == 1) + { + ans++; + } + return; + } + + use[d] = 1; + dfs(d + 1); + use[d] = 0; + dfs(d + 1); } ``` +## 选项 -```c -// 变种代码2, 注意第2组替换规则,包含了两行替换 -#include -int main(char** argv){ - print("Hello,Wrold!"); - return 0 +### A +```cpp +void dfs(int d) +{ + if (d > 7) + { + for (int i = 1; i <= 7; i++) + { + father[i] = i; + } + + for (int i = 1; i < 8; i++) + { + for (int j = 1; j < 8; j++) + { + if (e[i][j] == 1 && use[i] && use[j]) + { + int fx = find(i); + int fy = find(j); + if (fx != fy) + { + father[fx] = fy; + } + } + } + } + int k = 0; + for (int i = 1; i < 8; i++) + { + if (father[i] == i) + { + k++; + } + } + if (k == 1) + { + ans++; + } + return; + } + + use[d] = 1; + dfs(d + 1); + use[d] = 0; + dfs(d + 1); } ``` -```c -// 变种代码3 -int main(int argc, char** argv){ - print("Hello,Wrold!"); - return 0; +### B +```cpp +void dfs(int d) +{ + if (d > 7) + { + for (int i = 1; i <= 7; i++) + { + father[i] = i; + } + + for (int i = 1; i < 8; i++) + { + for (int j = 1; j < 8; j++) + { + if (e[i][j] == 1) + { + int fx = find(i); + int fy = find(j); + if (fx != fy) + { + father[fx] = fy; + } + } + } + } + int k = 0; + for (int i = 1; i < 8; i++) + { + if (use[i] && father[i] == i) + { + k++; + } + } + if (k == 1) + { + ans++; + } + return; + } + + use[d] = 1; + dfs(d + 1); + use[d] = 0; + dfs(d + 1); +} +``` + +### C +```cpp +void dfs(int d) +{ + if (d > 7) + { + for (int i = 1; i <= 7; i++) + { + father[i] = i; + } + + int k = 0; + for (int i = 1; i < 8; i++) + { + if (use[i] && father[i] == i) + { + k++; + } + } + if (k == 1) + { + ans++; + } + return; + } + + use[d] = 1; + dfs(d + 1); + use[d] = 0; + dfs(d + 1); } ``` +```` + +后续的处理程序会根据“答案”、“选项”等标题查找内容,选项章节内部的三级标题不会进入题目,可以用来标注选项信息,例如 +“语法错误”,“内存没有初始化”等等。 ## 技能树合成 diff --git "a/data/1.\347\256\227\346\263\225\345\210\235\351\230\266/1.\350\223\235\346\241\245\346\235\257/config.json" "b/data/1.\347\256\227\346\263\225\345\210\235\351\230\266/1.\350\223\235\346\241\245\346\235\257/config.json" index 3407cf95..39528eaf 100644 --- "a/data/1.\347\256\227\346\263\225\345\210\235\351\230\266/1.\350\223\235\346\241\245\346\235\257/config.json" +++ "b/data/1.\347\256\227\346\263\225\345\210\235\351\230\266/1.\350\223\235\346\241\245\346\235\257/config.json" @@ -1,4 +1,5 @@ { - "node_id": "569d5e11c4fc5de7844053d9a733c5e8", - "keywords": [] + "node_id": "opencv-5437ea08671b4d9c888ad064723cce4d", + "keywords": [], + "title": "蓝桥杯" } \ No newline at end of file diff --git "a/data/1.\347\256\227\346\263\225\345\210\235\351\230\266/config.json" "b/data/1.\347\256\227\346\263\225\345\210\235\351\230\266/config.json" index 3407cf95..06db73d8 100644 --- "a/data/1.\347\256\227\346\263\225\345\210\235\351\230\266/config.json" +++ "b/data/1.\347\256\227\346\263\225\345\210\235\351\230\266/config.json" @@ -1,4 +1,5 @@ { - "node_id": "569d5e11c4fc5de7844053d9a733c5e8", - "keywords": [] + "node_id": "opencv-978a11e5a53a4042bf096c5d244cb5ea", + "keywords": [], + "title": "算法初阶" } \ No newline at end of file diff --git a/data/config.json b/data/config.json index f1231c31..fce78923 100644 --- a/data/config.json +++ b/data/config.json @@ -1,5 +1,6 @@ { "tree_name": "algorithm", "keywords": [], - "node_id": "569d5e11c4fc5de7844053d9a733c5e8" + "node_id": "569d5e11c4fc5de7844053d9a733c5e8", + "title": "C" } \ No newline at end of file diff --git a/src/__pycache__/tree.cpython-38.pyc b/src/__pycache__/tree.cpython-38.pyc index 58a068be9479b965110581eeb78c80b1d70ad5c2..b034f05a92591857f96e5847b263a3032e6f682d 100644 GIT binary patch literal 4708 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 diff --git a/src/tree.py b/src/tree.py index 57aac855..3b72d84a 100644 --- a/src/tree.py +++ b/src/tree.py @@ -5,6 +5,8 @@ import uuid import sys import re +id_set = set() + def load_json(p): with open(p, 'r') as f: @@ -20,7 +22,7 @@ def dump_json(p, j, exist_ok=False, override=False): print(f"{p} already exist") sys.exit(0) - with open(p, 'w') as f: + with open(p, 'w+') as f: f.write(json.dumps(j, indent=2, ensure_ascii=False)) @@ -37,11 +39,26 @@ def parse_no_name(d): 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 ''.join(str(uuid.uuid5(uuid.NAMESPACE_URL, 'skill_tree')).split('-')) + return "opencv-" + uuid.uuid4().hex def list_dir(p): v = os.listdir(p) @@ -51,10 +68,44 @@ def gen_tree(data_path): if os.path.isdir(no_dir): yield no_dir, no_name - def ensure_node_id(cfg_path, cfg): - if cfg.get('node_id') is None: - cfg['node_id'] = gen_node_id() - dump_json(cfg_path, cfg, exist_ok=True, override=True) + 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 = {} @@ -69,7 +120,12 @@ def gen_tree(data_path): # 根节点 cfg_path = os.path.join(data_path, 'config.json') cfg = load_json(cfg_path) - ensure_node_id(cfg_path, cfg) + 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'], @@ -81,41 +137,63 @@ def gen_tree(data_path): 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) - cfg_path = os.path.join(level_no_dir, 'config.json') - cfg = load_json(cfg_path) - ensure_node_id(cfg_path, cfg) + 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, cfg['node_id'], cfg['keywords']) + 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) - cfg_path = os.path.join(chapter_no_dir, 'config.json') - ensure_node_id(cfg_path, cfg) - cfg = load_json(cfg_path) + 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, cfg['node_id'], cfg['keywords']) + 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): - section_name = section_no_name - cfg_path = os.path.join(section_no_dir, 'config.json') - ensure_node_id(cfg_path, cfg) - cfg = load_json(cfg_path) + 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, cfg['node_id'], cfg['keywords'], cfg['children']) + section_name, sec_cfg['node_id'], sec_cfg['keywords'], sec_cfg.get('children', [])) chapter_node_children.append(section_node) # 确保习题分配了习题ID - for export in cfg['export']: - if export.get('exercise_id') is None: - export['exercise_id'] = gen_node_id() - dump_json(cfg_path, cfg, exist_ok=True, override=True) + + 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') -- GitLab