Skip to content

Commit

Permalink
Merge pull request #2 from ruchuby/xxy
Browse files Browse the repository at this point in the history
CourseHelper 0.1.0
  • Loading branch information
AkiChase authored May 22, 2022
2 parents 084ebd0 + 17c93da commit f5339ac
Show file tree
Hide file tree
Showing 35 changed files with 2,694 additions and 178 deletions.
163 changes: 135 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Course Helper

一个Electron+Vue3作为前端,Python作为本地后端的桌面端软件。

[TOC]

## 需求分析
Expand All @@ -12,99 +14,204 @@ course平台已有十年的使用历史,由于缺少更新、维护,使用



## 软件功能
## 软件功能与特点

### 1. 快捷登录 ✔️

### 1. 快捷登录
用户在保存账号密码到本地后,启动即可快速登录 Course 网站。无需输入密码、拖动滑块验证码

用户在保存账号密码到本地后,启动即可快速登录course网站。无需输入密码、拖动滑块验证码。
### 2. UI界面 ✔️

### 2. 全新界面
~~功能可以差点,UI必须好看~~

提取关键元素、重构页面。(UI好才是真滴好~)
### 3. 课程列表查看 ✔️

### 3. 课件批量下载
可以查看课程列表与课程基本信息

进入某课程后,可以勾选需要下载的文件,批量下载
### 4. 课件批量下载 ✔️

### 4. 作业查看
进入某课程后,可以勾选需要下载的课程资源文件,批量下载

爬取课程的作业信息,显示DDL等信息
### 5. 作业查看 ✔️

### 5. 作业提交
爬取课程的作业列表,作业详情,简单高效地查看作业内容

富文本编辑框,绕过Flash限制。
### 6. 作业提交 🛠️

