├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── index.md
├── 快速开始
│ ├── index.md
│ └── school_args.md
└── 接口方法
│ ├── get_info.md
│ ├── get_schedule.md
│ ├── get_score.md
│ ├── index.md
│ ├── others.md
│ └── user_login.md
├── examples
├── base_sample.py
├── captcha_examples.py
├── config.py
├── cookie_login_example.py
└── dev_example.py
├── mkdocs.yml
├── requirement.txt
├── school_sdk
├── PyRsa
│ ├── __init__.py
│ ├── pyb64.py
│ ├── pyjsbn.py
│ ├── pyrng.py
│ ├── pyrsa.py
│ └── tools.py
├── __init__.py
├── check_code
│ ├── __init__.py
│ ├── captcha_setting.py
│ ├── dataset.py
│ ├── model.pkl
│ ├── model.py
│ ├── predict.py
│ └── type.py
├── client
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── check.py
│ │ ├── class_schedule.py
│ │ ├── login.py
│ │ ├── schedule_parse.py
│ │ ├── schedules.py
│ │ ├── score.py
│ │ └── user_info.py
│ ├── base.py
│ ├── exceptions.py
│ └── utils.py
├── config.py
├── session
│ └── __init__.py
├── type.py
└── utils.py
├── setup.py
├── test
└── test.py
└── zf-setup.py
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on:
3 | push:
4 | branches:
5 | - master
6 | - main
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: actions/setup-python@v2
13 | with:
14 | python-version: 3.x
15 | - run: pip install mkdocs-material
16 | - run: mkdocs gh-deploy --force
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | env
2 | __pyca*
3 | .pytest_cache/
4 | test/*
5 | *.json
6 | test.py
7 | *.sh
8 | *.pyc
9 | .idea
10 | conf.py
11 | school_sdk.egg-info
12 | build
13 | dist
14 | zf_school_sdk.egg-info
15 | .env
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 dairoot
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 新版正方系统 Python SDK。(支持自动识别、处理滑块验证码与常规验证码,如果觉得还不错,给个小星星趴~⭐)
2 |
3 | > 这是全网唯一一个自动处理验证码的新版教务系统 SDK :p
4 |
5 |
10 |
11 |
12 |
13 | [](https://pepy.tech/project/school-sdk)
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | [](https://pypi.python.org/pypi/school-sdk)
27 |
28 |
29 |
30 | [在线文档](https://farmerChillax.github.io/new-school-sdk/)
31 |
32 | [Roadmap](https://github.com/FarmerChillax/new-school-sdk/milestone/1)
33 |
34 | ## 支持的登录/验证码方式
35 | - 帐号密码登录
36 | - 滑块验证码登录
37 | - 图形验证码登录
38 | - cookie 登录(用于扫码登录、OIDC 等场景)
39 |
40 |
41 | ## 测试环境
42 | - Python == 3.8
43 | - 默认验证码识别方式: CPU
44 |
45 | ## Usage
46 | ```Shell
47 | $ pip install school-sdk
48 | # or
49 | $ pip install zf-school-sdk
50 | ```
51 |
52 | > 如果机器内存不足,可以使用 pip `--no-cache-dir` 选项来安装
53 | > e.g: `pip --no-cache-dir install school-sdk`
54 |
55 | ```Python
56 | from school_sdk import SchoolClient
57 | from school_sdk.client import UserClient
58 |
59 | # 先实例化一个学校,再实例化用户
60 | school = SchoolClient("172.16.254.1")
61 | user:UserClient = school.user_login("2018xxxxx", "xxxxxxxx")
62 |
63 | # 获取 2020 学年第二学期的课程
64 | course = user.get_schedule(year=2020, term=2)
65 | print(course)
66 | ```
67 |
68 | 使用示例参见 [examples](examples/)
69 |
70 | ## Api Function
71 |
72 | | Api | Description | Argument |
73 | | :------------ | :-------------------------- | :---------------- |
74 | | user_login | 登陆函数 | account, password |
75 | | get_schedule | 课表查询 | year, term |
76 | | get_score | 成绩查询 | year, term |
77 | | get_info | 获取个人信息 | None |
78 | | refresh_info | 刷新个人信息 | None |
79 | | check_session | 检查session并其失效后重登录 | None |
80 |
81 |
82 |
83 | ## School-SDK Options
84 |
85 | | Option | Default | Description |
86 | | :------------ | :----------- | :----------------------- |
87 | | host | 不存在默认值 | 教务系统地址(`必填`) |
88 | | port | 80 | 端口号 |
89 | | ssl | False | 教务系统是否使用https |
90 | | name | None | 学校名称 |
91 | | exist_verify | False | 是否存在验证码 |
92 | | captcha_type | captcha | 验证码类型,枚举类型(kaptcha: 常规 或 captcha: 滑块) |
93 | | retry | 10 | 登录重试次数 |
94 | | lan_host | None | 内网地址(暂不可用) |
95 | | lan_port | 80 | 内网地址端口(暂不可用) |
96 | | timeout | 10 | 全局请求延时 |
97 | | url_endpoints | None | 地址配置 |
98 |
99 | ## 相关项目
100 | > 帮教务系统做负载均衡:https://github.com/FarmerChillax/school-load-balance
101 | >
102 | > (如果你们学校教务系统抢课经常崩溃,可以考虑看看这个 repo)
103 |
104 |
105 | - 新版正方教务系统: https://github.com/FarmerChillax/new-school-sdk
106 | - 旧版正方教务系统: https://github.com/dairoot/school-api
107 | - SDK的Flask扩展: https://github.com/FarmerChillax/flask-school
108 | - 验证码识别: https://github.com/FarmerChillax/new-zfxfzb-code
109 |
110 |
114 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # New-School-SDK
2 |
3 | ---
4 |
5 | 新版正方系统爬虫--Python SDK
6 |
7 |
8 | [](https://pypi.org/project/school-sdk/)
9 | [](https://pepy.tech/project/school-sdk)
10 |
11 |
12 | new-school-sdk 是一个新版正方系统接口的第三方 Python SDK, 实现了用户成绩查询、课表查询以及用户信息查询。
13 |
14 |
15 | ## 安装模块
16 | ```Shell
17 | $ pip install school-sdk
18 | # or
19 | $ pip install zf-school-sdk
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/快速开始/index.md:
--------------------------------------------------------------------------------
1 | # 简单示例
2 |
3 | ## 使用示例
4 |
5 | ```py
6 | from school_sdk.client import UserClient
7 | from school_sdk import SchoolClient
8 |
9 | # 实例化学校
10 | Gdust = SchoolClient("172.16.254.1")
11 |
12 | # 实例化用户
13 | user:UserClient = Gdust.user_login("account", "password")
14 | ```
15 |
16 | ## 获取【个人】课表
17 |
18 | ```py
19 | # 获取课表
20 | course = user.get_schedule(year=2021, term=1)
21 | print(course)
22 | ```
23 |
24 | ## 获取成绩
25 | ```py
26 | # 获取成绩, 2020-2021学年第一学期的成绩
27 | score = user.get_score(year=2020, term=1)
28 | print(score)
29 | ```
30 |
31 | ## 获取个人信息
32 | ```py
33 | # 获取个人信息
34 | info = user.get_info()
35 | print(info)
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/快速开始/school_args.md:
--------------------------------------------------------------------------------
1 | # 学校参数
2 |
3 | | Option | Default | Description |
4 | | :------------ | :----------- | :----------------------- |
5 | | host | 不存在默认值 | 教务系统地址(`必填`) |
6 | | port | 80 | 端口号 |
7 | | ssl | False | 教务系统是否使用https |
8 | | name | None | 学校名称 |
9 | | exist_verify | False | 是否存在验证码 |
10 | | captcha_type | captcha | 验证码类型(常规 或 滑块) |
11 | | retry | 10 | 登录重试次数 |
12 | | lan_host | None | 内网地址 |
13 | | lan_port | 80 | 内网地址端口 |
14 | | timeout | 10 | 全局请求延时 |
15 | | url_endpoints | None | 地址配置 |
16 |
17 | ## 示例
18 |
19 | ### 使用验证码
20 | ```python
21 | from school_sdk.client import UserClient
22 | from school_sdk import SchoolClient
23 |
24 | # 实例化学校
25 | # 并根据验证码类型指定captcha_type为kap或者cap
26 | # 使用Kaptcha(与旧版系统类似的验证码)
27 | Gdust = SchoolClient("172.16.254.1", exist_verify=True, captcha_type="kaptcha")
28 | # 使用captcha(滑块验证码)
29 | Gdust = SchoolClient("172.16.254.1", exist_verify=True, captcha_type="captcha")
30 | ```
31 |
32 | ### 使用自定义路径
33 | ```python
34 | from school_sdk.client import UserClient
35 | from school_sdk import SchoolClient
36 |
37 | # 通过抓包填写以下路径,如有迷惑或错误烦请提issue并提供地址测试
38 | url_endpoints = {
39 | 'HOME_URL': "/xtgl/login_slogin.html", # 首页url
40 | 'LOGIN': {
41 | # 该模块表示登录使用到的端点
42 | 'INDEX': '/xtgl/login_slogin.html', # 首页,一般和上面保持一致
43 | 'CAPTCHA': '/zfcaptchaLogin', # 验证码url,貌似都一样
44 | 'PUBLIC_KEY': '/xtgl/login_getPublicKey.html', # RSA密钥端点
45 | },
46 | "SCORE_URL": "", # 未使用
47 | "INFO_URL": "", # 未使用
48 | # 课表页面的api
49 | "SCHEDULE": {
50 | "API": '/kbcx/xskbcx_cxXsKb.html',
51 | },
52 | # 成绩页面的api
53 | 'SCORE': {
54 | 'API': '/cjcx/cjcx_cxDgXscj.html'
55 | }
56 | }
57 |
58 | # 使用自定义的endpoints,实例化学校
59 | # exist_verify: 是否有验证码
60 | Gdust = SchoolClient("172.16.254.1", exist_verify=True, url_endpoints=url_endpoints)
61 | ```
62 |
63 |
--------------------------------------------------------------------------------
/docs/接口方法/get_info.md:
--------------------------------------------------------------------------------
1 | # 用户信息接口
--------------------------------------------------------------------------------
/docs/接口方法/get_schedule.md:
--------------------------------------------------------------------------------
1 | # 课表接口
2 |
3 | | 字段 | 默认值 | 类型 | 描述 |
4 | | ---- | ------ | ---- | ---------------------- |
5 | | year | None | int | 查询学年 |
6 | | term | 1 | int | 查询学期,默认第一学期 |
7 |
8 | ## 示例
9 | ```py
10 | from school_sdk.client import UserClient
11 | from school_sdk import SchoolClient
12 |
13 | # 实例化学校
14 | Gdust = SchoolClient("172.16.254.1")
15 |
16 | user:UserClient = Gdust.user_login("account", "password")
17 |
18 | # 获取课表
19 | course = user_1.get_schedule(year=2020, term=1)
20 | print(course)
21 | ```
22 |
23 |
--------------------------------------------------------------------------------
/docs/接口方法/get_score.md:
--------------------------------------------------------------------------------
1 | # 成绩接口
2 |
3 | | 字段 | 默认值 | 类型 | 描述 |
4 | | ---- | ------ | ---- | ---------------------- |
5 | | year | None | int | 查询学年 |
6 | | term | 1 | int | 查询学期,默认第一学期 |
7 |
8 |
9 | ## 示例
10 | ```py
11 | from school_sdk.client import UserClient
12 | from school_sdk import SchoolClient
13 |
14 | # 实例化学校
15 | Gdust = SchoolClient("172.16.254.1")
16 |
17 | user:UserClient = Gdust.user_login("account", "password")
18 |
19 | # 获取成绩, 2020-2021学年第一学期的成绩
20 | score = user.get_score(year=2020, term=1)
21 | print(score)
22 | ```
23 |
24 | ## 响应结果
25 | ```json
26 | {
27 | "形势与政策Ⅴ": {
28 | "course_name": "形势与政策Ⅴ",
29 | "course_nature": "公共必修课",
30 | "course_target": "主修",
31 | "teacher": "徐丹丹",
32 | "exam_method": "考查",
33 | "exam_nature": "正常考试",
34 | "exam_result": "81",
35 | "credit": "0.3",
36 | "course_group": "马克思主义学院",
37 | "grade": "2018",
38 | "grade_point": "3.10"
39 | },
40 | "统一建模语言": {
41 | "course_name": "统一建模语言",
42 | "course_nature": "任选课",
43 | "course_target": "主修",
44 | "teacher": "侯爱民",
45 | "exam_method": "考查",
46 | "exam_nature": "正常考试",
47 | "exam_result": "89",
48 | "credit": "2.0",
49 | "course_group": "计算机学院",
50 | "grade": "2018",
51 | "grade_point": "3.90"
52 | },
53 | "Java Web课程设计": {
54 | "course_name": "Java Web课程设计",
55 | "course_nature": "专项实践课",
56 | "course_target": "主修",
57 | "teacher": "李玉坤",
58 | "exam_method": "考查",
59 | "exam_nature": "正常考试",
60 | "exam_result": "91",
61 | "credit": "2.0",
62 | "course_group": "计算机学院",
63 | "grade": "2018",
64 | "grade_point": "4.10"
65 | },
66 | "JavaScript课程设计": {
67 | "course_name": "JavaScript课程设计",
68 | "course_nature": "专项实践课",
69 | "course_target": "主修",
70 | "teacher": "王荣福",
71 | "exam_method": "考查",
72 | "exam_nature": "正常考试",
73 | "exam_result": "92",
74 | "credit": "2.0",
75 | "course_group": "计算机学院",
76 | "grade": "2018",
77 | "grade_point": "4.20"
78 | }
79 | }
80 | ```
81 |
82 | ### 结果分析
83 | ```json
84 | "<课程名>": {
85 | "course_name": "JavaScript课程设计", // 课程名
86 | "course_nature": "专项实践课", // 课程属性
87 | "course_target": "主修", // 课程性质
88 | "teacher": "王荣福", // 授课老师
89 | "exam_method": "考查", // 考核方式
90 | "exam_nature": "正常考试",
91 | "exam_result": "92", // 考试成绩
92 | "credit": "2.0", // 学分
93 | "course_group": "计算机学院", // 课程归属学院
94 | "grade": "2018", // 学生年级
95 | "grade_point": "4.20" // 绩点
96 | }
97 | ```
--------------------------------------------------------------------------------
/docs/接口方法/index.md:
--------------------------------------------------------------------------------
1 | # 概览
2 |
3 |
4 | | Api | Description | Argument |
5 | | :----------------------------------------- | :-------------------------------------------------------------- | :---------------- |
6 | | [user_login](./user_login.md) | 登陆函数 | account, password |
7 | | [user_login_with_cookies](./user_login.md) | cookie 登陆函数 | cookies, account |
8 | | [init_dev_user](./user_login.md) | 开发测试函数,主要用于开发者调试使用,传入 cookie,无需频繁登录 | cookies |
9 | | [get_schedule](./get_schedule.md) | 课表查询 | year, term |
10 | | [get_score](./get_score.md) | 成绩查询 | year, term |
11 | | [get_info](./get_info.md) | 获取个人信息 | None |
12 | | [refresh_info](./others.md) | 刷新个人信息 | None |
13 | | [check_session](./others.md) | 检查session并其失效后重登录 | None |
14 |
15 |
--------------------------------------------------------------------------------
/docs/接口方法/others.md:
--------------------------------------------------------------------------------
1 | # 其他接口
--------------------------------------------------------------------------------
/docs/接口方法/user_login.md:
--------------------------------------------------------------------------------
1 | # 用户登录
2 |
3 | | 字段 | 默认值 | 类型 | 描述 |
4 | | -------- | ---- | ------ | ---- |
5 | | account | None | String | 用户账号 |
6 | | password | None | String | 用户密码 |
7 |
8 | ## 示例
9 | ```python
10 | from school_sdk.client import UserClient
11 | from school_sdk import SchoolClient
12 |
13 | # 实例化学校
14 | Gdust = SchoolClient("172.16.254.1")
15 |
16 | # 实例化用户
17 | user:UserClient = Gdust.user_login("account", "password")
18 | ```
19 |
20 | ## 图形验证码登录
21 |
22 | 如果需要以「图形验证码」的方式登录,则需要在初始化 SchoolClient 时设置 exist_verify 参数的值为 true (开启验证码登录)
23 | 与 captcha_type 参数的值设为 kaptcha (登录验证码类型为图形验证码)
24 |
25 | ```python
26 | from school_sdk.client import UserClient
27 | from school_sdk import SchoolClient
28 |
29 | # 实例化学校
30 | # 使用 kaptcha (与旧版系统类似的验证码)
31 | Gdust = SchoolClient("172.16.254.1", exist_verify=True, captcha_type="kaptcha")
32 |
33 | # 实例化用户
34 | user:UserClient = Gdust.user_login("account", "password")
35 |
36 | # 获取个人信息
37 | info = user.get_info()
38 | print(info)
39 | ```
40 |
41 | ## 滑块验证码登录
42 |
43 | 与「图形验证码」的差异点在于“初始化 SchoolClient 时 captcha_type 参数的值为 captcha”
44 |
45 | ```python
46 | from school_sdk.client import UserClient
47 | from school_sdk import SchoolClient
48 |
49 | # 实例化学校
50 | # 使用captcha(滑块验证码)
51 | Gdust = SchoolClient("172.16.254.1", exist_verify=True, captcha_type="captcha")
52 |
53 | # 实例化用户
54 | user:UserClient = Gdust.user_login("account", "password")
55 |
56 | # 获取个人信息
57 | info = user.get_info()
58 | print(info)
59 | ```
60 |
61 |
62 | ## 其他登录方式
63 | 更多登录 demo 详见仓库 [examples](https://github.com/FarmerChillax/new-school-sdk/tree/master/examples) 目录
64 |
--------------------------------------------------------------------------------
/examples/base_sample.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: base_sample.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/18 00:31:02
7 | '''
8 |
9 | from school_sdk.client import UserClient
10 | from school_sdk import SchoolClient
11 |
12 | # 实例化学校
13 | Gdust = SchoolClient("172.16.1.1")
14 |
15 | # 实例化用户
16 | user:UserClient = Gdust.user_login("account", "password")
17 |
18 | # 获取课表
19 | course = user.get_schedule(year=2021, term=1)
20 | print(course)
21 |
22 | # 获取成绩, 2020-2021学年第一学期的成绩
23 | score = user.get_score(year=2020, term=1)
24 | print(score)
25 |
26 | # 获取个人信息
27 | info = user.get_info()
28 | print(info)
29 |
--------------------------------------------------------------------------------
/examples/captcha_examples.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: use_verify.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/01/14 21:33:06
7 | '''
8 |
9 | from school_sdk.client import UserClient
10 | from school_sdk import SchoolClient
11 |
12 | # 实例化学校
13 | # 并根据验证码类型指定captcha_type为kap或者cap
14 | # 使用Kaptcha(与旧版系统类似的验证码)
15 | Gdust = SchoolClient("172.16.254.1", exist_verify=True, captcha_type="kaptcha")
16 | # 使用captcha(滑块验证码)
17 | Gdust = SchoolClient("172.16.254.1", exist_verify=True, captcha_type="captcha")
18 |
19 |
20 | # 实例化用户
21 | user:UserClient = Gdust.user_login("account", "password")
22 |
23 | # 获取课表
24 | course = user.get_schedule(year=2021, term=1)
25 | print(course)
26 |
27 | # 获取成绩, 2020-2021学年第一学期的成绩
28 | score = user.get_score(year=2020, term=1)
29 | print(score)
30 |
31 | # 获取个人信息
32 | info = user.get_info()
33 | print(info)
--------------------------------------------------------------------------------
/examples/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: config.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/10/17 16:54:09
7 | '''
8 | # 使用自定义路径
9 | from school_sdk.client import UserClient
10 | from school_sdk import SchoolClient
11 |
12 | # 通过抓包填写以下路径,如有迷惑或错误烦请提issue并提供地址测试
13 | url_endpoints = {
14 | 'HOME_URL': "/xtgl/login_slogin.html", # 首页url
15 | "INDEX_URL": "/xtgl/index_initMenu.html",
16 | 'LOGIN': {
17 | # 该模块表示登录使用到的端点
18 | 'INDEX': '/xtgl/login_slogin.html', # 首页,一般和上面保持一致
19 | 'CAPTCHA': '/zfcaptchaLogin', # 滑块验证码url,貌似都一样
20 | 'KCAPTCHA': '/kaptcha', # 图片验证码
21 | 'PUBLIC_KEY': '/xtgl/login_getPublicKey.html', # RSA密钥端点
22 | },
23 | "SCORE_URL": "", # 未使用
24 | "INFO_URL": "", # 未使用
25 | # 课表页面的api
26 | "SCHEDULE": {
27 | "API": '/kbcx/xskbcx_cxXsKb.html',
28 | },
29 | # 成绩页面的api
30 | 'SCORE': {
31 | 'API': '/cjcx/cjcx_cxDgXscj.html'
32 | },
33 | # 获取个人信息
34 | 'INFO': {
35 | 'API': '/xsxxxggl/xsgrxxwh_cxXsgrxx.html'
36 | }
37 | }
38 |
39 | # 使用自定义的endpoints,实例化学校
40 | # exist_verify: 是否有验证码
41 | Gdust = SchoolClient("172.16.254.1", exist_verify=True,
42 | url_endpoints=url_endpoints)
43 |
44 | # 实例化用户
45 | user: UserClient = Gdust.user_login("account", "password")
46 |
47 | # 获取课表
48 | course = user.get_schedule(year=2021, term=1)
49 | print(course)
50 |
51 | # 获取成绩, 2020-2021学年第一学期的成绩
52 | score = user.get_score(year=2020, term=1)
53 | print(score)
54 |
55 | # 获取个人信息
56 | info = user.get_info()
57 | print(info)
58 |
--------------------------------------------------------------------------------
/examples/cookie_login_example.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: dev_example.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/01/06 20:34:12
7 | '''
8 | from school_sdk import SchoolClient
9 | from school_sdk.client import UserClient
10 |
11 | # 实例化学校
12 | Gdust = SchoolClient("172.16.254.1", port=2333)
13 |
14 | # 实例化用户
15 | cookies_str = "" # e.g JSESSIONID=E738AE92B3CF133171F5B8E3E4643A5E
16 | user: UserClient = Gdust.user_login_with_cookies(cookies_str, account="2018xxxxxx")
17 |
18 | # 获取课表
19 | course = user.get_schedule(year=2020, term=2)
20 | print(course)
21 |
22 | # 获取成绩, 2020-2021学年第一学期的成绩
23 | score = user.get_score(year=2020, term=1)
24 | print(score)
25 |
26 | # 获取个人信息
27 | info = user.get_info()
28 | print(info)
29 |
30 |
--------------------------------------------------------------------------------
/examples/dev_example.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: dev_example.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/01/06 20:34:12
7 | '''
8 | from school_sdk import SchoolClient
9 | from school_sdk.client import UserClient
10 |
11 | # 实例化学校
12 | Gdust = SchoolClient("172.16.254.1", port=2333)
13 |
14 | # 实例化用户
15 | cookies_str = "" # e.g JSESSIONID=E738AE92B3CF133171F5B8E3E4643A5E
16 | user: UserClient = Gdust.init_dev_user(cookies_str)
17 |
18 | # 获取课表
19 | course = user.get_schedule(year=2020, term=2)
20 | print(course)
21 |
22 | # 获取成绩, 2020-2021学年第一学期的成绩
23 | score = user.get_score(year=2020, term=1)
24 | print(score)
25 |
26 | # 获取个人信息
27 | info = user.get_info()
28 | print(info)
29 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: school SDK
2 | theme:
3 | name: material
4 | language: zh
5 |
6 | icon:
7 | repo: fontawesome/brands/git-alt
8 |
9 | features:
10 | - navigation.top
11 |
12 |
13 | repo_url: https://github.com/Farmer-chong/new-school-sdk
14 | repo_name: Farmer/new-school-sdk
15 |
16 | extra:
17 | analytics:
18 | provider: google
19 | property: UA-XXXXXXXX-X
20 |
21 | markdown_extensions:
22 | - pymdownx.highlight
23 | - pymdownx.inlinehilite
24 |
25 |
--------------------------------------------------------------------------------
/requirement.txt:
--------------------------------------------------------------------------------
1 | autopep8==1.5.7
2 | beautifulsoup4==4.9.3
3 | bs4==0.0.1
4 | certifi==2021.5.30
5 | charset-normalizer==2.0.4
6 | cssselect==1.1.0
7 | fake-headers==1.0.2
8 | html5lib==1.1
9 | idna==3.2
10 | lxml==4.6.3
11 | numpy==1.22.0
12 | Pillow==8.3.1
13 | pycodestyle==2.7.0
14 | pyquery==1.4.3
15 | requests==2.26.0
16 | six==1.16.0
17 | soupsieve==2.2.1
18 | toml==0.10.2
19 | torch==1.10.1
20 | torchvision==0.11.2
21 | typing_extensions==4.0.1
22 | urllib3==1.26.6
23 | webencodings==0.5.1
24 |
--------------------------------------------------------------------------------
/school_sdk/PyRsa/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: __init__.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/02 19:13:12
7 | '''
8 |
9 | from .pyrsa import RsaKey
10 | from .pyb64 import Base64
11 | from .pyjsbn import Classic
12 | from .pyrng import ArcFour
13 |
--------------------------------------------------------------------------------
/school_sdk/PyRsa/pyb64.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Created on: 2019/9/29 11:21
4 | Author : zxt
5 | File : pyb64.py
6 | Software : PyCharm
7 | """
8 |
9 | import binascii
10 |
11 |
12 | class Base64:
13 | def __init__(self):
14 |
15 | self.b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
16 | self.b64pad = "="
17 |
18 | self.idx = "0123456789abcdefghijklmnopqrstuvwxyz"
19 |
20 | def hex2b64(self, h):
21 | ret = ''
22 | ii = 0
23 | for i in range(0, len(h) - 2, 3):
24 | c = int(h[i:i+3], 16)
25 | ret += self.b64map[c >> 6] + self.b64map[c & 63]
26 | ii = i
27 | ii += 3
28 | if ii + 1 == len(h):
29 | c = int(h[ii:ii+1], 16)
30 | ret += self.b64map[c << 2]
31 | elif ii + 2 == len(h):
32 | c = int(h[ii:ii + 2], 16)
33 | ret += self.b64map[c >> 2] + self.b64map[(c & 3) << 4]
34 | while (len(ret) & 3) > 0:
35 | ret += self.b64pad
36 | return ret
37 |
38 | def b64tohex(self, s):
39 | ret = ''
40 | k = 0
41 | slop = 0
42 | for i in range(len(s)):
43 | if s[i] == self.b64pad:
44 | break
45 | v = self.b64map.index(s[i])
46 | if v < 0:
47 | continue
48 | if k == 0:
49 | ret += self.idx[v >> 2]
50 | slop = v & 3
51 | k = 1
52 | elif k == 1:
53 | ret += self.idx[(v >> 4) | (slop << 2)]
54 | slop = v & 0xf
55 | k = 2
56 | elif k == 2:
57 | ret += self.idx[slop]
58 | ret += self.idx[v >> 2]
59 | slop = v & 3
60 | k = 3
61 | else:
62 | ret += self.idx[(slop << 2) | (v >> 4)]
63 | ret += self.idx[v & 0xf]
64 | k = 0
65 | if k == 1:
66 | ret += self.idx[slop << 2]
67 | return ret
68 |
69 | def b64toBA(self, s):
70 | h = self.b64tohex(s)
71 | a = []
72 | r = int(len(s) / 2) + 1
73 | for i in range(r):
74 | a.append(binascii.b2a_hex(h[2*i:2*i+2].encode('utf-8')))
75 | return a
76 |
77 |
78 | if __name__ == '__main__':
79 | h = "9134d5d73ad3c7e4224e47068308ea8a54f8bd9067aff8c1016c3809a652be529c03059366780c55496352eed46d632ebabedf05038f" \
80 | "123d124baf3f2cb1cbea6ff12e1a76023b7398dab734cad33f67aab2f36a3a592776aea30bfbb151db14c618fba3df8ef595a251270" \
81 | "858997a323ef743b83b19b89b74848a03737007e9"
82 | b = Base64()
83 | bh = b.hex2b64(h)
84 | ret = "kTTV1zrTx+QiTkcGgwjqilT4vZBnr/jBAWw4CaZSvlKcAwWTZngMVUljUu7UbWMuur7fBQOPEj0SS68/LLHL6m/xLhp2AjtzmNq3NMr" \
85 | "TP2eqsvNqOlkndq6jC/uxUdsUxhj7o9+O9ZWiUScIWJl6Mj73Q7g7GbibdISKA3NwB+k="
86 | print(bh == ret)
87 |
--------------------------------------------------------------------------------
/school_sdk/PyRsa/pyjsbn.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Created on: 2019/9/29 11:17
4 | Author : zxt
5 | File : pyjsbn.py
6 | Software : PyCharm
7 | """
8 |
9 | import math
10 | from .tools import unsigned_right_shift
11 |
12 |
13 | class Classic:
14 | def __init__(self, m):
15 | self.m = m
16 |
17 | def convert(self, x):
18 | if x.int_dict['s'] < 0 or x.compare2(self.m) >= 0:
19 | return x.mod(self.m)
20 | else:
21 | return x
22 |
23 | def revert(self, x):
24 | return x
25 |
26 | def reduce(self, x):
27 | x.rem2(self.m, None, x)
28 |
29 | def mul2(self, x, y, r):
30 | x.multiply2(y, r)
31 | self.reduce(r)
32 |
33 | def sqr2(self, x, r):
34 | x.square2(r)
35 | self.reduce(r)
36 |
37 |
38 | class Montgomery:
39 | def __init__(self, m):
40 | self.m = m
41 | self.mp = m.inv_digit()
42 | self.mpl = self.mp & 0x7fff
43 | self.mph = self.mp >> 15
44 | self.um = (1 << (m.DB - 15)) - 1
45 | self.mt2 = 2 * m.int_dict['t']
46 |
47 | def convert(self, x):
48 | r = BigInteger(None)
49 | x.abs().dl_shift2(self.m.int_dict['t'], r)
50 | r.rem2(self.m, None, r)
51 | if x.int_dict['s'] < 0 < r.compare2(ZERO):
52 | self.m.sub2(r, r)
53 | return r
54 |
55 | def reduce(self, x):
56 | while x.int_dict['t'] <= self.mt2:
57 | x.int_dict[x.int_dict['t']] = 0
58 | x.int_dict['t'] += 1
59 | for i in range(self.m.int_dict['t']):
60 | j = x.int_dict[i] & 0x7fff
61 | u0 = (j * self.mpl + (((j * self.mph + (x.int_dict[i] >> 15) * self.mpl) & self.um) << 15)) & x.DM
62 | j = i + self.m.int_dict['t']
63 | x.int_dict[j] += self.m.am(0, u0, x, i, 0, self.m.int_dict['t'])
64 | while x.int_dict[j] >= x.DV:
65 | x.int_dict[j] -= x.DV
66 | j += 1
67 | x.int_dict[j] += 1
68 | x.clamp()
69 | x.dr_shift2(self.m.int_dict['t'], x) # x.dr_shift2() 执行后数据与浏览器一致
70 | if x.compare2(self.m) >= 0:
71 | x.sub2(self.m, x)
72 |
73 | def sqr2(self, x, r):
74 | x.square2(r)
75 | self.reduce(r)
76 |
77 | def revert(self, x):
78 | r = BigInteger(None)
79 | x.copy2(r)
80 | self.reduce(r)
81 | return r
82 |
83 | def mul2(self, x, y, r):
84 | x.multiply2(y, r)
85 | self.reduce(r)
86 |
87 |
88 | class BigInteger:
89 | def __init__(self, a, b=None, c=None):
90 | self.int_dict = dict({i: None for i in range(37)}, **{'s': 0, 't': 0})
91 | self.BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"
92 | self.BI_RC = self.bi_rc()
93 | self.DB = 28
94 | self.DM = 268435455
95 | self.DV = 268435456
96 | self.F1 = 24
97 | self.F2 = 4
98 | self.FV = 4503599627370496
99 |
100 | if a is not None:
101 | if b is None and type(a) != str:
102 | self.from_string(a, 256)
103 | else:
104 | self.from_string(a, b)
105 | else:
106 | self.int_dict = {'s': 0, 't': 0}
107 |
108 | def __getitem__(self, item):
109 | return self.int_dict
110 |
111 | def int2char(self, n):
112 | return self.BI_RM[n]
113 |
114 | def am1(self, i, x, w, j, c, n):
115 | n -= 1
116 | while n >= 0:
117 | v = x * self.int_dict[i] + w.int_dict[j] + c
118 | i += 1
119 | c = int(v / 0x4000000)
120 | w.int_dict[j] = v & 0x3ffffff
121 | j += 1
122 | n -= 1
123 | return c
124 |
125 | def am2(self, i, x, w, j, c, n):
126 | xl = x & 0x7fff
127 | xh = x >> 15
128 | n -= 1
129 | while n >= 0:
130 | l = self.int_dict[i] & 0x7fff
131 | h = self.int_dict[i] >> 15
132 | i += 1
133 | m = xh * l + h * xl
134 | l = xl * l + ((m & 0x7fff) << 15) + w.int_dict[j] + (c & 0x3fffffff)
135 | c = unsigned_right_shift(l, 30) + unsigned_right_shift(m, 15) + xh * h + unsigned_right_shift(c, 30)
136 | w[j] = l & 0x3fffffff
137 | j += 1
138 | n -= 1
139 | return c
140 |
141 | def am(self, i, x, w, j, c, n):
142 | xl = x & 0x3fff
143 | xh = x >> 14
144 | for k in range(n - 1, -1, -1):
145 | ll = self.int_dict[i] & 0x3fff
146 | h = self.int_dict[i] >> 14
147 | i += 1
148 | m = xh * ll + h * xl
149 | ll = xl * ll + ((m & 0x3fff) << 14) + w.int_dict[j] + c
150 | c = (ll >> 28) + (m >> 14) + xh * h
151 | w.int_dict[j] = ll & 0xfffffff
152 | j += 1
153 | return c
154 |
155 | def nbv(self, i):
156 | r = BigInteger(None)
157 | r.from_int(i)
158 | return r
159 |
160 | def bi_rc(self):
161 | birc = {}
162 | rr = ord('0')
163 | for vv in range(10):
164 | birc[rr] = vv
165 | rr += 1
166 | rr = ord('a')
167 | for vv in range(10, 36):
168 | birc[rr] = vv
169 | rr += 1
170 | rr = ord('A')
171 | for vv in range(10, 36):
172 | birc[rr] = vv
173 | rr += 1
174 | return birc
175 |
176 | def from_int(self, x):
177 | self.int_dict['t'] = 1
178 | self.int_dict['s'] = -1 if x < 0 else 0
179 | if x > 0:
180 | self.int_dict[0] = x
181 | elif x < -1:
182 | self.int_dict[0] = x + self.DV
183 | else:
184 | self.int_dict['t'] = 0
185 |
186 | def from_string(self, s, b):
187 | k = int(math.log(b, 2))
188 | i = len(s)
189 | mi = False
190 | sh = 0
191 | i -= 1
192 | while i > 0:
193 | x = s[i] & 0xff if k == 8 else self.intat(s, i)
194 | if x < 0:
195 | if s[i] == '-':
196 | mi = True
197 | continue
198 | mi = False
199 | if sh == 0:
200 | self.int_dict[self.int_dict['t']] = x
201 | self.int_dict['t'] += 1
202 | elif sh + k > self.DB:
203 | self.int_dict[self.int_dict['t'] - 1] |= (x & ((1 << (self.DB - sh)) - 1)) << sh
204 | self.int_dict[self.int_dict['t']] = (x >> (self.DB - sh))
205 | self.int_dict['t'] += 1
206 | else:
207 | self.int_dict[self.int_dict['t'] - 1] |= x << sh
208 | sh += k
209 | if sh >= self.DB:
210 | sh -= self.DB
211 | i -= 1
212 | if k == 8 and (s[0] & 0x80) != 0:
213 | self.int_dict['s'] = -1
214 | if sh > 0:
215 | self.int_dict[self.int_dict['t'] - 1] |= ((1 << (self.DB - sh)) - 1) << sh
216 | self.clamp()
217 | if mi:
218 | self.sub2(self, self)
219 |
220 | def intat(self, s, i):
221 | try:
222 | c = self.BI_RC[ord(s[i])]
223 | except:
224 | return -1
225 | return c
226 |
227 | def clamp(self):
228 | c = self.int_dict['s'] & self.DM
229 | while self.int_dict['t'] > 0 and self.int_dict[self.int_dict['t'] - 1] == c:
230 | self.int_dict['t'] -= 1
231 |
232 | def to_string(self, b):
233 | if self.int_dict['s'] < 0:
234 | return '-' + self.negate().to_string(b)
235 | k = int(math.log(b, 2))
236 | km = (1 << k) - 1
237 | m = False
238 | r = ''
239 | i = self.int_dict['t']
240 | p = self.DB - (i * self.DB) % k
241 | if i > 0:
242 | i -= 1
243 | d = self.int_dict[i] >> p
244 | if p < self.DB and d > 0:
245 | m = True
246 | r = self.int2char(d)
247 | while i >= 0:
248 | if p < k:
249 | d = (self.int_dict[i] & ((1 << p) - 1)) << (k - p)
250 | i -= 1
251 | p += self.DB - k
252 | d |= self.int_dict[i] >> p
253 | else:
254 | p -= k
255 | d = (self.int_dict[i] >> p) & km
256 | if p <= 0:
257 | p += self.DB
258 | i -= 1
259 | if d > 0:
260 | m = True
261 | if m:
262 | r += self.int2char(d)
263 | return r if m else '0'
264 |
265 | def sub2(self, a, r):
266 | i = 0
267 | c = 0
268 | m = min(a.int_dict['t'], self.int_dict['t'])
269 | while i < m:
270 | c += self.int_dict[i] - a.int_dict[i]
271 | r.int_dict[i] = c & self.DM
272 | i += 1
273 | c >>= self.DB
274 | if a.int_dict['t'] < self.int_dict['t']:
275 | c -= a.int_dict['s']
276 | while i < self.int_dict['t']:
277 | c += self.int_dict[i]
278 | r.int_dict[i] = c & self.DM
279 | i += 1
280 | c >>= self.DB
281 | c += self.int_dict['s']
282 | else:
283 | c += self.int_dict['s']
284 | while i < a.int_dict['t']:
285 | c -= a.int_dict[i]
286 | r.int_dict[i] = c & self.DM
287 | i += 1
288 | c >>= self.DB
289 | c -= a.int_dict['s']
290 | r.int_dict['s'] = -1 if c < 0 else 0
291 | if c < -1:
292 | r.int_dict[i] = self.DV
293 | i += 1
294 | elif c > 0:
295 | r.int_dict[i] = c
296 | i += 1
297 | r.int_dict['t'] = i
298 | r.clamp()
299 |
300 | def copy2(self, r):
301 | for i in range(self.int_dict['t'] - 1, -1, -1):
302 | r.int_dict[i] = self.int_dict[i]
303 | r.int_dict['t'] = self.int_dict['t']
304 | r.int_dict['s'] = self.int_dict['s']
305 |
306 | def nbits(self, x):
307 | r = 1
308 | t = unsigned_right_shift(x, 16)
309 | if t != 0:
310 | x = t
311 | r += 16
312 | t = x >> 8
313 | if t != 0:
314 | x = t
315 | r += 8
316 | t = x >> 4
317 | if t != 0:
318 | x = t
319 | r += 4
320 | t = x >> 2
321 | if t != 0:
322 | x = t
323 | r += 2
324 | t = x >> 1
325 | if t != 0:
326 | x = t
327 | r += 1
328 | return r
329 |
330 | def negate(self):
331 | r = BigInteger(None)
332 | ZERO.sub2(self, r)
333 | return r
334 |
335 | def abs(self):
336 | return self.negate() if self.int_dict['s'] < 0 else self
337 |
338 | def compare2(self, a):
339 | r = self.int_dict['s'] - a.int_dict['s']
340 | if r != 0:
341 | return r
342 | i = self.int_dict['t']
343 | r = i - a.int_dict['t']
344 | if r != 0:
345 | return -r if self.int_dict['s'] < 0 else r
346 | for k in range(i - 1, -1, -1):
347 | r = self.int_dict[k] - a.int_dict[k]
348 | if r != 0:
349 | return r
350 | return 0
351 |
352 | def bit_length(self):
353 | if self.int_dict['t'] <= 0:
354 | return 0
355 | return self.DB * (self.int_dict['t'] - 1) + self.nbits(
356 | self.int_dict[self.int_dict['t'] - 1] ^ (self.int_dict['s'] & self.DM)
357 | )
358 |
359 | def dl_shift2(self, n, r):
360 | for i in range(self.int_dict['t'] - 1, -1, -1):
361 | r.int_dict[i + n] = self.int_dict[i]
362 | for i in range(n - 1, -1, -1):
363 | r.int_dict[i] = 0
364 | r.int_dict['t'] = self.int_dict['t'] + n
365 | r.int_dict['s'] = self.int_dict['s']
366 |
367 | def l_shift2(self, n, r):
368 | bs = n % self.DB
369 | cbs = self.DB - bs
370 | bm = (1 << cbs) - 1
371 | ds = int(n / self.DB)
372 | c = (self.int_dict['s'] << bs) & self.DM
373 | for i in range(self.int_dict['t'] - 1, -1, -1):
374 | r.int_dict[i + ds + 1] = (self.int_dict[i] >> cbs) | c
375 | c = (self.int_dict[i] & bm) << bs
376 | for i in range(ds - 1, -1, -1):
377 | r.int_dict[i] = 0
378 | r.int_dict[ds] = c
379 | r.int_dict['t'] = self.int_dict['t'] + ds + 1
380 | r.int_dict['s'] = self.int_dict['s']
381 | r.clamp()
382 |
383 | def dr_shift2(self, n, r):
384 | for i in range(n, self.int_dict['t']):
385 | r.int_dict[i - n] = self.int_dict[i]
386 | r.int_dict['t'] = max(self.int_dict['t'] - n, 0)
387 | r.int_dict['s'] = self.int_dict['s']
388 |
389 | def r_shift2(self, n, r):
390 | r.int_dict['s'] = self.int_dict['s']
391 | ds = int(n / self.DB)
392 | if ds >= self.int_dict['t']:
393 | r.int_dict['t'] = 0
394 | return
395 | bs = n % self.DB
396 | cbs = self.DB - bs
397 | bm = (1 << bs) - 1
398 | r.int_dict[0] = self.int_dict[ds] >> bs
399 | for i in range(ds + 1, self.int_dict['t']):
400 | r.int_dict[i - ds - 1] |= (self.int_dict[i] & bm) << cbs
401 | r.int_dict[i - ds] = self.int_dict[i] >> bs
402 | if bs > 0:
403 | r.int_dict[self.int_dict['t'] - ds - 1] |= (self.int_dict['s'] & bm) << cbs
404 | r.int_dict['t'] = self.int_dict['t'] - ds
405 | r.clamp()
406 |
407 | def multiply2(self, a, r):
408 | x = self.abs()
409 | y = a.abs()
410 | i = x.int_dict['t']
411 | r.int_dict['t'] = i + y.int_dict['t']
412 | i -= 1
413 | while i >= 0:
414 | r.int_dict[i] = 0
415 | i -= 1
416 | for i in range(y.int_dict['t']):
417 | r.int_dict[i + x.int_dict['t']] = x.am(0, y.int_dict[i], r, i, 0, x.int_dict['t'])
418 | r.int_dict['s'] = 0
419 | r.clamp()
420 | if self.int_dict['s'] != a.int_dict['s']:
421 | ZERO.sub2(r, r)
422 |
423 | def square2(self, r):
424 | x = self.abs()
425 | i = r.int_dict['t'] = 2 * x.int_dict['t']
426 | for k in range(i - 1, -1, -1):
427 | r.int_dict[k] = 0
428 | ii = 0
429 | for i in range(x.int_dict['t'] - 1):
430 | c = x.am(i, x.int_dict[i], r, 2 * i, 0, 1)
431 | r.int_dict[i + x.int_dict['t']] += x.am(i + 1, 2 * x.int_dict[i], r, 2 * i + 1, c, x.int_dict['t'] - i - 1)
432 | if r.int_dict[i + x.int_dict['t']] >= x.DV:
433 | r.int_dict[i + x.int_dict['t']] -= x.DV
434 | r.int_dict[i + x.int_dict['t'] + 1] = 1
435 | ii = i
436 | ii += 1
437 | if r.int_dict['t'] > 0:
438 | r.int_dict[r.int_dict['t'] - 1] += x.am(ii, x.int_dict[ii], r, 2 * ii, 0, 1)
439 | r.int_dict['s'] = 0
440 | r.clamp()
441 |
442 | def rem2(self, m, q=None, r=None):
443 | pm = m.abs()
444 | if pm.int_dict['t'] <= 0:
445 | return
446 | pt = self.abs()
447 | if pt.int_dict['t'] < pm.int_dict['t']:
448 | if q is not None:
449 | q.from_int(0)
450 | if r is not None:
451 | self.copy2(r)
452 | return
453 | if r is None:
454 | r = BigInteger(None)
455 | y = BigInteger(None)
456 | ts = self.int_dict['s']
457 | ms = m.int_dict['s']
458 | nsh = self.DB - self.nbits(pm.int_dict[pm.int_dict['t'] - 1])
459 | if nsh > 0:
460 | pm.l_shift2(nsh, y)
461 | pt.l_shift2(nsh, r)
462 | else:
463 | pm.copy2(y)
464 | pt.copy2(r)
465 | ys = y.int_dict['t']
466 | y0 = y.int_dict[ys - 1]
467 | if y0 == 0:
468 | return
469 | yt = y0 * (1 << self.F1) + (y.int_dict[ys - 2] >> self.F2 if ys > 1 else 0)
470 | d1 = self.FV / yt
471 | d2 = (1 << self.F1) / yt
472 | e = 1 << self.F2
473 | i = r.int_dict['t']
474 | j = i - ys
475 | t = BigInteger(None) if q is None else q
476 | y.dl_shift2(j, t)
477 | if r.compare2(t) >= 0:
478 | r.int_dict[r.int_dict['t']] = 1
479 | r.int_dict['t'] += 1
480 | r.sub2(t, r)
481 | ONE.dl_shift2(ys, t)
482 | t.sub2(y, y)
483 | while y.int_dict['t'] < ys:
484 | y.int_dict[y.int_dict['t']] = 0
485 | y.int_dict['t'] += 1
486 | for k in range(j - 1, -1, -1):
487 | i -= 1
488 | qd = self.DM if r.int_dict[i] == y0 else \
489 | int(r.int_dict[i] * d1 + (r.int_dict[i-1] + e) * d2)
490 | r.int_dict[i] += y.am(0, qd, r, k, 0, ys)
491 | if r.int_dict[i] < qd:
492 | y.dl_shift2(k, t)
493 | r.sub2(t, r)
494 | qd -= 1
495 | while r.int_dict[i] < qd:
496 | r.sub2(t, r)
497 | qd -= 1
498 | if q is not None:
499 | r.dr_shift2(ys, q)
500 | if ts != ms:
501 | ZERO.sub2(q, q)
502 | r.int_dict['t'] = ys
503 | r.clamp()
504 | if nsh > 0:
505 | r.r_shift2(nsh, r)
506 | if ts < 0:
507 | ZERO.sub2(r, r)
508 |
509 | def pow_int(self, e, m):
510 | if e < 256 or m.is_even():
511 | z = Classic(m)
512 | else:
513 | z = Montgomery(m)
514 | return self.exp(e, z)
515 |
516 | def is_even(self):
517 | return (self.int_dict[0] & 1 if self.int_dict['t'] > 0 else self.int_dict['s']) == 0
518 |
519 | def inv_digit(self):
520 | if self.int_dict['t'] < 1:
521 | return 0
522 | x = self.int_dict[0]
523 | if (x & 1) == 0:
524 | return 0
525 | y = x & 3
526 | y = (y * (2 - (x & 0xf) * y)) & 0xf
527 | y = (y * (2 - (x & 0xff) * y)) & 0xff
528 | y = (y * (2 - (((x & 0xffff) * y) & 0xffff))) & 0xffff
529 | y = (y * (2 - x * y % self.DV)) % self.DV
530 | return self.DV - y if y > 0 else -y
531 |
532 | def exp(self, e, z):
533 | if e > 0xffffffff or e < 1:
534 | return ONE
535 | r = BigInteger(None)
536 | r2 = BigInteger(None)
537 | g = z.convert(self)
538 | i = self.nbits(e) - 1
539 | g.copy2(r) # g.copy() 方法前数据与浏览器一致
540 | for k in range(i - 1, -1, -1):
541 | z.sqr2(r, r2)
542 | if e & (1 << k) > 0:
543 | z.mul2(r2, g, r)
544 | else:
545 | r, r2 = r2, r
546 | return z.revert(r) # 循环后参数 g, r2, self, z 与浏览器数据一致,r 数据不同
547 |
548 | def mod(self, a):
549 | r = BigInteger(None)
550 | self.abs().rem2(a, None, r)
551 | if self.int_dict['s'] < 0 < r.compare2(ZERO):
552 | a.sub2(r, r)
553 | return r
554 |
555 |
556 | ZERO = BigInteger(None).nbv(0)
557 | ONE = BigInteger(None).nbv(1)
558 |
--------------------------------------------------------------------------------
/school_sdk/PyRsa/pyrng.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Created on: 2019/9/30 17:37
4 | Author : zxt
5 | File : pyrng.py
6 | Software : PyCharm
7 | """
8 |
9 | import time
10 | import random
11 | from .tools import unsigned_right_shift
12 |
13 |
14 | class ArcFour:
15 | def __init__(self):
16 | self.i = 0
17 | self.j = 0
18 | self.S = {}
19 |
20 | def init(self, key):
21 | for i in range(256):
22 | self.S[i] = i
23 | j = 0
24 | for i in range(256):
25 | j = (j + self.S[i] + key[i % len(key)]) & 255
26 | self.S[i], self.S[j] = self.S[j], self.S[i]
27 | self.i = 0
28 | self.j = 0
29 |
30 | def next(self):
31 | self.i = (self.i + 1) & 255
32 | self.j = (self.j + self.S[self.i]) & 255
33 | t = self.S[self.i]
34 | self.S[self.i] = self.S[self.j]
35 | self.S[self.j] = t
36 | return self.S[(t + self.S[self.i]) & 255]
37 |
38 |
39 | class SecureRandom:
40 | def __init__(self):
41 | self.rng_state = None
42 | self.rng_pool = None
43 | self.rng_pptr = None
44 | self.rng_psize = 256
45 |
46 | if self.rng_pool is None:
47 | self.rng_pool = {}
48 | self.rng_pptr = 0
49 | while self.rng_pptr < self.rng_psize:
50 | self.t = int(65536 * random.random())
51 | self.rng_pool[self.rng_pptr] = unsigned_right_shift(self.t, 8)
52 | self.rng_pptr += 1
53 | self.rng_pool[self.rng_pptr] = self.t & 255
54 | self.rng_pptr += 1
55 | self.rng_pptr = 0
56 | self.rng_seed_time()
57 |
58 | def rng_seed_int(self, x):
59 | self.rng_pool[self.rng_pptr] ^= x & 255
60 | self.rng_pptr += 1
61 | self.rng_pool[self.rng_pptr] ^= (x >> 8) & 255
62 | self.rng_pptr += 1
63 | self.rng_pool[self.rng_pptr] ^= (x >> 8) & 255
64 | self.rng_pptr += 1
65 | self.rng_pool[self.rng_pptr] ^= (x >> 24) & 255
66 | if self.rng_pptr >= self.rng_psize:
67 | self.rng_pptr -= self.rng_psize
68 |
69 | def rng_seed_time(self):
70 | self.rng_seed_int(int(time.time() * 1000))
71 |
72 | def rng_get_byte(self):
73 | if self.rng_state is None:
74 | self.rng_seed_time()
75 | self.rng_state = ArcFour()
76 | self.rng_state.init(self.rng_pool)
77 | for self.rng_ppt in range(len(self.rng_pool)):
78 | self.rng_pool[self.rng_pptr] = 0
79 | self.rng_pptr = 0
80 | return self.rng_state.next()
81 |
82 | def rng_get_bytes(self, ba):
83 | for i in range(len(ba)):
84 | ba[i] = self.rng_get_byte()
85 |
86 |
--------------------------------------------------------------------------------
/school_sdk/PyRsa/pyrsa.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Created on: 2019/9/29 11:04
4 | Author : zxt
5 | File : PyRsa.py
6 | Software : PyCharm
7 | """
8 |
9 |
10 | import binascii
11 | from .pyrng import SecureRandom
12 | from .pyjsbn import BigInteger
13 | from .pyb64 import Base64
14 |
15 |
16 | class RsaKey:
17 | def __init__(self):
18 | self.n = None
19 | self.e = 0
20 | self.d = None
21 | self.p = None
22 | self.q = None
23 | self.dmp1 = None
24 | self.dmq1 = None
25 | self.coeff = None
26 |
27 | def set_public(self, n, e):
28 | if n is not None and e is not None and len(n) > 0 and len(e) > 0:
29 | self.n = BigInteger(n, 16)
30 | self.e = int(e, 16)
31 | else:
32 | raise ValueError("Invalid RSA public key")
33 |
34 | def linkbrk(self, s, n):
35 | ret = ''
36 | i = 0
37 | while i + n < len(s):
38 | ret += s[i, i + n] + '\n'
39 | i += n
40 | return ret + s[i, len(s)]
41 |
42 | def byte2hex(self, b):
43 | if b < 0x10:
44 | return '0' + binascii.b2a_hex(b)
45 | else:
46 | return binascii.b2a_hex(b)
47 |
48 | def pkcs1pad2(self, s, n):
49 | if n < len(s) + 11:
50 | print("Message too long for RSA")
51 | exit()
52 | ba = {}
53 | i = len(s) - 1
54 | while i >= 0 and n > 0:
55 | c = ord(s[i])
56 | i -= 1
57 | if c < 128:
58 | n -= 1
59 | ba[n] = c
60 | elif 127 < c < 2048:
61 | n -= 1
62 | ba[n] = (c & 63) | 128
63 | n -= 1
64 | ba[n] = (c >> 6) | 192
65 | else:
66 | n -= 1
67 | ba[n] = (c & 63) | 128
68 | n -= 1
69 | ba[n] = ((c >> 6) & 63) | 128
70 | n -= 1
71 | ba[n] = (c >> 12) | 224
72 | n -= 1
73 | ba[n] = 0
74 | rng = SecureRandom()
75 | x = {}
76 | while n > 2:
77 | """
78 | 产生与时间相关的随机阵列对按字符解析后的 ba 进行填充
79 | """
80 | x[0] = 0
81 | while x[0] == 0:
82 | rng.rng_get_bytes(x)
83 | n -= 1
84 | ba[n] = x[0]
85 | n -= 1
86 | ba[n] = 2
87 | n -= 1
88 | ba[n] = 0
89 | # print()
90 | bi = BigInteger(ba)
91 | # print()
92 | return BigInteger(ba)
93 |
94 | def do_public(self, x):
95 | return x.pow_int(self.e, self.n)
96 |
97 | def rsa_encrypt(self, text):
98 | m = self.pkcs1pad2(text, (self.n.bit_length() + 7) >> 3)
99 | if m is None:
100 | return None
101 | c = self.do_public(m)
102 | if c is None:
103 | return None
104 | h = c.to_string(16)
105 | if len(h) & 1 == 0:
106 | return h
107 | else:
108 | return '0' + h
109 |
110 |
111 | if __name__ == '__main__':
112 | rsa = RsaKey()
113 | m = "AKRB6FwmOe0hE9Uo6LMKoDE5U9JU9lH1v8Uv7ATjRj2W+aTPlR9Hfm8fR782pzGwDsTD4Yr7tBHQ1cuEnGrqrJn5HuPiLqmSg4Z/AwS+Rq8eE7T+ZaGoUtpqvcoSffSJOW29RNVMwT391ona/+eK5B3RkC9WaJFYiZai7FiQDeXT"
114 | e = 'AQAB'
115 | rsa.set_public(Base64().b64tohex(m), Base64().b64tohex(e))
116 | rr = rsa.rsa_encrypt('1234567890')
117 | enpsw = Base64().hex2b64(rr)
118 | print(enpsw)
119 |
--------------------------------------------------------------------------------
/school_sdk/PyRsa/tools.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Created on: 2019/10/1 18:02
4 | Author : zxt
5 | File : tools.py
6 | Software : PyCharm
7 | """
8 |
9 |
10 | import ctypes
11 |
12 |
13 | def unsigned_right_shift(n, i):
14 | """
15 | 无符号整数右移
16 | :param n:
17 | :param i:
18 | :return:
19 | """
20 | def int_overflow(val):
21 | maxint = 2147483647
22 | if not -maxint - 1 <= val <= maxint:
23 | val = (val + (maxint + 1)) % (2 * (maxint + 1)) - maxint - 1
24 | return val
25 |
26 | # 数字小于0,则转为32位无符号uint
27 | if n < 0:
28 | n = ctypes.c_uint32(n).value
29 | # 正常位移位数是为正数,但是为了兼容js之类的,负数就右移变成左移好了
30 | if i < 0:
31 | return -int_overflow(n << abs(i))
32 | # print(n)
33 | return int_overflow(n >> i)
34 |
--------------------------------------------------------------------------------
/school_sdk/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: __init__.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/02 19:18:47
7 | '''
8 |
9 | from .client import SchoolClient, UserClient
10 | from .type import CAPTCHA, KCAPTCHA
11 |
--------------------------------------------------------------------------------
/school_sdk/check_code/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: __intit__.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/02 19:19:08
7 | '''
8 | from typing import Tuple, Union
9 | from PIL import Image
10 | from io import BytesIO
11 | scan_height = 50
12 |
13 | class ZFCaptchaDistinguish():
14 |
15 | def __init__(self, image_stream, verify_func = None) -> None:
16 | stream = BytesIO(image_stream)
17 | # # 'kaptcha' 滑动 or 'captcha' 图形
18 | # self.method = 'kaptcha' or 'captcha'
19 | self.image = Image.open(stream)
20 | self.X = -1
21 | self.Y = -1
22 | self.verify_func = verify_func
23 | # self.image.show()
24 |
25 | def _is_continuity_in_y(self, image:Image, x, y, height) -> int:
26 | count = 0
27 | img = image.load()
28 | for i in range(height):
29 | if img[x+1, y+i] > img[x, y + i]:
30 | count += 1
31 | return count
32 |
33 | def verify(self) -> Union[Tuple[int, int], str]:
34 | return self.verify_func(self.image)
35 |
36 |
37 | def verify_with_slide(self):
38 | img = self.image.convert("L")
39 | img_x, img_y = img.size
40 | for y in range(0, img_y - scan_height):
41 | for x in range(1, img_x - 1):
42 | pixel_shallow_count = self._is_continuity_in_y(img, x, y, scan_height)
43 | if pixel_shallow_count == scan_height:
44 | self.X = x
45 | self.Y = y
46 | # self.image.crop((x, y, img_x, img_y)).show()
47 | return x, y
48 |
49 | def verify_with_discern(self):
50 | return self.verify_func()
51 |
52 | # def (self)
53 |
54 |
55 |
--------------------------------------------------------------------------------
/school_sdk/check_code/captcha_setting.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: captcha_setting.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/01/14 21:30:22
7 | '''
8 | # 验证码中的字符
9 | # string.digits + string.ascii_uppercase
10 | NUMBER = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
11 |
12 | ALPHABET = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
13 | 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
14 | 'Z']+['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
15 | 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
16 |
17 | ALL_CHAR_SET = NUMBER + ALPHABET
18 | ALL_CHAR_SET_LEN = len(ALL_CHAR_SET)
19 | MAX_CAPTCHA = 6
20 |
21 | # 图像大小
22 | IMAGE_HEIGHT = 50
23 | IMAGE_WIDTH = 200
24 |
--------------------------------------------------------------------------------
/school_sdk/check_code/dataset.py:
--------------------------------------------------------------------------------
1 | from email import header
2 |
3 |
4 | # -*- coding: utf-8 -*-
5 | '''
6 | :file: dataset.py
7 | :author: -Farmer
8 | :url: https://blog.farmer233.top
9 | :date: 2022/01/13 15:37:31
10 | '''
11 | from PIL import Image
12 | from torch.utils.data import Dataset
13 | import torchvision.transforms as transforms
14 | from torch.utils.data.dataloader import DataLoader
15 |
16 | transform = transforms.Compose([
17 | transforms.Grayscale(),
18 | transforms.ToTensor(),
19 | ])
20 |
21 |
22 | class MyDataset(Dataset):
23 | def __init__(self, image: Image = None, transform=None) -> None:
24 | self.image = image
25 | self.transform = transform
26 |
27 | def get_img(self):
28 | if self.transform is not None:
29 | self.image = self.transform(self.image)
30 | return self.image
31 |
32 | def __len__(self):
33 | return 1
34 |
35 | def __getitem__(self, index):
36 | if self.transform is not None:
37 | image = self.transform(self.image)
38 |
39 | return image
40 |
41 | def get_predict_data_loader(img: Image):
42 | dataset = MyDataset(img, transform=transform)
43 | return DataLoader(dataset, batch_size=1, shuffle=True)
44 |
--------------------------------------------------------------------------------
/school_sdk/check_code/model.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FarmerChillax/new-school-sdk/c338e4aeff1279a3990308b5f277f9ba660465ab/school_sdk/check_code/model.pkl
--------------------------------------------------------------------------------
/school_sdk/check_code/model.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: model.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/01/14 17:56:45
7 | '''
8 | import torch.nn as nn
9 |
10 | from school_sdk.check_code import captcha_setting
11 |
12 |
13 | # CNN Model (2 conv layer)
14 | class CNN(nn.Module):
15 | def __init__(self):
16 | super(CNN, self).__init__()
17 | self.layer1 = nn.Sequential(
18 | nn.Conv2d(1, 32, kernel_size=3, padding=1),
19 | nn.BatchNorm2d(32),
20 | nn.Dropout(0.5), # drop 50% of the neuron
21 | nn.ReLU(),
22 | nn.MaxPool2d(2))
23 | self.layer2 = nn.Sequential(
24 | nn.Conv2d(32, 64, kernel_size=3, padding=1),
25 | nn.BatchNorm2d(64),
26 | nn.Dropout(0.5), # drop 50% of the neuron
27 | nn.ReLU(),
28 | nn.MaxPool2d(2))
29 | self.layer3 = nn.Sequential(
30 | nn.Conv2d(64, 64, kernel_size=3, padding=1),
31 | nn.BatchNorm2d(64),
32 | nn.Dropout(0.5), # drop 50% of the neuron
33 | nn.ReLU(),
34 | nn.MaxPool2d(2))
35 | self.fc = nn.Sequential(
36 | nn.Linear((captcha_setting.IMAGE_WIDTH//8)*(captcha_setting.IMAGE_HEIGHT//8)*64, 1024),
37 | nn.Dropout(0.5), # drop 50% of the neuron
38 | nn.ReLU())
39 | self.rfc = nn.Sequential(
40 | nn.Linear(1024, captcha_setting.MAX_CAPTCHA*captcha_setting.ALL_CHAR_SET_LEN),
41 | )
42 |
43 | def forward(self, x):
44 | out = self.layer1(x)
45 | out = self.layer2(out)
46 | out = self.layer3(out)
47 | out = out.view(out.size(0), -1)
48 | out = self.fc(out)
49 | out = self.rfc(out)
50 | return out
51 |
52 |
--------------------------------------------------------------------------------
/school_sdk/check_code/predict.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: predict.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/01/14 17:56:00
7 | '''
8 | import os
9 | import torch
10 | from PIL import Image
11 | import numpy as np
12 | from torch.autograd import Variable
13 | from school_sdk.check_code import captcha_setting
14 | from school_sdk.check_code.dataset import get_predict_data_loader
15 | from school_sdk.check_code.model import CNN
16 |
17 | data_file = os.path.dirname(os.path.realpath(__file__)) + os.sep + 'model.pkl'
18 |
19 | # device = torch.device("cpu")
20 | # cnn = CNN().to(device=device)
21 | cnn = CNN()
22 | cnn.eval()
23 | cnn.load_state_dict(torch.load(data_file, map_location="cpu"))
24 |
25 |
26 | def check(image: Image) -> str:
27 |
28 | predict_dataloader = get_predict_data_loader(image)
29 |
30 | for i, (img) in enumerate(predict_dataloader):
31 | vimage = Variable(img)
32 | predict_label = cnn(vimage)
33 | c0 = captcha_setting.ALL_CHAR_SET[np.argmax(
34 | predict_label[0, 0:captcha_setting.ALL_CHAR_SET_LEN].data.numpy())]
35 | c1 = captcha_setting.ALL_CHAR_SET[np.argmax(
36 | predict_label[0, captcha_setting.ALL_CHAR_SET_LEN:2 * captcha_setting.ALL_CHAR_SET_LEN].data.numpy())]
37 | c2 = captcha_setting.ALL_CHAR_SET[np.argmax(
38 | predict_label[0, 2 * captcha_setting.ALL_CHAR_SET_LEN:3 * captcha_setting.ALL_CHAR_SET_LEN].data.numpy())]
39 | c3 = captcha_setting.ALL_CHAR_SET[np.argmax(
40 | predict_label[0, 3 * captcha_setting.ALL_CHAR_SET_LEN:4 * captcha_setting.ALL_CHAR_SET_LEN].data.numpy())]
41 | c4 = captcha_setting.ALL_CHAR_SET[np.argmax(
42 | predict_label[0, 4 * captcha_setting.ALL_CHAR_SET_LEN:5 * captcha_setting.ALL_CHAR_SET_LEN].data.numpy())]
43 | c5 = captcha_setting.ALL_CHAR_SET[np.argmax(
44 | predict_label[0, 5 * captcha_setting.ALL_CHAR_SET_LEN:6 * captcha_setting.ALL_CHAR_SET_LEN].data.numpy())]
45 |
46 | c = f'{c0}{c1}{c2}{c3}{c4}{c5}'
47 |
48 | return c
49 |
--------------------------------------------------------------------------------
/school_sdk/check_code/type.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: type.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/01/14 17:28:49
7 | '''
8 | from ast import Bytes
9 | from io import BytesIO
10 | from typing import Tuple
11 | from PIL import Image
12 |
13 | scan_height = 50
14 |
15 | def captcha_func(image:Image) -> Tuple[int, int]:
16 | """滑块验证码回调函数
17 | """
18 |
19 | def _is_continuity_in_y(image:Image, x, y, height) -> int:
20 | count = 0
21 | img = image.load()
22 | for i in range(height):
23 | if img[x+1, y+i] > img[x, y + i]:
24 | count += 1
25 | return count
26 |
27 | img = image.convert("L")
28 | img_x, img_y = img.size
29 | for y in range(0, img_y - scan_height):
30 | for x in range(1, img_x - 1):
31 | pixel_shallow_count = _is_continuity_in_y(img, x, y, scan_height)
32 | if pixel_shallow_count == scan_height:
33 | return x, y
34 |
35 | return 0, 0
36 |
37 |
38 | def kaptcha_func(image:Image) -> str:
39 | """图片验证码回调函数
40 | """
41 | from school_sdk.check_code.predict import check
42 |
43 | code = check(image)
44 | # print(code)
45 | return code
46 |
--------------------------------------------------------------------------------
/school_sdk/client/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: __init__.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/02 22:20:52
7 | '''
8 | from typing import Dict, Union
9 | import requests
10 | from school_sdk.client.api.class_schedule import ScheduleClass
11 | from school_sdk.client.api.score import Score
12 | from school_sdk.client.api.user_info import Info
13 | from school_sdk.client.utils import user_is_login
14 | from school_sdk.config import URL_ENDPOINT
15 | from school_sdk.client.api.schedules import Schedule
16 | from school_sdk.client.exceptions import LoginException
17 | import time
18 | from school_sdk.client.api.login import ZFLogin
19 | from school_sdk.client.base import BaseUserClient
20 |
21 |
22 | CAPTCHA: str = 'captcha'
23 | KCAPTCHA: str = 'kap'
24 |
25 | class SchoolClient():
26 |
27 | def __init__(self, host, port: int = 80, ssl: bool = False, name=None, exist_verify: bool = False,
28 | captcha_type: str = "captcha", retry: int = 10, lan_host=None, lan_port=80, timeout=10,
29 | login_url_path=None, url_endpoints=None) -> None:
30 | """初始化学校配置
31 |
32 | Args:
33 | host (str): 主机地址
34 | port (int, optional): 端口号. Defaults to 80.
35 | ssl (bool, optional): 是否启用HTTPS. Defaults to False.
36 | name (str, optional): 学校名称. Defaults to None.
37 |
38 | exist_verify (bool, optional): 是否有验证码. Defaults to False.
39 | captcha_type (str, optional): 验证码类型. Defaults to captcha.
40 | 滑块传入cap开头, 图片传入kap开头 与教务系统的url地址对应, 默认识别滑块验证码.
41 | retry (int, optional): 登录重试次数. Defaults to 10.
42 |
43 | lan_host (str, optional): 内网主机地址. Defaults to None.
44 | lan_port (int, optional): 内网主机端口号. Defaults to 80.
45 | timeout (int, optional): 请求超时时间. Defaults to 10.
46 | login_url_path ([type], optional): 登录地址. Defaults to None.
47 | url_endpoints ([dict], optional): 地址列表. Defaults to None.
48 | """
49 | school = {
50 | "name": name,
51 | "exist_verify": exist_verify,
52 | "captcha_type": captcha_type,
53 | "retry": retry,
54 | "lan_host": lan_host,
55 | "lan_port": lan_port,
56 | "timeout": timeout,
57 | "login_url_path": login_url_path,
58 | "url_endpoints": url_endpoints or URL_ENDPOINT
59 | }
60 |
61 | self.base_url = f'https://{host}:{port}' if ssl else f'http://{host}:{port}'
62 | self.config: dict = school
63 |
64 | def user_login(self, account: str, password: str, **kwargs):
65 | """用户登录
66 |
67 | Args:
68 | account (str): 用户账号
69 | password (str): 用户密码
70 | """
71 | user = UserClient(self, account=account, password=password, **kwargs)
72 | return user.login()
73 |
74 | def user_login_with_cookies(self, cookies: str, account: str = "cookie login account", **kwargs):
75 | """使用cookies登录
76 | 该方法因缺失帐号密码,因此无法刷新session,需要手动刷新
77 | 传参中的 account 主要用于标识当前登录用户、记录日志信息等用途,不会用于登录
78 |
79 | Args:
80 | cookies (str): Cookies字符串
81 | account (str, optional): 账号. Defaults to "cookie login account".
82 | """
83 |
84 | user = UserClient(self, account=account, password="cookies login password", **kwargs)
85 | return user.get_dev_user(cookies)
86 |
87 | def init_dev_user(self, cookies: str = None):
88 | dev_user = UserClient(self, account="dev account",
89 | password="dev password")
90 | return dev_user.get_dev_user(cookies)
91 |
92 |
93 | class UserClient(BaseUserClient):
94 | schedule: Schedule = None
95 | score: Score = None
96 | info = None
97 | schedule_class: ScheduleClass = None
98 |
99 | def __init__(self, school: SchoolClient, account, password) -> None:
100 | """初始化用户类
101 | 用户类继承自学校
102 |
103 | Args:
104 | school (SchoolClient): 学校实例
105 | account (str): 账号
106 | password (str): 密码
107 | """
108 | self.BASE_URL = school.base_url
109 | self.account = account
110 | self.password = password
111 | self.school: SchoolClient = school
112 | self._csrf = None
113 | self.t = int(time.time() * 1000)
114 | self._image = None
115 |
116 | def login(self):
117 | """用户登录,通过SchoolClient调用
118 | """
119 | user = ZFLogin(user_client=self)
120 | user.get_login()
121 | self._http = user._http
122 | return self
123 |
124 | def init_schedule(self):
125 | if self.schedule is None:
126 | self.schedule = Schedule(self)
127 |
128 | def set_schedule_time(self, schedule_time: dict):
129 | self.schedule.schedule_parse.set_schedule_time(schedule_time=schedule_time)
130 |
131 | def get_schedule(self, year: int, term: int = 1, schedule_time: dict = None, **kwargs):
132 | """获取课表"""
133 | kwargs.setdefault("year", year)
134 | kwargs.setdefault("term", term)
135 | if self.schedule is None:
136 | self.schedule = Schedule(self, schedule_time)
137 |
138 | return self.schedule.get_schedule_dict(**kwargs)
139 |
140 | def get_class_schedule(self, year: int, term:int = 1, **kwargs):
141 | self.schedule_class = ScheduleClass(self)
142 | self.schedule_class._get_raw(year=2021, term=1, **kwargs)
143 | return "dev"
144 |
145 | def get_score(self, year: int, term: int = 1, **kwargs):
146 | """获取成绩"""
147 | kwargs.setdefault("year", year)
148 | kwargs.setdefault("term", term)
149 | if self.score is None:
150 | self.score = Score(self)
151 | return self.score.get_score(**kwargs)
152 |
153 | def get_info(self, **kwargs):
154 | """获取个人信息"""
155 | if self.info is None:
156 | self.info = Info(self)
157 | return self.info.get_info(**kwargs)
158 |
159 | def refresh_info(self, **kwargs):
160 | self.info = None
161 | return self.get_info(**kwargs)
162 |
163 | def check_session(self) -> bool:
164 | url = self.school.config.get("url_endpoints")["INDEX_URL"]
165 | resp = self.get(url)
166 | try:
167 | if not user_is_login(self.account, resp.text):
168 | # 重新登录
169 | # print('开始重新登陆')
170 | new_user = ZFLogin(user_client=self)
171 | new_user.get_login()
172 | self._http = new_user._http
173 | except LoginException as le:
174 | # print(le)
175 | raise LoginException(
176 | 400, f"重新登录出错: 账号{self.account}的 session 已过期, 重新登录失败: {str(le)}")
177 | return True
178 |
179 | # dev options
180 | def get_cookies(self):
181 | return self._http.cookies
182 |
183 | def set_cookies(self, cookies: str, **kwargs):
184 | """设置user cookies
185 |
186 | Args:
187 | cookies (str): Cookies 字符串
188 | """
189 | cookies = cookies.strip()
190 | key, value = cookies.split('=')
191 | self._http.cookies.set(key, value)
192 |
193 | def get_dev_user(self, cookies: str, **kwargs):
194 | self._http = requests.Session()
195 | self.set_cookies(cookies=cookies, **kwargs)
196 | return self
197 |
198 | def __repr__(self) -> str:
199 | return f''
--------------------------------------------------------------------------------
/school_sdk/client/api/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: __init__.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/02 22:19:19
7 | '''
8 |
9 | # from school_sdk.client.api.login_manage import LoginManagement
10 | # from school_sdk.client.settings import HOST
11 | import re
12 | import requests
13 | from fake_headers import Headers
14 | import time
15 | from pyquery import PyQuery as pq
16 | from school_sdk.client.exceptions import LoginException
17 |
18 |
19 | class BaseCrawler():
20 |
21 | BASE_URL = ''
22 | TERM = {1: 3, 2: 12, 3: 16}
23 | def __init__(self, user_client) -> None:
24 | self.user_client = user_client
25 | self.school = user_client.school or None
26 | self._http:requests.Session = user_client._http or requests.Session()
27 | self.t = int(time.time() * 1000)
28 | self.BASE_URL = user_client.school.base_url
29 |
30 | @property
31 | def account(self):
32 | return self.user_client.account
33 |
34 | def generate_headers(self, **kwargs):
35 | headers = Headers(browser="chrome", os="win", headers=True).generate()
36 | return headers
37 |
38 | def _requests(self, method: str, url_or_endpoint: str, **kwargs) -> requests.Response:
39 | if not url_or_endpoint.startswith(('http://', 'https://')):
40 | url = f'{self.BASE_URL}{url_or_endpoint}'
41 | else:
42 | url = url_or_endpoint
43 | res = self._http.request(method=method, url=url, **kwargs)
44 | return res
45 |
46 | def get(self, url, **kwargs) -> requests.Response:
47 | return self._requests(method='GET', url_or_endpoint=url, **kwargs)
48 |
49 | def post(self, url, **kwargs) -> requests.Response:
50 | return self._requests(method='POST', url_or_endpoint=url, **kwargs)
51 |
52 | def update_headers(self, headers: dict):
53 | self._client.headers.update(headers)
54 |
55 | def is_login(self, html:str):
56 | re_str = f'value="{self.account}"'
57 | result = re.search(re_str, html)
58 | if result:
59 | return True
60 | doc = pq(html)
61 | err_msg = doc('#tips').text()
62 | print(err_msg)
63 | if '验证码' in err_msg:
64 | return False
65 | raise LoginException(400, err_msg)
--------------------------------------------------------------------------------
/school_sdk/client/api/check.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: check.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/02/04 17:42:44
7 | '''
8 | from school_sdk.client.api import BaseCrawler
9 | class CheckSession(BaseCrawler):
10 |
11 | def __init__(self, user_client) -> None:
12 | super().__init__(user_client)
13 |
14 | # def check
--------------------------------------------------------------------------------
/school_sdk/client/api/class_schedule.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: class_schedule.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/02/15 13:15:14
7 | '''
8 |
9 | import re
10 | from school_sdk.client.api import BaseCrawler
11 |
12 |
13 | url = '/kbdy/bjkbdy_cxBjKb.html?gnmkdm=N214505&su=2018133209'
14 |
15 | payload = {
16 | 'xnm': 2021,
17 | 'xqm': 3,
18 | 'xnmc': '2021-2022',
19 | 'xqmmc': 1,
20 | 'xqh_id': '1, 1',
21 | 'njdm_id': 2018,
22 | 'zyh_id': 1008,
23 | 'bh_id': 10081802,
24 | 'tjkbzdm': 1,
25 | 'tjkbzxsdm': 0,
26 | 'zymc': '软件工程',
27 | 'jgmc': '计算机学院',
28 | 'bj': '18软件本科2班',
29 | 'xkrs': 34,
30 | 'zxszjjs': False,
31 | 'kzlx': 'ck'
32 | }
33 |
34 |
35 | class ScheduleClass(BaseCrawler):
36 | year = None
37 | term = None
38 |
39 | def __init__(self, user_client) -> None:
40 | super().__init__(user_client)
41 | self.raw_schedule = None
42 | self.class_schedule = None
43 |
44 | def _get_raw(self, year, term=1, grade=None, user_info=None, **kwargs):
45 | self.year = year
46 | self.term = term
47 | params = {
48 | 'gnmkdm': 'N214505',
49 | 'su': self.account
50 | }
51 | data = {
52 | 'xnm': self.year,
53 | 'xqm': self.TERM.get(term, 1),
54 | # 'xnmc': f'{self.year}-{self.year + 1}',
55 | # 'xqmmc': self.term,
56 | # 'xqh_id': '4,4',
57 | 'njdm_id': 2019,
58 | 'zyh_id': 1008,
59 | 'bh_id': 10081910,
60 | # 'tjkbzdm': 1,
61 | # 'tjkbzxsdm': 0,
62 | # 'zymc': '软件工程',
63 | # 'jgmc': '计算机学院',
64 | # 'bj': '19软件本科10班',
65 | # 'xkrs': 51,
66 | # 'zxszjjs': False,
67 | 'kzlx': 'ck'
68 | }
69 |
70 | url = self.school.config['url_endpoints']['CLASS_SCHEDULE']['API']
71 |
72 | res = self.post(url=url, params=params, data=data, **kwargs)
73 | print(res.json(), res.status_code)
74 | import json
75 |
76 | with open("test.json", 'w', encoding='utf-8') as f:
77 | f.write(json.dumps(res.json(), ensure_ascii=False))
--------------------------------------------------------------------------------
/school_sdk/client/api/login.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: login.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/02 22:12:00
7 | '''
8 | from school_sdk.check_code.type import kaptcha_func, captcha_func
9 | from school_sdk.client.exceptions import LoginException, RTKException
10 | from school_sdk.check_code import ZFCaptchaDistinguish
11 | from school_sdk.client.api import BaseCrawler
12 | from school_sdk.PyRsa.pyb64 import Base64
13 | from school_sdk.PyRsa import RsaKey
14 | from pyquery import PyQuery as pq
15 | import time
16 | import re
17 | import json
18 | import base64
19 |
20 | class ZFLogin(BaseCrawler):
21 |
22 | LOGIN_EXTEND = b'{"appName":"Netscape","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36","appVersion":"5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36"}'
23 |
24 | def get_login(self, **kwargs):
25 | """对外登录接口
26 | 1. 获取csrf token 和 原始的cookie
27 | 2. 获取rsa加密公钥
28 | 3. 通过学校配置,决定是否启用验证码验证
29 | 4. 发起登录请求
30 | Raises:
31 | LoginException: 登录失败提示
32 | """
33 | self.get_raw_csrf_and_cookie() # 获取csrf与cookie
34 | self.get_rsa_publick_key() # 获取rsa公钥
35 | if self.school.config['exist_verify']:
36 | # 处理验证码
37 | if self.captcha_type.startswith("cap"):
38 | # 滑块验证码
39 | for _ in range(self.retry):
40 | if self.verification_captcha():
41 | break
42 | if not self._post_login():
43 | raise LoginException("xxx", "滑块登录失败")
44 | return True
45 | if self.captcha_type.startswith('kap'):
46 | # 图形识别验证码
47 | for i in range(self.retry):
48 | verify_code = self.verification_kaptcha()
49 | # print(f'第{i}次验证, 识别结果: {verify_code}')
50 | is_login = self._kaptcha_login(verify_code=verify_code)
51 | if is_login:
52 | return is_login
53 | raise LoginException("xxx", "验证码登录失败")
54 | else:
55 | # 没有验证码登录
56 | if self._post_login():
57 | return True
58 |
59 | raise LoginException("xxx", "登录失败")
60 |
61 | def __init__(self, user_client) -> None:
62 | super().__init__(user_client)
63 | self.password = self.user_client.password
64 | self._csrf = None
65 | self._b64 = Base64()
66 | self._image = None
67 | self.path = self.school.config["url_endpoints"]['LOGIN']
68 | self.captcha_type:str = self.school.config['captcha_type']
69 | self.retry:int = self.school.config["retry"]
70 |
71 | def get_raw_csrf_and_cookie(self):
72 | """获取CSRF令牌
73 | """
74 | url = self.path['INDEX']
75 | res = self.get(url)
76 | doc = pq(res.text)
77 | csrf = doc("#csrftoken").attr("value")
78 | self._csrf = csrf
79 |
80 | def get_rsa_publick_key(self):
81 | """获取RSA公钥信息
82 |
83 | Returns:
84 | str: return RSA moduls and exponent
85 | """
86 | url = self.path["PUBLIC_KEY"]
87 | params = {"time": self.t, "_": self.t}
88 | headers = self.generate_headers()
89 | res = self.get(url=url, params=params, headers=headers)
90 | result_json = res.json()
91 | return result_json.get("modulus"), result_json.get('exponent')
92 |
93 | def verification_captcha(self) -> bool:
94 | """滑块验证
95 | 1. 获取图片
96 | 2. 获取验证偏移量
97 | 3. 发起验证请求
98 | """
99 | rtk = self._get_rtk()
100 | self._image = self._get_captcha_image()
101 | cap = ZFCaptchaDistinguish(self._image, captcha_func)
102 | x, y = cap.verify()
103 | track = self._get_track(x, y)
104 | captcha_verify_result = json.dumps(track).encode('utf-8')
105 | url = self.path["CAPTCHA"]
106 | data = {
107 | "instanceId": "zfcaptchaLogin",
108 | "rtk": rtk,
109 | "time": int(time.time() * 1000),
110 | "mt": base64.b64encode(captcha_verify_result),
111 | "extend": base64.b64encode(self.LOGIN_EXTEND),
112 | "type": "verify"
113 | }
114 | res = self.post(url=url, data=data)
115 | if res.status_code == 200 and res.json().get("status") == "success":
116 | return True
117 | return False
118 |
119 | def verification_kaptcha(self) -> str:
120 | """图形验证码识别"""
121 | # 下载验证码
122 | self._image = self._get_kaptcha()
123 | cap = ZFCaptchaDistinguish(self._image, kaptcha_func)
124 | return cap.verify()
125 |
126 | def _get_kaptcha(self) -> bytes:
127 | params = {"time": self.t}
128 | url = self.path['KCAPTCHA']
129 | res = self.get(url, params=params)
130 | if res.status_code == 200:
131 | return res.content
132 |
133 |
134 | def _kaptcha_login(self, verify_code:str) -> bool:
135 | """发送登录请求
136 |
137 | Returns:
138 | bool: 是否登录成功
139 | """
140 | rsa_key = RsaKey()
141 | m, e = self.get_rsa_publick_key()
142 | rsa_key.set_public(self._b64.b64tohex(m), self._b64.b64tohex(e))
143 | rr = rsa_key.rsa_encrypt(self.password)
144 | data = {
145 | 'csrftoken': self._csrf,
146 | 'language': 'zh_CN',
147 | 'yhm': self.account,
148 | 'mm': self._b64.hex2b64(rr),
149 | 'yzm': verify_code
150 | }
151 | params = {"time": self.t}
152 | url = self.path['INDEX']
153 | res = self.post(url, params=params, data=data)
154 | return self._is_login(res.text)
155 |
156 | def _post_login(self) -> bool:
157 | """发送登录请求
158 |
159 | Returns:
160 | bool: 是否登录成功
161 | """
162 | rsa_key = RsaKey()
163 | m, e = self.get_rsa_publick_key()
164 | rsa_key.set_public(self._b64.b64tohex(m), self._b64.b64tohex(e))
165 | rr = rsa_key.rsa_encrypt(self.password)
166 | data = {
167 | 'csrftoken': self._csrf,
168 | 'yhm': self.account,
169 | 'mm': self._b64.hex2b64(rr)
170 | }
171 | params = {"time": self.t}
172 | url = self.path['INDEX']
173 | res = self.post(url, params=params, data=data)
174 | return self._is_login(res.text)
175 |
176 | def _is_login(self, html) -> bool:
177 | """工具函数,判断是否登录成功
178 |
179 | Args:
180 | html (str): html string.
181 |
182 | Returns:
183 | bool: html string 是否存在用户
184 | """
185 | re_str = f'value="{self.account}"'
186 | result = re.search(re_str, html)
187 | if result:
188 | return True
189 | # 错误流程
190 | doc = pq(html)
191 | err_msg = doc('#tips').text()
192 | if '验证码' in err_msg:
193 | return False
194 | raise LoginException(400, err_msg)
195 |
196 |
197 | def _get_captcha_image(self):
198 | """获取验证码
199 | 1. 获取rtk、si、imtk等信息
200 | 2. 下载图片
201 | """
202 | params = {
203 | "type": "refresh",
204 | "time": {self.t},
205 | "instanceId": "zfcaptchaLogin"
206 | }
207 | url = self.path["CAPTCHA"]
208 | res = self.get(url, params=params)
209 | captcha_data = res.json()
210 | params.update({
211 | "type": "image",
212 | "imtk": captcha_data.get("imtk"),
213 | "id": captcha_data.get("si")
214 | })
215 | url = self.path["CAPTCHA"]
216 | res = self.get(url=url, params=params)
217 | if res.status_code == 200:
218 | return res.content
219 |
220 | def _get_rtk(self) -> str:
221 | """获取rtk
222 | 从JavaScript文件中提前rtk
223 | """
224 | url = self.path['CAPTCHA']
225 | params = {
226 | "type": "resource",
227 | "instanceId": "zfcaptchaLogin",
228 | "name": "zfdun_captcha.js"
229 | }
230 | res = self.get(url, params=params)
231 | result = re.search("tk:'(.*)',", res.text)
232 | try:
233 | return result.group(1)
234 | except:
235 | raise RTKException("rtk解析错误")
236 |
237 | def _get_track(self, distance, y) -> list:
238 | """模拟人手滑动
239 | 通过设置前快后慢的加速度,模拟人手滑动
240 |
241 | Args:
242 | distance ([int]): [移动距离]
243 | y ([int]): [滑块Y值]
244 |
245 | Returns:
246 | [list]: [坐标数组]
247 | """
248 | start = 1200
249 | current = 0
250 | track = []
251 | # 减速阈值
252 | mid = distance * 4 / 5
253 | # 计算间隔
254 | t = 0.2
255 | # 初速度
256 | v = 0
257 | while current < distance:
258 | # 加速->加速度为 2; 减速->加速度为-3
259 | a = 2 if current < mid else -3
260 | v0 = v
261 | # 当前速度 v = v0 + at
262 | v = v0 + a * t
263 | # 移动距离 x = v0t + 1/2 * a * t^2
264 | move = v0 * t + 1 / 2 * a * t * t
265 | # 当前位移量
266 | current += move
267 | # 加入轨迹
268 | track.append({"x": start + int(current), "y": y,
269 | "t": int(time.time() * 1000)})
270 | time.sleep(0.01)
271 | return track
272 |
273 |
274 |
275 |
--------------------------------------------------------------------------------
/school_sdk/client/api/schedule_parse.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: schedule_parse.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/14 13:03:36
7 | '''
8 | import re
9 |
10 |
11 | class ScheduleParse():
12 | __SCHEDULE_TIME = {
13 | "1": [8, 30],
14 | "2": [9, 20],
15 | "3": [10, 25],
16 | "4": [11, 15],
17 | "5": [14, 40],
18 | "6": [15, 30],
19 | "7": [16, 30],
20 | "8": [17, 20],
21 | "9": [19, 30],
22 | "10": [20, 20]
23 | }
24 |
25 | def __init__(self, content=None, schedule_time:dict=None) -> None:
26 | self.raw = content
27 | self.parse_list:list = []
28 | self.parse_dict:dict = {}
29 | self.parse_ics = None
30 | if schedule_time != None:
31 | self.SCHEDULE_TIME = schedule_time or self.__SCHEDULE_TIME
32 |
33 | def set_schedule_time(self, schedule_time:dict):
34 | self.SCHEDULE_TIME = schedule_time or self.__SCHEDULE_TIME
35 |
36 | def get_dict(self):
37 | return self.parse_dict
38 |
39 | def get_list(self):
40 | return self.parse_list
41 |
42 | def load(self, content):
43 | """初始化
44 |
45 | Args:
46 | content (string): 课表原始数据
47 | """
48 | self.raw = content
49 | self._parse()
50 |
51 | def _get_color(self, index):
52 | t = ['green', 'blue', 'purple', 'red', 'yellow']
53 | return t[index]
54 |
55 | def get_color(self, compare_target, compare_list):
56 | for item in compare_list:
57 | if compare_target.get('kcmc') == item.get("course") and compare_target.get('kcmc') != None:
58 | return item.get('color')
59 | # 随机返回颜色
60 | time = compare_target.get('jcs').split('-')
61 | return self._get_color((int(compare_target.get('xqj')) * 3 + int(time[0]) + 1) % 5)
62 |
63 | def _parse(self):
64 | """解析课表
65 | 姓名、班级、课程、时间、地点、校区、节数、周数等详细信息
66 | """
67 | self.parse_list:list = []
68 | self.parse_dict:dict = {}
69 | self.parse_ics = None
70 |
71 | user_message: dict = self.raw.get("xsxx")
72 | schedule_list: list = self.raw.get("kbList")
73 | # 用户基本信息
74 | user_class_name = user_message.get("BJMC")
75 | username = user_message.get("XM")
76 |
77 | # get schedule items
78 | for course in schedule_list:
79 | weeks_arr = self.get_course_week(course.get('zcd'))
80 | time_text = f"{course.get('xqjmc')} {course.get('jc')}"
81 | time = self.get_class_time(course.get('jcs'))
82 | color = self.get_color(course, self.parse_list)
83 | self.parse_list.append({
84 | "course": course.get('kcmc', "找不到课程名"),
85 | "place": course.get('cdmc', "找不到上课地点"),
86 | "campus": course.get('xqmc', "南城"),
87 | "teacher": course.get('xm'),
88 | "weeks_text": course.get('zcd'),
89 | "week_day": course.get('xqj'),
90 | "week_day_text": course.get('xqjmc'),
91 | "time_text": time_text,
92 | "weeks_arr": weeks_arr,
93 | "time": time,
94 | "color": color,
95 | "section": course.get('jcs')
96 | })
97 |
98 | self.parse_dict.setdefault("class_name", user_class_name)
99 | self.parse_dict.setdefault("username", username)
100 | self.parse_dict.setdefault("course_list", self.parse_list)
101 |
102 | def get_class_time(self, b2e:str):
103 | """获取课程的开始和结束的上课时间
104 | 如某课程为早上一二节(1-2), 则开始时间为第一节的时间, 结束时间为第二节课的上课时间。
105 | 注意:是开始和结束的`上课时间`
106 | e.g: 第一二节为8.30-9.15, 中间休息5分钟, 9.20-10.05
107 | 返回: {
108 | 'start': [8, 30],
109 | 'last': [9, 20]
110 | }
111 | Args:
112 | b2e (str): 原始范围字符串, 如`1-2`
113 |
114 | Returns:
115 | [type]: 课程开始和课程结束的时间
116 | """
117 | start, end = b2e.split('-')
118 | start_time = self.SCHEDULE_TIME[start]
119 | end_time = self.SCHEDULE_TIME[end]
120 | return {"start": start_time, "last": end_time}
121 |
122 | def get_course_week(self, week_text: str) -> list:
123 | """获得这门课程一共要上的详细周数
124 |
125 | Args:
126 | zcd (str): zcd正方原始数据对应的key名
127 |
128 | Returns:
129 | list: 要上该门课的星期
130 | """
131 | interval = week_text.split(",")
132 | weeks = []
133 |
134 | for week in interval:
135 | leap = 1
136 | if "(单)" in week or "(双)" in week:
137 | week = week.replace("(双)", "")
138 | week = week.replace("(单)", "")
139 | leap = 2
140 | re_result = re.search("(\d+).?(\d*).*", week)
141 | real = re_result.groups()
142 | if real[-1] == '':
143 | weeks += [int(real[0])]
144 | else:
145 | # for start to end week
146 | weeks += [i for i in range(
147 | int(real[0]), int(real[1]) + 1, leap)]
148 |
149 | return weeks
150 |
--------------------------------------------------------------------------------
/school_sdk/client/api/schedules.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: schedules.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/08 11:16:22
7 | '''
8 |
9 |
10 | from school_sdk.client.api.schedule_parse import ScheduleParse
11 | from school_sdk.client.api import BaseCrawler
12 | from school_sdk.client.exceptions import LoginException
13 | from school_sdk.client.utils import user_is_login
14 |
15 |
16 | class Schedule(BaseCrawler):
17 | year = None
18 | term = None
19 | def __init__(self, user_client, schedule_time:dict = None) -> None:
20 | """课表类
21 |
22 | Args:
23 | user_client (UserClient): 已登录用户实例
24 | """
25 | super().__init__(user_client=user_client)
26 | self.raw_schedule = None
27 | self.schedule = None
28 | self.schedule_parse: ScheduleParse = ScheduleParse(schedule_time=schedule_time)
29 |
30 | def refresh_schedule(self):
31 | """刷新课表数据
32 | """
33 | self.raw_schedule = None
34 | self.schedule = None
35 | self.load_schedule()
36 |
37 | def get_schedule_dict(self, **kwargs) -> dict:
38 | """获取解析后的课表数据
39 |
40 | Returns:
41 | dict: 解析后的课表数据
42 | """
43 | if not self.is_load_schedule():
44 | self.load_schedule(**kwargs)
45 | if kwargs.get("year") != self.year or kwargs.get("term") != self.term:
46 | self.load_schedule(**kwargs)
47 | return self.schedule_parse.get_dict()
48 |
49 | def get_schedule_list(self, **kwargs):
50 | """获取解析后的课表列表
51 | 仅课表列表
52 | Returns:
53 | list: 仅课表列表
54 | """
55 | if not self.is_load_schedule():
56 | self.load_schedule()
57 | return self.schedule_parse.get_list()
58 |
59 | def get_raw_schedule(self, **kwargs):
60 | """获取原始课表数据
61 |
62 | Returns:
63 | [json]: 原始课表数据
64 | """
65 | if self.raw_schedule is None:
66 | self.load_schedule()
67 | return self.raw_schedule
68 |
69 | def parse_ics(self):
70 | """解析成ics日历格式
71 | """
72 | pass
73 |
74 | def is_load_schedule(self):
75 | return False if self.raw_schedule is None else True
76 |
77 | def load_schedule(self, **kwargs):
78 | """加载课表
79 | """
80 | self.raw_schedule = self._get_student_schedule(**kwargs)
81 | self.schedule_parse.load(self.raw_schedule)
82 |
83 | def _get_student_schedule(self, year, term, **kwargs):
84 | self.year = year
85 | self.term = term
86 | params = {
87 | "gnmkdm": "N2151",
88 | "su": self.account
89 | }
90 |
91 | data = {
92 | "xnm": year,
93 | "xqm": self.TERM.get(term, 1),
94 | "kzlx": "ck"
95 | }
96 | url = self.school.config['url_endpoints']['SCHEDULE']['API']
97 |
98 | res = self.post(url=url, params=params, data=data, **kwargs)
99 | # print(res.text, res, res.status_code)
100 | if user_is_login(self.account, res.text):
101 | return res.json()
102 | raise LoginException()
103 |
--------------------------------------------------------------------------------
/school_sdk/client/api/score.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: score.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/20 20:06:29
7 | '''
8 | # cjcx/cjcx_cxDgXscj.html?doType=query&gnmkdm=N305005&su=2018133209
9 |
10 | from school_sdk.client.api import BaseCrawler
11 |
12 |
13 | class Score(BaseCrawler):
14 | year = None
15 | term = None
16 | def __init__(self, user_client) -> None:
17 | super().__init__(user_client)
18 | self.endpoints: dict = self.school.config['url_endpoints']
19 | self.raw_score = None
20 | self.score_dict:dict = {}
21 | self.score_list:list = []
22 |
23 | def get_score(self, **kwargs):
24 | return self.get_score_dict(**kwargs)
25 |
26 | def get_score_list(self, **kwargs):
27 | """获取成绩清单-列表
28 |
29 | Returns:
30 | list: 成绩列表
31 | """
32 | if not self.score_list:
33 | self.parse(**kwargs)
34 | return self.score_list
35 |
36 | def get_score_dict(self, **kwargs):
37 | """获取成绩清单-字典
38 |
39 | Returns:
40 | dict: 成绩字典清单
41 | """
42 | if not self.score_dict:
43 | self.parse(**kwargs)
44 | if kwargs.get('year') != self.year or kwargs.get('term') != self.term:
45 | self.raw_score = None
46 | self.parse(**kwargs)
47 | return self.score_dict
48 |
49 | def parse(self, **kwargs):
50 | """解析数据
51 | """
52 | if self.raw_score is None:
53 | self.load_score(**kwargs)
54 | self._parse(self.raw_score)
55 |
56 | def load_score(self, **kwargs) -> None:
57 | """加载课表
58 | """
59 | self.raw_score = self._get_score(**kwargs)
60 |
61 | def _get_score(self, year: int, term: int = 1, **kwargs):
62 | """获取教务系统成绩
63 |
64 | Args:
65 | year (int): 学年
66 | term (int, optional): 学期. Defaults to 1.
67 |
68 | Returns:
69 | json: json数据
70 | """
71 | self.year = year
72 | self.term = term
73 | url = self.endpoints['SCORE']['API']
74 |
75 | params = {
76 | 'doType': 'query',
77 | 'gnmkdm': 'N305005',
78 | 'su': self.account
79 | }
80 |
81 | data = {
82 | 'xnm': year,
83 | 'xqm': self.TERM.get(term, 3),
84 | '_search': False,
85 | 'nd': self.t,
86 | 'queryModel.showCount': 500,
87 | 'queryModel.currentPage': 1,
88 | 'queryModel.sortName': None,
89 | 'queryModel.sortOrder': 'asc',
90 | 'time': 4,
91 | }
92 |
93 | res = self.post(url=url, params=params, data=data, **kwargs)
94 | return res.json()
95 |
96 | def _parse(self, raw: dict):
97 | # kcmc -> 课程名称 # kch -> 课程号 # kcxzmc -> 课程性质名称 # kcbj -> 课程标记
98 | # jsxm -> 教师姓名 # tjsj -> 提交时间 # khfsmc -> 考核方式 # ksxz -> 考试性质
99 | # cj -> 成绩 # bfzcj -> 百分制成绩 # xf -> 学分 # kkbmmc -> 开课部门名称
100 | # njdm_id -> 年级代码 # jd -> 绩点 # bzxx -> 备注信息
101 | """解析教务系统成绩
102 |
103 | Args:
104 | raw (dict): 教务系统的原始数据
105 | """
106 | self.score_dict:dict = {}
107 | self.score_list:list = []
108 | items = raw.get('items')
109 | for item in items:
110 | format_item = {
111 | "course_name": item.get('kcmc'), # kcmc -> 课程名称
112 | "course_code": item.get('kch'), # kch -> 课程号
113 | 'course_nature': item.get('kcxzmc'), # kcxzmc -> 课程性质名称
114 | 'course_target': item.get('kcbj'), # kcbj -> 课程标记
115 | 'teacher': item.get('jsxm'), # jsxm -> 教师姓名
116 | 'submitted_at': item.get('tjsj'), # tjsj -> 提交时间
117 | 'exam_method': item.get('khfsmc'), # khfsmc -> 考核方式
118 | 'exam_nature': item.get('ksxz'), # ksxz -> 考试性质
119 | 'exam_result': item.get('cj'), # cj -> 成绩
120 | 'exam_score': item.get('bfzcj'), # bfzcj -> 百分制成绩
121 | 'credit': item.get('xf'), # xf -> 学分
122 | 'course_group': item.get('kkbmmc'), # kkbmmc -> 开课部门名称
123 | 'grade': item.get('njdm_id'), # njdm_id -> 年级代码
124 | 'grade_point': item.get('jd'), # jd -> 绩点
125 | 'reason': item.get('bzxx') # bzxx -> 备注信息
126 | }
127 | self.score_list.append(format_item)
128 | self.score_dict.setdefault(item.get('kcmc'), format_item)
129 |
--------------------------------------------------------------------------------
/school_sdk/client/api/user_info.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: user_info.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/11/25 11:50:16
7 | '''
8 |
9 | from school_sdk.client.api import BaseCrawler
10 | from pyquery import PyQuery as pq
11 |
12 |
13 |
14 | class Info(BaseCrawler):
15 |
16 | def __init__(self, user_client) -> None:
17 | """获取用户信息类
18 |
19 | Args:
20 | user_client (UserClient): 已登录的用户实例
21 | """
22 | super().__init__(user_client)
23 | self.raw_info = None
24 | self.info = None
25 |
26 | def get_info(self, **kwargs):
27 | if self.raw_info is None:
28 | self.raw_info = self._get_raw_info(**kwargs)
29 | self.info = self._parse(self.raw_info)
30 | return self.info
31 |
32 |
33 | def _get_raw_info(self, **kwargs):
34 | """获取用户信息原始数据
35 | """
36 |
37 | params = {
38 | "gnmkdm": "N100801",
39 | "layout": "default",
40 | "su": self.account
41 | }
42 | url = self.school.config['url_endpoints']['INFO']['API']
43 |
44 | result = self.get(url=url, params=params, **kwargs)
45 | return result.content
46 |
47 | def _parse(self, html:str) -> dict:
48 | doc = pq(html)
49 | info = {
50 | 'student_number': doc('#ajaxForm > div > div.panel-heading > div > div:nth-child(1) > div > div > p').text(),
51 | 'name': doc('#ajaxForm > div > div.panel-heading > div > div:nth-child(2) > div > div > p').text(),
52 | 'department_name': doc('#col_jg_id > p').text(),
53 | 'class_name': doc('#col_bh_id > p').text(),
54 | 'grade': doc('#col_njdm_id > p').text(),
55 | 'graduation_school': doc('#col_byzx > p').text(),
56 | 'major': doc('#col_zyfx_id > p').text(),
57 | 'gender': doc('#col_xbm > p').text()
58 | }
59 |
60 | return info
61 |
--------------------------------------------------------------------------------
/school_sdk/client/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: base.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/08 00:17:24
7 | '''
8 | from school_sdk.utils import is_endpoint
9 | import requests
10 | from fake_headers import Headers
11 |
12 |
13 | class BaseUserClient():
14 | BASE_URL = ""
15 | _http = None
16 |
17 | def __init__(self) -> None:
18 | self._http = requests.Session()
19 | self._http.headers.update({
20 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) '
21 | 'AppleWebKit/537.36 (KHTML, like Gecko) '
22 | 'Chrome/62.0.3202.89 Safari/537.36',
23 | 'Content-Type': 'application/x-www-form-urlencoded',
24 | })
25 |
26 | def _generate_headers(self, **kwargs):
27 | headers = Headers(browser="chrome", os="win", headers=True).generate()
28 | return headers
29 |
30 | def _request(self, method, url_or_endpoint, **kwargs) -> requests.models.Response:
31 | if is_endpoint(url_or_endpoint=url_or_endpoint):
32 | url = f'{self.BASE_URL}{url_or_endpoint}'
33 | else:
34 | url = url_or_endpoint
35 | res = self._http.request(
36 | method=method,
37 | url = url,
38 | **kwargs
39 | )
40 | return res
41 |
42 | def get(self, url, **kwargs):
43 | return self._request(method='GET', url_or_endpoint=url, **kwargs)
44 |
45 | def post(self, url, **kwargs):
46 | return self._request(method='POST', url_or_endpoint=url, **kwargs)
47 |
48 | def _update_headers(self, headers_dict):
49 | self._http.headers.update(headers_dict)
50 |
51 |
52 |
--------------------------------------------------------------------------------
/school_sdk/client/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: exceptions.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/02 23:49:32
7 | '''
8 |
9 | class SchoolException(Exception):
10 | """Base exception for school-api"""
11 |
12 | def __init__(self, name, school_code, errmsg):
13 | self.name = name
14 | self.errmsg = errmsg
15 | self.school_code = school_code
16 |
17 | def __str__(self):
18 | msg = f'{self.errmsg}'
19 | return msg
20 |
21 | class LoginException(SchoolException):
22 |
23 | def __init__(self, school_code, errmsg):
24 | super(LoginException, self).__init__('登录错误', school_code, errmsg)
25 |
26 |
27 | class RTKException(ValueError):
28 |
29 | def __init__(self, *args: object) -> None:
30 | super().__init__(*args)
--------------------------------------------------------------------------------
/school_sdk/client/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: utils.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/02/04 01:39:51
7 | '''
8 | import re
9 | from pyquery import PyQuery as pq
10 | from school_sdk.client.exceptions import LoginException
11 |
12 | def user_is_login(account, html) -> bool:
13 | """工具函数,判断是否登录成功
14 |
15 | Args:
16 | account (str): 教务系统账号.
17 | html (str): html string.
18 |
19 | Raises:
20 | LoginException: 教务系统错误信息
21 |
22 | Returns:
23 | bool: html string 是否存在用户
24 | """
25 |
26 | re_str = f'value="{account}"'
27 | result = re.search(re_str, html)
28 | if result:
29 | return True
30 |
31 | re_str = f'id="tips"'
32 | result = re.search(re_str, html)
33 | if not result:
34 | return True
35 |
36 | doc = pq(html)
37 | err_msg = doc('#tips').text()
38 |
39 | if err_msg == "":
40 | return False
41 | # 错误流程
42 | if '验证码' in err_msg:
43 | return False
44 | return False
45 | raise LoginException(400, err_msg)
46 |
47 |
--------------------------------------------------------------------------------
/school_sdk/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: config.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/04 23:39:20
7 | '''
8 |
9 | # INDEX http://192.168.2.123:7899/xtgl/index_initMenu.html?jsdm=xs&_t=1643879775142
10 |
11 | URL_ENDPOINT = {
12 | "HOME_URL": "/xtgl/login_slogin.html",
13 | "INDEX_URL": "/xtgl/index_initMenu.html",
14 | 'LOGIN': {
15 | 'INDEX': '/xtgl/login_slogin.html',
16 | 'CAPTCHA': '/zfcaptchaLogin',
17 | 'KCAPTCHA': '/kaptcha',
18 | 'PUBLIC_KEY': '/xtgl/login_getPublicKey.html',
19 | },
20 | "SCORE_URL": "",
21 | "INFO_URL": "",
22 | "SCHEDULE": {
23 | "API": '/kbcx/xskbcx_cxXsKb.html',
24 | },
25 | "CLASS_SCHEDULE": {
26 | "API": '/kbdy/bjkbdy_cxBjKb.html'
27 | },
28 | 'SCORE': {
29 | # cjcx_cxXsgrcj
30 | 'API': '/cjcx/cjcx_cxDgXscj.html'
31 | },
32 | 'INFO': {
33 | 'API': '/xsxxxggl/xsgrxxwh_cxXsgrxx.html'
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/school_sdk/session/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: __init__.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2022/02/03 23:54:56
7 | '''
8 |
9 | class RedisStorage(object):
10 |
11 | def __init__(self, redis) -> None:
12 | pass
13 |
14 | def name(self):
15 | pass
--------------------------------------------------------------------------------
/school_sdk/type.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: __init__.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2024/01/12 19:18:47
7 | '''
8 |
9 | from .client import CAPTCHA, KCAPTCHA
10 |
--------------------------------------------------------------------------------
/school_sdk/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: utils.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/04 23:45:40
7 | '''
8 |
9 | class ObjectDict(dict):
10 | """:copyright: (c) 2014 by messense.
11 | Makes a dictionary behave like an object, with attribute-style access.
12 | """
13 |
14 | def __getattr__(self, key):
15 | if key in self:
16 | return self[key]
17 | return None
18 |
19 | def __setattr__(self, key, value):
20 | self[key] = value
21 |
22 | def __getstate__(self):
23 | return None
24 |
25 |
26 | def is_endpoint(url_or_endpoint:str) -> bool:
27 | """判断是不是端点
28 |
29 | Args:
30 | url_or_endpoint (str): url 或 端点字符串
31 |
32 | Returns:
33 | bool: 不是http则返回False
34 | """
35 | if url_or_endpoint.startswith(('http://', 'https://')):
36 | return False
37 | return True
38 |
39 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: setup.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/20 11:11:54
7 | '''
8 |
9 | from os import path
10 | from setuptools import setup, find_packages
11 |
12 | basedir = path.abspath(path.dirname(__file__))
13 |
14 | with open(path.join(basedir, "README.md"), encoding='utf-8') as f:
15 | long_description = f.read()
16 |
17 |
18 | setup(
19 | name="school-sdk",
20 | author="farmer.chillax",
21 | version="1.6.0",
22 | license='MIT',
23 | author_email="farmer-chong@qq.com",
24 | description="zf School SDK for Python",
25 | long_description=long_description,
26 | long_description_content_type='text/markdown',
27 | url='https://github.com/Farmer-chong/new-school-sdk',
28 | packages=find_packages(),
29 | package_data={"school_sdk": ['check_code/model.pkl']},
30 |
31 | include_package_data=True,
32 | platforms='any',
33 | zip_safe=False,
34 |
35 | install_requires=[
36 | 'requests',
37 | 'pyquery',
38 | 'bs4',
39 | 'Pillow',
40 | 'fake-headers',
41 | 'torch',
42 | 'torchvision',
43 | ],
44 | classifiers=[
45 | 'Environment :: Web Environment',
46 | 'Intended Audience :: Developers',
47 | 'License :: OSI Approved :: MIT License',
48 | 'Operating System :: OS Independent',
49 | 'Programming Language :: Python :: 3.8',
50 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
51 | 'Topic :: Software Development :: Libraries :: Python Modules'
52 | ]
53 | )
54 |
55 |
56 | # python setup.py bdist_wheel sdist
57 | # twine upload dist/*
--------------------------------------------------------------------------------
/test/test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: test.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/02 23:09:54
7 | '''
8 |
9 |
10 | import sys
11 | import os
12 |
13 | cur_path = os.path.abspath(__file__)
14 | parent = os.path.dirname
15 | sys.path.append(parent(parent(cur_path)))
16 | from dotenv import load_dotenv, find_dotenv
17 | load_dotenv(find_dotenv(), override=True)
18 |
19 | import ssl
20 | ssl.OPENSSL_VERSION = ssl.OPENSSL_VERSION.replace("LibreSSL", "OpenSSL")
21 | from school_sdk.client import UserClient
22 | from school_sdk import SchoolClient
23 |
24 | SCHOOL_HOST = os.getenv("SCHOOL_HOST")
25 | SCHOOL_ACCOUNT = os.getenv("SCHOOL_ACCOUNT")
26 | SCHOOL_PASSWORD = os.getenv("SCHOOL_PASSWORD")
27 |
28 | # 实例化学校
29 | Gdust = SchoolClient(host=SCHOOL_HOST, port=443,ssl=True, exist_verify=False)
30 |
31 | # 实例化用户
32 | user:UserClient = Gdust.user_login(SCHOOL_ACCOUNT, SCHOOL_PASSWORD)
33 | print(user.check_session())
34 |
35 | # 获取课表
36 | course = user.get_schedule(year=2022, term=2, schedule_time={
37 | "1": [8, 30],
38 | "2": [9, 20],
39 | "3": [10, 10],
40 | "4": [11, 00],
41 | "5": [13, 30],
42 | "6": [14, 20],
43 | "7": [15, 10],
44 | "8": [16, 00],
45 | "9": [18, 30],
46 | "10": [19, 20],
47 | "11": [20, 10],
48 | })
49 | # user.get_schedule()
50 | print(course)
51 |
52 | # 获取成绩, 2020-2021学年第一学期的成绩
53 | score = user.get_score(year=2022, term=1)
54 | print(score)
55 |
56 | # 获取个人信息
57 | info = user.get_info()
58 | print(info)
--------------------------------------------------------------------------------
/zf-setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''
3 | :file: setup.py
4 | :author: -Farmer
5 | :url: https://blog.farmer233.top
6 | :date: 2021/09/20 11:11:54
7 | '''
8 |
9 | from os import path
10 | from setuptools import setup, find_packages
11 |
12 | basedir = path.abspath(path.dirname(__file__))
13 |
14 | with open(path.join(basedir, "README.md"), encoding='utf-8') as f:
15 | long_description = f.read()
16 |
17 |
18 | setup(
19 | name="zf-school-sdk",
20 | author="farmer.chillax",
21 | version="1.6.0",
22 | license='MIT',
23 | author_email="farmer-chong@qq.com",
24 | description="zf School SDK for Python",
25 | long_description=long_description,
26 | long_description_content_type='text/markdown',
27 | url='https://github.com/Farmer-chong/new-school-sdk',
28 | packages=find_packages(),
29 | # package_data={},
30 | package_data={"school_sdk": ['check_code/model.pkl']},
31 | include_package_data=True,
32 | platforms='any',
33 | zip_safe=False,
34 |
35 | install_requires=[
36 | 'requests',
37 | 'pyquery',
38 | 'bs4',
39 | 'Pillow',
40 | 'fake-headers',
41 | 'torch',
42 | 'torchvision',
43 | ],
44 | classifiers=[
45 | 'Environment :: Web Environment',
46 | 'Intended Audience :: Developers',
47 | 'License :: OSI Approved :: MIT License',
48 | 'Operating System :: OS Independent',
49 | 'Programming Language :: Python :: 3.8',
50 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
51 | 'Topic :: Software Development :: Libraries :: Python Modules'
52 | ]
53 | )
54 |
55 | # python zf-setup.py bdist_wheel sdist
56 | # twine upload dist/*
--------------------------------------------------------------------------------