diff --git a/README.md b/README.md index ce2edc4264839e443541b26a8b2328d0a8f91e06..679c01df84db81fc17e8495ff9fb372fb2ff60b0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # pandasUI -堪比 Excel 的界面操作,所以操作都能生成 pandas 操作的 python 代码 +堪比 Excel 的界面操作,所有操作都能生成 pandas 操作的 python 代码 +![](assets/分组功能示例.gif) # 构成 diff --git "a/assets/\345\210\206\347\273\204\345\212\237\350\203\275\347\244\272\344\276\213.gif" "b/assets/\345\210\206\347\273\204\345\212\237\350\203\275\347\244\272\344\276\213.gif" new file mode 100644 index 0000000000000000000000000000000000000000..5f1be670f048eae4c835afbf110b45c611dcb891 Binary files /dev/null and "b/assets/\345\210\206\347\273\204\345\212\237\350\203\275\347\244\272\344\276\213.gif" differ diff --git "a/doc/\345\212\237\350\203\275.md" "b/doc/\345\212\237\350\203\275.md" new file mode 100644 index 0000000000000000000000000000000000000000..c6909dcf4c7f6ee6a9322b09e33acbf9586daa0e --- /dev/null +++ "b/doc/\345\212\237\350\203\275.md" @@ -0,0 +1,74 @@ + + + + + + +# UI + +## 关于表格操作 + +目前尝试了两种表格的ui库: + +1. vue_table +2. ant-vue 中的 table + +点1 其实比较符合需求,但是他有一个bug验证影响了核心功能(由单层表头转多层表头时,顺序会错乱),并且他本身不能很好支持多层行索引的显示 + +点2 时目前所用,但他实在比较难用 + + + +目前考虑是否有能力完全自制表格,这样子也许能获得最大的自由度,但是这严重考验我的菜鸟级前端能力 + + + +## 快速功能 + +我不希望为各种功能疲于奔命,因此希望通过简单的json配置,配合python装饰器,能够快速把自定义的某个函数与界面上的菜单自动绑定。 + +例如,工具缺少某个功能,但我有一定的python的pandas 处理能力。因此可以 + +定义一个 json: + +```jso +{ + "group":"其他功能", + "name":"列去重", + "function":"my_col_duplicated" + "ui":{ + "content":[ + {"title":"选择列","type":"select"} + ] + } +} +``` + + + +然后定义 python 文件: + +```python +@ui_bind() +def my_col_duplicated(col): + return xxx +``` + + + +只要把2个文件放置在指定目录下,程序启动就可以加载他们。 + +界面上有一个"其他功能区",可以从中搜索到对应的按钮,点击就会根据 json 中定义的内容,构造界面。点击后就会到后台执行 上述的python自定义函数。 + + + +这个方向主要研究 vue 关于渲染函数的使用 + + + + + + + + + diff --git "a/doc/\350\256\276\350\256\241\346\200\235\350\267\257.md" "b/doc/\350\256\276\350\256\241\346\200\235\350\267\257.md" new file mode 100644 index 0000000000000000000000000000000000000000..32a2256c5ef163e3d7285139fb88ebca35243f74 --- /dev/null +++ "b/doc/\350\256\276\350\256\241\346\200\235\350\267\257.md" @@ -0,0 +1,191 @@ + + +[TOC] + + + +# UI(目录起始:\ui) + +主要采用 vue 3.x 构建,整体结构: + +1. api,src\api 目录下,负责发送请求 +2. applications ,src\applications 目录下,负责 调用 api 发送请求,接收结果并更新数据(store) +3. store,src\store目录下 ,全局数据中心,比如加载文件的数据,代码生成的文本等全局使用的数据 +4. models,src\models目录下 ,定义了各种数据的结构 +5. components ,src\components 目录下 ,界面上的各种组件 + + + +大致流程: + +1. 界面出现时,实际执行了对应 components 下对应组件的方法,通常是 通过 store 把数据绑定到界面上 +2. 用户点击了某个功能,触发 components 下某个组件中定义的某个方法 +3. 方法中,收集界面上所需的数据,通过 applications 的具体方法,执行功能 +4. applications 间接调用 api ,与后端通信获得结果,并更新数据到 store +5. 由于点1已经绑定到界面,所以 store 的更新会反映到界面上 + +> 由于我的前端水平是菜鸟级,以上流程是一次次重构摸索出来,目前项目可能不是所有地方都按照这种流程编写代码,但希望一步步完善,并使用统一的机制 + + + +--- + +# python 部分的结构(目录起始:\py) + +python 部分整体分2个部分: + +1. web ,主要在 main.py 文件中 +2. 代码生成与操作记录,主要在 src\core 目录下 + + + +## web + +目前就是简单使用 flask 实现最简单的 web api 方式通信。日后可以考虑使用 graphql 重构(看后续的功能情况) + + + + + +## 代码生成与操作记录 + + + +### MethodCall 与 Statement + +对于一个方法调用: + +```py +df.query('xxxxx') +``` + + + +由3个部分组成: + +1. df,调用者 +2. query,方法名 +3. 'xxxxx' ,参数 + +对于一个方法调用,我使用 MethodCall 定义(src\core\MethodCall.py) ,他记录了方法名及其调用所需的全部参数 + +而一句完整的代码,我使用 Statement 定义(src\core\Commander.py) ,他保存了 调用者名字、多个 MethodCall 、结果接收名字。 + +比如: + +```py +ret = df.query('xxx').groupby('key').agg({'col1':[sum,'mean']}) +``` + +这是一个 Statement ,其中: + +1. 调用者变量名字,df +2. 有3个 MethodCall ,分别是 query、groupby、agg,各自存放相应的调用入参 +3. 接收变量名字,ret + + + +--- + +### Commander + +最后,多个 Statement 组成一个用户的功能操作,我使用 Commander 定义。 + +他结构非常简单: + +1. 操作的名字 +2. 一个 Statement 的集合 + + + +--- + +### 代理 + +目前设计并非直接拼接字符串实现,而是使用代理类 模拟一个 dataframe 的调用,记录调用信息。 + +Statement 就是这个代理类,比如: + +```py +st = Statement('df') + +st.query('name=="x1"') +code =st.to_code() + +# code 的值为 '''df.query('name=="x1"')''' +``` + +此处,st 可以调用任意的方法以及传入任意的参数,他就会记录所有调用过程。 + +甚至连续的调用: + +```py +st = Statement('df') + +st.query('name=="x1"').groupby('name').agg({'col1':[sum]}) +``` + + + + + +to_code 方法 只是把记录的过程转成文本而已。 + +他实际与 pandas 没有任何关系,可以模拟任意的方法 + +具体使用方式,可以查看项目中的单元测试(testing 目录下) + +--- + + + + +### 缺陷 +这种设计方式的优点是新增功能是非常容易,且代码直观。 + +但是目前有些逻辑他难以表达,例如: + +```py +(df['name']==1) & (df['age']>=25) +``` + +也许,可以结合下面介绍的其他方式 + + + + + + + +--- + +### 其他想法 + +#### 拼接字符串 +一开始我考虑了一种最简单直接的实现方式——拼接字符串。 + +大致流程如下: + +1. 调用每一个应用功能时,直接使用字符串,完整表达整个调用 +2. 执行时,所有的字符串拼接成完整的语句,并使用 python 的 eval 或 exec 函数执行 + +在项目分支 feature_test_py_ast 中,采用了这种实现方式。但是他的主要问题在于,每个功能的制作都需要复杂的处理字符串逻辑,尤其是参数比较多的时候,功能代码会变得非常不直观。 + +当初只是试验性尝试,或者深入研究一下可以作为另一种实现方式。 + +此外,现有结构可以简单修改为这种实现,因为 与最上层 web api 交互的只是 Commander + + +#### 语法树 +还有一种基于静态语法分析的模块 ast,这是python内置。这可能是一种更有效的实现方式。 +但是他仍然是基于静态的语法树生成,这意味着应用上每一次调用都必需构造对应的代码语句字符串,可能对新功能开发效率有一定影响。 + + + + + + + + + + diff --git a/py/build/build.py b/py/build/build.py index f1d865b0e24d657812159be858a6320635087cc1..e583646bf47a4824d666666510ac035ecdc42872 100644 --- a/py/build/build.py +++ b/py/build/build.py @@ -7,6 +7,7 @@ import os os.chdir(plib.Path(__file__).parent) vinfo = sys.version_info +py_num = f'{vinfo.major}{vinfo.minor}' m_dist_app_path = plib.Path(f'../dist/app{vinfo.major}{vinfo.minor}') @@ -44,7 +45,7 @@ def to_folder(file: plib.Path): return parts -fs = plib.Path('../src').rglob('*.cpython-37.pyc') +fs = plib.Path('../src').rglob(f'*.cpython-{py_num}.pyc') fs = [(p, to_folder(p)) for p in fs] for f in fs: @@ -54,7 +55,7 @@ for f in fs: shutil.copyfile(f[0], f[1]) -shutil.copyfile('../__pycache__/main.cpython-37.pyc', m_dist_main_path) +shutil.copyfile(f'../__pycache__/main.cpython-{py_num}.pyc', m_dist_main_path) shutil.copytree('../web', m_dist_web_path)