├── .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 | [![Downloads](https://pepy.tech/badge/school-sdk)](https://pepy.tech/project/school-sdk) 14 | 15 | python3 16 | 17 | 18 | license 19 | 20 | 21 | stars 22 | 23 | 24 | forks 25 | 26 | [![PyPI Version](http://img.shields.io/pypi/v/school-sdk.svg)](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 | [![pypi](https://img.shields.io/pypi/v/school-sdk.svg)](https://pypi.org/project/school-sdk/) 9 | [![Downloads](https://pepy.tech/badge/school-sdk)](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/* --------------------------------------------------------------------------------