├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── .idea ├── .gitignore ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── new_xxt.iml └── vcs.xml ├── LICENSE ├── README.md ├── answers └── 27835863.json ├── config.py ├── img ├── img.png ├── img_1.png ├── img_2.png ├── img_3.png └── img_4.png ├── main.py ├── my_xxt ├── __pycache__ │ ├── answer_type.cpython-310.pyc │ ├── api.cpython-310.pyc │ ├── findAnswer.cpython-310.pyc │ ├── login.cpython-310.pyc │ ├── my_tools.cpython-310.pyc │ └── question_type.cpython-310.pyc ├── answer_type.py ├── api.py ├── findAnswer.py ├── login.py ├── my_tools.py └── question_type.py ├── requirements.txt ├── test └── test.py ├── upload.py └── user.json /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: 9 | - red 10 | tags: # 当我们提交代码为tag 是以'v'开头的时候才会触发自动部署到服务端 如 git push tag v0.1.0 11 | - 'v*' 12 | 13 | permissions: 14 | contents: read 15 | 16 | 17 | 18 | jobs: 19 | build: 20 | strategy: 21 | matrix: 22 | os: [ windows-latest ] 23 | runs-on: ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up Python 3.10 27 | uses: actions/setup-python@v3 28 | with: 29 | python-version: "3.10" 30 | - name: Pypi Install 31 | run: | 32 | python -m pip install --upgrade pip 33 | 34 | - name: Pypi Install 35 | run: | 36 | python -m pip install --upgrade pip 37 | 38 | - name: dependencies Install 39 | run: | 40 | pip install -r requirements.txt 41 | 42 | - name: pyinstaller package exe 43 | run: | 44 | pyinstaller --noconfirm --onedir --console --name "xxt" --add-data "LICENSE;." --add-data "README.md;." --add-data "config.py;." --add-data "user.json;." --add-data "answers;answers/" "main.py" 45 | - name: output to zip 46 | run: | 47 | 7z a -tzip new_xxt_For_${{ runner.os }}_x64.zip ./dist/xxt/* 48 | 49 | - name: Create Release 50 | id: create_release 51 | uses: actions/create-release@master 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.XXT }} 54 | with: 55 | tag_name: ${{ github.ref }} # (tag)标签名称 56 | release_name: ${{ github.ref }} 57 | draft: false # 是否是草稿 58 | prerelease: false # 是否是预发布 59 | 60 | - name: Upload Release Asset 61 | id: upload-release-asset 62 | uses: actions/upload-release-asset@master 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.XXT }} 65 | with: 66 | upload_url: ${{ steps.create_release.outputs.upload_url }} # 上传地址,通过创建Release获取到的 67 | asset_path: ./new_xxt_For_${{ runner.os }}_x64.zip # 要上传文件 68 | asset_name: new_xxt_For_${{ runner.os }}_x64.zip # 上传后的文件名 69 | asset_content_type: application/zip # 文件类型 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | /output/ 3 | /user.json 4 | /upload.py 5 | /__pycache__ 6 | /logfile.log 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 53 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/new_xxt.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 aglorice 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 学习通,泛雅,超星 作业一键完成(全新版本✨✨✨) 4 |

5 | 6 |

该脚本仅用于爬虫技术的学习,如果你有好的功能或者想法,欢迎提交pr

7 |

🛸注意:该脚本完成作业的答案需要从已完成作业账号中导出,仅适合自用!!!

