├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
13 |
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 | 
11 | 
12 | 
13 | 
14 | 
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 | 
135 | #### 选择功能
136 | 
137 | #### 批量完成作业
138 | 
139 |
140 | ## 4️⃣注意事项
141 | - 仓库发布的`new_xxt`项目中涉及的任何脚本,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。
142 | - 本项目遵循MIT License协议,如果本特别声明与MIT License协议有冲突之处,以本特别声明为准。
143 | - 以任何方式查看此项目的人或直接或间接使用`new_xxt`项目的任何脚本的使用者都应仔细阅读此声明。`aglorice` 保留随时更改或补充此免责声明的权利。一旦使用并复制了任何相关脚本或`new_xxt`项目,则视为您已接受此免责声明。
144 |
145 |
146 | ### 如果你觉得不错的话,就请我吃颗糖吧。😁😁😁
147 |
148 |
149 | ### Star History
150 |
151 | [](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 | }
--------------------------------------------------------------------------------