├── .editorconfig ├── .gitbook.yaml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── README.md ├── SUMMARY.md ├── apis │ ├── README.md │ ├── card.md │ ├── course.md │ ├── create.md │ ├── exam.md │ ├── person.md │ └── public.md ├── changelog.md ├── install.md └── quickstart.md ├── hdu_api ├── __init__.py ├── __version__.py ├── _internal_utils.py ├── _pyDes.py ├── api.py ├── config.py ├── exceptions.py ├── models.py └── sessions.py ├── setup.cfg └── setup.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.py] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.html] 14 | indent_size = 2 15 | 16 | [*.json] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # idea 107 | .idea 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE pytest.ini requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | HDU-API 3 |

4 |

5 | A simple SDK for HDU. 6 |

7 |

8 | 9 | 10 | 11 | 12 | 996.icu 13 |

14 | 15 | --- 16 | 17 | hdu-api 是一个集结 HDU 所有教务管理服务的 SDK,提供了一卡通服务、考试、课表、选课和一些公共信息如空闲教室、上课时间等信息的 API。 hdu-api 主要基于 Requests 库和 Beautiful Soup 库写成。 18 | 19 | ## 特性 20 | 21 | * 支持一卡通服务的信息查询 22 | * 支持教务管理系统的考试、课程等信息查询 23 | * 支持学生管理系统的信息查询 24 | * 支持 ihdu PC 版和手机版的信息查询 25 | * 易用,友好的 API 26 | * 基于 requests 库,支持每个网站的 session 使用和管理,重用性高 27 | * 自定义,对返回数据进行自定义化 28 | 29 | ## 安装 30 | 31 | 使用包管理器安装,如 pip: 32 | 33 | ```text 34 | pip install hdu-api 35 | ``` 36 | 37 | ## 快速开始 38 | 39 | ```text 40 | >>> import hdu_api 41 | >>> hdu = hdu_api.HDU('学号', '密码') 42 | >>> client = hdu.create() 43 | >>> client.exam.schedule_current() 44 | [{'classroom': '第12教研楼201', 45 | 'course_name': '操作系统(甲)', 46 | 'exam_time': '2019年1月17日(09:00-11:00)', 47 | 'exam_type': '', 48 | 'seat': '10', 49 | 'select_code': '(2018-2019-1)-A0507050-06018-1', 50 | 'staff_name': 'xxx'}, 51 | 52 | ... 53 | 54 | {'classroom': '第6教研楼北308', 55 | 'course_name': '软件工程(甲)', 56 | 'exam_time': '2019年1月9日(13:45-15:45)', 57 | 'exam_type': '', 58 | 'seat': '24', 59 | 'select_code': '(2018-2019-1)-A0507190-06061-2', 60 | 'staff_name': 'xxx'}] 61 | 62 | >>> client.card.balance() 63 | [{'account_id': 'xxxxxx', 64 | 'balance': '69.97', 65 | 'card_id': 'xxxxxx', 66 | 'staff_id': 'xxxxxx', 67 | 'staff_name': 'xxx'}] 68 | ``` 69 | 70 | ## 文档 71 | 72 | https://liuxingran.gitbook.io/hdu-api/ 73 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | > hdu-api: A simple SDK for HDU. 4 | 5 | ![](https://img.shields.io/pypi/v/hdu-api.svg?style=flat) ![](https://img.shields.io/pypi/pyversions/hdu-api.svg?style=flat) ![](https://img.shields.io/pypi/l/hdu-api.svg?style=flat) 6 | 7 | ```text 8 | ___ ___ ___ ___ 9 | / /\ / /\ / /\ / /\ ___ ___ 10 | / /:/ / /::\ / /:/ / /::\ / /\ /__/\ 11 | / /:/ / /:/\:\ / /:/ / /:/\:\ / /::\ \__\:\ 12 | / /::\ ___ / /:/ \:\ / /:/ / /::\ \:\ / /:/\:\ / /::\ 13 | /__/:/\:\ /\/__/:/ \__\:|/__/:/ /\/__/:/\:\_\:\ / /::\ \:\ __/ /:/\/ 14 | \__\/ \:\/:/\ \:\ / /:/\ \:\ /:/\__\/ \:\/://__/:/\:\_\:\/__/\/:/~~ 15 | \__\::/ \ \:\ /:/ \ \:\ /:/ \__\::/ \__\/ \:\/:/\ \::/ 16 | / /:/ \ \:\/:/ \ \:\/:/ / /:/ \ \::/ \ \:\ 17 | /__/:/ \__\::/ \ \::/ /__/:/ \__\/ \__\/ 18 | \__\/ ~~ \__\/ \__\/ 19 | ``` 20 | 21 | hdu-api 是一个集结 HDU 所有教务管理服务的 SDK,提供了一卡通服务、考试、课表、选课和一些公共信息如空闲教室、上课时间等信息的 API。 hdu-api 主要基于 Requests 库和 Beautiful Soup 库写成。 22 | 23 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [简介](README.md) 4 | * [安装](install.md) 5 | * [快速上手](quickstart.md) 6 | * [APIS](apis/README.md) 7 | * [hdu\_api](apis/create.md) 8 | * [一卡通 API](apis/card.md) 9 | * [考试 API](apis/exam.md) 10 | * [课程 API](apis/course.md) 11 | * [个人信息 API](apis/person.md) 12 | * [公共信息 API](apis/public.md) 13 | * [changelog](changelog.md) 14 | 15 | -------------------------------------------------------------------------------- /docs/apis/README.md: -------------------------------------------------------------------------------- 1 | # APIS 2 | 3 | `hdu-api` 提供了非常丰富的 API,此文档包含了全部 API 的详细描述,请选择你感兴趣的部分开始了解它们吧。 4 | 5 | {% page-ref page="create.md" %} 6 | 7 | {% page-ref page="card.md" %} 8 | 9 | {% page-ref page="exam.md" %} 10 | 11 | {% page-ref page="course.md" %} 12 | 13 | {% page-ref page="person.md" %} 14 | 15 | {% page-ref page="public.md" %} 16 | 17 | -------------------------------------------------------------------------------- /docs/apis/card.md: -------------------------------------------------------------------------------- 1 | # 一卡通 API 2 | 3 | 此章节是关于一卡通 API 的描述。 4 | 5 | ## `Card` objects 6 | 7 | [`Card`](card.md#card-objects) 对象提供了一系列一卡通信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Card**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `CardSession` | true | 无 | 一个已初始化的 `CardSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Card.**account**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 20 | 21 |   一卡通账户信息。 22 | 23 | - _classmethod_ Card.**balance**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 24 | 25 |   一卡通余额。 26 | 27 | - _classmethod_ Card.**consume**(*year, month, raw=False, dictionary=DEFAULT_DICTIONARY*) 28 | 29 |   某年某月一卡通流水。 30 | 31 | - _classmethod_ Card.**consume_today**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 32 | 33 |   今日一卡通流水。 34 | 35 | - _classmethod_ Card.**statistics**(*year, month, raw=False, dictionary=DEFAULT_DICTIONARY*) 36 | 37 |   月交易统计。 38 | 39 | 参数说明: 40 | 41 | - raw - 是否输出为原始格式 42 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 43 | - year - 年份,如 2019 44 | - month - 月份,如 1, 3, 4, 12 45 | 46 | **Class attributes**: 47 | 48 | - Card.session 49 | 50 |   `CardSession` 对象。 51 | -------------------------------------------------------------------------------- /docs/apis/course.md: -------------------------------------------------------------------------------- 1 | # 课程 API 2 | 3 | 此章节是关于课程 API 的描述。 4 | 5 | ## `Course` objects 6 | 7 | [`Course`](course.md#course-objects) 对象提供了一系列课程信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Course**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `CourseSession` | true | 无 | 一个已初始化的 `CourseSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Course.**selected**(_year, term, raw=False, dictionary=DEFAULT\_DICTIONARY_) 20 | 21 |   已选课程。 22 | 23 | - _classmethod_ Course.**selected_current**(_raw=False, dictionary=DEFAULT\_DICTIONARY_) 24 | 25 |   本学期已选课程。 26 | 27 | - _classmethod_ Course.**schedule**(_year, term, raw=False, dictionary=DEFAULT\_DICTIONARY_) 28 | 29 |   课表。 30 | 31 | - _classmethod_ Course.**schedule_current**(_raw=False, dictionary=DEFAULT\_DICTIONARY_) 32 | 33 |   本学期课表。 34 | 35 | 参数说明: 36 | 37 | - raw - 是否输出为原始格式 38 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 39 | - year - 学年,格式如 '2018-2019' 40 | - term - 学期,1 表示第一个学期,2 表示第二个学期 41 | 42 | **Class attributes**: 43 | 44 | - Course.**session** 45 | 46 |   `CourseSession` 对象。 -------------------------------------------------------------------------------- /docs/apis/create.md: -------------------------------------------------------------------------------- 1 | # hdu\_api 2 | 3 | 此章节是关于如何创建一个 client 的描述。 4 | 5 | ## `HDU` Objects 6 | 7 | [`HDU`](create.md#hdu-objects) 对象是使用 `hdu-api` 的起点,它提供了创建 `HduClient` 对象的方法。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**HDU**(_username, password, \*\*kwargs_) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | username | str | true | 无 | 学号 | 16 | | password | str | true | 无 | 密码 | 17 | | kwargs | dict | false | 无 | | 18 | 19 | **Class methods**: 20 | 21 | - _classmethod_ HDU.**create**\(_\*args_) 22 | 23 |   返回一个 `HduClient` 对象。 24 | 25 | **Class attributes**: 26 | 27 | - HDU.**username** 28 | 29 |   学号,字符串类型。 30 | 31 | - HDU.**password** 32 | 33 |   密码,数字杭电的密码,字符串类型。 34 | 35 | ## `HduClient` Objects 36 | 37 | [`HduClient`](create.md#hduclient-objects) 对象提供了对 API 访问的通道。 38 | 39 | **Constructor**: 40 | 41 | _class_ hdu\_api.**HduClient**(_sess\_mgr, \*\*kwargs_) 42 | 43 | | 参数 | type | required | default | 备注 | 44 | | :---: | :---: | :---: | :---: | :---: | 45 | | sess\_mgr | objects `SessionManager` | true | 无 | | 46 | | kwargs | dict | false | 无 | | 47 | 48 | **Class methods**: 49 | 50 | 51 | **Class attributes**: 52 | 53 | - HduClient.**sess\_mgr** 54 | 55 |   `SessionManager` 对象,提供了 session 管理。 56 | 57 | - HduClient.**username** 58 | 59 |   学号,字符串类型。 60 | 61 | - HduClient.**card** 62 | 63 |   `Card` 对象,提供了对一卡通 API 的访问,详情请查看[这里](card.md)。 64 | 65 | - HduClient.**exam** 66 | 67 |   `Exam` 对象,提供了对考试 API 的访问,详情请查看[这里](exam.md)。 68 | 69 | - HduClient.**person** 70 | 71 |   `Person` 对象,提供了对个人信息 API 的访问,详情请查看[这里](person.md)。 72 | 73 | - HduClient.**course** 74 | 75 |   `Course` 对象,提供了对课程 API 的访问,详情请查看[这里](course.md)。 76 | 77 | - HduClient.**public** 78 | 79 |   `Public` 对象,提供了对公共信息 API 的访问,详情请查看[这里](public.md)。 80 | -------------------------------------------------------------------------------- /docs/apis/exam.md: -------------------------------------------------------------------------------- 1 | # 考试 API 2 | 3 | 此章节是关于考试 API 的描述。 4 | 5 | ## `Exam` objects 6 | 7 | [`Exam`](exam.md#exam-objects) 对象提供了一系列考试信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Exam**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `ExamSession` | true | 无 | 一个已初始化的 `ExamSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Exam.**grade**(*year, term, raw=False, dictionary=DEFAULT_DICTIONARY*) 20 | 21 |   学期成绩。 22 | 23 | - _classmethod_ Exam.**grade_current**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 24 | 25 |   本学期成绩。 26 | 27 | - _classmethod_ Exam.**level_grade**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 28 | 29 |   等级考试成绩,如 CET-4 成绩。 30 | 31 | - _classmethod_ Exam.**schedule**(*year, term, raw=False, dictionary=DEFAULT_DICTIONARY*) 32 | 33 |   考试安排。 34 | 35 | - _classmethod_ Exam.**schedule_current**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 36 | 37 |   本学期考试安排。 38 | 39 | - _classmethod_ Exam.**schedule_make_up**(*term, raw=False, dictionary=DEFAULT_DICTIONARY*) 40 | 41 |   补考安排。 42 | 43 | 参数说明: 44 | 45 | - raw - 是否输出为原始格式 46 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 47 | - year - 学年,格式如 '2018-2019' 48 | - term - 学期,1 表示第一个学期,2 表示第二个学期 49 | 50 | **Class attributes**: 51 | 52 | - Exam.**session** 53 | 54 |   `ExamSession` 对象。 55 | -------------------------------------------------------------------------------- /docs/apis/person.md: -------------------------------------------------------------------------------- 1 | # 个人信息 API 2 | 3 | 此章节是关于个人信息 API 的描述。 4 | 5 | ## `Person` objects 6 | 7 | [`Person`](person.md#person-objects) 对象提供了一系列个人信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Person**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `PersonSession` | true | 无 | 一个已初始化的 `PersonSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Person.**profile**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 20 | 21 |   基本个人信息。 22 | 23 | - _classmethod_ Person.**instructor**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 24 | 25 |   辅导员信息。 26 | 27 | - _classmethod_ Person.**status**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 28 | 29 |   学籍信息。 30 | 31 | - _classmethod_ Person.**accommodation**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 32 | 33 |   住宿信息。 34 | 35 | - _classmethod_ Person.**award**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 36 | 37 |   奖项信息。 38 | 39 | - _classmethod_ Person.**profile_all**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 40 | 41 |   全部个人信息,即以上信息的集合。 42 | 43 | 参数说明: 44 | 45 | - raw - 是否输出为原始格式 46 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 47 | 48 | **Class attributes**: 49 | 50 | - Person.**session** 51 | 52 |   `PersonSession` 对象。 -------------------------------------------------------------------------------- /docs/apis/public.md: -------------------------------------------------------------------------------- 1 | # 公共信息 API 2 | 3 | 此章节是关于公共信息 API 的描述。 4 | 5 | ## `Public` objects 6 | 7 | [`Public`](public.md#public-objects) 对象提供了一系列公共信息 API。 8 | 9 | **Constructor**: 10 | 11 | _class_ hdu\_api.**Public**(*session*) 12 | 13 | | 参数 | type | required | default | 备注 | 14 | | :---: | :---: | :---: | :---: | :---: | 15 | | session | objects `PublicSession` | true | 无 | 一个已初始化的 `PublicSession` 对象 16 | 17 | **Class methods**: 18 | 19 | - _classmethod_ Public.**classroom_free**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 20 | 21 |   空闲教室。 22 | 23 | - _classmethod_ Public.**classroom_in_use**(*raw=False, dictionary=DEFAULT_DICTIONARY*) 24 | 25 |   在使用的教室。 26 | 27 | - _classmethod_ Public.**schooltime**(*location=0, raw=False, dictionary=DEFAULT_DICTIONARY*) 28 | 29 |   上课时间表。 30 | 31 | 参数说明: 32 | 33 | - raw - 是否输出为原始格式 34 | - dictionary - raw 为 true 时,使用该字典替换返回数据的 key 35 | - location - 校区,0 表示下沙, 1 表示青山湖 36 | 37 | **Class attributes**: 38 | 39 | - Public.**session** 40 | 41 |   `PublicSession` 对象。 -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | 3 | ## 0.0.1b1 - 2019-02-13 4 | 5 | ### Fixed 6 | 7 | * 修复对 Python2 的兼容性 8 | 9 | ## 0.0.1b0 - 2019-02-11 10 | 11 | ### Added 12 | 13 | * 提供对 HDU 所有教务管理信息 API 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | 3 | 这一部分文档包含了 `hdu-api` 的安装方式。 4 | 5 | ## pip install hdu-api 6 | 7 | 使用包管理器 `pip` 安装 `hdu-api`,在你的终端运行这个简单的命令: 8 | 9 | ```text 10 | $ pip install hdu-api 11 | ``` 12 | 13 | ## 从源码安装 14 | 15 | `hdu-api` 的源代码公布在 Github 上,你从[这里](https://github.com/Cyrus97/hdu-api)可以获取到最新的代码。 16 | 17 | 你可以先克隆该仓库: 18 | 19 | ```text 20 | $ git clone https://github.com/Cyrus97/hdu-api 21 | ``` 22 | 23 | 之后再从源码安装: 24 | 25 | ```text 26 | $ cd hdu-api 27 | 28 | # 确保当前目录在 hdu-api 文件夹 29 | $ pip install . 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # 快速上手 2 | 3 | 这一文档部分介绍了如何快速上手 `hdu-api`。现已假设你已安装 `hdu-api`,如果还没有安装,请去[安装](install.md)一节进行安装。 4 | 5 | 现在让我们从一些简单的示例开始吧! 6 | 7 | ## 创建 client 8 | 9 | 要想使用 `hdu-api` 访问到一卡通、课表等信息,必须先创建一个 `client`。 10 | 11 | 首先我们先导入使用的模块: 12 | 13 | ```text 14 | >>> from hdu_api import HDU 15 | ``` 16 | 17 | 之后传入正确的 `学号` 和 `密码` 创建一个 `HDU` 对象: 18 | 19 | ```text 20 | >>> hdu = HDU('学号', '密码') 21 | ``` 22 | 23 | 最后使用该对象创建一个 client: 24 | 25 | ```text 26 | >>> client = hdu.create() 27 | ``` 28 | 29 | 我们最后得到了一个名为 `client` 的 `HduClient` 对象,通过该对象可以获取到任意的信息。 30 | 31 | ## 访问信息 32 | 33 | 通过上面,我们已经创建了一个名为 `client` 的 `HduClient` 对象,现在我们看看如何访问我们想要的信息。 34 | 35 | ### 获取一卡通相关的信息 36 | 37 | 通过 `HduClient.card` 可以获取到使用一卡通的信息。 38 | 39 | 一卡通账户: 40 | 41 | ```text 42 | >>> from hdu_api import HDU 43 | >>> hdu = HDU('学号', '密码') 44 | >>> client = hdu.create() 45 | >>> client.card.account() 46 | [{'account_id': '30***86', 47 | 'card_id': '13***42', 48 | 'card_type': 'M1', 49 | 'department': '1***7', 50 | 'expiry_date': '2020年8月29日', 51 | 'gender': '*', 52 | 'id_card': '', 53 | 'id_type': '', 54 | 'identity': '***', 55 | 'staff_id': '1***7', 56 | 'staff_name': '***', 57 | 'status': '有效卡'}] 58 | ``` 59 | 60 | 一卡通余额: 61 | 62 | ```text 63 | >>> client.card.balance() 64 | [{'account_id': '3***6', 65 | 'balance': '76.07', 66 | 'card_id': '1***2', 67 | 'staff_id': '1***7', 68 | 'staff_name': '***'}] 69 | ``` 70 | 71 | 更多一卡通信息 API 使用可以查看[这里](apis/card.md)。 72 | 73 | ### 获取考试相关的信息 74 | 75 | 通过 `HduClient.exam` 可以获取到使用考试相关的信息。 76 | 77 | 考试成绩: 78 | 79 | ```text 80 | >>> client.exam.grade_current() 81 | [{'college': '外国语学院', 82 | 'comments': '', 83 | 'comments_of_makeup': '', 84 | 'course_attribution': '通识必修', 85 | 'course_code': 'A1103780', 86 | 'course_name': '实用翻译', 87 | 'course_type': '外语模块', 88 | 'credit': '2.0', 89 | 'experimental_score': '', 90 | 'final_score': '76.5', 91 | 'mid_score': '', 92 | 'retake_or_not': '', 93 | 'retest_mark': '', 94 | 'school_year': '2018-2019', 95 | 'score': '82', 96 | 'semester': '1', 97 | 'usual_score': '91'}, 98 | 99 | ... 100 | 101 | {'college': '计算机学院', 102 | 'comments': '', 103 | 'comments_of_makeup': '', 104 | 'course_attribution': '', 105 | 'course_code': 'B0507370', 106 | 'course_name': '数据挖掘', 107 | 'course_type': '专业限选', 108 | 'credit': '3.0', 109 | 'experimental_score': '', 110 | 'final_score': '85', 111 | 'mid_score': '', 112 | 'retake_or_not': '', 113 | 'retest_mark': '', 114 | 'school_year': '2018-2019', 115 | 'score': '84', 116 | 'semester': '1', 117 | 'usual_score': '83'}] 118 | ``` 119 | 120 | 考试安排: 121 | 122 | ```text 123 | >>> client.exam.schedule_current() 124 | [{'classroom': '第12教研楼201', 125 | 'course_name': '操作系统(甲)', 126 | 'exam_time': '2019年1月17日(09:00-11:00)', 127 | 'exam_type': '', 128 | 'seat': '10', 129 | 'select_code': '(2018-2019-1)-A0507050-06018-1', 130 | 'staff_name': 'xxx'}, 131 | 132 | ... 133 | 134 | {'classroom': '第6教研楼北308', 135 | 'course_name': '软件工程(甲)', 136 | 'exam_time': '2019年1月9日(13:45-15:45)', 137 | 'exam_type': '', 138 | 'seat': '24', 139 | 'select_code': '(2018-2019-1)-A0507190-06061-2', 140 | 'staff_name': 'xxx'}] 141 | ``` 142 | 143 | 更多考试相关信息 API 使用可以查看[这里](apis/exam.md)。 144 | 145 | {% hint style="info" %} 146 | 当然,`hdu-api` 提供的 API 访问服务不仅仅只有这些,想查看所有的 API 使用吗?请访问[这里](apis/)。 147 | {% endhint %} 148 | 149 | -------------------------------------------------------------------------------- /hdu_api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # __ __ ____ __ __ ___ ____ ____ 5 | # / / / // __ \ / / / / / | / __ \ / _/ 6 | # / /_/ // / / // / / /______ / /| | / /_/ / / / 7 | # / __ // /_/ // /_/ //_____// ___ | / ____/_/ / 8 | # /_/ /_//_____/ \____/ /_/ |_|/_/ /___/ 9 | # 10 | # 11 | 12 | """ 13 | HDU-API 14 | ------- 15 | 16 | HDU-API is a simple sdk for hdu to access the information in the school's websites, written in Python, 17 | basing on the requests library and the bs4 library mainly. 18 | """ 19 | 20 | hard_dependencies = ("requests", "bs4", "lxml") 21 | missing_dependencies = [] 22 | 23 | for dependency in hard_dependencies: 24 | try: 25 | __import__(dependency) 26 | except ImportError as e: 27 | missing_dependencies.append(dependency) 28 | 29 | if missing_dependencies: 30 | raise ImportError( 31 | "Missing required dependencies {0}".format(missing_dependencies)) 32 | del hard_dependencies, missing_dependencies 33 | 34 | from .__version__ import __title__, __description__, __url__, __version__ 35 | from .__version__ import __build__, __author__, __author_email__, __license__ 36 | 37 | from .api import HDU, HduClient 38 | from .sessions import SessionManager, TeachingSession, CardSession, StudentSession, IHDUSession, IHDUPhoneSession 39 | from .models import Card, Exam, Course, Person, Public, get_current_term 40 | -------------------------------------------------------------------------------- /hdu_api/__version__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __title__ = 'hdu-api' 4 | __description__ = 'A simple SDK for HDU.' 5 | __url__ = 'https://github.com/Cyrus97/hdu-api' 6 | __version__ = '0.1.0' 7 | __build__ = 0x000100 8 | __author__ = 'Cyrus Liu' 9 | __author_email__ = 'liuxingran97@gmail.com' 10 | __license__ = 'MIT' 11 | -------------------------------------------------------------------------------- /hdu_api/_internal_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | hdu_api._internal_utils 5 | ----------------------- 6 | 7 | 8 | """ 9 | import sys 10 | 11 | from hdu_api import _pyDes 12 | 13 | _ver = sys.version_info 14 | 15 | #: Python 2.x? 16 | is_py2 = (_ver[0] == 2) 17 | 18 | #: Python 3.x? 19 | is_py3 = (_ver[0] == 3) 20 | 21 | 22 | def encrypt(data, first_key, second_key, third_key): 23 | bts_data = extend_to_16bits(data) 24 | bts_first_key = extend_to_16bits(first_key) 25 | bts_second_key = extend_to_16bits(second_key) 26 | bts_third_key = extend_to_16bits(third_key) 27 | i = 0 28 | bts_result = [] 29 | while i < len(bts_data): 30 | # 将data分成每64位一段,分段加密 31 | bts_temp = bts_data[i:i + 8] 32 | j, k, z = 0, 0, 0 33 | while j < len(bts_first_key): 34 | # 分别取出 first_key 的64位作为密钥 35 | des_k = _pyDes.des(bts_first_key[j: j + 8], _pyDes.ECB) 36 | bts_temp = list(des_k.encrypt(bts_temp)) 37 | j += 8 38 | while k < len(bts_second_key): 39 | des_k = _pyDes.des(bts_second_key[k:k + 8], _pyDes.ECB) 40 | bts_temp = list(des_k.encrypt(bts_temp)) 41 | k += 8 42 | while z < len(bts_third_key): 43 | des_k = _pyDes.des(bts_third_key[z:z + 8], _pyDes.ECB) 44 | bts_temp = list(des_k.encrypt(bts_temp)) 45 | z += 8 46 | 47 | bts_result.extend(bts_temp) 48 | i += 8 49 | str_result = '' 50 | for each in bts_result: 51 | if is_py2: 52 | each = ord(each) 53 | # 分别加密data的各段,串联成字符串 54 | str_result += '%02X' % each 55 | return str_result 56 | 57 | 58 | def extend_to_16bits(data): 59 | """ 60 | 将字符串的每个字符前插入 0,变成16位,并在后面补0,使其长度是64位整数倍 61 | :param data: 62 | :return: 63 | """ 64 | bts = data.encode() 65 | c = 0 66 | if is_py2: 67 | c = chr(c) 68 | filled_bts = [] 69 | for each in bts: 70 | # 每个字符前插入 0 71 | filled_bts.extend([c, each]) 72 | # 长度扩展到8的倍数,若不是8的倍数,后面添加0,便于DES加密时分组 73 | while len(filled_bts) % 8 != 0: 74 | filled_bts.append(c) 75 | return filled_bts 76 | -------------------------------------------------------------------------------- /hdu_api/_pyDes.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # Documentation # 3 | ############################################################################# 4 | 5 | # Author: Todd Whiteman 6 | # Date: 16th March, 2009 7 | # Verion: 2.0.0 8 | # License: MIT 9 | # Homepage: http://twhiteman.netfirms.com/des.html 10 | # 11 | # This is a pure python implementation of the DES encryption algorithm. 12 | # It's pure python to avoid portability issues, since most DES 13 | # implementations are programmed in C (for performance reasons). 14 | # 15 | # Triple DES class is also implemented, utilising the DES base. Triple DES 16 | # is either DES-EDE3 with a 24 byte key, or DES-EDE2 with a 16 byte key. 17 | # 18 | # See the README.txt that should come with this python module for the 19 | # implementation methods used. 20 | # 21 | # Thanks to: 22 | # * David Broadwell for ideas, comments and suggestions. 23 | # * Mario Wolff for pointing out and debugging some triple des CBC errors. 24 | # * Santiago Palladino for providing the PKCS5 padding technique. 25 | # * Shaya for correcting the PAD_PKCS5 triple des CBC errors. 26 | # 27 | """A pure python implementation of the DES and TRIPLE DES encryption algorithms. 28 | 29 | Class initialization 30 | -------------------- 31 | pyDes.des(key, [mode], [IV], [pad], [padmode]) 32 | pyDes.triple_des(key, [mode], [IV], [pad], [padmode]) 33 | 34 | key -> Bytes containing the encryption key. 8 bytes for DES, 16 or 24 bytes 35 | for Triple DES 36 | mode -> Optional argument for encryption type, can be either 37 | pyDes.ECB (Electronic Code Book) or pyDes.CBC (Cypher Block Chaining) 38 | IV -> Optional Initial Value bytes, must be supplied if using CBC mode. 39 | Length must be 8 bytes. 40 | pad -> Optional argument, set the pad character (PAD_NORMAL) to use during 41 | all encrypt/decrpt operations done with this instance. 42 | padmode -> Optional argument, set the padding mode (PAD_NORMAL or PAD_PKCS5) 43 | to use during all encrypt/decrpt operations done with this instance. 44 | 45 | I recommend to use PAD_PKCS5 padding, as then you never need to worry about any 46 | padding issues, as the padding can be removed unambiguously upon decrypting 47 | data that was encrypted using PAD_PKCS5 padmode. 48 | 49 | Common methods 50 | -------------- 51 | encrypt(data, [pad], [padmode]) 52 | decrypt(data, [pad], [padmode]) 53 | 54 | data -> Bytes to be encrypted/decrypted 55 | pad -> Optional argument. Only when using padmode of PAD_NORMAL. For 56 | encryption, adds this characters to the end of the data block when 57 | data is not a multiple of 8 bytes. For decryption, will remove the 58 | trailing characters that match this pad character from the last 8 59 | bytes of the unencrypted data block. 60 | padmode -> Optional argument, set the padding mode, must be one of PAD_NORMAL 61 | or PAD_PKCS5). Defaults to PAD_NORMAL. 62 | 63 | 64 | Example 65 | ------- 66 | from pyDes import * 67 | 68 | data = "Please encrypt my data" 69 | k = des("DESCRYPT", CBC, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) 70 | # For Python3, you'll need to use bytes, i.e.: 71 | # data = b"Please encrypt my data" 72 | # k = des(b"DESCRYPT", CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) 73 | d = k.encrypt(data) 74 | print "Encrypted: %r" % d 75 | print "Decrypted: %r" % k.decrypt(d) 76 | assert k.decrypt(d, padmode=PAD_PKCS5) == data 77 | 78 | 79 | See the module source (_pyDes.py) for more examples of use. 80 | You can also run the _pyDes.py file without and arguments to see a simple test. 81 | 82 | Note: This code was not written for high-end systems needing a fast 83 | implementation, but rather a handy portable solution with small usage. 84 | 85 | """ 86 | 87 | import sys 88 | 89 | # _pythonMajorVersion is used to handle Python2 and Python3 differences. 90 | _pythonMajorVersion = sys.version_info[0] 91 | 92 | # Modes of crypting / cyphering 93 | ECB = 0 94 | CBC = 1 95 | 96 | # Modes of padding 97 | PAD_NORMAL = 1 98 | PAD_PKCS5 = 2 99 | 100 | 101 | # PAD_PKCS5: is a method that will unambiguously remove all padding 102 | # characters after decryption, when originally encrypted with 103 | # this padding mode. 104 | # For a good description of the PKCS5 padding technique, see: 105 | # http://www.faqs.org/rfcs/rfc1423.html 106 | 107 | # The base class shared by des and triple des. 108 | class _baseDes(object): 109 | def __init__(self, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): 110 | if IV: 111 | IV = self._guardAgainstUnicode(IV) 112 | if pad: 113 | pad = self._guardAgainstUnicode(pad) 114 | self.block_size = 8 115 | # Sanity checking of arguments. 116 | if pad and padmode == PAD_PKCS5: 117 | raise ValueError("Cannot use a pad character with PAD_PKCS5") 118 | if IV and len(IV) != self.block_size: 119 | raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") 120 | 121 | # Set the passed in variables 122 | self._mode = mode 123 | self._iv = IV 124 | self._padding = pad 125 | self._padmode = padmode 126 | 127 | def getKey(self): 128 | """getKey() -> bytes""" 129 | return self.__key 130 | 131 | def setKey(self, key): 132 | """Will set the crypting key for this object.""" 133 | key = self._guardAgainstUnicode(key) 134 | self.__key = key 135 | 136 | def getMode(self): 137 | """getMode() -> pyDes.ECB or pyDes.CBC""" 138 | return self._mode 139 | 140 | def setMode(self, mode): 141 | """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" 142 | self._mode = mode 143 | 144 | def getPadding(self): 145 | """getPadding() -> bytes of length 1. Padding character.""" 146 | return self._padding 147 | 148 | def setPadding(self, pad): 149 | """setPadding() -> bytes of length 1. Padding character.""" 150 | if pad is not None: 151 | pad = self._guardAgainstUnicode(pad) 152 | self._padding = pad 153 | 154 | def getPadMode(self): 155 | """getPadMode() -> pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" 156 | return self._padmode 157 | 158 | def setPadMode(self, mode): 159 | """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" 160 | self._padmode = mode 161 | 162 | def getIV(self): 163 | """getIV() -> bytes""" 164 | return self._iv 165 | 166 | def setIV(self, IV): 167 | """Will set the Initial Value, used in conjunction with CBC mode""" 168 | if not IV or len(IV) != self.block_size: 169 | raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") 170 | IV = self._guardAgainstUnicode(IV) 171 | self._iv = IV 172 | 173 | def _padData(self, data, pad, padmode): 174 | # Pad data depending on the mode 175 | if padmode is None: 176 | # Get the default padding mode. 177 | padmode = self.getPadMode() 178 | if pad and padmode == PAD_PKCS5: 179 | raise ValueError("Cannot use a pad character with PAD_PKCS5") 180 | 181 | if padmode == PAD_NORMAL: 182 | if len(data) % self.block_size == 0: 183 | # No padding required. 184 | return data 185 | 186 | if not pad: 187 | # Get the default padding. 188 | pad = self.getPadding() 189 | if not pad: 190 | raise ValueError("Data must be a multiple of " + str( 191 | self.block_size) + " bytes in length. Use padmode=PAD_PKCS5 or set the pad character.") 192 | data += (self.block_size - (len(data) % self.block_size)) * pad 193 | 194 | elif padmode == PAD_PKCS5: 195 | pad_len = 8 - (len(data) % self.block_size) 196 | if _pythonMajorVersion < 3: 197 | data += pad_len * chr(pad_len) 198 | else: 199 | data += bytes([pad_len] * pad_len) 200 | 201 | return data 202 | 203 | def _unpadData(self, data, pad, padmode): 204 | # Unpad data depending on the mode. 205 | if not data: 206 | return data 207 | if pad and padmode == PAD_PKCS5: 208 | raise ValueError("Cannot use a pad character with PAD_PKCS5") 209 | if padmode is None: 210 | # Get the default padding mode. 211 | padmode = self.getPadMode() 212 | 213 | if padmode == PAD_NORMAL: 214 | if not pad: 215 | # Get the default padding. 216 | pad = self.getPadding() 217 | if pad: 218 | data = data[:-self.block_size] + \ 219 | data[-self.block_size:].rstrip(pad) 220 | 221 | elif padmode == PAD_PKCS5: 222 | if _pythonMajorVersion < 3: 223 | pad_len = ord(data[-1]) 224 | else: 225 | pad_len = data[-1] 226 | data = data[:-pad_len] 227 | 228 | return data 229 | 230 | def _guardAgainstUnicode(self, data): 231 | # Only accept byte strings or ascii unicode values, otherwise 232 | # there is no way to correctly decode the data into bytes. 233 | if _pythonMajorVersion < 3: 234 | if isinstance(data, unicode): 235 | raise ValueError("pyDes can only work with bytes, not Unicode strings.") 236 | else: 237 | if isinstance(data, str): 238 | # Only accept ascii unicode values. 239 | try: 240 | return data.encode('ascii') 241 | except UnicodeEncodeError: 242 | pass 243 | raise ValueError("pyDes can only work with encoded strings, not Unicode.") 244 | return data 245 | 246 | 247 | ############################################################################# 248 | # DES # 249 | ############################################################################# 250 | class des(_baseDes): 251 | """DES encryption/decrytpion class 252 | 253 | Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. 254 | 255 | pyDes.des(key,[mode], [IV]) 256 | 257 | key -> Bytes containing the encryption key, must be exactly 8 bytes 258 | mode -> Optional argument for encryption type, can be either pyDes.ECB 259 | (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) 260 | IV -> Optional Initial Value bytes, must be supplied if using CBC mode. 261 | Must be 8 bytes in length. 262 | pad -> Optional argument, set the pad character (PAD_NORMAL) to use 263 | during all encrypt/decrpt operations done with this instance. 264 | padmode -> Optional argument, set the padding mode (PAD_NORMAL or 265 | PAD_PKCS5) to use during all encrypt/decrpt operations done 266 | with this instance. 267 | """ 268 | 269 | # Permutation and translation tables for DES 270 | __pc1 = [ 271 | 56, 48, 40, 32, 24, 16, 8, 0, 272 | 57, 49, 41, 33, 25, 17, 9, 1, 273 | 58, 50, 42, 34, 26, 18, 10, 2, 274 | 59, 51, 43, 35, 27, 19, 11, 3, 275 | 60, 52, 44, 36, 28, 20, 12, 4, 276 | 61, 53, 45, 37, 29, 21, 13, 5, 277 | 62, 54, 46, 38, 30, 22, 14, 6 278 | ] 279 | 280 | # number left rotations of pc1 281 | __left_rotations = [ 282 | 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 283 | ] 284 | 285 | # permuted choice key (table 2) 286 | __pc2 = [ 287 | 13, 16, 10, 23, 0, 4, 288 | 2, 27, 14, 5, 20, 9, 289 | 22, 18, 11, 3, 25, 7, 290 | 15, 6, 26, 19, 12, 1, 291 | 40, 51, 30, 36, 46, 54, 292 | 29, 39, 50, 44, 32, 47, 293 | 43, 48, 38, 55, 33, 52, 294 | 45, 41, 49, 35, 28, 31 295 | ] 296 | 297 | # initial permutation IP 298 | __ip = [57, 49, 41, 33, 25, 17, 9, 1, 299 | 59, 51, 43, 35, 27, 19, 11, 3, 300 | 61, 53, 45, 37, 29, 21, 13, 5, 301 | 63, 55, 47, 39, 31, 23, 15, 7, 302 | 56, 48, 40, 32, 24, 16, 8, 0, 303 | 58, 50, 42, 34, 26, 18, 10, 2, 304 | 60, 52, 44, 36, 28, 20, 12, 4, 305 | 62, 54, 46, 38, 30, 22, 14, 6 306 | ] 307 | 308 | # Expansion table for turning 32 bit blocks into 48 bits 309 | __expansion_table = [ 310 | 31, 0, 1, 2, 3, 4, 311 | 3, 4, 5, 6, 7, 8, 312 | 7, 8, 9, 10, 11, 12, 313 | 11, 12, 13, 14, 15, 16, 314 | 15, 16, 17, 18, 19, 20, 315 | 19, 20, 21, 22, 23, 24, 316 | 23, 24, 25, 26, 27, 28, 317 | 27, 28, 29, 30, 31, 0 318 | ] 319 | 320 | # The (in)famous S-boxes 321 | __sbox = [ 322 | # S1 323 | [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 324 | 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 325 | 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 326 | 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], 327 | 328 | # S2 329 | [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 330 | 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, 331 | 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 332 | 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], 333 | 334 | # S3 335 | [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 336 | 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, 337 | 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 338 | 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], 339 | 340 | # S4 341 | [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 342 | 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, 343 | 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 344 | 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], 345 | 346 | # S5 347 | [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 348 | 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, 349 | 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 350 | 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], 351 | 352 | # S6 353 | [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 354 | 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, 355 | 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 356 | 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], 357 | 358 | # S7 359 | [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 360 | 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, 361 | 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 362 | 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], 363 | 364 | # S8 365 | [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 366 | 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 367 | 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 368 | 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11], 369 | ] 370 | 371 | # 32-bit permutation function P used on the output of the S-boxes 372 | __p = [ 373 | 15, 6, 19, 20, 28, 11, 374 | 27, 16, 0, 14, 22, 25, 375 | 4, 17, 30, 9, 1, 7, 376 | 23, 13, 31, 26, 2, 8, 377 | 18, 12, 29, 5, 21, 10, 378 | 3, 24 379 | ] 380 | 381 | # final permutation IP^-1 382 | __fp = [ 383 | 39, 7, 47, 15, 55, 23, 63, 31, 384 | 38, 6, 46, 14, 54, 22, 62, 30, 385 | 37, 5, 45, 13, 53, 21, 61, 29, 386 | 36, 4, 44, 12, 52, 20, 60, 28, 387 | 35, 3, 43, 11, 51, 19, 59, 27, 388 | 34, 2, 42, 10, 50, 18, 58, 26, 389 | 33, 1, 41, 9, 49, 17, 57, 25, 390 | 32, 0, 40, 8, 48, 16, 56, 24 391 | ] 392 | 393 | # Type of crypting being done 394 | ENCRYPT = 0x00 395 | DECRYPT = 0x01 396 | 397 | # Initialisation 398 | def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): 399 | # Sanity checking of arguments. 400 | if len(key) != 8: 401 | raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") 402 | _baseDes.__init__(self, mode, IV, pad, padmode) 403 | self.key_size = 8 404 | 405 | self.L = [] 406 | self.R = [] 407 | self.Kn = [[0] * 48] * 16 # 16 48-bit keys (K1 - K16) 408 | self.final = [] 409 | 410 | self.setKey(key) 411 | 412 | def setKey(self, key): 413 | """Will set the crypting key for this object. Must be 8 bytes.""" 414 | _baseDes.setKey(self, key) 415 | self.__create_sub_keys() 416 | 417 | def __String_to_BitList(self, data): 418 | """Turn the string data, into a list of bits (1, 0)'s""" 419 | if _pythonMajorVersion < 3: 420 | # Turn the strings into integers. Python 3 uses a bytes 421 | # class, which already has this behaviour. 422 | data = [ord(c) for c in data] 423 | l = len(data) * 8 424 | result = [0] * l 425 | pos = 0 426 | for ch in data: 427 | i = 7 428 | while i >= 0: 429 | if ch & (1 << i) != 0: 430 | result[pos] = 1 431 | else: 432 | result[pos] = 0 433 | pos += 1 434 | i -= 1 435 | 436 | return result 437 | 438 | def __BitList_to_String(self, data): 439 | """Turn the list of bits -> data, into a string""" 440 | result = [] 441 | pos = 0 442 | c = 0 443 | while pos < len(data): 444 | c += data[pos] << (7 - (pos % 8)) 445 | if (pos % 8) == 7: 446 | result.append(c) 447 | c = 0 448 | pos += 1 449 | 450 | if _pythonMajorVersion < 3: 451 | return ''.join([chr(c) for c in result]) 452 | else: 453 | return bytes(result) 454 | 455 | def __permutate(self, table, block): 456 | """Permutate this block with the specified table""" 457 | return list(map(lambda x: block[x], table)) 458 | 459 | # Transform the secret key, so that it is ready for data processing 460 | # Create the 16 subkeys, K[1] - K[16] 461 | def __create_sub_keys(self): 462 | """Create the 16 subkeys K[1] to K[16] from the given key""" 463 | key = self.__permutate(des.__pc1, self.__String_to_BitList(self.getKey())) 464 | i = 0 465 | # Split into Left and Right sections 466 | self.L = key[:28] 467 | self.R = key[28:] 468 | while i < 16: 469 | j = 0 470 | # Perform circular left shifts 471 | while j < des.__left_rotations[i]: 472 | self.L.append(self.L[0]) 473 | del self.L[0] 474 | 475 | self.R.append(self.R[0]) 476 | del self.R[0] 477 | 478 | j += 1 479 | 480 | # Create one of the 16 subkeys through pc2 permutation 481 | self.Kn[i] = self.__permutate(des.__pc2, self.L + self.R) 482 | 483 | i += 1 484 | 485 | # Main part of the encryption algorithm, the number cruncher :) 486 | def __des_crypt(self, block, crypt_type): 487 | """Crypt the block of data through DES bit-manipulation""" 488 | block = self.__permutate(des.__ip, block) 489 | self.L = block[:32] 490 | self.R = block[32:] 491 | 492 | # Encryption starts from Kn[1] through to Kn[16] 493 | if crypt_type == des.ENCRYPT: 494 | iteration = 0 495 | iteration_adjustment = 1 496 | # Decryption starts from Kn[16] down to Kn[1] 497 | else: 498 | iteration = 15 499 | iteration_adjustment = -1 500 | 501 | i = 0 502 | while i < 16: 503 | # Make a copy of R[i-1], this will later become L[i] 504 | tempR = self.R[:] 505 | 506 | # Permutate R[i - 1] to start creating R[i] 507 | self.R = self.__permutate(des.__expansion_table, self.R) 508 | 509 | # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here 510 | self.R = list(map(lambda x, y: x ^ y, self.R, self.Kn[iteration])) 511 | B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], 512 | self.R[42:]] 513 | # Optimization: Replaced below commented code with above 514 | # j = 0 515 | # B = [] 516 | # while j < len(self.R): 517 | # self.R[j] = self.R[j] ^ self.Kn[iteration][j] 518 | # j += 1 519 | # if j % 6 == 0: 520 | # B.append(self.R[j-6:j]) 521 | 522 | # Permutate B[1] to B[8] using the S-Boxes 523 | j = 0 524 | Bn = [0] * 32 525 | pos = 0 526 | while j < 8: 527 | # Work out the offsets 528 | m = (B[j][0] << 1) + B[j][5] 529 | n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] 530 | 531 | # Find the permutation value 532 | v = des.__sbox[j][(m << 4) + n] 533 | 534 | # Turn value into bits, add it to result: Bn 535 | Bn[pos] = (v & 8) >> 3 536 | Bn[pos + 1] = (v & 4) >> 2 537 | Bn[pos + 2] = (v & 2) >> 1 538 | Bn[pos + 3] = v & 1 539 | 540 | pos += 4 541 | j += 1 542 | 543 | # Permutate the concatination of B[1] to B[8] (Bn) 544 | self.R = self.__permutate(des.__p, Bn) 545 | 546 | # Xor with L[i - 1] 547 | self.R = list(map(lambda x, y: x ^ y, self.R, self.L)) 548 | # Optimization: This now replaces the below commented code 549 | # j = 0 550 | # while j < len(self.R): 551 | # self.R[j] = self.R[j] ^ self.L[j] 552 | # j += 1 553 | 554 | # L[i] becomes R[i - 1] 555 | self.L = tempR 556 | 557 | i += 1 558 | iteration += iteration_adjustment 559 | 560 | # Final permutation of R[16]L[16] 561 | self.final = self.__permutate(des.__fp, self.R + self.L) 562 | return self.final 563 | 564 | # Data to be encrypted/decrypted 565 | def crypt(self, data, crypt_type): 566 | """Crypt the data in blocks, running it through des_crypt()""" 567 | 568 | # Error check the data 569 | if not data: 570 | return '' 571 | if len(data) % self.block_size != 0: 572 | if crypt_type == des.DECRYPT: # Decryption must work on 8 byte blocks 573 | raise ValueError( 574 | "Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") 575 | if not self.getPadding(): 576 | raise ValueError("Invalid data length, data must be a multiple of " + str( 577 | self.block_size) + " bytes\n. Try setting the optional padding character") 578 | else: 579 | data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() 580 | # print "Len of data: %f" % (len(data) / self.block_size) 581 | 582 | if self.getMode() == CBC: 583 | if self.getIV(): 584 | iv = self.__String_to_BitList(self.getIV()) 585 | else: 586 | raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") 587 | 588 | # Split the data into blocks, crypting each one seperately 589 | i = 0 590 | dict = {} 591 | result = [] 592 | # cached = 0 593 | # lines = 0 594 | while i < len(data): 595 | # Test code for caching encryption results 596 | # lines += 1 597 | # if dict.has_key(data[i:i+8]): 598 | # print "Cached result for: %s" % data[i:i+8] 599 | # cached += 1 600 | # result.append(dict[data[i:i+8]]) 601 | # i += 8 602 | # continue 603 | 604 | block = self.__String_to_BitList(data[i:i + 8]) 605 | 606 | # Xor with IV if using CBC mode 607 | if self.getMode() == CBC: 608 | if crypt_type == des.ENCRYPT: 609 | block = list(map(lambda x, y: x ^ y, block, iv)) 610 | # j = 0 611 | # while j < len(block): 612 | # block[j] = block[j] ^ iv[j] 613 | # j += 1 614 | 615 | processed_block = self.__des_crypt(block, crypt_type) 616 | 617 | if crypt_type == des.DECRYPT: 618 | processed_block = list(map(lambda x, y: x ^ y, processed_block, iv)) 619 | # j = 0 620 | # while j < len(processed_block): 621 | # processed_block[j] = processed_block[j] ^ iv[j] 622 | # j += 1 623 | iv = block 624 | else: 625 | iv = processed_block 626 | else: 627 | processed_block = self.__des_crypt(block, crypt_type) 628 | 629 | # Add the resulting crypted block to our list 630 | # d = self.__BitList_to_String(processed_block) 631 | # result.append(d) 632 | result.append(self.__BitList_to_String(processed_block)) 633 | # dict[data[i:i+8]] = d 634 | i += 8 635 | 636 | # print "Lines: %d, cached: %d" % (lines, cached) 637 | 638 | # Return the full crypted string 639 | if _pythonMajorVersion < 3: 640 | return ''.join(result) 641 | else: 642 | return bytes.fromhex('').join(result) 643 | 644 | def encrypt(self, data, pad=None, padmode=None): 645 | """encrypt(data, [pad], [padmode]) -> bytes 646 | 647 | data : Bytes to be encrypted 648 | pad : Optional argument for encryption padding. Must only be one byte 649 | padmode : Optional argument for overriding the padding mode. 650 | 651 | The data must be a multiple of 8 bytes and will be encrypted 652 | with the already specified key. Data does not have to be a 653 | multiple of 8 bytes if the padding character is supplied, or 654 | the padmode is set to PAD_PKCS5, as bytes will then added to 655 | ensure the be padded data is a multiple of 8 bytes. 656 | """ 657 | data = self._guardAgainstUnicode(data) 658 | if pad is not None: 659 | pad = self._guardAgainstUnicode(pad) 660 | data = self._padData(data, pad, padmode) 661 | return self.crypt(data, des.ENCRYPT) 662 | 663 | def decrypt(self, data, pad=None, padmode=None): 664 | """decrypt(data, [pad], [padmode]) -> bytes 665 | 666 | data : Bytes to be encrypted 667 | pad : Optional argument for decryption padding. Must only be one byte 668 | padmode : Optional argument for overriding the padding mode. 669 | 670 | The data must be a multiple of 8 bytes and will be decrypted 671 | with the already specified key. In PAD_NORMAL mode, if the 672 | optional padding character is supplied, then the un-encrypted 673 | data will have the padding characters removed from the end of 674 | the bytes. This pad removal only occurs on the last 8 bytes of 675 | the data (last data block). In PAD_PKCS5 mode, the special 676 | padding end markers will be removed from the data after decrypting. 677 | """ 678 | data = self._guardAgainstUnicode(data) 679 | if pad is not None: 680 | pad = self._guardAgainstUnicode(pad) 681 | data = self.crypt(data, des.DECRYPT) 682 | return self._unpadData(data, pad, padmode) 683 | 684 | 685 | ############################################################################# 686 | # Triple DES # 687 | ############################################################################# 688 | class triple_des(_baseDes): 689 | """Triple DES encryption/decrytpion class 690 | 691 | This algorithm uses the DES-EDE3 (when a 24 byte key is supplied) or 692 | the DES-EDE2 (when a 16 byte key is supplied) encryption methods. 693 | Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. 694 | 695 | pyDes.des(key, [mode], [IV]) 696 | 697 | key -> Bytes containing the encryption key, must be either 16 or 698 | 24 bytes long 699 | mode -> Optional argument for encryption type, can be either pyDes.ECB 700 | (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) 701 | IV -> Optional Initial Value bytes, must be supplied if using CBC mode. 702 | Must be 8 bytes in length. 703 | pad -> Optional argument, set the pad character (PAD_NORMAL) to use 704 | during all encrypt/decrpt operations done with this instance. 705 | padmode -> Optional argument, set the padding mode (PAD_NORMAL or 706 | PAD_PKCS5) to use during all encrypt/decrpt operations done 707 | with this instance. 708 | """ 709 | 710 | def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): 711 | _baseDes.__init__(self, mode, IV, pad, padmode) 712 | self.setKey(key) 713 | 714 | def setKey(self, key): 715 | """Will set the crypting key for this object. Either 16 or 24 bytes long.""" 716 | self.key_size = 24 # Use DES-EDE3 mode 717 | if len(key) != self.key_size: 718 | if len(key) == 16: # Use DES-EDE2 mode 719 | self.key_size = 16 720 | else: 721 | raise ValueError("Invalid triple DES key size. Key must be either 16 or 24 bytes long") 722 | if self.getMode() == CBC: 723 | if not self.getIV(): 724 | # Use the first 8 bytes of the key 725 | self._iv = key[:self.block_size] 726 | if len(self.getIV()) != self.block_size: 727 | raise ValueError("Invalid IV, must be 8 bytes in length") 728 | self.__key1 = des(key[:8], self._mode, self._iv, 729 | self._padding, self._padmode) 730 | self.__key2 = des(key[8:16], self._mode, self._iv, 731 | self._padding, self._padmode) 732 | if self.key_size == 16: 733 | self.__key3 = self.__key1 734 | else: 735 | self.__key3 = des(key[16:], self._mode, self._iv, 736 | self._padding, self._padmode) 737 | _baseDes.setKey(self, key) 738 | 739 | # Override setter methods to work on all 3 keys. 740 | 741 | def setMode(self, mode): 742 | """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" 743 | _baseDes.setMode(self, mode) 744 | for key in (self.__key1, self.__key2, self.__key3): 745 | key.setMode(mode) 746 | 747 | def setPadding(self, pad): 748 | """setPadding() -> bytes of length 1. Padding character.""" 749 | _baseDes.setPadding(self, pad) 750 | for key in (self.__key1, self.__key2, self.__key3): 751 | key.setPadding(pad) 752 | 753 | def setPadMode(self, mode): 754 | """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" 755 | _baseDes.setPadMode(self, mode) 756 | for key in (self.__key1, self.__key2, self.__key3): 757 | key.setPadMode(mode) 758 | 759 | def setIV(self, IV): 760 | """Will set the Initial Value, used in conjunction with CBC mode""" 761 | _baseDes.setIV(self, IV) 762 | for key in (self.__key1, self.__key2, self.__key3): 763 | key.setIV(IV) 764 | 765 | def encrypt(self, data, pad=None, padmode=None): 766 | """encrypt(data, [pad], [padmode]) -> bytes 767 | 768 | data : bytes to be encrypted 769 | pad : Optional argument for encryption padding. Must only be one byte 770 | padmode : Optional argument for overriding the padding mode. 771 | 772 | The data must be a multiple of 8 bytes and will be encrypted 773 | with the already specified key. Data does not have to be a 774 | multiple of 8 bytes if the padding character is supplied, or 775 | the padmode is set to PAD_PKCS5, as bytes will then added to 776 | ensure the be padded data is a multiple of 8 bytes. 777 | """ 778 | ENCRYPT = des.ENCRYPT 779 | DECRYPT = des.DECRYPT 780 | data = self._guardAgainstUnicode(data) 781 | if pad is not None: 782 | pad = self._guardAgainstUnicode(pad) 783 | # Pad the data accordingly. 784 | data = self._padData(data, pad, padmode) 785 | if self.getMode() == CBC: 786 | self.__key1.setIV(self.getIV()) 787 | self.__key2.setIV(self.getIV()) 788 | self.__key3.setIV(self.getIV()) 789 | i = 0 790 | result = [] 791 | while i < len(data): 792 | block = self.__key1.crypt(data[i:i + 8], ENCRYPT) 793 | block = self.__key2.crypt(block, DECRYPT) 794 | block = self.__key3.crypt(block, ENCRYPT) 795 | self.__key1.setIV(block) 796 | self.__key2.setIV(block) 797 | self.__key3.setIV(block) 798 | result.append(block) 799 | i += 8 800 | if _pythonMajorVersion < 3: 801 | return ''.join(result) 802 | else: 803 | return bytes.fromhex('').join(result) 804 | else: 805 | data = self.__key1.crypt(data, ENCRYPT) 806 | data = self.__key2.crypt(data, DECRYPT) 807 | return self.__key3.crypt(data, ENCRYPT) 808 | 809 | def decrypt(self, data, pad=None, padmode=None): 810 | """decrypt(data, [pad], [padmode]) -> bytes 811 | 812 | data : bytes to be encrypted 813 | pad : Optional argument for decryption padding. Must only be one byte 814 | padmode : Optional argument for overriding the padding mode. 815 | 816 | The data must be a multiple of 8 bytes and will be decrypted 817 | with the already specified key. In PAD_NORMAL mode, if the 818 | optional padding character is supplied, then the un-encrypted 819 | data will have the padding characters removed from the end of 820 | the bytes. This pad removal only occurs on the last 8 bytes of 821 | the data (last data block). In PAD_PKCS5 mode, the special 822 | padding end markers will be removed from the data after 823 | decrypting, no pad character is required for PAD_PKCS5. 824 | """ 825 | ENCRYPT = des.ENCRYPT 826 | DECRYPT = des.DECRYPT 827 | data = self._guardAgainstUnicode(data) 828 | if pad is not None: 829 | pad = self._guardAgainstUnicode(pad) 830 | if self.getMode() == CBC: 831 | self.__key1.setIV(self.getIV()) 832 | self.__key2.setIV(self.getIV()) 833 | self.__key3.setIV(self.getIV()) 834 | i = 0 835 | result = [] 836 | while i < len(data): 837 | iv = data[i:i + 8] 838 | block = self.__key3.crypt(iv, DECRYPT) 839 | block = self.__key2.crypt(block, ENCRYPT) 840 | block = self.__key1.crypt(block, DECRYPT) 841 | self.__key1.setIV(iv) 842 | self.__key2.setIV(iv) 843 | self.__key3.setIV(iv) 844 | result.append(block) 845 | i += 8 846 | if _pythonMajorVersion < 3: 847 | data = ''.join(result) 848 | else: 849 | data = bytes.fromhex('').join(result) 850 | else: 851 | data = self.__key3.crypt(data, DECRYPT) 852 | data = self.__key2.crypt(data, ENCRYPT) 853 | data = self.__key1.crypt(data, DECRYPT) 854 | return self._unpadData(data, pad, padmode) 855 | -------------------------------------------------------------------------------- /hdu_api/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | hdu_api.api 5 | ----------- 6 | 7 | This module implements the hdu_api API. 8 | """ 9 | 10 | from hdu_api.models import Card, Exam, Person, Course, Public 11 | from hdu_api.sessions import SessionManager 12 | 13 | 14 | class HDU(object): 15 | def __init__(self, username, password, **kwargs): 16 | self.username = username 17 | self.password = password 18 | 19 | def __str__(self): 20 | return "".format(self.username) 21 | 22 | def create(self, multi=True, *args): 23 | """Create a client to use API.""" 24 | sess_mgr = SessionManager(self.username, self.password) 25 | sess_mgr.create(multi) 26 | return HduClient(sess_mgr) 27 | 28 | 29 | class HduClient(object): 30 | """ 31 | Use a HduClient to access all APIs. 32 | """ 33 | 34 | def __init__(self, sess_mgr, **kwargs): 35 | if isinstance(sess_mgr, SessionManager): 36 | self.sess_mgr = sess_mgr 37 | self.username = sess_mgr.username 38 | self.card = Card(sess_mgr.get_card_session()) 39 | self.exam = Exam(sess_mgr.get_teaching_session()) 40 | self.person = Person(sess_mgr.get_student_session()) 41 | self.course = Course(sess_mgr.get_teaching_session()) 42 | self.public = Public(sess_mgr.get_ihdu_phone_session()) 43 | 44 | else: 45 | raise ValueError('sess_mgr must be SessionManager.') 46 | 47 | def __str__(self): 48 | return ">> Card.consume_today() 203 | [{'流水号': '232205811', '帐号': '30003086', '卡片类型': 'M1', '交易类型': '卡户存款', '商户': '', 204 | '站点': '校付宝', '终端号': '0', '交易额': '50', '到帐时间': '2019-01-30 19:48', '钱包名称': '1号钱包', 205 | '卡余额': 'N/A'}] 206 | 207 | :return: 208 | """ 209 | results = None 210 | 211 | rsp = self.session.get(CARD_URLS['today'], allow_redirects=False) 212 | if rsp.status_code == 200: 213 | # TODO: 没有处理无的情况 214 | results = self._process_consume_data(rsp, raw=raw, dictionary=dictionary) 215 | 216 | return results 217 | 218 | def consume_week(self, raw=False, dictionary=DEFAULT_DICTIONARY): 219 | """ 220 | 查询这一周的消费记录。 221 | 222 | :return: 223 | """ 224 | pass 225 | 226 | def statistics(self, year, month, raw=False, dictionary=DEFAULT_DICTIONARY): 227 | """ 228 | 查询某个月的交易统计。 229 | 230 | :param year: 231 | :param month: 232 | :param raw: 233 | :param dictionary: 234 | :return: 235 | """ 236 | results = None 237 | 238 | payload = self._prepare_payload(CARD_URLS['statistics'], year, month) 239 | 240 | rsp = self.session.post(CARD_URLS['statistics'], data=payload, allow_redirects=False) 241 | if rsp.headers['location'] == '/zytk32portal/Cardholder/QueryMonthResult.aspx': 242 | rsp = self.session.get(CARD_URLS['statistics_result'], allow_redirects=False) 243 | if rsp.status_code == 200: 244 | results = [] 245 | 246 | soup = BeautifulSoup(rsp.text, 'lxml') 247 | rows = soup.find('table', id='Table13').next_sibling.next_sibling.find_all('tr')[2].find( 248 | 'table').find_all('tr') 249 | 250 | keys = [] 251 | row = rows.pop(0) 252 | tds = row.find_all('td')[1:] 253 | for td in tds: 254 | key = td.get_text(strip=True) 255 | if not raw: 256 | key = dictionary[key] 257 | keys.append(key) 258 | 259 | for row in rows: 260 | result = {} 261 | tds = row.find_all('td')[1:] 262 | for key, td in zip(keys, tds): 263 | result.update({key: td.get_text(strip=True)}) 264 | results.append(result) 265 | 266 | return results 267 | 268 | def _prepare_payload(self, url, year, month): 269 | """准备表单数据。 270 | 271 | :param url: 272 | :param year: 273 | :param month: 274 | :return: 275 | """ 276 | if url not in self.reuse_form_data['__VIEWSTATE']: 277 | rsp = self.session.get(url, allow_redirects=False) 278 | soup = BeautifulSoup(rsp.text, 'lxml') 279 | viewstate = soup.find('input', id='__VIEWSTATE')['value'] 280 | self.reuse_form_data['__VIEWSTATE'].update({url: viewstate}) 281 | payload = { 282 | '__VIEWSTATE': self.reuse_form_data['__VIEWSTATE'][url], 283 | 'ddlYear': year, 284 | 'ddlMonth': month, 285 | 'txtMonth': month, 286 | 'ImageButton1.x': 33, 287 | 'ImageButton1.y': 5, 288 | 289 | } 290 | 291 | return payload 292 | 293 | @staticmethod 294 | def _process_consume_data(rsp, raw, dictionary): 295 | results = [] 296 | 297 | soup = BeautifulSoup(rsp.text, 'lxml') 298 | try: 299 | rows = soup.find('table', id='dgShow').find_all('tr') 300 | 301 | keys = [] 302 | row = rows.pop(0) 303 | tds = row.find_all('td') 304 | for td in tds: 305 | key = td.get_text(strip=True) 306 | if not raw: 307 | key = dictionary[key] 308 | keys.append(key) 309 | 310 | for row in rows: 311 | result = {} 312 | tds = row.find_all('td') 313 | for key, td in zip(keys, tds): 314 | result.update({key: td.get_text(strip=True)}) 315 | 316 | results.append(result) 317 | except: 318 | if "" in rsp.text: 319 | return results 320 | else: 321 | return None 322 | 323 | return results 324 | 325 | 326 | class Exam(TeachingBaseModel): 327 | """考试相关. 328 | 329 | """ 330 | 331 | def grade(self, year, term, raw=False, dictionary=DEFAULT_DICTIONARY): 332 | """ 333 | 查询学期成绩。 334 | 335 | :param year: 336 | :param term: 337 | :param raw: 338 | :param dictionary: 339 | :return: 340 | """ 341 | results = None 342 | 343 | url = EXAM_URLS['grade'].format(username=self.username, realname=self.realname) 344 | payload = self._prepare_payload(url, year, term) 345 | 346 | rsp = self.session.post(url, data=payload, allow_redirects=False) 347 | if rsp.status_code == 200: 348 | results = self._process_data(rsp, raw=raw, dictionary=dictionary) 349 | 350 | return results 351 | 352 | def grade_current(self, raw=False, dictionary=DEFAULT_DICTIONARY): 353 | """查询本学期的成绩。 354 | 355 | :return: 356 | """ 357 | year, term = get_current_term() 358 | return self.grade(year, term, raw=raw, dictionary=dictionary) 359 | 360 | def level_grade(self, raw=False, dictionary=DEFAULT_DICTIONARY): 361 | """查询等级考试成绩 362 | 363 | :return: 364 | """ 365 | results = None 366 | 367 | url = EXAM_URLS['level_grade'].format(username=self.username, realname=self.realname) 368 | rsp = self.session.get(url, allow_redirects=False) 369 | 370 | if rsp.status_code == 200: 371 | results = self._process_data(rsp, raw=raw, dictionary=dictionary) 372 | 373 | return results 374 | 375 | def schedule(self, year, term, raw=False, dictionary=DEFAULT_DICTIONARY): 376 | """ 377 | 查询考试安排。 378 | 379 | :param year: 380 | :param term: 381 | :param raw: 382 | :param dictionary: 383 | :return: 384 | """ 385 | results = None 386 | 387 | url = EXAM_URLS['schedule'].format(username=self.username, realname=self.realname) 388 | payload = self._prepare_payload(url, year, term) 389 | 390 | rsp = self.session.post(url, data=payload, allow_redirects=False) 391 | if rsp.status_code == 200: 392 | results = self._process_data(rsp, raw=raw, dictionary=dictionary) 393 | 394 | return results 395 | 396 | def schedule_current(self, raw=False, dictionary=DEFAULT_DICTIONARY): 397 | """查询本学期考试安排。 398 | 399 | :return: 400 | """ 401 | year, term = get_current_term() 402 | return self.schedule(year, term, raw=raw, dictionary=dictionary) 403 | 404 | def schedule_make_up(self, term, raw=False, dictionary=DEFAULT_DICTIONARY): 405 | """查询补考安排""" 406 | results = None 407 | 408 | url = EXAM_URLS['schedule_make_up'].format(username=self.username, realname=self.realname) 409 | payload = self._prepare_payload(url, None, term) 410 | 411 | rsp = self.session.post(url, data=payload, allow_redirects=False) 412 | if rsp.status_code == 200: 413 | results = self._process_data(rsp, raw=raw, dictionary=dictionary) 414 | 415 | return results 416 | 417 | def _prepare_payload(self, url, year, term): 418 | if url not in self.reuse_form_data['__VIEWSTATE'] or url not in self.reuse_form_data['__EVENTVALIDATION']: 419 | rsp = self.session.get(url, allow_redirects=False) 420 | if rsp.status_code == 200: 421 | soup = BeautifulSoup(rsp.text, 'lxml') 422 | try: 423 | viewstate = soup.find('input', id='__VIEWSTATE')['value'] 424 | eventvalidation = soup.find('input', id='__EVENTVALIDATION')['value'] 425 | self.reuse_form_data['__VIEWSTATE'].update({url: viewstate}) 426 | self.reuse_form_data['__EVENTVALIDATION'].update({url: eventvalidation}) 427 | except: 428 | pass 429 | 430 | # 表单数据混合了两个,但无影响 431 | paylaod = { 432 | '__EVENTTARGET': '', 433 | '__EVENTARGUMENT': '', 434 | '__LASTFOCUS': '', 435 | '__VIEWSTATE': self.reuse_form_data['__VIEWSTATE'][url], 436 | '__EVENTVALIDATION': self.reuse_form_data['__EVENTVALIDATION'][url], 437 | 'xnd': year, 438 | 'xqd': term, 439 | 'ddlxn': year, 440 | 'ddlxq': term, 441 | 'btnCx': ' 查 询 ', 442 | } 443 | 444 | return paylaod 445 | 446 | @staticmethod 447 | def _process_data(rsp, raw, dictionary): 448 | results = [] 449 | 450 | soup = BeautifulSoup(rsp.text, 'xml') 451 | 452 | try: 453 | rows = soup.find('table', id='DataGrid1').find_all('tr') 454 | 455 | keys = [] 456 | row = rows.pop(0) 457 | tds = row.find_all('td') 458 | for td in tds: 459 | key = td.get_text(strip=True) 460 | if not raw: 461 | key = dictionary[key] 462 | keys.append(key) 463 | 464 | for row in rows: 465 | result = {} 466 | tds = row.find_all('td') 467 | for key, td in zip(keys, tds): 468 | result.update({key: td.get_text(strip=True)}) 469 | 470 | results.append(result) 471 | except: 472 | if 'Object moved to' in rsp.text: 473 | return None 474 | 475 | return results 476 | 477 | 478 | class Course(TeachingBaseModel): 479 | """ 480 | 481 | """ 482 | 483 | def selected(self, year, term, raw=False, dictionary=DEFAULT_DICTIONARY): 484 | results = None 485 | 486 | url = COURSE_URLS['selected'].format(username=self.username, realname=self.realname) 487 | payload = self._prepare_payload(url, year, term) 488 | rsp = self.session.post(url, data=payload, allow_redirects=False) 489 | 490 | if rsp.status_code == 200: 491 | results = self._process_data(rsp, raw=raw, dictionary=dictionary) 492 | 493 | return results 494 | 495 | @staticmethod 496 | def _process_data(rsp, raw, dictionary): 497 | results = [] 498 | 499 | soup = BeautifulSoup(rsp.text, 'lxml') 500 | try: 501 | rows = soup.find('table', id='DBGrid').find_all('tr') 502 | 503 | keys = [] 504 | row = rows.pop(0) 505 | tds = row.find_all('td')[:-4] # 这里把后面 4 项省去了 506 | for td in tds: 507 | key = td.get_text(strip=True) 508 | if not raw: 509 | key = dictionary[key] 510 | keys.append(key) 511 | for row in rows: 512 | result = {} 513 | tds = row.find_all('td')[:-4] 514 | for key, td in zip(keys, tds): 515 | result.update({key: td.get('title') if td.get('title') else td.get_text(strip=True)}) 516 | 517 | results.append(result) 518 | except: 519 | if 'Object moved to' in rsp.text: 520 | return None 521 | 522 | return results 523 | 524 | def selected_current(self, raw=False, dictionary=DEFAULT_DICTIONARY): 525 | year, term = get_current_term() 526 | return self.selected(year, term, raw, dictionary) 527 | 528 | def schedule(self, year, term, raw=False, dictionary=DEFAULT_DICTIONARY): 529 | """ 530 | 查询某学期的课表。 531 | 532 | :param year: 533 | :param term: 534 | :param raw: 535 | :param dictionary: 536 | :return: 537 | """ 538 | results = None 539 | 540 | # url = COURSE_URLS['schedule'].format(username=self.username, realname=self.realname) 541 | # payload = self._prepare_payload(url, year, term) 542 | # 543 | # rsp = self.session.post(url, data=payload, allow_redirects=False) 544 | # if rsp.status_code == 200: 545 | # results = self._process_course_data(rsp) 546 | 547 | selected = self.selected(year, term, raw=True, dictionary=dictionary) 548 | if selected: 549 | results = self._process_course_data(selected, raw=raw, dictionary=dictionary) 550 | 551 | return results 552 | 553 | @staticmethod 554 | def _process_course_data(selected, raw, dictionary): 555 | """ 556 | 处理课表数据。 557 | 558 | :param selected: 559 | :param raw: 560 | :param dictionary: 561 | :return: 562 | """ 563 | 564 | results = [] 565 | 566 | for s in selected: 567 | name = s.get('课程名称') 568 | teacher = s.get('教师姓名') 569 | classroom = s.get('上课地点').split(';') 570 | classtime = s.get('上课时间').split(';') 571 | weekday = None 572 | start_section = None 573 | end_section = None 574 | start_week = None 575 | end_week = None 576 | distribute = '每周' 577 | 578 | for i in range(len(classtime)): 579 | m1 = re.match(r'^(周.{1})第(\d*).*?(\d*)节{第(\d*)-(\d*)周\|?([单双]周)?}$', classtime[i]) 580 | m2 = re.match(r'^第(\d*)周/(周.{1})/(.*)第(\d*)周/(周.{1})/(.*)$', classtime[i]) 581 | if m1: 582 | time_t = m1.groups() 583 | weekday = time_t[0] 584 | start_section = time_t[1] 585 | end_section = time_t[2] 586 | start_week = time_t[3] 587 | end_week = time_t[4] 588 | distribute = time_t[5] if time_t[5] else '每周' 589 | elif m2: 590 | time_t = m2.groups() 591 | start_week = time_t[0] 592 | start_section = time_t[2] 593 | end_week = time_t[3] 594 | end_section = time_t[5] 595 | weekday = time_t[1] + ' - ' + time_t[4] 596 | 597 | if raw: 598 | course = { 599 | '课程名称': name, 600 | '教师姓名': teacher, 601 | '上课地点': classroom[i], 602 | '星期': weekday, 603 | '开始节数': start_section, 604 | '结束节数': end_section, 605 | '开始周': start_week, 606 | '结束周': end_week, 607 | '课程分布': distribute, 608 | } 609 | else: 610 | course = { 611 | dictionary.get('课程名称', '课程名称'): name, 612 | dictionary.get('教师姓名', '教师姓名'): teacher, 613 | dictionary.get('上课地点', '上课地点'): classroom[i], 614 | dictionary.get('星期', '星期'): weekday, 615 | dictionary.get('开始节数', '开始节数'): start_section, 616 | dictionary.get('结束节数', '结束节数'): end_section, 617 | dictionary.get('开始周', '开始周'): start_week, 618 | dictionary.get('结束周', '结束周'): end_week, 619 | dictionary.get('课程分布', '课程分布'): distribute, 620 | } 621 | 622 | results.append(course) 623 | 624 | return results 625 | 626 | def schedule_current(self, raw=False, dictionary=DEFAULT_DICTIONARY): 627 | year, term = get_current_term() 628 | return self.schedule(year, term, raw=raw, dictionary=dictionary) 629 | 630 | def _prepare_payload(self, url, year, term): 631 | if url not in self.reuse_form_data['__VIEWSTATE'] or url not in self.reuse_form_data['__EVENTVALIDATION']: 632 | rsp = self.session.get(url, allow_redirects=False) 633 | if rsp.status_code == 200: 634 | soup = BeautifulSoup(rsp.text, 'lxml') 635 | try: 636 | viewstate = soup.find('input', id='__VIEWSTATE')['value'] 637 | eventvalidation = soup.find('input', id='__EVENTVALIDATION')['value'] 638 | self.reuse_form_data['__VIEWSTATE'].update({url: viewstate}) 639 | self.reuse_form_data['__EVENTVALIDATION'].update({url: eventvalidation}) 640 | except: 641 | pass 642 | 643 | # 表单数据混合了两个,但无影响 644 | paylaod = { 645 | '__EVENTTARGET': '', 646 | '__EVENTARGUMENT': '', 647 | '__LASTFOCUS': '', 648 | '__VIEWSTATE': self.reuse_form_data['__VIEWSTATE'][url], 649 | '__EVENTVALIDATION': self.reuse_form_data['__EVENTVALIDATION'][url], 650 | 'xnd': year, 651 | 'xqd': term, 652 | # 'ddlxn': year, 653 | # 'ddlxq': term, # 和 ddlXQ 冲突 654 | 'ddlXN': year, 655 | 'ddlXQ': term, 656 | 'btnCx': ' 查 询 ', 657 | } 658 | 659 | return paylaod 660 | 661 | 662 | class Person(StudentBaseModel): 663 | """ 664 | 个人信息。 665 | """ 666 | 667 | def _get_common_page(self): 668 | rsp = self.session.get(PERSON_URLS['common'], allow_redirects=False) 669 | if rsp.status_code == 200: 670 | soup = BeautifulSoup(rsp.text, 'lxml') 671 | rows = soup.find('div', class_='cg-form-elements').find_all('tr') 672 | return rows 673 | 674 | @staticmethod 675 | def _process_data_from_rows(rows, raw, dictionary): 676 | result = None 677 | if rows: 678 | result = {} 679 | for row in rows: 680 | tds = row.find_all('td') 681 | for i in range(len(tds) // 2): 682 | key = tds[2 * i].get_text(strip=True).replace(':', '').replace(' ', '') 683 | if key == '': 684 | continue 685 | if not raw: 686 | key = dictionary[key] 687 | result.update({key: tds[2 * i + 1].get_text(strip=True)}) 688 | 689 | return result 690 | 691 | @staticmethod 692 | def _process_award_data_form_rows(rows, raw, dictionary): 693 | result = None 694 | if rows: 695 | result = {} 696 | 697 | keys = [] 698 | row = rows.pop(0) 699 | tds = row.find_all('td')[1:] 700 | # 生成数据模板 {'2018-2019': {各奖项}, '2017-2018':{}, ...} 701 | for td in tds: 702 | key = td.get_text(strip=True) 703 | if not raw: 704 | key = dictionary[key] 705 | keys.append(key) 706 | result.update({key: dict()}) 707 | 708 | for row in rows[:-5]: 709 | tds = row.find_all('td') 710 | key = tds.pop(0).get_text(strip=True) 711 | for i in range(len(tds)): 712 | result[keys[i]].update({key: tds[i].get_text(strip=True)}) 713 | 714 | tds = rows[-3].find_all('td') # 学期标题 715 | term_keys = [] 716 | for i in range(len(tds) // 2): # 学期标题,两组一学年 717 | for j in range(2): 718 | key = tds[2 * i + j].get_text(strip=True) 719 | if not raw: 720 | key = dictionary[key] 721 | term_keys.append(key) 722 | result[keys[i]].update({key: dict()}) 723 | 724 | for row in rows[-2:]: 725 | tds = row.find_all('td') 726 | key = tds.pop(0).get_text(strip=True) 727 | for i in range(len(tds) // 2): 728 | result[keys[i]][term_keys[2 * i]].update({key: tds[2 * i].get_text(strip=True)}) 729 | result[keys[i]][term_keys[2 * i + 1]].update({key: tds[2 * i + 1].get_text(strip=True)}) 730 | 731 | return result 732 | 733 | def profile(self, raw=False, dictionary=DEFAULT_DICTIONARY): 734 | """查询基本个人信息。 735 | 736 | :return: 737 | """ 738 | rows = self._get_common_page() 739 | return self._process_data_from_rows(rows[1:9], raw=raw, dictionary=dictionary) 740 | 741 | def instructor(self, raw=False, dictionary=DEFAULT_DICTIONARY): 742 | """辅导员信息。""" 743 | rows = self._get_common_page() 744 | return self._process_data_from_rows(rows[10:12], raw=raw, dictionary=dictionary) 745 | 746 | def status(self, raw=False, dictionary=DEFAULT_DICTIONARY): 747 | """查询学籍信息。""" 748 | rows = self._get_common_page() 749 | return self._process_data_from_rows(rows[13:17], raw=raw, dictionary=dictionary) 750 | 751 | def accommodation(self, raw=False, dictionary=DEFAULT_DICTIONARY): 752 | """住宿信息。 753 | 754 | :return: 755 | """ 756 | rows = self._get_common_page() 757 | return self._process_data_from_rows(rows[18:20], raw=raw, dictionary=dictionary) 758 | 759 | def award(self, raw=False, dictionary=DEFAULT_DICTIONARY): 760 | """查询奖项。""" 761 | rows = self._get_common_page() 762 | return self._process_award_data_form_rows(rows[-18:-4], raw=raw, dictionary=dictionary) 763 | 764 | def profile_all(self, raw=False, dictionary=DEFAULT_DICTIONARY): 765 | """查询详细的个人信息。 766 | 767 | :return: 768 | """ 769 | rows = self._get_common_page() 770 | return { 771 | # 基本信息 772 | rows[0].get_text(strip=True): self._process_data_from_rows(rows[1:9], raw=raw, dictionary=dictionary), 773 | # 辅导员信息 774 | rows[9].get_text(strip=True): self._process_data_from_rows(rows[10:12], raw=raw, dictionary=dictionary), 775 | # 学籍信息 776 | rows[12].get_text(strip=True): self._process_data_from_rows(rows[13:17], raw=raw, dictionary=dictionary), 777 | # 宿舍信息 778 | rows[17].get_text(strip=True): self._process_data_from_rows(rows[18:20], raw=raw, dictionary=dictionary), 779 | # 获奖信息 780 | rows[-20].get_text(strip=True): self._process_award_data_form_rows(rows[-18:-4], raw=raw, 781 | dictionary=dictionary), 782 | } 783 | 784 | 785 | class Public(IHDUPhoneBaseModel): 786 | """ 787 | 788 | """ 789 | 790 | def classroom_free(self, raw=False, dictionary=DEFAULT_DICTIONARY): 791 | """查询空闲教室。 792 | 793 | :return: 794 | """ 795 | 796 | results = None 797 | 798 | rsp = self.session.get(PUBLIC_URLS['class_free'], allow_redirects=False) 799 | if rsp.status_code == 200: 800 | results = self._process_classroom_data(rsp, raw=raw, dictionary=dictionary) 801 | return results 802 | 803 | def classroom_in_use(self, raw=False, dictionary=DEFAULT_DICTIONARY): 804 | """查询有课的教室。 805 | 806 | :return: 807 | """ 808 | results = None 809 | 810 | rsp = self.session.get(PUBLIC_URLS['class_in_use'], allow_redirects=False) 811 | if rsp.status_code == 200: 812 | results = self._process_classroom_data(rsp, raw=raw, dictionary=dictionary) 813 | return results 814 | 815 | @staticmethod 816 | def schooltime(location=0, raw=False, dictionary=DEFAULT_DICTIONARY): 817 | """ 818 | 819 | :param location: 0 means 下沙, 1 means 青山湖 820 | :param raw: 821 | :param dictionary: 822 | :return: 823 | """ 824 | results = None 825 | 826 | rsp = BaseSession().get(PUBLIC_URLS['school_time'], allow_redirects=False) 827 | if rsp.status_code == 200: 828 | results = [] 829 | 830 | div_id = 'xiashaTime' 831 | if int(location) == 1: 832 | div_id = 'xingongTime' 833 | soup = BeautifulSoup(rsp.text, 'lxml') 834 | rows = soup.find('div', id=div_id).find_all('tr') 835 | 836 | for row in rows: 837 | result = {} 838 | tds = row.find_all('td') 839 | key1 = '节数' 840 | key2 = '开始时间' 841 | key3 = '结束时间' 842 | if not raw: 843 | key1 = dictionary[key1] 844 | key2 = dictionary[key2] 845 | key3 = dictionary[key3] 846 | times = tds[-1].get_text(strip=True).split('~') 847 | times.append('') # 有些只有一个 848 | result.update({ 849 | key1: tds[-2].get_text(strip=True), 850 | key2: times[0].strip(), 851 | key3: times[1].strip(), 852 | }) 853 | results.append(result) 854 | 855 | return results 856 | 857 | @staticmethod 858 | def _process_classroom_data(rsp, raw, dictionary): 859 | results = [] 860 | soup = BeautifulSoup(rsp.text, 'lxml') 861 | 862 | try: 863 | tables = soup.find_all('table')[1:] 864 | if tables: 865 | for table in tables: 866 | result = {} 867 | 868 | key1 = '教研楼' 869 | key2 = '教室' 870 | if not raw: 871 | key1 = dictionary[key1] 872 | key2 = dictionary[key2] 873 | location = table.find('tr').get_text(strip=True) 874 | classroom = [] 875 | 876 | ths = table.find_all('th', class_='xl1') 877 | for th in ths: 878 | classroom.append(th.get_text(strip=True)) 879 | 880 | result.update({ 881 | key1: location, 882 | key2: classroom, 883 | }) 884 | 885 | results.append(result) 886 | except: 887 | return None 888 | 889 | return results 890 | 891 | 892 | def get_current_term(): 893 | localtime = time.localtime(time.time()) 894 | 895 | if localtime.tm_mon in range(3, 9): # 下学期 3, 4, 5, 6, 7, 8 896 | year = '{}-{}'.format(localtime.tm_year - 1, localtime.tm_year) 897 | term = 2 898 | else: # 上学期 9, 10, 11, 12, 1, 2 899 | term = 1 900 | if localtime.tm_mon in range(9, 13): # 上学期 9, 10, 11, 12, 年份向前看 901 | year = '{}-{}'.format(localtime.tm_year, localtime.tm_year + 1) 902 | else: 903 | year = '{}-{}'.format(localtime.tm_year - 1, localtime.tm_year) 904 | 905 | return year, term 906 | -------------------------------------------------------------------------------- /hdu_api/sessions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | hdu_api.sessions 5 | ---------------- 6 | 7 | 8 | This module implement the management of session for hdu_api. 9 | """ 10 | 11 | from __future__ import unicode_literals 12 | 13 | import re 14 | from threading import Thread 15 | 16 | from bs4 import BeautifulSoup 17 | from requests import Session 18 | 19 | from hdu_api._internal_utils import encrypt 20 | from hdu_api.config import CAS_LOGIN_HEADERS, TEACHING_HEADERS, STUDENT_HEADERS, CARD_HEADERS, IHDU_HEADERS, \ 21 | IHDU_PHONE_HEADERS 22 | from hdu_api.config import HOME_URLS, CAS_LOGIN_URLS, CARD_ERROR_URL 23 | from hdu_api.exceptions import LoginFailException 24 | 25 | # 登录重试次数 26 | RETRY = 3 27 | 28 | CARD_SESSION_NAME = 'card' 29 | TEACHING_SEESION_NAME = 'teaching' 30 | STUDENT_SESSION_NAME = 'student' 31 | IHDU_SESSION_NAME = 'ihdu' 32 | IHDU_PHONE_SESSION_NAME = 'ihdu_phone' 33 | 34 | 35 | class BaseSession(Session): 36 | """基本 session, 继承自 requests.Session""" 37 | 38 | 39 | class BaseSessionLoginMixin(BaseSession): 40 | """混合登录功能""" 41 | 42 | def __init__(self, username, password): 43 | super(BaseSessionLoginMixin, self).__init__() 44 | self.username = username 45 | self.password = password 46 | self.realname = None 47 | self.home_url = None 48 | 49 | def login(self, headers): 50 | retry = 0 51 | while retry < RETRY: 52 | retry += 1 53 | self._do_login() 54 | 55 | # headers['referer'] = self.home_url 56 | self.headers.update(headers) # 更新 headers 57 | 58 | if self._check_sess_vaild(): 59 | return 60 | else: 61 | self.cookies.clear() # 直接清除 cookie 有点问题 62 | self.headers.clear() 63 | 64 | if retry == RETRY: 65 | raise LoginFailException('登录失败.') 66 | 67 | def _do_login(self): 68 | raise NotImplementedError 69 | 70 | def _get_payload(self, url): 71 | rsp = self.get(url) 72 | if rsp.status_code != 200: 73 | return None 74 | soup = BeautifulSoup(rsp.text, 'lxml').find('script', id='password_template') 75 | soup = BeautifulSoup(soup.contents[0], 'lxml') 76 | lt = soup.find('input', id='lt')['value'] 77 | execution = soup.find('input', attrs={'name': 'execution'})['value'] 78 | _eventId = soup.find('input', attrs={'name': '_eventId'})['value'] 79 | rsa = encrypt(self.username + self.password + lt, '1', '2', '3') 80 | payload = { 81 | 'rsa': rsa, 82 | 'ul': len(self.username), 83 | 'pl': len(self.password), 84 | 'lt': lt, 85 | 'execution': execution, 86 | '_eventId': _eventId, 87 | } 88 | 89 | return payload 90 | 91 | def _check_sess_vaild(self): 92 | raise NotImplementedError 93 | 94 | @staticmethod 95 | def is_valid_url(self, url): 96 | reg = r'^http[s]*://.+$' 97 | return re.match(reg, url) 98 | 99 | def refresh(self): 100 | """ 101 | 刷新 seesion 102 | 103 | :rtype: bool 104 | """ 105 | rsp = self.get(self.home_url, allow_redirects=False) 106 | if rsp.status_code == 200: 107 | return True 108 | 109 | return False 110 | 111 | 112 | class TeachingSessionLoginMixin(BaseSessionLoginMixin): 113 | """ 114 | 混合教务管理系统(http://jxgl.hdu.edu.cn)的登录功能. 115 | """ 116 | 117 | def __init__(self, username, password): 118 | super(TeachingSessionLoginMixin, self).__init__(username, password) 119 | self.home_url = HOME_URLS['teaching'].format(username=username) 120 | 121 | def login(self, headers=TEACHING_HEADERS): 122 | super(TeachingSessionLoginMixin, self).login(headers) 123 | 124 | def _do_login(self): 125 | """登录数字杭电,然后转跳到教务系统。""" 126 | payload = self._get_payload(CAS_LOGIN_URLS['teaching']) 127 | 128 | # 通过智慧杭电认证 129 | 130 | rsp = self.post(CAS_LOGIN_URLS['teaching'], data=payload, headers=CAS_LOGIN_HEADERS, allow_redirects=False) 131 | 132 | # 选课系统转跳 133 | next_url = rsp.headers['Location'] 134 | rsp = self.get(next_url, allow_redirects=False) 135 | 136 | def _check_sess_vaild(self): 137 | cookies_keys = list(self.cookies.get_dict().keys()) 138 | if 'ASP.NET_SessionId' in cookies_keys and 'route' in cookies_keys: 139 | rsp = self.get(self.home_url, allow_redirects=False) 140 | if 'Object moved' not in rsp.text: 141 | soup = BeautifulSoup(rsp.text, 'lxml') 142 | try: 143 | self.realname = soup.find('form').find('div', class_='info').find('span', 144 | id='xhxm').get_text().replace( 145 | '同学', '') 146 | except: 147 | return False 148 | return True 149 | 150 | return False 151 | 152 | 153 | class CardSessionLoginMixin(BaseSessionLoginMixin): 154 | """ 155 | 混合一卡通系统(http://ykt.hdu.edu.cn/zytk32portal/Cardholder/Cardholder.aspx)的登录功能. 156 | """ 157 | 158 | def __init__(self, username, password): 159 | super(CardSessionLoginMixin, self).__init__(username, password) 160 | self.home_url = HOME_URLS['card'] 161 | 162 | def login(self, headers=CARD_HEADERS): 163 | super(CardSessionLoginMixin, self).login(headers) 164 | 165 | def _do_login(self): 166 | payload = self._get_payload(CAS_LOGIN_URLS['card']) 167 | 168 | try: 169 | rsp = self.post(CAS_LOGIN_URLS['card'], data=payload, headers=CAS_LOGIN_HEADERS, 170 | allow_redirects=False) 171 | next_url = rsp.headers['Location'] 172 | rsp = self.get(next_url, allow_redirects=False) 173 | next_url = rsp.headers['Location'] 174 | rsp = self.get(next_url, allow_redirects=False) 175 | next_url = rsp.headers['Location'] 176 | rsp = self.get(next_url, allow_redirects=False) 177 | 178 | # next_url = rsp.headers['Location'] 179 | # next_url = "http://ykt.hdu.edu.cn" + next_url 180 | # self.home_url = next_url 181 | # rsp = self.session.get(self.home_url, allow_redirects=False) 182 | # print(rsp.headers['Location']) 183 | except: 184 | return 185 | 186 | def _check_sess_vaild(self): 187 | cookies_keys = list(self.cookies.get_dict().keys()) 188 | if 'ASP.NET_SessionId' in cookies_keys: 189 | rsp = self.get(self.home_url, allow_redirects=False) 190 | if rsp.status_code == 302 and rsp.headers['location'] in CARD_ERROR_URL and 'Object moved' in rsp.text: 191 | return False 192 | else: 193 | soup = BeautifulSoup(rsp.text, 'lxml') 194 | try: 195 | self.realname = soup.find('span', id='lblInName').get_text() 196 | return True 197 | except: 198 | return False 199 | 200 | return False 201 | 202 | 203 | class StudentSessionLoginMixin(BaseSessionLoginMixin): 204 | """ 205 | 混合学生管理系统(http://xgxt.hdu.edu.cn/login)的登录功能. 206 | """ 207 | 208 | def __init__(self, username, password): 209 | super(StudentSessionLoginMixin, self).__init__(username, password) 210 | self.home_url = HOME_URLS['student'] 211 | 212 | def login(self, headers=STUDENT_HEADERS): 213 | super(StudentSessionLoginMixin, self).login(headers) 214 | 215 | def _do_login(self): 216 | payload = self._get_payload(CAS_LOGIN_URLS['student']) 217 | 218 | try: 219 | rsp = self.post(CAS_LOGIN_URLS['student'], data=payload, headers=CAS_LOGIN_HEADERS, 220 | allow_redirects=False) 221 | next_url = rsp.headers['location'] 222 | rsp = self.get(next_url, allow_redirects=False) 223 | next_url = rsp.headers['location'] 224 | 225 | self.home_url = next_url 226 | except: 227 | pass 228 | 229 | def _check_sess_vaild(self): 230 | cookies_keys = list(self.cookies.get_dict().keys()) 231 | if 'route' in cookies_keys and 'JSESSIONID' in cookies_keys: 232 | rsp = self.get(self.home_url, allow_redirects=False) 233 | if 'Object moved' in rsp.text: 234 | return False 235 | else: 236 | soup = BeautifulSoup(rsp.text, 'lxml') 237 | try: 238 | self.realname = soup.find('span', id='login-username').get_text().strip().split()[0] 239 | return True 240 | except: 241 | return False 242 | 243 | return False 244 | 245 | 246 | class IHDUSessionLoginMixin(BaseSessionLoginMixin): 247 | """ 248 | 混合 ihdu(https://i.hdu.edu.cn/tp_up/view?m=up) 的登录功能. 249 | """ 250 | 251 | def __init__(self, username, password): 252 | super(IHDUSessionLoginMixin, self).__init__(username, password) 253 | self.home_url = HOME_URLS['ihdu'] 254 | 255 | def login(self, headers=IHDU_HEADERS): 256 | super(IHDUSessionLoginMixin, self).login(headers) 257 | 258 | def _do_login(self): 259 | payload = self._get_payload(CAS_LOGIN_URLS['ihdu']) 260 | 261 | try: 262 | rsp = self.post(CAS_LOGIN_URLS['ihdu'], data=payload, headers=CAS_LOGIN_HEADERS, 263 | allow_redirects=False) 264 | next_url = rsp.headers['location'] 265 | rsp = self.get(next_url, allow_redirects=False) 266 | except: 267 | pass 268 | 269 | def _check_sess_vaild(self): 270 | cookies_keys = list(self.cookies.get_dict().keys()) 271 | if 'tp_up' in cookies_keys: 272 | rsp = self.get(self.home_url, allow_redirects=False) 273 | if rsp.status_code != 200 or 'Object moved' in rsp.text: 274 | return False 275 | else: 276 | soup = BeautifulSoup(rsp.text, 'lxml') 277 | try: 278 | self.realname = soup.find('div', id='user-con').find('span', class_='tit').get_text().strip() 279 | return True 280 | except: 281 | return False 282 | 283 | return False 284 | 285 | 286 | class IHDUPhoneSessionLoginMixin(BaseSessionLoginMixin): 287 | """ 288 | 混合 ihdu手机版(http://once.hdu.edu.cn/dcp/xphone/m.jsp)的登录功能. 289 | """ 290 | 291 | def __init__(self, username, password): 292 | super(IHDUPhoneSessionLoginMixin, self).__init__(username, password) 293 | self.home_url = HOME_URLS['ihdu_phone'] 294 | 295 | def login(self, headers=IHDU_PHONE_HEADERS): 296 | super(IHDUPhoneSessionLoginMixin, self).login(headers) 297 | 298 | def _do_login(self): 299 | payload = self._get_payload(CAS_LOGIN_URLS['ihdu_phone']) 300 | 301 | try: 302 | rsp = self.post(CAS_LOGIN_URLS['ihdu_phone'], data=payload, headers=CAS_LOGIN_HEADERS, 303 | allow_redirects=False) 304 | next_url = rsp.headers['location'] 305 | rsp = self.get(next_url, allow_redirects=False) 306 | except: 307 | pass 308 | 309 | def _check_sess_vaild(self): 310 | cookies_keys = list(self.cookies.get_dict().keys()) 311 | if 'route' in cookies_keys and 'key_dcp_v5' in cookies_keys: 312 | rsp = self.get(self.home_url, allow_redirects=False) 313 | if rsp.status_code != 200 or 'Object moved' in rsp.text: 314 | return False 315 | else: 316 | soup = BeautifulSoup(rsp.text, 'lxml') 317 | try: 318 | uls = soup.find('div', class_='webkitbox').find_all('ul') 319 | if len(uls) == 3: 320 | return True 321 | except: 322 | return False 323 | 324 | return False 325 | 326 | 327 | class TeachingSession(TeachingSessionLoginMixin): 328 | """教务管理系统(http://jxgl.hdu.edu.cn)的 session""" 329 | 330 | 331 | class CardSession(CardSessionLoginMixin): 332 | """一卡通系统(http://ykt.hdu.edu.cn/zytk32portal/Cardholder/Cardholder.aspx)的 session""" 333 | 334 | 335 | class StudentSession(StudentSessionLoginMixin): 336 | """学生管理系统(http://xgxt.hdu.edu.cn/login)的 session""" 337 | 338 | 339 | class IHDUSession(IHDUSessionLoginMixin): 340 | """ihdu(https://i.hdu.edu.cn/tp_up/view?m=up)的 session""" 341 | 342 | 343 | class IHDUPhoneSession(IHDUPhoneSessionLoginMixin): 344 | """ihdu 手机版(http://once.hdu.edu.cn/dcp/xphone/m.jsp) 的 session""" 345 | 346 | 347 | class SessionManager(object): 348 | def __init__(self, username, password, **kwargs): 349 | self.username = username 350 | self.password = password 351 | self.sessions = dict() 352 | 353 | def create(self, multi=True): 354 | """ 355 | create and return a new and usable session dict. 356 | """ 357 | 358 | card_sess = CardSession(self.username, self.password) 359 | teaching_sess = TeachingSession(self.username, self.password) 360 | student_sess = StudentSession(self.username, self.password) 361 | ihdu_phone_sess = IHDUPhoneSession(self.username, self.password) 362 | ihdu_sess = IHDUSession(self.username, self.password) 363 | 364 | if multi: 365 | threads = [ 366 | Thread(target=card_sess.login), 367 | Thread(target=teaching_sess.login), 368 | Thread(target=student_sess.login), 369 | Thread(target=ihdu_phone_sess.login), 370 | Thread(target=ihdu_sess.login), 371 | ] 372 | 373 | for t in threads: 374 | t.start() 375 | 376 | for t in threads: 377 | t.join() 378 | 379 | else: 380 | card_sess.login() 381 | teaching_sess.login() 382 | student_sess.login() 383 | ihdu_phone_sess.login() 384 | ihdu_sess.login() 385 | 386 | self.sessions.update({ 387 | CARD_SESSION_NAME: card_sess, 388 | TEACHING_SEESION_NAME: teaching_sess, 389 | STUDENT_SESSION_NAME: student_sess, 390 | IHDU_SESSION_NAME: ihdu_sess, 391 | IHDU_PHONE_SESSION_NAME: ihdu_phone_sess, 392 | }) 393 | 394 | return self.sessions 395 | 396 | def get_teaching_session(self): 397 | return self.sessions.get(TEACHING_SEESION_NAME) 398 | 399 | def get_card_session(self): 400 | return self.sessions.get(CARD_SESSION_NAME) 401 | 402 | def get_student_session(self): 403 | return self.sessions.get(STUDENT_SESSION_NAME) 404 | 405 | def get_ihdu_session(self): 406 | return self.sessions.get(IHDU_SESSION_NAME) 407 | 408 | def get_ihdu_phone_session(self): 409 | return self.sessions.get(IHDU_PHONE_SESSION_NAME) 410 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE 3 | 4 | [bdist_wheel] 5 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | import setuptools 7 | 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | # 'setup.py publish' shortcut. 11 | if sys.argv[-1] == 'publish': 12 | os.system('python setup.py sdist bdist_wheel') 13 | os.system('twine upload dist/*') 14 | sys.exit() 15 | 16 | packages = ['hdu_api'] 17 | requires = [ 18 | 'requests', 19 | 'beautifulsoup4', 20 | 'lxml', 21 | ] 22 | 23 | about = {} 24 | with open(os.path.join(here, 'hdu_api', '__version__.py'), 'r', encoding='utf-8') as f: 25 | exec(f.read(), about) 26 | 27 | with open('README.md', 'r', encoding='utf-8') as f: 28 | long_description = f.read() 29 | 30 | setuptools.setup( 31 | name=about['__title__'], 32 | version=about['__version__'], 33 | author=about['__author__'], 34 | author_email=about['__author_email__'], 35 | description=about['__description__'], 36 | long_description=long_description, 37 | long_description_content_type="text/markdown", 38 | url=about['__url__'], 39 | packages=packages, 40 | classifiers=[ 41 | 'Development Status :: 5 - Production/Stable', 42 | 'Intended Audience :: Developers', 43 | "License :: OSI Approved :: MIT License", 44 | 'Programming Language :: Python', 45 | 'Programming Language :: Python :: 2', 46 | 'Programming Language :: Python :: 2.7', 47 | 'Programming Language :: Python :: 3', 48 | 'Programming Language :: Python :: 3.4', 49 | 'Programming Language :: Python :: 3.5', 50 | 'Programming Language :: Python :: 3.6', 51 | 'Programming Language :: Python :: 3.7', 52 | ], 53 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4', 54 | install_requires=requires, 55 | package_data={'': ['LICENSE', ]}, 56 | ) 57 | --------------------------------------------------------------------------------