8 |
9 | 10 | ![GitHub stars](https://img.shields.io/github/stars/aglorice/new_xxt.svg) 11 | ![python](https://img.shields.io/badge/python-3.10-blue) 12 | ![version](https://img.shields.io/badge/version-v0.5.6-blue) 13 | ![contributors](https://badgen.net/github/contributors/aglorice/new_xxt) 14 | ![prs](https://badgen.net/github/prs/aglorice/new_xxt) 15 | 16 | ## 1️⃣实现方法 17 | - 从已完成该课程的作业的账号中获取作业答案 18 | - 登录到自己的账号获取题目顺序,与之前的答案进行比较获取到正确的选项或者填空 19 | - 构造表单数据发起请求完成提交作业 20 | 21 | 22 | ## 2️⃣功能支持列表 23 | > 如果你在使用的时候遇到了问题,欢迎提交[issue](https://github.com/aglorice/new_xxt/issues)😍😍😍 24 | ### 🚀已实现的功能 25 | * [x] 日志功能 26 | * [x] 查询所有的课程 27 | * [x] 查询课程作业 28 | * [x] 提取已完成的作业的答案 29 | * [x] 答案保存为json格式 30 | * [x] 一键完成作业 31 | * [x] 支持选择题,填空题,多选题,简答题,论述题,判断题等 32 | * [x] 查询所有课程的未做作业 33 | * [x] 查看作业的成绩以及完成状态 34 | * [x] 获取个人信息 35 | * [x] 查看作业是否可以重做 36 | * [x] 手机号密码登录 37 | * [x] 实现扫码登录 38 | * [x] 在没有正确答案的情况下提取自己的答案(测试) 39 | * [x] 多用户批量完成作业 40 | * [x] 批量提取一个课程所有的作业 41 | * [x] 自动识别匹配答案策略 42 | * [x] 新增自动进行重做操作😁 43 | ### todo✨✨✨ 44 | * [ ] 对接题库 45 | * [ ] ai自动识别答案(chatgpt3.5) 46 | * [ ] 提取待批阅作业的答案 47 | * [ ] 保存答案 48 | ## 目前对题型支持 49 | 50 | | 题型 | 完成状态 | 51 | |-----|----------| 52 | | 单选题 | ✅支持 | 53 | | 多选题 | ✅支持 | 54 | | 判断题 | ✅支持 | 55 | | 简答题 | ✅支持(未测试) | 56 | | 填空题 | ✅支持 | 57 | | 论述题 | ✅支持(未测试) | 58 | | 其他 | ✅支持(未测试) | 59 | | 编程题 | ✅支持 | 60 | 61 | ## 3️⃣使用方法 62 | #### 1.克隆到本地 63 | ```bash 64 | git clone https://github.com/aglorice/new_xxt.git 65 | ``` 66 | 67 | #### 2.进入目录 68 | ```bash 69 | cd new_xxt 70 | ``` 71 | #### 3.安装依赖 72 | ```bash 73 | pip install -r requirements.txt 74 | ``` 75 | #### 4.运行`main.py` 76 | ```bash 77 | python main.py 78 | ``` 79 | #### 5.使用爬取功能去爬取已完成的作业的账号,得到答案文件后,你可以选择将得到的json文件移动到`answers`文件下,然后使用完成作业的功能即可。使用批量功能前,请提前配置 `user.json` 80 | #### 如果觉得上述方式比较麻烦,该项目提供exe版本,打开 [releases](https://github.com/aglorice/new_xxt/releases),找到 `xxt.zip`,下载下来,解压后运行 `xxt/xxt.exe` 即可。 81 | 82 | ### 🐞🐞🐞如果提交的时候显示无效的参数请重新运行一次就可以了。 83 | `answers/27835863.json` 84 | ```json 85 | { 86 | "27835863": [ 87 | { 88 | "id": "163497980", 89 | "title": "1.(单选题)在数据结构中,与所使用的计算机无关的是数据的()结构。", 90 | "type": "单选题", 91 | "answer": "A", 92 | "option": [ 93 | "A. 逻辑", 94 | "B. 存储", 95 | "C. 逻辑和存储", 96 | "D. 物理" 97 | ] 98 | }, 99 | { 100 | "id": "163498096", 101 | "title": "2.(单选题)算法分析的两个主要方面是()。", 102 | "type": "单选题", 103 | "answer": "A", 104 | "option": [ 105 | "A. 空间复杂度和时间复杂度", 106 | "B. 正确性和简明性", 107 | "C. 可读性和文档性", 108 | "D. 数据复杂性和程序复杂性" 109 | ] 110 | }, 111 | ... 112 | ] 113 | } 114 | ``` 115 | `user.json` 116 | ```json 117 | { 118 | "users": [ 119 | { 120 | "phone":"你的手机号", 121 | "password": "密码", 122 | "name": "test1" 123 | }, 124 | { 125 | "phone":"你的手机号", 126 | "password": "密码", 127 | "name": "test1" 128 | } 129 | ] 130 | } 131 | ``` 132 | 133 | ### 终端运行结果(可能会和新版存在差异,以最新版本为主): 134 | ![](img/img.png) 135 | #### 选择功能 136 | ![](img/img_1.png) 137 | #### 批量完成作业 138 | ![](img/img_2.png) 139 | 140 | ## 4️⃣注意事项 141 | - 仓库发布的`new_xxt`项目中涉及的任何脚本,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。 142 | - 本项目遵循MIT License协议,如果本特别声明与MIT License协议有冲突之处,以本特别声明为准。 143 | - 以任何方式查看此项目的人或直接或间接使用`new_xxt`项目的任何脚本的使用者都应仔细阅读此声明。`aglorice` 保留随时更改或补充此免责声明的权利。一旦使用并复制了任何相关脚本或`new_xxt`项目,则视为您已接受此免责声明。 144 | 145 | 146 | ### 如果你觉得不错的话,就请我吃颗糖吧。😁😁😁 147 | img_4 148 | 149 | ### Star History 150 | 151 | [![Star History Chart](https://api.star-history.com/svg?repos=aglorice/new_xxt&type=Date)](https://star-history.com/#aglorice/new_xxt&Date) 152 | -------------------------------------------------------------------------------- /answers/27835863.json: -------------------------------------------------------------------------------- 1 | { 2 | "27835863": [ 3 | { 4 | "id": "163657918", 5 | "title": "1. (单选题)在一个无向图G中,所有顶点的度数之和等于所有边数之和的(   )倍。", 6 | "type": "单选题", 7 | "answer": "C", 8 | "option": [ 9 | "A.l/2", 10 | "B.1", 11 | "C.2", 12 | "D.4" 13 | ] 14 | }, 15 | { 16 | "id": "163657961", 17 | "title": "2. (单选题)一个具有n个顶点的无向联通图至少包含(    )条边。", 18 | "type": "单选题", 19 | "answer": "C", 20 | "option": [ 21 | "A.n", 22 | "B.n+1", 23 | "C.n-1", 24 | "D.n/2" 25 | ] 26 | }, 27 | { 28 | "id": "163657966", 29 | "title": "3. (单选题)对于一个具有n个顶点和e条边的无向图,若采用邻接表表示,则所有顶点邻接表中的结点总数为(    )。", 30 | "type": "单选题", 31 | "answer": "D", 32 | "option": [ 33 | "A.n", 34 | "B.e", 35 | "C.2n", 36 | "D.2e" 37 | ] 38 | }, 39 | { 40 | "id": "163657970", 41 | "title": "4. (单选题)在有向图的邻接表中,每个顶点邻接链表链接着该顶点所有(    )邻接点。", 42 | "type": "单选题", 43 | "answer": "B", 44 | "option": [ 45 | "A.入边", 46 | "B.出边", 47 | "C.入边和出边", 48 | "D.不是入边也不是出边" 49 | ] 50 | }, 51 | { 52 | "id": "163657973", 53 | "title": "5. (单选题)在有向图的逆邻接表中,每个顶点邻接链表链接着该顶点所有(    )邻接点。", 54 | "type": "单选题", 55 | "answer": "A", 56 | "option": [ 57 | "A.入边", 58 | "B.出边", 59 | "C.入边和出边", 60 | "D.不是人边也不是出边" 61 | ] 62 | }, 63 | { 64 | "id": "163657982", 65 | "title": "6. (单选题)邻接表是图的一种(    )。", 66 | "type": "单选题", 67 | "answer": "B", 68 | "option": [ 69 | "A.顺序存储结构", 70 | "B.链式存储结构", 71 | "C.索引存储结构", 72 | "D.散列存储结构" 73 | ] 74 | }, 75 | { 76 | "id": "163657992", 77 | "title": "7. (单选题)无向图的邻接矩阵是一个(    )。", 78 | "type": "单选题", 79 | "answer": "A", 80 | "option": [ 81 | "A.对称矩阵", 82 | "B.零矩阵", 83 | "C.上三角矩阵", 84 | "D.对角矩阵" 85 | ] 86 | }, 87 | { 88 | "id": "163658067", 89 | "title": "8. (单选题)下列有关图遍历的说法不正确的是(    )。", 90 | "type": "单选题", 91 | "answer": "C", 92 | "option": [ 93 | "A.连通图的深度优先搜索是一个递归过程", 94 | "B.图的广度优先搜索中邻接点的寻找具有“先进先出”的特征", 95 | "C.非连通图不能用深度优先搜索法", 96 | "D.图的遍历要求每一顶点仅被访问一次" 97 | ] 98 | }, 99 | { 100 | "id": "163658076", 101 | "title": "9. (单选题)采用邻接表存储的图的宽度优先遍历算法类似于二叉树的( ) 。", 102 | "type": "单选题", 103 | "answer": "D", 104 | "option": [ 105 | "A.先序遍历", 106 | "B.中序遍历", 107 | "C.后序遍历", 108 | "D.按层遍历" 109 | ] 110 | }, 111 | { 112 | "id": "163658127", 113 | "title": "10. (单选题)判定一个有向图是否存在回路除了可以利用拓扑排序方法外,还可以利用( )。", 114 | "type": "单选题", 115 | "answer": "D", 116 | "option": [ 117 | "A.求关键路径的方法", 118 | "B.求最短路径的Dijkstm方法", 119 | "C.宽度优先遍历算法", 120 | "D.深度优先遍历算法" 121 | ] 122 | }, 123 | { 124 | "id": "163658719", 125 | "title": "11. (填空题)在图中,任何两个数据元素之间都可能存在关系,因此图的数据元素之间是一种________________的关系。", 126 | "type": "填空题", 127 | "answer": [ 128 | "(1)多对多" 129 | ], 130 | "option": [] 131 | }, 132 | { 133 | "id": "163658725", 134 | "title": "12. (填空题)如果图中的边或弧带有权,则称这种图为_______________。", 135 | "type": "填空题", 136 | "answer": [ 137 | "(1)网" 138 | ], 139 | "option": [] 140 | }, 141 | { 142 | "id": "163658736", 143 | "title": "13. (填空题)若一条路径上的开始结点和结束结点为同一个顶点,则称该路径为________或_______。同时除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路称_________或__________。", 144 | "type": "填空题", 145 | "answer": [ 146 | "(1)回路", 147 | "(2)环", 148 | "(3)简单回路", 149 | "(4)简单环" 150 | ], 151 | "option": [] 152 | }, 153 | { 154 | "id": "163659901", 155 | "title": "14. (填空题)邻接表(Adjacency List)是图的一种_______存储结构。在邻接表中,对图中每个顶点建立一个_________,第i个单链表中的结点表示依附于顶点vi的边(对无向图)或弧(对有向图)。", 156 | "type": "填空题", 157 | "answer": [ 158 | "(1)链式", 159 | "(2)单链表" 160 | ], 161 | "option": [] 162 | }, 163 | { 164 | "id": "163659921", 165 | "title": "15. (填空题)构造一棵最小生成树往往都要利用最小生成树的一种简称为MST的性质。常见的构造最小生成树的______________算法和___________算法都利用了MST性质。", 166 | "type": "填空题", 167 | "answer": [ 168 | "(1)普里姆(Prim)", 169 | "(2)克鲁斯卡尔(Kruskal)" 170 | ], 171 | "option": [] 172 | }, 173 | { 174 | "id": "163658608", 175 | "title": "16. (判断题)有n个结点的无向图中,若边数大于n-1,则该图是连通的。(   )", 176 | "type": "判断题", 177 | "answer": "错", 178 | "option": [] 179 | }, 180 | { 181 | "id": "163658611", 182 | "title": "17. (判断题)AOV网拓扑排序的结果是惟一的。(   )", 183 | "type": "判断题", 184 | "answer": "错", 185 | "option": [] 186 | }, 187 | { 188 | "id": "163658616", 189 | "title": "18. (判断题)图的广度优先搜索序列是惟一的。(  )", 190 | "type": "判断题", 191 | "answer": "错", 192 | "option": [] 193 | }, 194 | { 195 | "id": "163658618", 196 | "title": "19. (判断题)若一个有向图的邻接矩阵中对角线以下元素均为零,则该图的拓扑有序序列必定存。(  )", 197 | "type": "判断题", 198 | "answer": "对", 199 | "option": [] 200 | }, 201 | { 202 | "id": "163658632", 203 | "title": "20. (判断题)具有n个顶点的无向图采用邻接矩阵表示,图中的边数等于邻接矩阵中非零元素之和的一半。(  )", 204 | "type": "判断题", 205 | "answer": "对", 206 | "option": [] 207 | }, 208 | { 209 | "id": "163658636", 210 | "title": "21. (判断题)若连通图上各边权值均不相同,则该图的最小生成树是惟一的。( )", 211 | "type": "判断题", 212 | "answer": "对", 213 | "option": [] 214 | }, 215 | { 216 | "id": "163658641", 217 | "title": "22. (判断题)无向图的邻接矩阵一定是对称的。(    )", 218 | "type": "判断题", 219 | "answer": "对", 220 | "option": [] 221 | }, 222 | { 223 | "id": "163658643", 224 | "title": "23. (判断题)有向图的邻接矩阵一定是非对称的。(   )", 225 | "type": "判断题", 226 | "answer": "错", 227 | "option": [] 228 | }, 229 | { 230 | "id": "163658645", 231 | "title": "24. (判断题)图G的某一最小生成树的代价一定小于其他生成树的代价。(  )", 232 | "type": "判断题", 233 | "answer": "错", 234 | "option": [] 235 | }, 236 | { 237 | "id": "163658661", 238 | "title": "25. (判断题)对任意一个图从它的某个顶点出发进行一次深度优先或广度优先搜索遍历可访问到该图的每个顶点。(   )", 239 | "type": "判断题", 240 | "answer": "错", 241 | "option": [] 242 | } 243 | ], 244 | "info": { 245 | "work_id": "1", 246 | "id": "27835863", 247 | "work_name": "第7章 图-作业", 248 | "work_status": "已完成", 249 | "work_url": "https://mooc1.chaoxing.com/mooc2/work/task?courseId=204924252&classId=73792554&cpi=269513956&workId=27835863&answerId=51875280&enc=3b8e82deed331864daac4e8af6e9f3b5", 250 | "course_name": "数据结构与算法", 251 | "isRedo": "no", 252 | "score": "100分" 253 | } 254 | } -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __VERSION__ = "v0.5.6" 4 | __AUTHOR__ = "aglorice" 5 | # 日志文件 6 | __LOG_FILE__ = "logfile.log" 7 | # 屏幕宽度 8 | __SCREEN_WIDTH__ = 120 -------------------------------------------------------------------------------- /img/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/img/img.png -------------------------------------------------------------------------------- /img/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/img/img_1.png -------------------------------------------------------------------------------- /img/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/img/img_2.png -------------------------------------------------------------------------------- /img/img_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/img/img_3.png -------------------------------------------------------------------------------- /img/img_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/img/img_4.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | from rich.traceback import install 3 | 4 | from my_xxt.my_tools import select_menu, show_start 5 | from my_xxt.api import NewXxt 6 | from my_xxt.login import login 7 | from config import __LOG_FILE__, __SCREEN_WIDTH__ 8 | 9 | 10 | class MyConsole(Console): 11 | 12 | def __init__(self, *args, **kwargs): 13 | super().__init__(*args, **kwargs) 14 | self.log_file = open(__LOG_FILE__, "w", encoding="utf-8") 15 | 16 | def log(self, *args, **kwargs): 17 | super().log(*args, **kwargs) 18 | if not self.log_file.closed: 19 | self.log_file.write("\n"+" ".join(map(str, args)) + "\n") 20 | 21 | def print(self, *args, **kwargs): 22 | super().print(*args, **kwargs) 23 | captured_output_value = super().export_text() 24 | if not self.log_file.closed: 25 | self.log_file.write("\n"+captured_output_value+"\n") 26 | 27 | def cleanup(self): 28 | if not self.log_file.closed: 29 | self.log_file.close() 30 | 31 | def __del__(self): 32 | self.cleanup() 33 | 34 | 35 | def main(): 36 | console = MyConsole(width=__SCREEN_WIDTH__, record=True) 37 | install(console=console, show_locals=True, width=__SCREEN_WIDTH__) 38 | 39 | try: 40 | while True: 41 | show_start(console) 42 | new_xxt_instance = NewXxt() 43 | login(console, new_xxt_instance) 44 | select_menu(console, new_xxt_instance) 45 | except Exception as e: 46 | console.log(f"程序退出: {e}", style="bold red") 47 | console.log_exception(e) 48 | console.log("如果需要反馈错误信息,请将对应的日志文件以及问题反映给作者", style="bold red") 49 | 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /my_xxt/__pycache__/answer_type.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/my_xxt/__pycache__/answer_type.cpython-310.pyc -------------------------------------------------------------------------------- /my_xxt/__pycache__/api.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/my_xxt/__pycache__/api.cpython-310.pyc -------------------------------------------------------------------------------- /my_xxt/__pycache__/findAnswer.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/my_xxt/__pycache__/findAnswer.cpython-310.pyc -------------------------------------------------------------------------------- /my_xxt/__pycache__/login.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/my_xxt/__pycache__/login.cpython-310.pyc -------------------------------------------------------------------------------- /my_xxt/__pycache__/my_tools.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/my_xxt/__pycache__/my_tools.cpython-310.pyc -------------------------------------------------------------------------------- /my_xxt/__pycache__/question_type.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/my_xxt/__pycache__/question_type.cpython-310.pyc -------------------------------------------------------------------------------- /my_xxt/answer_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding = utf-8 -*- 2 | # @Time :2023/5/17 14:31 3 | # @Author :小岳 4 | # @Email :401208941@qq.com 5 | # @PROJECT_NAME :xxt_cli 6 | # @File : answer_type.py 7 | import bs4 8 | from rich.console import Console 9 | 10 | 11 | class AnswerType: 12 | @staticmethod 13 | def multipleChoice(item: bs4.element.Tag, console: Console) -> dict: 14 | try: 15 | option = [] 16 | option_list = item.find_all_next("ul")[0] 17 | for _option in option_list: 18 | if _option == "\n": 19 | continue 20 | option.append(my_replace(_option.text)) 21 | answer = getAnswer(item) 22 | title = item.find_all("h3", attrs={"class": "mark_name colorDeep"})[0].text 23 | question_answer = { 24 | "id": item.attrs['data'], 25 | "title": title, 26 | "type": "单选题", 27 | "answer": answer, 28 | "option": option 29 | } 30 | return question_answer 31 | except Exception as e: 32 | console.print(f"[bold red]单选题题目解析错误[/bold red]") 33 | console.print(f"[bold red]错误信息:{e}[/bold red]") 34 | 35 | @staticmethod 36 | def multipleChoices(item: bs4.element.Tag, console: Console) -> dict: 37 | try: 38 | option = [] 39 | option_list = item.find_all_next("ul")[0] 40 | for _option in option_list: 41 | if _option == "\n": 42 | continue 43 | option.append(my_replace(_option.text)) 44 | answer = getAnswer(item) 45 | title = item.find_all("h3", attrs={"class": "mark_name colorDeep"})[0].text 46 | question_answer = { 47 | "id": item.attrs['data'], 48 | "title": title, 49 | "type": "多选题", 50 | "answer": answer, 51 | "option": option 52 | } 53 | return question_answer 54 | except Exception as e: 55 | console.print(f"[bold red]多选题题目解析错误[/bold red]") 56 | console.print(f"[bold red]错误信息:{e}[/bold red]") 57 | 58 | @staticmethod 59 | def judgeChoice(item: bs4.element.Tag, console: Console) -> dict: 60 | try: 61 | option = [] 62 | answer = getAnswer(item) 63 | title = item.find_all("h3", attrs={"class": "mark_name colorDeep"})[0].text 64 | question_answer = { 65 | "id": item.attrs['data'], 66 | "title": title, 67 | "type": "判断题", 68 | "answer": answer, 69 | "option": option 70 | } 71 | return question_answer 72 | except Exception as e: 73 | console.print(f"[bold red]判断题题目解析错误[/bold red]") 74 | console.print(f"[bold red]错误信息:{e}[/bold red]") 75 | 76 | @staticmethod 77 | def comprehensive(item: bs4.element.Tag, console: Console) -> dict: 78 | try: 79 | option = [] 80 | answer = [] 81 | answer_list = item.find_all_next("dl", attrs={"class": "mark_fill colorGreen"})[0].find_all("dd") 82 | for _answer in answer_list: 83 | answer.append(my_replace(_answer.text)) 84 | title = item.find_all("h3", attrs={"class": "mark_name colorDeep"})[0].text 85 | question_answer = { 86 | "id": item.attrs['data'], 87 | "title": title, 88 | "type": "填空题", 89 | "answer": answer, 90 | "option": option 91 | } 92 | return question_answer 93 | except Exception as e: 94 | console.print(f"[bold red]填空题题目解析错误[/bold red]") 95 | console.print(f"[bold red]错误信息:{e}[/bold red]") 96 | console.print(f"[bold red]题目信息:{item}[/bold red]") 97 | 98 | @staticmethod 99 | def shortAnswer(item: bs4.element.Tag, console: Console) -> dict: 100 | try: 101 | option = [] 102 | answer = item.find("dd").text 103 | title = item.find_all("h3", attrs={"class": "mark_name colorDeep"})[0].text 104 | question_answer = { 105 | "id": item.attrs['data'], 106 | "title": title, 107 | "type": "简答题", 108 | "answer": answer, 109 | "option": option 110 | } 111 | return question_answer 112 | except Exception as e: 113 | console.print(f"[bold red]简答题题目解析错误[/bold red]") 114 | console.print(f"[bold red]错误信息:{e}[/bold red]") 115 | 116 | @staticmethod 117 | def essayQuestion(item: bs4.element.Tag, console: Console): 118 | try: 119 | option = [] 120 | answer = item.find("dd").text 121 | title = item.find_all("h3", attrs={"class": "mark_name colorDeep"})[0].text 122 | question_answer = { 123 | "id": item.attrs['data'], 124 | "title": title, 125 | "type": "论述题", 126 | "answer": answer, 127 | "option": option 128 | } 129 | return question_answer 130 | except Exception as e: 131 | console.print(f"[bold red]论述题题目解析错误[/bold red]") 132 | console.print(f"[bold red]错误信息:{e}[/bold red]") 133 | @staticmethod 134 | def programme(item: bs4.element.Tag, console: Console): 135 | try: 136 | option = [] 137 | answer = [] 138 | answer_list = item.find_all_next("dl", attrs={"class": "mark_fill colorGreen"})[0].find_all("dd") 139 | for _answer in answer_list: 140 | answer.append(my_replace(_answer.text)) 141 | title = item.find_all("h3", attrs={"class": "mark_name colorDeep"})[0].text 142 | question_answer = { 143 | "id": item.attrs['data'], 144 | "title": title, 145 | "type": "编程题", 146 | "answer": answer, 147 | "option": option 148 | } 149 | return question_answer 150 | except Exception as e: 151 | console.print(f"[bold red]编程题题目解析错误[/bold red]") 152 | console.print(f"[bold red]错误信息:{e}[/bold red]") 153 | 154 | 155 | @staticmethod 156 | def other(item: bs4.element.Tag, console: Console): 157 | try: 158 | option = [] 159 | answer = item.find("dd").text 160 | title = item.find_all("h3", attrs={"class": "mark_name colorDeep"})[0].text 161 | question_answer = { 162 | "id": item.attrs['data'], 163 | "title": title, 164 | "type": "其他", 165 | "answer": answer, 166 | "option": option 167 | } 168 | return question_answer 169 | except Exception as e: 170 | console.log(f"[bold red]其他题目解析错误[/bold red]") 171 | console.log(f"[bold red]错误信息:{e}[/bold red]") 172 | 173 | 174 | @staticmethod 175 | def error(item: bs4.element.Tag, console: Console): 176 | try: 177 | title_type = item.find_next("span").string.split(",")[0].replace("(", "").replace(")", "") 178 | console.log(f"[bold red]该题目类型[bold green]{title_type}[/bold green]还没有支持,请到本项目提交issue[/bold red]") 179 | return {} 180 | except Exception as e: 181 | console.log(f"[bold red]题目解析错误[/bold red]") 182 | console.log(f"[bold red]错误信息:{e}[/bold red]") 183 | return None 184 | 185 | 186 | def my_replace(_string: str): 187 | if _string is None: 188 | return Exception 189 | return _string.replace("\xa0", ' ').replace("\n", "").replace(" ", "").replace("\t", "").replace(" ", "").replace( 190 | "\r", "") 191 | 192 | 193 | def getAnswer(item) -> str: 194 | try: 195 | answer = item.find_all_next("span", attrs={"class": "colorGreen marginRight40 fl"})[0].text.replace( 196 | "正确答案: ", "") 197 | answer = my_replace(answer) 198 | except Exception as e: 199 | answer = item.find_all_next("span", attrs={"class": "colorDeep marginRight40 fl"})[0].text.replace("我的答案: ", 200 | "") 201 | answer = my_replace(answer) 202 | return answer 203 | -------------------------------------------------------------------------------- /my_xxt/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding = utf-8 -*- 2 | # @Time :2023/5/16 17:09 3 | # @Author :小岳 4 | # @Email :401208941@qq.com 5 | # @PROJECT_NAME :xxt_cli 6 | # @File : api.py 7 | import base64 8 | import re 9 | import secrets 10 | import time 11 | import random 12 | import traceback 13 | from functools import cache 14 | from types import NoneType 15 | from urllib import parse 16 | from urllib.parse import urlparse 17 | from bs4 import BeautifulSoup 18 | from Crypto.Cipher import AES 19 | from Crypto.Util.Padding import pad 20 | import requests as requests 21 | from rich.console import Console 22 | 23 | from my_xxt.answer_type import AnswerType 24 | from my_xxt.question_type import QuestionType 25 | 26 | # 加密密钥 27 | key = b"u2oh6Vu^HWe4_AES" 28 | 29 | # 登录接口(post) 30 | LOGIN_URL = "http://passport2.chaoxing.com/fanyalogin" 31 | 32 | # 获取课程(get) 33 | GET_COURSE_URl = "https://mooc2-ans.chaoxing.com/mooc2-ans/visit/courses/list" 34 | 35 | # 获取课程作业(get) 36 | GET_WORK_URL = "https://mooc1.chaoxing.com/mooc2/work/list" 37 | 38 | # 提交作业(post) 39 | COMMIT_WORK = "https://mooc1.chaoxing.com/work/addStudentWorkNewWeb" 40 | 41 | # 确认提交(get) 42 | IS_COMMIT = "https://mooc1.chaoxing.com/work/validate" 43 | 44 | # 获取个人信息(get) 45 | SELF_INFO = "http://passport2.chaoxing.com/mooc/accountManage" 46 | 47 | # 获取二维码图片(get) 48 | GET_LOGIN_QR = "https://passport2.chaoxing.com/createqr" 49 | 50 | # 判断是否二维码登录成功(post) 51 | IS_QR_LOGIN = "https://passport2.chaoxing.com/getauthstatus" 52 | 53 | # 登录页面首页(get) 54 | HOME_LOGIN = "https://passport2.chaoxing.com/login" 55 | 56 | # 重做作业(get) 57 | REDO_WORK = "https://mooc1.chaoxing.com/work/phone/redo" 58 | 59 | # 答案题目类型 60 | answer_type = [ 61 | {"type": "单选题", "fun": "multipleChoice", "key": "0"}, 62 | {"type": "多选题", "fun": "multipleChoices", "key": "1"}, 63 | {"type": "判断题", "fun": "judgeChoice", "key": "3"}, 64 | {"type": "填空题", "fun": "comprehensive", "key": "2"}, 65 | {"type": "简答题", "fun": "shortAnswer", "key": "4"}, 66 | {"type": "论述题", "fun": "essayQuestion", "key": "6"}, 67 | {"type": "编程题", "fun": "programme", "key": "9"}, 68 | {"type": "其他", "fun": "other", "key": "8"}, 69 | 70 | ] 71 | # 问题题目类型 72 | question_type = [ 73 | {"type": "单选题", "fun": "multipleChoice"}, 74 | {"type": "多选题", "fun": "multipleChoices"}, 75 | {"type": "判断题", "fun": "judgeChoice"}, 76 | {"type": "填空题", "fun": "comprehensive"}, 77 | {"type": "简答题", "fun": "shortAnswer"}, 78 | {"type": "论述题", "fun": "essayQuestion"}, 79 | {"type": "编程题", "fun": "programme"}, 80 | {"type": "其他", "fun": "other"} 81 | ] 82 | 83 | 84 | class NewXxt: 85 | def __init__(self): 86 | # 创建一个会话实例 87 | self.qr_enc = None 88 | self.qr_uuid = None 89 | 90 | self.randomOptions = "false" 91 | self.sees = requests.session() 92 | 93 | self.header = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " \ 94 | "Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63 " 95 | 96 | def qr_get(self) -> None: 97 | """获取二维码登录key""" 98 | self.sees.cookies.clear() 99 | resp = self.sees.get( 100 | HOME_LOGIN, headers={"User-Agent": self.get_ua("web")} # 这里不可以用移动端 UA 否则鉴权失败 101 | ) 102 | resp.raise_for_status() 103 | html = BeautifulSoup(resp.text, "lxml") 104 | self.qr_uuid = html.find("input", id="uuid")["value"] 105 | self.qr_enc = html.find("input", id="enc")["value"] 106 | 107 | # 激活qr但忽略返回的图片bin 108 | resp = self.sees.get(GET_LOGIN_QR, params={"uuid": self.qr_uuid, "fid": -1}) 109 | resp.raise_for_status() 110 | 111 | def qr_geturl(self) -> str: 112 | """合成二维码内容url""" 113 | return f"https://passport2.chaoxing.com/toauthlogin?uuid={self.qr_uuid}&enc={self.qr_enc}&xxtrefer=&clientid=&type=0&mobiletip=" 114 | 115 | def login_qr(self) -> dict: 116 | """使用二维码登录""" 117 | resp = self.sees.post(IS_QR_LOGIN, data={"enc": self.qr_enc, "uuid": self.qr_uuid}) 118 | resp.raise_for_status() 119 | content_json = resp.json() 120 | return content_json 121 | 122 | def login(self, phone: str, password: str) -> dict: 123 | """ 124 | 登录学习通 125 | :param phone: 手机号 126 | :param password: 密码 127 | :return: 登录结果 128 | """ 129 | # 开始加密参数 130 | cryptor = AES.new(key, AES.MODE_CBC, key) 131 | phone = base64.b64encode(cryptor.encrypt(pad(phone.encode(), 16))).decode() 132 | cryptor = AES.new(key, AES.MODE_CBC, key) 133 | password = base64.b64encode(cryptor.encrypt(pad(password.encode(), 16))).decode() 134 | resp = self.sees.post( 135 | url=LOGIN_URL, 136 | headers={ 137 | "Host": "passport2.chaoxing.com", 138 | "Origin": "http://passport2.chaoxing.com", 139 | "User-Agent": self.header, 140 | }, 141 | data={ 142 | 'uname': phone, 143 | 'password': password, 144 | 't': "true", 145 | 'validate': None, 146 | 'doubleFactorLogin': "0", 147 | 'independentId': "0" 148 | }, 149 | ) 150 | return resp.json() 151 | 152 | @cache 153 | def getCourse(self) -> list: 154 | """ 155 | 获取所有的课程 156 | :return: 所有的课程信息 157 | """ 158 | # 所有的课程信息 159 | course_list = [] 160 | course = self.sees.get( 161 | url=GET_COURSE_URl, 162 | headers={ 163 | "User-Agent": self.header, 164 | }, 165 | params={ 166 | "v": time.time(), 167 | "start": 0, 168 | "size": 500, 169 | "catalogId": 0, 170 | "superstarClass": 0, 171 | } 172 | ) 173 | course = BeautifulSoup(course.text, "lxml") 174 | course_div_list = course.find_all("div", attrs={"class": "course-info"}) 175 | if len(course_div_list) == 0: 176 | return course_list 177 | i = 1 178 | for course_div in course_div_list: 179 | try: 180 | course_dict = { 181 | "id": str(i), 182 | "course_name": self.my_replace(course_div.span.string), 183 | "course_url": course_div.a.attrs['href'], 184 | "course_teacher": self.my_replace( 185 | course_div.find_next("p", attrs={"class", "line2 color3"}).get("title", "小明")), 186 | "course_class": course_div.find_next("p", attrs={"class", "overHidden1"}).text 187 | } 188 | course_list.append(course_dict) 189 | except NoneType: 190 | traceback.print_exc() 191 | i = i + 1 192 | return course_list 193 | 194 | def getInfo(self) -> dict: 195 | """ 196 | 获取个人信息 197 | :return: 个人信息 198 | """ 199 | info_html = self.sees.get( 200 | url=SELF_INFO, 201 | headers={ 202 | "User-Agent": self.header, 203 | }, 204 | ) 205 | info_html_soup = BeautifulSoup(info_html.text, "lxml") 206 | info_html_soup = info_html_soup.find("div", attrs={"class": "infoDiv"}) 207 | info = { 208 | "name": info_html_soup.find("span", attrs={"id": "messageName"})["value"], 209 | "student_number": info_html_soup.find("p", attrs={"class": "xuehao"}).text.replace("学号/工号:", "") 210 | } 211 | return info 212 | 213 | def getWorks(self, course_url: str, course_name: str) -> list: 214 | """ 215 | 获取一个课程的作业 216 | :param course_url: 课程的url 217 | :param course_name: 课程的名称 218 | :return:该课程所有的作业 219 | """ 220 | works = [] 221 | work_view = self.sees.get( 222 | url=course_url, 223 | headers={ 224 | "User-Agent": self.header, 225 | }, 226 | params={ 227 | "v": time.time(), 228 | "start": 0, 229 | "size": 500, 230 | "catalogId": 0, 231 | "superstarClass": 0, 232 | } 233 | ) 234 | # 解析url中的重要参数 235 | url_params = urlparse(work_view.url) 236 | work_date = parse.parse_qs(url_params.query) 237 | # 获取workEnc 238 | work_enc = BeautifulSoup(work_view.text, "lxml") 239 | work_enc = work_enc.find("input", id="workEnc")["value"] 240 | 241 | get_work_date = { 242 | "courseId": work_date["courseid"][0], 243 | "classId": work_date["clazzid"][0], 244 | "cpi": work_date["cpi"][0], 245 | "ut": "s", 246 | "enc": work_enc, # workEnc 247 | } 248 | work_list = self.sees.get( 249 | url=GET_WORK_URL, 250 | params=get_work_date, 251 | headers={ 252 | "Host": "mooc1.chaoxing.com", 253 | "Referer": "https://mooc2-ans.chaoxing.com/", 254 | "User-Agent": self.header, 255 | }, 256 | allow_redirects=True 257 | ) 258 | work_li_list_soup = BeautifulSoup(work_list.text, "lxml") 259 | work_li_list = work_li_list_soup.find_all("li") 260 | 261 | i = 1 262 | for work in work_li_list: 263 | url_params = urlparse(work.attrs['data']) 264 | work_date = parse.parse_qs(url_params.query) 265 | _work = { 266 | "work_id": str(i), 267 | "id": work_date["workId"][0], 268 | "work_name": work.find_next('p').string, 269 | "work_status": work.find_next("p").find_next("p").string, 270 | "work_url": work.attrs['data'], 271 | "course_name": course_name, 272 | "isRedo": self.getIsRedo(work.attrs['data']), 273 | "score": self.getWorkScore(work.attrs['data']), 274 | "courseId": get_work_date["courseId"], 275 | } 276 | i = i + 1 277 | works.append(_work) 278 | return works 279 | 280 | def getWorkScore(self, work_url: str) -> str: 281 | """ 282 | 获取一个作业的分数 283 | :param work_url: 作业的url 284 | :return: 285 | """ 286 | result = self.sees.get( 287 | url=work_url, 288 | headers={ 289 | "User-Agent": self.header, 290 | }, 291 | ) 292 | result_view = BeautifulSoup(result.text, "lxml") 293 | try: 294 | result_view_soup = result_view.find("span", attrs={"class": "resultNum"}) 295 | return result_view_soup.text 296 | except Exception as e: 297 | try: 298 | result_view_soup = result_view.find("p", attrs={"class": "Finalresult"}) 299 | result_view_soup = result_view_soup.find("span") 300 | return result_view_soup.text 301 | except Exception as e: 302 | return "无" 303 | 304 | def getIsRedo(self, work_url: str) -> str: 305 | """ 306 | 获取作业重做状态 307 | :param work_url: 308 | :return: 309 | """ 310 | result = self.sees.get( 311 | url=work_url, 312 | headers={ 313 | "User-Agent": self.header, 314 | }, 315 | ) 316 | try: 317 | result_view = BeautifulSoup(result.text, "lxml") 318 | result_view_soup = result_view.find("a", attrs={"class": "redo"}) 319 | if "重做" in result_view_soup: 320 | return "yes" 321 | except Exception as e: 322 | return "no" 323 | 324 | def redoWork(self, work_url: str) -> dict: 325 | """ 326 | 重做作业 327 | :param work_url: 328 | :return: 329 | """ 330 | response = self.sees.get( 331 | url=work_url, 332 | headers={ 333 | "User-Agent": self.header, 334 | }, 335 | ) 336 | work_view_html_soup = BeautifulSoup(response.text, "lxml") 337 | input_date = work_view_html_soup.find_all("input") 338 | redo_date = {} 339 | try: 340 | for _input in input_date: 341 | _key = re.findall(r'id="(.*?)"', str(_input)) 342 | _value = str(_input.attrs['value']) 343 | redo_date[_key[0]] = _value 344 | redo_date = redo_date 345 | except Exception as e: 346 | redo_date = redo_date 347 | result = self.sees.get( 348 | url=REDO_WORK, 349 | headers={ 350 | "User-Agent": self.header, 351 | }, 352 | params={ 353 | "courseId": redo_date["courseId"], 354 | "classId": redo_date["classId"], 355 | "cpi": redo_date["cpi"], 356 | "workId": redo_date["workId"], 357 | "workAnswerId": redo_date["answerId"] 358 | } 359 | ) 360 | return result.json() 361 | 362 | def getAnswer(self, work_url: str, ) -> list: 363 | """ 364 | 爬取已完成作业的答案 365 | :param work_url: 366 | :return: 367 | """ 368 | work_answer = [] 369 | work_answer_view = self.sees.get( 370 | url=work_url, 371 | headers={ 372 | "User-Agent": self.header, 373 | }, 374 | ) 375 | 376 | work_view_soup = BeautifulSoup(work_answer_view.text, "lxml") 377 | work_view = work_view_soup.find_all("div", attrs={"class": "marBom60 questionLi singleQuesId"}) 378 | _answer_type = AnswerType() 379 | for item in work_view: 380 | title_type = item.find_next("span").string.split(",")[0].replace("(", "").replace(")", "") 381 | # 根据选项去自动调用对应的方法来解析数据 382 | func_name = self.selectFunc(title_type, answer_type) 383 | func = getattr(_answer_type, func_name) 384 | work_answer.append(func(item, Console(width=100))) 385 | return work_answer 386 | 387 | def create_from(self, work_url: str) -> dict: 388 | """ 389 | 构造提交作业所需要的数据 390 | :param work_url: 391 | :return: 392 | """ 393 | work_view_html = self.sees.get( 394 | url=work_url, 395 | headers={ 396 | "User-Agent": self.header, 397 | }, 398 | ) 399 | work_view_html_soup = BeautifulSoup(work_view_html.text, "lxml") 400 | commit_date = work_view_html_soup.find("form")["action"] 401 | # 解析form表单的url中的重要参数 402 | url_params = urlparse(commit_date) 403 | work_date = parse.parse_qs(url_params.query) 404 | # 查找构造表单所需的数据 405 | input_date = work_view_html_soup.find_all("input") 406 | commit_date_form = {} 407 | # 解析做作业的页面的url 408 | do_work_url_params = urlparse(work_view_html.url) 409 | do_work_params = parse.parse_qs(do_work_url_params.query) 410 | 411 | for _input in input_date: 412 | try: 413 | _key = re.findall(r'id="(.*?)"', str(_input)) 414 | _value = str(_input.attrs['value']) 415 | commit_date_form[_key[0]] = _value 416 | except Exception as e: 417 | continue 418 | 419 | commit_date = { 420 | "_classId": work_date["_classId"][0], 421 | "courseid": work_date["courseid"][0], 422 | "token": work_date["token"][0], 423 | "totalQuestionNum": work_date["totalQuestionNum"][0], 424 | "ua": "pc", 425 | "formType": "post", 426 | "saveStatus": "1", 427 | "version": "1", 428 | "workRelationId": do_work_params["workId"][0], 429 | "workAnswerId": do_work_params["answerId"][0], 430 | "standardEnc": do_work_params["standardEnc"][0] 431 | } 432 | commit_date = dict(commit_date_form, **commit_date) 433 | return commit_date 434 | 435 | def commit_before(self, commit_date: dict) -> str: 436 | """ 437 | 在作业提交提交之前需要发送一个请求 438 | :param commit_date: 439 | :return: 440 | """ 441 | ret = self.sees.get( 442 | url=IS_COMMIT, 443 | params={ 444 | "courseId": commit_date['courseId'], 445 | "classId": commit_date["classId"], 446 | "cpi": commit_date["cpi"] 447 | }, 448 | headers={ 449 | "User-Agent": self.header, 450 | }, 451 | ) 452 | return ret.text 453 | 454 | def commit_work(self, answer: list, work: dict) -> dict: 455 | """ 456 | 提交作业 457 | :param answer: 458 | :param work: 459 | :return: 460 | """ 461 | commit_from = self.create_from(work_url=work["work_url"]) 462 | params = self.create_params(commit_from) 463 | data = self.create_from_data(commit_from, answer) 464 | self.commit_before(commit_from) 465 | ret = self.sees.post( 466 | url=COMMIT_WORK, 467 | params=params, 468 | data=data, 469 | headers={ 470 | "User-Agent": self.header, 471 | }, 472 | ) 473 | return ret.json() 474 | 475 | def get_question(self, work_url: str, params=None) -> list: 476 | """ 477 | 获取未完成作业的题目 478 | :param params: 479 | :param work_url: 480 | :return: 481 | """ 482 | work_question = [] 483 | 484 | work_question_view = self.sees.get( 485 | url=work_url, 486 | headers={ 487 | "User-Agent": self.header, 488 | }, 489 | params=params, 490 | ) 491 | 492 | work_view_soup = BeautifulSoup(work_question_view.text, "lxml") 493 | randomOptions_soup = work_view_soup.find_all("input", attrs={"id": "randomOptions"}) 494 | 495 | # 判断选项是否是乱序的 496 | randomOptions = re.findall(r'value="(.*?)"', str(randomOptions_soup))[0] 497 | self.randomOptions = randomOptions 498 | 499 | work_view = work_view_soup.find_all("div", attrs={"class": "padBom50 questionLi fontLabel singleQuesId"}) 500 | 501 | 502 | _question_type = QuestionType() 503 | for item in work_view: 504 | title_type = item.find_next("span").string.split(",")[0].replace("(", "").replace(")", "") 505 | func_name = self.selectFunc(title_type, question_type) 506 | func = getattr(_question_type, func_name) 507 | work_question.append(func(item)) 508 | return work_question 509 | 510 | @staticmethod 511 | def my_replace(_string: str): 512 | if _string is None: 513 | return Exception 514 | return _string.replace("\xa0", ' ').replace("\n", "").replace(" ", "").replace("\t", "").replace(" ", "") 515 | 516 | @staticmethod 517 | def selectFunc(type_question: str, _type: list) -> str: 518 | # 选择题目类型 519 | for item in _type: 520 | if item["type"] == type_question: 521 | return item["fun"] 522 | # 如果没有找到对应的题目类型,返回error 523 | return "error" 524 | 525 | @staticmethod 526 | def escape_tags(html_str: str): 527 | # 判断是否有html标签 528 | if bool(BeautifulSoup(html_str, "html.parser").find()): 529 | # 匹配html标签的正则表达式 530 | pattern = re.compile(r'<.+?>') 531 | 532 | # 将匹配到的html标签进行转义,但不包含标签中的内容 533 | escaped_str = pattern.sub(lambda m: m.group(0).replace("<", "<").replace(">", ">"), html_str) 534 | return escaped_str 535 | else: 536 | return html_str 537 | 538 | @staticmethod 539 | def allQuestionId(answer: list) -> str: 540 | """ 541 | 将代做的题目id连接在一起 542 | :return: 连接好的id 543 | """ 544 | answer_id_all = "" 545 | for item in answer: 546 | answer_id_all = answer_id_all + item['id'] + ',' 547 | return answer_id_all 548 | 549 | @staticmethod 550 | def create_params(commit_from: dict) -> dict: 551 | params = { 552 | "_classId": commit_from["_classId"], 553 | "courseid": commit_from["courseid"], 554 | "token": commit_from["token"], 555 | "totalQuestionNum": commit_from["totalQuestionNum"], 556 | "ua": commit_from["ua"], 557 | "formType": commit_from["formType"], 558 | "saveStatus": commit_from["saveStatus"], 559 | "version": commit_from["version"], 560 | } 561 | return params 562 | 563 | @staticmethod 564 | def get_ua(ua_type: str) -> str: 565 | """获取UA""" 566 | match ua_type: 567 | case "mobile": 568 | return f"Dalvik/2.1.0 (Linux; U; Android {random.randint(9, 12)}; MI{random.randint(10, 12)} Build/SKQ1.210216.001) (device:MI{random.randint(10, 12)}) Language/zh_CN com.chaoxing.mobile/ChaoXingStudy_3_5.1.4_android_phone_614_74 (@Kalimdor)_{secrets.token_hex(16)}" 569 | case "web": 570 | return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35" 571 | case _: 572 | raise NotImplementedError 573 | 574 | def create_from_data(self, commit_from: dict, answer: list) -> dict: 575 | """ 576 | 将答案构造成提交所需要的格式 577 | :param commit_from: 578 | :param answer: 579 | :return: 580 | """ 581 | commit_from_1 = { 582 | "courseId": commit_from["courseid"], 583 | "classId": commit_from["_classId"], 584 | "knowledgeId": commit_from["knowledgeId"], 585 | "cpi": commit_from["cpi"], 586 | "workRelationId": commit_from["workRelationId"], 587 | "workAnswerId": commit_from["workAnswerId"], 588 | "jobid": commit_from["jobid"], 589 | "standardEnc": commit_from["standardEnc"], 590 | "enc_work": commit_from["token"], 591 | "totalQuestionNum": commit_from["totalQuestionNum"], 592 | "pyFlag": commit_from["pyFlag"], 593 | "answerwqbid": self.allQuestionId(answer), 594 | "mooc2": commit_from["mooc2"], 595 | "randomOptions": commit_from["randomOptions"], 596 | } 597 | from_date_2 = {} 598 | for item in answer: 599 | if "单选题" in item["title"]: 600 | key1 = "answertype" + item["id"] 601 | from_date_2[key1] = 0 602 | key2 = "answer" + item["id"] 603 | from_date_2[key2] = item["answer"] 604 | elif "填空题" in item["title"]: 605 | key3 = "answertype" + item["id"] 606 | from_date_2[key3] = 2 607 | key4 = "tiankongsize" + item["id"] 608 | from_date_2[key4] = len(item["answer"]) 609 | for answer_item in range(1, len(item["answer"]) + 1): 610 | _key = "answerEditor" + item["id"] + str(answer_item) 611 | from_date_2[_key] = "

" + item["answer"][answer_item - 1][3:] + "

" 612 | elif "判断题" in item["title"]: 613 | key5 = "answertype" + item["id"] 614 | from_date_2[key5] = 3 615 | key6 = "answer" + item["id"] 616 | if item["answer"] == "错": 617 | _answer = "false" 618 | else: 619 | _answer = "true" 620 | from_date_2[key6] = _answer 621 | elif "简答题" in item["title"]: 622 | key6 = "answertype" + item["id"] 623 | from_date_2[key6] = 4 624 | key7 = "answer" + item["id"] 625 | from_date_2[key7] = item["answer"] 626 | elif "论述题" in item["title"]: 627 | key8 = "answertype" + item["id"] 628 | from_date_2[key8] = 6 629 | key9 = "answer" + item["id"] 630 | from_date_2[key9] = item["answer"] 631 | elif "多选题" in item["title"]: 632 | key9 = "answertype" + item["id"] 633 | from_date_2[key9] = 1 634 | key10 = "answer" + item["id"] 635 | from_date_2[key10] = item["answer"] 636 | elif "编程题" in item["title"]: 637 | key11 = "answertype" + item["id"] 638 | from_date_2[key11] = 9 639 | key12 = "tiankongsize" + item["id"] 640 | from_date_2[key12] = len(item["answer"]) 641 | for answer_item in range(1, len(item["answer"]) + 1): 642 | _key = "answerEditor" + item["id"] + str(answer_item) 643 | from_date_2[_key] = "

" + self.escape_tags(item["answer"][answer_item - 1][3:]) + "

" 644 | elif "其他" in item['title']: 645 | key13 = "answertype" + item["id"] 646 | from_date_2[key13] = 8 647 | key14 = "answer" + item["id"] 648 | from_date_2[key14] = item["answer"] 649 | 650 | commit_from_1 = dict(from_date_2, **commit_from_1) 651 | return commit_from_1 652 | -------------------------------------------------------------------------------- /my_xxt/findAnswer.py: -------------------------------------------------------------------------------- 1 | # -*- coding = utf-8 -*- 2 | # @Time :2023/5/23 20:27 3 | # @Author :小岳 4 | # @Email :401208941@qq.com 5 | # @PROJECT_NAME :xxt_cli 6 | # @File : findAnswer.py 7 | import difflib 8 | 9 | 10 | def match_answer(answer_list: list, question_list: list, question_randomOption: str): 11 | """ 12 | 根据问题寻找答案 13 | :param question_randomOption: 判读作业是否是乱序的 14 | :param answer_list: 15 | :param question_list: 16 | :return: 17 | """ 18 | 19 | answer = [] 20 | for question in question_list: 21 | for item in answer_list: 22 | if question["id"] == item["id"]: 23 | if question["type"] == "单选题" or question["type"] == "多选题": 24 | # 判断是否是乱序的 25 | if question_randomOption == "false": 26 | answer_str = item["answer"] 27 | else: 28 | answer_str = find_answer(question["option"], item["option"], item["answer"], question["type"]) 29 | else: 30 | answer_str = item['answer'] 31 | answer.append({ 32 | "id": question["id"], 33 | "title": question["title"], 34 | "option": question["option"], 35 | "answer": answer_str 36 | }) 37 | return answer 38 | 39 | 40 | def find_answer(question_option: list, answer_option: list, answer, answer_type: str): 41 | """ 42 | 选择题匹配答案 43 | :param answer_type: 44 | :param question_option: 45 | :param answer_option: 46 | :param answer: 47 | :return: 48 | """ 49 | # 多选题 50 | if answer_type == "多选题": 51 | temp = [] 52 | my_answer = "" 53 | for item in answer_option: 54 | if item.split(".")[0] in answer: 55 | temp.append(item) 56 | continue 57 | # 选项相似度匹配 58 | for item in temp: 59 | index = diffOption(item, question_option) 60 | 61 | my_answer = my_answer + question_option[index].split(" ")[0] 62 | my_answer = "".join(sorted(my_answer)) 63 | elif answer_type == "单选题": 64 | temp = "" 65 | for item in answer_option: 66 | if item.split(".")[0] == answer: 67 | temp = item 68 | index = diffOption(temp, question_option) 69 | my_answer = question_option[index][0] 70 | else: 71 | my_answer = answer 72 | return my_answer 73 | 74 | 75 | def diffOption(item, options): 76 | """ 77 | 使用相似度来匹配对应的选项 78 | :param item: 79 | :param options: 80 | :return: 81 | """ 82 | sample = [] 83 | for i in options: 84 | temp = i.split(" ")[0] 85 | i = i.replace(temp, "").replace(" ", "") 86 | sample.append(difflib.SequenceMatcher(None, i, 87 | item.split(item[1])[1]).quick_ratio()) 88 | return sample.index(max(sample)) 89 | -------------------------------------------------------------------------------- /my_xxt/login.py: -------------------------------------------------------------------------------- 1 | # -*- coding = utf-8 -*- 2 | # @Time :2023/5/16 20:05 3 | # @Author :小岳 4 | # @Email :401208941@qq.com 5 | # @PROJECT_NAME :xxt_cli 6 | # @File : login.py 7 | import os 8 | import time 9 | 10 | from qrcode import QRCode 11 | from rich.console import Console, Group 12 | from rich.panel import Panel 13 | from rich.table import Table 14 | from rich.text import Text 15 | 16 | from my_xxt.api import NewXxt 17 | from my_xxt.my_tools import select_error, jsonFileToDate 18 | 19 | 20 | def login(console: Console, xxt: NewXxt) -> None: 21 | while True: 22 | choose = select_login(console) 23 | if choose == "1": 24 | ret = phone_login(console, xxt) 25 | if ret: 26 | break 27 | elif choose == "2": 28 | ret = qr_login(console, xxt) 29 | if ret: 30 | break 31 | elif choose == "3": 32 | dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 33 | path = os.path.join(dir_path, "user.json") 34 | ret = jsonFileToDate(path) 35 | user = select_users(ret, console) 36 | login_status = xxt.login(user["phone"], user["password"]) 37 | if not login_status["status"]: 38 | flag = console.input("[green]登录失败,请重新尝试!(按q退出,按任意键继续):") 39 | if flag == 'q': 40 | exit() 41 | else: 42 | info = xxt.getInfo() 43 | console.print(f"[green]登录成功,{info['name']}") 44 | break 45 | else: 46 | select_error(console) 47 | 48 | 49 | def select_login(console: Console): 50 | console.print(Panel( 51 | title="[green]登陆方式", 52 | renderable= 53 | Group( 54 | Text("1.手机号密码登录", justify="center", style="bold blue"), 55 | Text("2.扫码登录", justify="center", style="bold blue"), 56 | Text("3.查看当前所有账号(选择其中的一个登录)", justify="center", style="bold blue"), 57 | ), 58 | style="bold green", 59 | width=120, 60 | )) 61 | choose = console.input("请选择你要登录的方式:") 62 | return choose 63 | 64 | 65 | def phone_login(console: Console, xxt: NewXxt): 66 | while True: 67 | phone = console.input("[yellow]请输入手机号:") 68 | password = console.input("[yellow]请输入密码:") 69 | login_status = xxt.login(phone, password) 70 | if not login_status["status"]: 71 | flag = console.input("[green]登录失败,请重新尝试!(按q退出,按任意键继续):") 72 | if flag == 'q': 73 | exit() 74 | else: 75 | info = xxt.getInfo() 76 | console.log(f"[green]登录成功,{info['name']}") 77 | return True 78 | 79 | 80 | def qr_login(console: Console, xxt: NewXxt): 81 | xxt.qr_get() 82 | qr = QRCode() 83 | qr.add_data(xxt.qr_geturl()) 84 | qr.print_ascii(invert=True) 85 | console.log("[yellow]等待扫描") 86 | flag_scan = False 87 | while True: 88 | qr_status = xxt.login_qr() 89 | if qr_status["status"]: 90 | info = xxt.getInfo() 91 | console.log(f"[green]登录成功,{info['name']}") 92 | return True 93 | match qr_status.get("type"): 94 | case "1": 95 | console.log("[red]二维码验证错误") 96 | break 97 | case "2": 98 | console.log("[red]二维码已失效") 99 | break 100 | case "4": 101 | if not flag_scan: 102 | console.log( 103 | f"[green]二维码已扫描" 104 | ) 105 | flag_scan = True 106 | time.sleep(1.0) 107 | 108 | 109 | def select_users(users: dict, console: Console): 110 | tb = Table("序号", "账号", "密码", "姓名", border_style="blue",width=116) 111 | i = 0 112 | for user in users["users"]: 113 | tb.add_row( 114 | f"[green]{i + 1}", 115 | user["phone"], 116 | user["password"], 117 | user["name"], 118 | style="bold yellow" 119 | ) 120 | i = i + 1 121 | console.print( 122 | Panel( 123 | title="用户表", 124 | renderable=tb, 125 | style="bold green", 126 | ) 127 | ) 128 | while True: 129 | user_id = int(console.input("[yellow]请选择你要登录的账号:")) 130 | try: 131 | return users["users"][user_id - 1] 132 | except Exception as e: 133 | select_error(console) 134 | -------------------------------------------------------------------------------- /my_xxt/my_tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding = utf-8 -*- 2 | # @Time :2023/5/16 18:36 3 | # @Author :小岳 4 | # @Email :401208941@qq.com 5 | # @PROJECT_NAME :xxt_cli 6 | # @File : my_tools.py 7 | import difflib 8 | import json 9 | import os 10 | import shutil 11 | import time 12 | 13 | from rich.console import Console, Group 14 | from rich.panel import Panel 15 | from rich.table import Table 16 | from rich.text import Text 17 | 18 | from my_xxt.findAnswer import match_answer 19 | from my_xxt.api import NewXxt 20 | 21 | from config import __VERSION__ 22 | 23 | 24 | def select_error(console: Console): 25 | console.log("[green]发生了一些未知的错误!") 26 | flag = console.input("[green]请选择是否重新选择!(按q退出,按任意键继续)") 27 | if flag == 'q': 28 | exit(0) 29 | 30 | 31 | def select_course(console: Console, courses: list) -> dict: 32 | while True: 33 | index = console.input("[yellow]请输入你要打开的课程的序号:") 34 | for item in courses: 35 | if item["id"] == index: 36 | return item 37 | select_error(console) 38 | 39 | 40 | def select_work(console: Console, works: list) -> dict: 41 | if not len(works): 42 | return {} 43 | while True: 44 | index = console.input("[yellow]请输入作业答案的id:") 45 | for item in works: 46 | if item["work_id"] == index: 47 | return item 48 | select_error(console) 49 | 50 | 51 | def select_works(console: Console, works: list) -> list: 52 | works_list = [] 53 | if not len(works): 54 | return [] 55 | while True: 56 | index = console.input("[yellow]请输入作业答案的id(如果是爬取多个作业,序号按照(1,2,3)英文逗号):") 57 | index = index.replace(" ", "") 58 | index_list = index.split(",") 59 | 60 | for item in works: 61 | for choose in index_list: 62 | if item["work_id"] == choose: 63 | works_list.append(item) 64 | if len(works_list): 65 | return works_list 66 | select_error(console) 67 | 68 | 69 | def select_menu(console: Console, xxt: NewXxt) -> None: 70 | while True: 71 | show_menu(console) 72 | index = console.input("[yellow]请输入你想选择的功能:") 73 | # 查看课程 74 | if index == "1": 75 | courses = xxt.getCourse() 76 | show_course(courses, console) 77 | continue 78 | # 查看所有的答案文件 79 | elif index == "2": 80 | show_all_answer_file(console) 81 | continue 82 | # 查看左右为完成的作业 83 | elif index == "3": 84 | courses = xxt.getCourse() 85 | not_work = get_not_work(courses, xxt, console, sleep_time=2) 86 | show_not_work(not_work, console) 87 | continue 88 | # 清除所有的答案文件 89 | elif index == "4": 90 | dirpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 91 | path = os.path.join(dirpath, "answers") 92 | del_file(path) 93 | console.log(f"[green]清空完毕") 94 | continue 95 | # 爬取指定课程的指定的作业(已完成) 96 | elif index == "5": 97 | courses = xxt.getCourse() 98 | show_course(courses, console) 99 | course = select_course(console, courses) 100 | works = xxt.getWorks(course["course_url"], course["course_name"]) 101 | show_works(works, console) 102 | 103 | # 爬取答案 104 | work = select_work(console, works) 105 | if work == {}: 106 | console.print("[red]该课程下没有作业") 107 | continue 108 | if work["work_status"] == "已完成": 109 | answer_list = xxt.getAnswer(work["work_url"]) 110 | else: 111 | console.print("[red]该作业似乎没有完成") 112 | continue 113 | # 保存答案至answer文件 114 | dateToJsonFile(answer_list, work) 115 | console.log(f"[green]爬取成功,答案已经保存至{work['id']}.json") 116 | continue 117 | # 批量导出指定课程的作业 118 | elif index == "6": 119 | courses = xxt.getCourse() 120 | show_course(courses, console) 121 | course = select_course(console, courses) 122 | works = xxt.getWorks(course["course_url"], course["course_name"]) 123 | show_works(works, console) 124 | 125 | # 爬取答案 126 | works = select_works(console, works) 127 | if works == []: 128 | console.log("[red]该课程下没有作业") 129 | continue 130 | i = 0 131 | for work in works: 132 | with console.status(f"[red]正在查找《{work['work_name']}》...[{i + 1}/{len(works)}]"): 133 | i = i + 1 134 | try: 135 | if work["work_status"] == "已完成": 136 | answer_list = xxt.getAnswer(work["work_url"]) 137 | else: 138 | console.log("[red]该作业似乎没有完成") 139 | continue 140 | # 保存答案至answer文件 141 | dateToJsonFile(answer_list, work) 142 | continue 143 | except Exception as e: 144 | console.log("[red]出现了一点小意外", e) 145 | console.log("[green]爬取成功") 146 | continue 147 | # 完成指定课程的指定作业(未完成) 148 | elif index == "7": 149 | courses = xxt.getCourse() 150 | show_course(courses, console) 151 | course = select_course(console, courses) 152 | works = xxt.getWorks(course["course_url"], course["course_name"]) 153 | show_works(works, console) 154 | 155 | # 完成作业 156 | work = select_work(console, works) 157 | if work == {}: 158 | console.log("[red]该课程下没有作业") 159 | continue 160 | if work["work_status"] == "已完成" and work["isRedo"] == "no": 161 | console.log("[red]该作业已经完成了") 162 | continue 163 | else: 164 | if work["isRedo"] == "yes": 165 | choose = console.input("[yellow]该作业可以重做,请确认是否要重做:(yes/no)") 166 | if choose == "no": 167 | continue 168 | else: 169 | result = xxt.redoWork(work["work_url"]) 170 | if result["status"] == 1: 171 | console.log("[green]作业重做成功") 172 | else: 173 | console.log(f"[red]作业重做失败,错误原因[red]{result['msg']}[/red]") 174 | questions = xxt.get_question(work["work_url"]) 175 | if not is_exist_answer_file(f"{work['id']}.json"): 176 | console.log("[green]没有在答案文件中匹配到对应的答案文件") 177 | continue 178 | else: 179 | dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 180 | path = os.path.join(dir_path, "answers", f"{work['id']}.json") 181 | answer = match_answer(jsonFileToDate(path)[work["id"]], questions, xxt.randomOptions) 182 | show_answer(answer_list=answer, console=console) 183 | 184 | choose = console.input("[yellow]是否继续进行提交:(yes/no)") 185 | if not (choose == "yes"): 186 | continue 187 | ret = xxt.commit_work(answer, work) 188 | console.log(ret) 189 | works = xxt.getWorks(course["course_url"], course["course_name"]) 190 | show_works(works, console) 191 | continue 192 | # 批量完成作业 193 | elif index == "8": 194 | courses = xxt.getCourse() 195 | show_course(courses, console) 196 | course = select_course(console, courses) 197 | works = xxt.getWorks(course["course_url"], course["course_name"]) 198 | show_works(works, console) 199 | 200 | work = select_work(console, works) 201 | dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 202 | path = os.path.join(dir_path, "user.json") 203 | users = jsonFileToDate(path) 204 | show_users(users, console) 205 | 206 | users_select = select_users(users, console) 207 | i = 0 208 | success_count = 0 209 | fail_count = 0 210 | for user in users_select: 211 | i = i + 1 212 | xxt = NewXxt() 213 | login_status = xxt.login(user["phone"], user["password"]) 214 | # 判断登录成功与否 215 | if login_status["status"] == True: 216 | courses = xxt.getCourse() 217 | course = find_course(courses, work["courseId"]) 218 | works = xxt.getWorks(course["course_url"], course["course_name"]) 219 | work = find_work(works, work["work_id"]) 220 | # 判断是否存在作业或者课程 221 | if course == {} or work == {}: 222 | console.log( 223 | f"({i}) [green]{user['name']}---该用户的作业操作失败[red]该账号不存在该课程或者作业") 224 | fail_count = fail_count + 1 225 | else: 226 | # 判断作业是什么状态 227 | if work["work_status"] == "已完成": 228 | console.log(f"({i}) [green]{user['name']}---该用户的作业操作失败[red]该账号已完成该作业") 229 | fail_count = fail_count + 1 230 | continue 231 | else: 232 | questions = xxt.get_question(work["work_url"]) 233 | # 判断是否存在答案文件 234 | if not is_exist_answer_file(f"{work['id']}.json"): 235 | console.log( 236 | f"({i}) [green]{user['name']}---该用户的作业操作失败[red]没有在答案文件中匹配到对应的答案文件") 237 | fail_count = fail_count + 1 238 | continue 239 | else: 240 | dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 241 | path = os.path.join(dir_path, "answers", f"{work['id']}.json") 242 | answer = match_answer(jsonFileToDate(path)[work["id"]], questions, xxt.randomOptions) 243 | ret = xxt.commit_work(answer, work) 244 | # 作业提交成功 245 | if ret["msg"] == 'success!': 246 | works = xxt.getWorks(course["course_url"], course["course_name"]) 247 | work = find_work(works, work["work_id"]) 248 | console.log( 249 | f"({i}) [green]{user['name']}---该用户的作业操作成功[blue]最终分数为{work['score']}") 250 | success_count = success_count + 1 251 | else: 252 | console.log( 253 | f"({i}) [green]{user['name']}---该用户的作业操作失败 {ret},你可以再次尝试一次。") 254 | fail_count = fail_count + 1 255 | continue 256 | else: 257 | console.log(f"({i}) [green]{user['name']}---该用户的作业操作失败:[red]账号或者密码错误[/red][/green]") 258 | fail_count = fail_count + 1 259 | console.log(f"[yellow]一共成功{success_count},失败数为{fail_count}[/yellow]") 260 | continue 261 | # 退出登录 262 | elif index == "9": 263 | return 264 | select_error(console) 265 | 266 | 267 | def find_course(courses: list, course_id: str) -> dict: 268 | for course in courses: 269 | # url 里面有course_id 270 | if course_id in course["course_url"]: 271 | return course 272 | return {} 273 | 274 | 275 | def find_work(works: list, work_id: str) -> dict: 276 | for work in works: 277 | if work["work_id"] == work_id: 278 | return work 279 | return {} 280 | 281 | 282 | def show_start(console: Console) -> None: 283 | 284 | console.print(Panel( 285 | title="[white]欢迎使用该做题脚本", 286 | renderable= 287 | Group( 288 | Text(" ███╗ ██╗███████╗██╗ ██╗ ██╗ ██╗██╗ ██╗████████╗\n \ 289 | ████╗ ██║██╔════╝██║ ██║ ╚██╗██╔╝╚██╗██╔╝╚══██╔══╝\n \ 290 | ██╔██╗ ██║█████╗ ██║ █╗ ██║ ╚███╔╝ ╚███╔╝ ██║\n\ 291 | ██║╚██╗██║██╔══╝ ██║███╗██║ ██╔██╗ ██╔██╗ ██║\n\ 292 | ██║ ╚████║███████╗╚███╔███╔╝███████╗██╔╝ ██╗██╔╝ ██╗ ██║\n\ 293 | ╚═╝ ╚═══╝╚══════╝ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ", justify="center",style="bold white"), 294 | Text("注意:该脚本仅供学习参考,详细信息请参考https://github.com/aglorice/new_xxt", justify="center", style="bold red"), 295 | Text(f"当前版本为 {__VERSION__}", justify="center", style="bold red"), 296 | ), 297 | style="bold green", 298 | width=120, 299 | )) 300 | 301 | 302 | def show_users(users: dict, console: Console) -> None: 303 | tb = Table("序号", "账号", "密码", "姓名", border_style="blue", width=116) 304 | i = 0 305 | 306 | for user in users["users"]: 307 | tb.add_row( 308 | f"[green]{i + 1}[/green]", 309 | user["phone"], 310 | user["password"], 311 | user["name"], 312 | ) 313 | i = i + 1 314 | 315 | console.print( 316 | Panel( 317 | title="[blue]用户表[/blue]", 318 | renderable=tb, 319 | style="bold green", 320 | ) 321 | ) 322 | 323 | 324 | def show_course(courses: list, console: Console) -> None: 325 | tb = Table("序号", "课程名", "老师名", border_style="blue", width=116) 326 | for course in courses: 327 | tb.add_row( 328 | f"[green]{course['id']}[/green]", 329 | course["course_name"], 330 | course["course_teacher"], 331 | style="bold yellow" 332 | ) 333 | console.print( 334 | Panel( 335 | title="[blue]课程信息[/blue]", 336 | renderable=tb, 337 | style="bold green", 338 | ) 339 | ) 340 | 341 | 342 | def select_users(users: dict, console: Console) -> list: 343 | user_list = [] 344 | users_id = console.input("请选择你要完成此作业的账号 如(1,2,3) 英文逗号 全选请输入 all :") 345 | if users_id == "all": 346 | return users["users"] 347 | users_id = users_id.replace(" ", "") 348 | users_id_list = users_id.split(",") 349 | try: 350 | for user in users_id_list: 351 | user_list.append(users["users"][int(user) - 1]) 352 | return user_list 353 | except Exception as e: 354 | select_error(console) 355 | 356 | 357 | def show_menu(console: Console) -> None: 358 | console.print(Panel( 359 | title="[green]菜单", 360 | renderable= 361 | Group( 362 | Text("1.查看课程", justify="center", style="bold yellow"), 363 | Text("2.查看当前所有答案文件", justify="center", style="bold yellow"), 364 | Text("3.查询所有未完成的作业", justify="center", style="bold yellow"), 365 | Text("4.清除所有答案文件", justify="center", style="bold yellow"), 366 | Text("5.爬取指定作业的答案", justify="center", style="bold yellow"), 367 | Text("6.批量爬取指定课程的答案", justify="center", style="bold yellow"), 368 | Text("7.完成作业(请先确认是否已经爬取了你想要完成作业的答案)", justify="center", style="bold yellow"), 369 | Text("8.批量完成作业完成作业(请先确认是否已经爬取了你想要完成作业的答案,请填好user.json里的账号数据)", 370 | justify="center", style="bold yellow"), 371 | Text("9.退出登录", justify="center", style="bold yellow"), 372 | ), 373 | style="bold green", 374 | width=120, 375 | )) 376 | 377 | 378 | def show_works(works: list, console: Console) -> None: 379 | tb = Table("id", "作业名称", "作业状态", "分数", "是否可以重做", border_style="blue", width=116) 380 | for work in works: 381 | tb.add_row( 382 | f"[green]{work['work_id']}[/green]", 383 | work['work_name'], 384 | work["work_status"], 385 | work["score"], 386 | work["isRedo"], 387 | style="bold yellow" 388 | ) 389 | console.print( 390 | Panel( 391 | title="[blue]作业信息", 392 | renderable=tb, 393 | style="bold green", 394 | ) 395 | ) 396 | 397 | 398 | def show_answer(console: Console, answer_list: list) -> None: 399 | tb = Table("id", "题目名称", "答案", border_style="blue", width=116) 400 | for answer in answer_list: 401 | tb.add_row( 402 | f"[green]{answer['id']}[/green]", 403 | answer['title'], 404 | str(answer["answer"]), 405 | style="bold yellow" 406 | ) 407 | console.print( 408 | Panel( 409 | title="[blue]检查答案", 410 | renderable=tb, 411 | style="bold green", 412 | ) 413 | ) 414 | 415 | 416 | def dateToJsonFile(answer: list, info: dict) -> None: 417 | """ 418 | 将答案写入文件保存为json格式 419 | :param answer: 420 | :param info: 421 | :return: 422 | """ 423 | to_dict = { 424 | info["id"]: answer, 425 | "info": info 426 | } 427 | # json.dumps 序列化时对中文默认使用的ascii编码.想输出真正的中文需要指定ensure_ascii=False 428 | json_data = json.dumps(to_dict, ensure_ascii=False) 429 | path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 430 | path = os.path.join(path, "answers", f"{info['id']}.json") 431 | with open(path, 'w', encoding="utf-8") as f_: 432 | f_.write(json_data) 433 | 434 | 435 | def jsonFileToDate(file: str) -> dict: 436 | with open(file, 'r', encoding="utf-8") as f_: 437 | json_data = dict(json.loads(f_.read())) 438 | return json_data 439 | 440 | 441 | def show_all_answer_file(console: Console) -> None: 442 | answer_files = [] 443 | answer_file_info = [] 444 | dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 445 | path = os.path.join(dir_path, "answers") 446 | for root, dirs, files in os.walk(path): 447 | answer_files.append(files) 448 | for item in answer_files[0]: 449 | _path = os.path.join(path, item) 450 | answer_file_info.append(jsonFileToDate(_path)["info"]) 451 | 452 | tb = Table("id", "作业名", "文件名称", "课程名称", border_style="blue", width=116) 453 | for work_info in answer_file_info: 454 | tb.add_row( 455 | f"[green]{work_info['id']}[/green]", 456 | work_info["work_name"], 457 | work_info["id"] + ".json", 458 | work_info["course_name"], 459 | style="bold yellow" 460 | ) 461 | console.print( 462 | Panel( 463 | title="[blue]作业文件列表[/blue]", 464 | renderable=tb, 465 | style="bold green", 466 | ) 467 | ) 468 | 469 | 470 | def is_exist_answer_file(work_file_name: str) -> bool: 471 | answer_files = [] 472 | dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 473 | path = os.path.join(dir_path, "answers") 474 | for root, dirs, files in os.walk(path): 475 | answer_files.append(files) 476 | if work_file_name in answer_files[0]: 477 | return True 478 | else: 479 | return False 480 | 481 | 482 | def del_file(path_data: str): 483 | for i in os.listdir(path_data): # os.listdir(path_data)#返回一个列表,里面是当前目录下面的所有东西的相对路径 484 | file_data = path_data + "\\" + i # 当前文件夹的下面的所有东西的绝对路径 485 | if os.path.isfile(file_data) == True: # os.path.isfile判断是否为文件,如果是文件,就删除.如果是文件夹.递归给del_file. 486 | os.remove(file_data) 487 | else: 488 | del_file(file_data) 489 | 490 | 491 | def get_not_work(courses: list, xxt: NewXxt, console: Console, sleep_time: float = 1) -> list: 492 | not_work = [] 493 | for course in courses: 494 | with console.status(f"[red]正在查找《{course['course_name']}》...[{course['id']}/{len(courses)}][/red]"): 495 | time.sleep(sleep_time) 496 | try: 497 | works = xxt.getWorks(course["course_url"], course["course_name"]) 498 | for work in works: 499 | if work["work_status"] == "未交": 500 | not_work.append({ 501 | "id": work["id"], 502 | "work_name": work["work_name"], 503 | "work_status": work["work_status"], 504 | "course_name": work["course_name"] 505 | }) 506 | except Exception as e: 507 | console.log(f"[red]在查找课程[green]《{course['course_name']}》[/green]出现了一点小意外[/red]") 508 | return not_work 509 | 510 | 511 | def show_not_work(not_work: list, console: Console) -> None: 512 | tb = Table("id", "作业名", "课程名称", "作业状态", border_style="blue", width=116) 513 | for work in not_work: 514 | tb.add_row( 515 | f"[green]{work['id']}[/green]", 516 | work["work_name"], 517 | work["course_name"], 518 | f"[green]{work['work_status']}", 519 | style="bold yellow" 520 | ) 521 | 522 | console.print( 523 | Panel( 524 | title="[blue]作业文件列表[/blue]", 525 | renderable=tb, 526 | style="bold green", 527 | ) 528 | ) 529 | -------------------------------------------------------------------------------- /my_xxt/question_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding = utf-8 -*- 2 | # @Time :2023/5/23 20:34 3 | # @Author :小岳 4 | # @Email :401208941@qq.com 5 | # @PROJECT_NAME :xxt_cli 6 | # @File : question_type.py 7 | import bs4 8 | 9 | 10 | class QuestionType: 11 | @staticmethod 12 | def multipleChoice(item: bs4.element.Tag) -> dict: 13 | option = [] 14 | title = item.find("h3", attrs={"class": "mark_name colorDeep fontLabel"}).text 15 | option_list = item.find_all("div", attrs={"class": "clearfix answerBg"}) 16 | for _option in option_list: 17 | _option = _option["aria-label"] 18 | option.append(_option.replace("选择", "")) 19 | question = { 20 | "id": item.attrs['data'], 21 | "title": my_replace(title), 22 | "type": "单选题", 23 | "option": option, 24 | "answer": None 25 | } 26 | return question 27 | 28 | @staticmethod 29 | def multipleChoices(item: bs4.element.Tag) -> dict: 30 | option = [] 31 | title = item.find("h3", attrs={"class": "mark_name colorDeep fontLabel"}).text 32 | option_list = item.find_all("div", attrs={"class": "clearfix answerBg"}) 33 | for _option in option_list: 34 | _option = _option["aria-label"] 35 | option.append(_option.replace("选择", "")) 36 | question = { 37 | "id": item.attrs['data'], 38 | "title": my_replace(title), 39 | "type": "多选题", 40 | "option": option, 41 | "answer": None 42 | } 43 | return question 44 | 45 | @staticmethod 46 | def judgeChoice(item: bs4.element.Tag) -> dict: 47 | title = item.find("h3", attrs={"class": "mark_name colorDeep fontLabel"}).text 48 | question_answer = { 49 | "id": item.attrs['data'], 50 | "title": my_replace(title), 51 | "type": "判断题", 52 | "answer": None, 53 | "option": None 54 | } 55 | return question_answer 56 | 57 | @staticmethod 58 | def comprehensive(item: bs4.element.Tag) -> dict: 59 | title = item.find("h3", attrs={"class": "mark_name colorDeep fontLabel"}).text 60 | question_answer = { 61 | "id": item.attrs['data'], 62 | "title": my_replace(title), 63 | "type": "填空题", 64 | "answer": None, 65 | "option": None 66 | } 67 | return question_answer 68 | 69 | @staticmethod 70 | def shortAnswer(item: bs4.element.Tag) -> dict: 71 | title = item.find("h3", attrs={"class": "mark_name colorDeep fontLabel"}).text 72 | question_answer = { 73 | "id": item.attrs['data'], 74 | "title": my_replace(title), 75 | "type": "简答题", 76 | "answer": None, 77 | "option": None 78 | } 79 | return question_answer 80 | 81 | @staticmethod 82 | def essayQuestion(item: bs4.element.Tag): 83 | title = item.find("h3", attrs={"class": "mark_name colorDeep fontLabel"}).text 84 | question_answer = { 85 | "id": item.attrs['data'], 86 | "title": my_replace(title), 87 | "type": "论述题", 88 | "answer": None, 89 | "option": None 90 | } 91 | return question_answer 92 | 93 | @staticmethod 94 | def programme(item: bs4.element.Tag): 95 | title = item.find("h3", attrs={"class": "mark_name colorDeep fontLabel"}).text 96 | question_answer = { 97 | "id": item.attrs['data'], 98 | "title": my_replace(title), 99 | "type": "编程题", 100 | "answer": None, 101 | "option": None 102 | } 103 | return question_answer 104 | 105 | @staticmethod 106 | def other(item: bs4.element.Tag): 107 | title = item.find("h3", attrs={"class": "mark_name colorDeep fontLabel"}).text 108 | question_answer = { 109 | "id": item.attrs['data'], 110 | "title": my_replace(title), 111 | "type": "其他", 112 | "answer": None, 113 | "option": None 114 | } 115 | return question_answer 116 | 117 | @staticmethod 118 | def error(item: bs4.element.Tag): 119 | print("该题型暂不支持") 120 | 121 | 122 | def my_replace(_string: str): 123 | if _string is None: 124 | return Exception 125 | return _string.replace("\xa0", ' ').replace("\n", "").replace(" ", "").replace("\t", "").replace(" ", "").replace( 126 | "\r", "") 127 | 128 | 129 | def getAnswer(item) -> str: 130 | try: 131 | answer = item.find_all_next("span", attrs={"class": "colorGreen marginRight40 fl"})[0].text.replace( 132 | "正确答案: ", "") 133 | answer = my_replace(answer) 134 | except Exception as e: 135 | answer = item.find_all_next("span", attrs={"class": "colorDeep marginRight40 fl"})[0].text.replace("我的答案: ", 136 | "") 137 | answer = my_replace(answer) 138 | return answer 139 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aglorice/new_xxt/771d0c66f53b90d422afc0a8595dff5da53299fd/requirements.txt -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | 3 | console = Console(record=True) # 设置 record=True 4 | 5 | console.print("Hello, World!") 6 | 7 | captured_output_value = console.export_text() 8 | print("Captured Output:", captured_output_value) 9 | -------------------------------------------------------------------------------- /upload.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from config import __VERSION__ 5 | 6 | """ 7 | 修改README.md文件中的版本号 8 | """ 9 | 10 | 11 | def update_version(): 12 | with open("README.md", "r", encoding="utf-8") as f: 13 | content = f.read() 14 | content = re.sub(r"v\d+\.\d+\.\d+", f"{__VERSION__}", content) 15 | with open("README.md", "w", encoding="utf-8") as f: 16 | f.write(content) 17 | print("1.修改README.md文件中的版本号成功!") 18 | 19 | 20 | """ 21 | 提交新修改的README.md文件并推送到github 22 | """ 23 | 24 | 25 | def git_push_readme(): 26 | os.system("git add README.md") 27 | os.system("git commit -m 'update version'") 28 | os.system("git push origin red") 29 | print("2.提交新修改的README.md文件并推送到github成功!") 30 | 31 | 32 | """ 33 | 自动生成创建新git tag标签,并上穿到github 34 | """ 35 | 36 | 37 | def git_tag(): 38 | os.system("git tag -a {} -m '{}'".format(__VERSION__, __VERSION__)) 39 | os.system("git push origin {}".format(__VERSION__)) 40 | print("3.自动生成创建新git tag标签,并上穿到github成功!") 41 | 42 | 43 | if __name__ == '__main__': 44 | update_version() 45 | git_push_readme() 46 | git_tag() 47 | -------------------------------------------------------------------------------- /user.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "phone":"你的手机号", 5 | "password": "密码", 6 | "name": "test1" 7 | } 8 | ] 9 | } --------------------------------------------------------------------------------