使用[wangEditor](https://www.wangeditor.com/)富文本编辑器进行作业内容编辑与提交



## 难点分析与解决

### 前后端通信 ⭐⭐⭐⭐
### 前后端通信 ⭐⭐⭐⭐

通信方式的选择:最早打算使用RPC等通信,但是问题很多,最后还是决定主体使用本地HTTP通信(fastapi)

虽然HTTP通信速度上不如RPC通信,但是用于本地HTTP通信速度差异可以忽略不计。
虽然HTTP通信速度上不如RPC通信,但是用于本地HTTP通信,小小的速度差异还是可以接受的。



此外,本来不想使用其他通信方式的,但是碰到了技术上的难点。

本来不想使用其他通信方式的,但是碰到了技术上的难点
某些功能需要双向通信,(后端能够主动向前端发送请求),不得不额外使用了WebSocket

为了双向通信,让后端能够主动向前端发送请求,不得不额外使用了WebSocket
然后在使用WebSocket时又出现了新的问题,因为**WS通信不像HTTP能有每个请求的回复**,需要进一步处理

然后在使用WebSocket时又出现了新的问题,因为WS通信不像HTTP能有每个请求的回复,需要进一步处理
最后通过**添加消息id**判断出每个消息所属的请求,并且使用`asyncio.Future`**等待消息回复**,成功拿到回复

最后通过添加消息id判断出每个消息所属的请求,并且使用`asyncio.Future`来等待消息回复,成功拿到回复
后续可以进一步添加**超时时间**,但是目前对超时判断的需求不大



### 登录 ⭐⭐⭐⭐⭐

存在诸多阻碍,统一身份认证和VPN验证,网站频繁的重定向,对爬虫非常非常不友好
存在诸多阻碍,统一身份认证和VPN验证,网站频繁的重定向,对爬虫非常非常不友好。

本来打算用`selenium``pyppeteer`蒙混过关。

但万幸,~~某智教育公司、某瑞达公司~~没把纯`requests`的路给堵死

**重难点:**

1. vpn滑块验证码

获取滑块验证码的图片,PIL解析滑块图片,post通过



2. 统一身份认证的请求加密

key藏在页面源码内里,简单找一找就行,但是用于加密的js代码比较麻烦。

js源码可以取到,但是不方便直接用python调用(考虑到用户的电脑不一定装了nodejs)

所以只能用最稳妥的前后端通信的方式,让前端把加密代码执行后返回给后端

~~感觉多此一举,但是谁让这是Python的大作业,Electron前端只负责展示数据~~



2. 登录状态的维护

使用同一个request.Session进行请求,维持前后的cookie等缓存

并且再每次请求前检查登录状态,及时重新登录



理论上虽然能保证登录状态,但是<u>偶尔Session对course网站突然无响应的情况依然存在</u>,

暂时没做更进一步的登录维护,如果**出现无法连接的解决方案**

1. 退出登录,重新登录(会重置Session)
2. 重启后端
3. 重启前后端



### UI设计与实现 ⭐⭐⭐⭐⭐

第一次使用 `Electron + Vue3`,不得不说这俩虽然开发起来很简单,但是真的会遇到**非常多问题**

**Electron 真的很多问题**

按时间顺序列举一下**从迈出第一步****比较流畅地开发**的这段历程:

1. 解决electron环境配置问题
2. electron的不同进程通信,简单入门
3. Vue2的学习
4. Vue2迁移到Vue3的学习
5. electron使用vue的配置
6. 简单使用electron+vue3
7. 各种组件通信
8. Vuex,Vue Router的学习和使用
9. UI组件库使用(Native UI)
10. 解决electron打包的各种问题



### 信息的爬取⭐⭐⭐

course使用**jsp构建的动态网页**,使用`正则 + lxml`提取需要的信息

总有个别页面的信息提取格外**繁琐**

比如课程资源的**文件树**(需要递归)、**作业详情**(藏在input内的纯html)



### 文件下载⭐⭐⭐⭐

因为course网站的下载速度还是非常快的,所以简单的使用了Session去请求文件然后写入磁盘。

但是,除了直接请求下载,其他都是问题。

#### 1. 下载文件名中文乱码

研究了很久才把请求头中的乱码中文重新解析为正常显示的中文



#### 2. 下载流程

**开启下载流程:**

1. 前端向后端发起请求:附加course资源文件的file_id,res_id,文件保存路径
2. 后端检验文件夹是否存在
3. 后端获取下载响应头
4. 后端解析文件名、大小信息
5. 若存在同名文件则为其添加名称后缀
6. 异步进行下载,并在下载过程中发送ws消息更新下载进度 (不阻塞下一步返回下载文件信息)
7. 返回下载文件的信息(文件保存路径,文件大小,下载请求id)



**更新下载进度流程:**

1. 计算下载进度

2. WS通信

3. vuex修改数据

4. 成功更新下载进度



### 信息的爬取⭐⭐
流程看上去问题不大,但是一写就出问题。

course使用jsp构建的动态网页,使用正则+lxml提取需要的信息
小问题略去不提,就记录一下最主要的问题:**线程占用问题**

(以下是对问题的个人理解)

因为下载文件是使用for循环迭代响应体,设置了每次迭代的大小为1B,所以存在一个很高的**线程占用率**。而python的WebSocket库使用异步,内部可能还套了未等待协同的异步,导致每次虽然执行了 `await send_text(xxx)`,但是消息都**堆积着没发送**,直到下载完毕才一股脑发出去。

### 部分页面的元素不方便提取后重构 ⭐⭐⭐
**解决方案**:在迭代满足更新条件,发送消息后,加上 `await asyncio.sleep(0.01)`,避免高占用阻塞消息发送。

使用原始网页,但使用js修改页面内容


#### 3. 目录结构

### 作业提交Flash问题 ⭐⭐⭐
因为course网站的课程资源使用文件树状结构,软件需要提供下载**保留目录结构**的可选功能。

Electron 使用 Pepper Flash 插件,从而启用Flash
**解决方案****递归算法**计算每个要下载文件在文件树中的路径,从而生成相应的目录结构。



### 配置文件
### 软件打包 ⭐

仅有少量数据,无需数据库,使用YAML配置文件,方便数组等结构读写。配置文件用于前端
Python,Electron的打包,那是大哥不说二哥。1.5⭐是Electron的



## 技术栈

### python(后端)
### Python(后端)

开发快捷,且期末大作业要求。

### electron+vue3(前端)
### Electron+Vue3(前端)

开发快速,UI美观,虽然体积偏大,但具有浏览器性质,方便本项目的一些操作。

Expand Down
5 changes: 4 additions & 1 deletion app/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!-- <script src="http://localhost:8098"></script>-->
<title><%= htmlWebpackPlugin.options.title %></title>
<style>
html, body, #app {
height: 100%;
}

::-webkit-scrollbar {
display: none;
}
</style>
</head>
<body>
Expand Down
77 changes: 58 additions & 19 deletions app/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,80 @@
<div class="container">
<TopBar class="top-bar"/>
<NavigationBar class="nav-bar"/>
<div class="viewer">
<n-message-provider>
<router-view/>
</n-message-provider>
</div>
<n-message-provider>
<n-loading-bar-provider>
<n-dialog-provider>
<div class="viewer">
<router-view v-slot="{ Component }">
<transition name="fade">
<keep-alive :include="keepAlive">
<component :is="Component"/>
</keep-alive>
</transition>
</router-view>
</div>
</n-dialog-provider>
</n-loading-bar-provider>
</n-message-provider>
</div>
</n-config-provider>

</template>

<script>
import TopBar from "@/components/TopBar";
import NavigationBar from "@/components/NavigationBar";
import {NGrid, NGi, darkTheme, NConfigProvider, NMessageProvider} from "naive-ui";
import {computed, onMounted} from "vue";
import wsHelper from "@/utils/wsHelper";
import {darkTheme, NConfigProvider, NDialogProvider, NGi, NGrid, NLoadingBarProvider, NMessageProvider} from "naive-ui";
import {computed, watch} from "vue";
import {useStore} from "vuex";
export default {
name: "Header",
components: {
NavigationBar,
TopBar,
NGrid, NGi, NConfigProvider, NMessageProvider
NGrid, NGi, NConfigProvider, NMessageProvider, NLoadingBarProvider, NDialogProvider,
},
setup() {
const store = useStore()
const theme = computed(() => store.state.themeValue === 'darkTheme' ? darkTheme : null)
onMounted(() => {
window.$ws.connect()
window.$ws.injectCallback(
() => store.commit('SET_CONNECT_STATE', {state: true}),
() => store.commit('SET_CONNECT_STATE', {state: false})
)
wsHelper.connect()
wsHelper.injectCallback(
() => store.commit('SET_CONNECT_STATE', {state: true}),
() => store.commit('SET_CONNECT_STATE', {state: false})
)
store.commit('SET_DOWNLOAD_RECORDS', {
data: window.$electron.store.get('downloadRecords', [])
})
window.onbeforeunload = (e) => {
window.$electron.store.set('downloadRecords', JSON.parse(JSON.stringify((store.state.downloadRecords))))
console.log('下载记录已保存')
}
watch(
() => store.state.loginState,
(newState, preState) => {
if (newState === false && preState === true) {
console.log('登出')
window.$routerPush({name: 'login'})
}
}
)
return {
theme
theme,
keepAlive: ['Home', 'Login', 'Course', 'HomeworkDetails', 'Download']
}
}
}
</script>


<style scoped>
::-webkit-scrollbar {
display: none;
}
.container {
display: grid;
Expand Down Expand Up @@ -81,5 +108,17 @@ export default {
grid-row-end: 3;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>

<style>
.no-select {
user-select: none;
}
</style>
Loading

0 comments on commit f5339ac

Please sign in to comment.