Support get score

Co-authored-by: brian <brian@xtaolabs.com>
This commit is contained in:
brian 2023-03-08 20:48:12 +08:00 committed by GitHub
parent 499fd90264
commit 60c32861ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 262 additions and 160 deletions

View File

@ -1,7 +1,6 @@
import asyncio import asyncio
from typing import Coroutine, Optional from typing import Coroutine, Optional
from httpx import AsyncClient, Cookies from httpx import AsyncClient, Cookies
from urllib.parse import urlparse
from cqwu.methods import Methods from cqwu.methods import Methods
from cqwu.types import User from cqwu.types import User
@ -28,19 +27,8 @@ class Client(Methods):
else: else:
self.host = "http://ehall.cqwu.edu.cn" self.host = "http://ehall.cqwu.edu.cn"
self.auth_host = "http://authserver.cqwu.edu.cn" self.auth_host = "http://authserver.cqwu.edu.cn"
self.headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Host": urlparse(self.host).netloc,
"Origin": self.host,
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36"
}
self.cookies = Cookies() self.cookies = Cookies()
self.sub_cookies = Cookies() self.request = AsyncClient()
self.init_sub_web = []
self.request = AsyncClient
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.me: Optional[User] = None self.me: Optional[User] = None

View File

@ -1 +0,0 @@
from .order import OrderStatus

View File

@ -1,9 +0,0 @@
from enum import Enum
class OrderStatus(Enum):
NO_PAY = 0
NO_PAY_RE = 1
SUCCESS = 2
FAILURE = 3
EXPIRED = 4

View File

@ -1,6 +1,19 @@
class UsernameOrPasswordError(Exception): from .base import CQWUEhallError
class AuthError(CQWUEhallError):
pass pass
class CookieError(Exception): class UsernameOrPasswordError(AuthError):
pass pass
class CookieError(AuthError):
pass
class NeedCaptchaError(AuthError):
""" 需要验证码才能登录 """
def __init__(self, captcha: bytes):
self.captcha = captcha

3
cqwu/errors/base.py Normal file
View File

@ -0,0 +1,3 @@
class CQWUEhallError(Exception):
"""Base class for exceptions in this module."""
pass

View File

@ -1,11 +1,13 @@
from .auth import Auth from .auth import Auth
from .epay import EPay from .epay import EPay
from .users import Users from .users import Users
from .xg import XG
class Methods( class Methods(
Auth, Auth,
EPay, EPay,
Users Users,
XG,
): ):
pass pass

View File

@ -1,15 +1,19 @@
from .login_with_password import LoginWithPassword from .check_captcha import CheckCaptcha
from .export_cookie_to_file import ExportCookieToFile
from .login import Login
from .login_with_cookie import LoginWithCookie from .login_with_cookie import LoginWithCookie
from .login_with_cookie_file import LoginWithCookieFile from .login_with_cookie_file import LoginWithCookieFile
from .export_cookie_to_file import ExportCookieToFile from .login_with_password import LoginWithPassword
from .oauth import Oauth from .oauth import Oauth
class Auth( class Auth(
LoginWithPassword, CheckCaptcha,
ExportCookieToFile,
Login,
LoginWithCookie, LoginWithCookie,
LoginWithCookieFile, LoginWithCookieFile,
ExportCookieToFile, LoginWithPassword,
Oauth Oauth
): ):
pass pass

View File

@ -0,0 +1,34 @@
import time
import cqwu
from cqwu.errors.auth import NeedCaptchaError
class CheckCaptcha:
async def check_captcha(
self: "cqwu.Client",
username: int = None,
show_qrcode: bool = True,
):
""" 检查是否需要验证码 """
username = username or self.username
params = {
"username": username,
"pwdEncrypt2": "pwdEncryptSalt",
"_": str(round(time.time() * 1000))
}
url = f"{self.auth_host}/authserver/needCaptcha.html"
captcha_html = await self.request.get(url, params=params, follow_redirects=False)
if captcha_html.text == 'true':
params = {
"ts": str(round(time.time()))
}
captcha_url = f"{self.auth_host}/authserver/captcha.html"
res = await self.request.get(captcha_url, params=params, follow_redirects=False)
if not show_qrcode:
raise NeedCaptchaError(res.content)
with open("captcha.jpg", mode="wb") as f:
f.write(res.content)
print("验证码已保存在当前目录下的 captcha.jpg 文件中。")
return self.get_input("验证码")
return False

View File

@ -0,0 +1,22 @@
import contextlib
from os.path import exists
import cqwu
from cqwu.errors.auth import CookieError
class Login:
async def login(
self: "cqwu.Client",
):
""" 登录 """
with contextlib.suppress(CookieError):
if self.cookie:
await self.login_with_cookie()
elif exists(self.cookie_file_path):
await self.login_with_cookie_file()
return
if self.username and self.password:
await self.login_with_password()
else:
raise CookieError()

View File

@ -5,13 +5,15 @@ from cqwu.errors.auth import CookieError
class LoginWithCookie: class LoginWithCookie:
async def login_with_cookie( async def login_with_cookie(
self: "cqwu.Client", self: "cqwu.Client",
cookie: str = None,
): ):
""" """
使用 cookie 登录 使用 cookie 登录
""" """
if not self.cookie: cookie = cookie or self.cookie
if not cookie:
raise CookieError() raise CookieError()
self.cookie = cookie # noqa
try: try:
data = self.cookie.split(";") data = self.cookie.split(";")
for cookie in data: for cookie in data:
@ -19,7 +21,7 @@ class LoginWithCookie:
continue continue
key, value = cookie.split("=") key, value = cookie.split("=")
self.cookies.set(key, value) self.cookies.set(key, value)
self.sub_cookies.set(key, value) self.request.cookies.set(key, value)
self.me = await self.get_me() # noqa self.me = await self.get_me() # noqa
except: except Exception as e:
raise CookieError() raise CookieError() from e

View File

@ -18,5 +18,5 @@ class LoginWithCookieFile:
with open(self.cookie_file_path, "r") as f: with open(self.cookie_file_path, "r") as f:
self.cookie = f.read() # noqa self.cookie = f.read() # noqa
await self.login_with_cookie() await self.login_with_cookie()
except: except Exception as e:
raise CookieError() raise CookieError() from e

View File

@ -1,7 +1,3 @@
import time
from datetime import datetime
from urllib.parse import urlencode, urlparse
from lxml import etree from lxml import etree
import cqwu import cqwu
@ -11,25 +7,22 @@ from cqwu.utils.auth import encode_password
class LoginWithPassword: class LoginWithPassword:
async def login_with_password( async def login_with_password(
self: "cqwu.Client", self: "cqwu.Client",
captcha_code: str = None,
show_qrcode: bool = True,
): ):
""" """
使用学号加密码登录 使用学号加密码登录
""" """
headers = { headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'max-age=0', 'Cache-Control': 'max-age=0',
'Connection': 'keep-alive', 'Connection': 'keep-alive',
'DNT': '1',
'Upgrade-Insecure-Requests': '1', 'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.167 Safari/537.36', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
"Referer": f"{self.auth_host}/authserver/login",
"Origin": self.auth_host,
"Host": urlparse(self.auth_host).netloc
} }
session = self.request(headers=self.headers, follow_redirects=True) html = await self.request.get(f"{self.auth_host}/authserver/login", headers=headers, follow_redirects=True)
html = await session.get(f"{self.host}/authserver/login")
self.cookies.update(html.cookies) self.cookies.update(html.cookies)
tree = etree.HTML(html.text) tree = etree.HTML(html.text)
pwd_default_encrypt_salt = tree.xpath('//*[@id="pwdDefaultEncryptSalt"]/@value')[0] pwd_default_encrypt_salt = tree.xpath('//*[@id="pwdDefaultEncryptSalt"]/@value')[0]
@ -42,33 +35,27 @@ class LoginWithPassword:
'_eventId': tree.xpath('//*[@id="casLoginForm"]/input[4]/@value')[0], '_eventId': tree.xpath('//*[@id="casLoginForm"]/input[4]/@value')[0],
'rmShown': tree.xpath('//*[@id="casLoginForm"]/input[5]/@value')[0] 'rmShown': tree.xpath('//*[@id="casLoginForm"]/input[5]/@value')[0]
} }
# 是否需要验证码 # 是否需要验证码
params = { if not captcha_code:
"username": self.username, form_data['captchaResponse'] = await self.check_captcha(show_qrcode=show_qrcode)
"pwdEncrypt2": "pwdEncryptSalt",
"_": str(round(time.time() * 1000))
}
need_captcha_url = f"{self.auth_host}/authserver/needCaptcha.html?{urlencode(params)}"
async with self.request() as client:
html = await client.get(need_captcha_url, follow_redirects=False)
if html.text == 'true':
ts = round(datetime.now().microsecond / 1000) # get milliseconds
captcha_url = f"{self.auth_host}/authserver/captcha.html?" + urlencode({"ts": ts})
async with self.request() as client:
res = await client.get(captcha_url, follow_redirects=False)
with open("captcha.jpg", mode="wb") as f:
f.write(res.content)
print("验证码已保存在当前目录下的 captcha.jpg 文件中。")
code = self.get_input("验证码")
form_data['captchaResponse'] = code
# 登录 # 登录
async with self.request(headers=headers, cookies=self.cookies) as client: headers = {
html = await client.post(f"{self.auth_host}/authserver/login", data=form_data, follow_redirects=False) 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'max-age=0',
'Connection': 'keep-alive',
'Origin': self.auth_host,
'Referer': f'{self.auth_host}/authserver/login',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
}
html = await self.request.post(
f"{self.auth_host}/authserver/login",
headers=headers,
data=form_data,
follow_redirects=False,
)
if 'CASTGC' not in html.cookies.keys(): if 'CASTGC' not in html.cookies.keys():
raise UsernameOrPasswordError raise UsernameOrPasswordError
self.cookies.update(html.cookies) self.cookies.update(html.cookies)
self.sub_cookies.update(html.cookies)
self.me = await self.get_me() # noqa self.me = await self.get_me() # noqa

View File

@ -13,10 +13,5 @@ class Oauth:
使用 统一身份认证平台 登录子系统并且保存 cookie 使用 统一身份认证平台 登录子系统并且保存 cookie
""" """
host = host or urlparse(url).hostname host = host or urlparse(url).hostname
async with self.request(cookies=self.sub_cookies, follow_redirects=True) as client: html = await self.request.get(url, follow_redirects=True)
html = await client.get(url) return None if html.url.host != host else html
for history in html.history:
self.sub_cookies.update(history.cookies)
if host not in self.init_sub_web:
self.init_sub_web.append(host)
return None if html.url.host != host else html

View File

@ -1,11 +1,9 @@
from .gen_pay_qrcode import GenPayQrcode from .gen_pay_qrcode import GenPayQrcode
from .get_balance import GetBalance from .get_balance import GetBalance
from .get_orders import GetOrders
class EPay( class EPay(
GenPayQrcode, GenPayQrcode,
GetBalance, GetBalance,
GetOrders
): ):
pass pass

View File

@ -2,6 +2,7 @@ import qrcode
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import cqwu import cqwu
from cqwu.errors.auth import CookieError
class GenPayQrcode: class GenPayQrcode:
@ -11,9 +12,12 @@ class GenPayQrcode:
""" """
生成支付二维码 生成支付二维码
""" """
html = await self.oauth("http://218.194.176.214:8382/epay/thirdconsume/qrcode") url = "http://218.194.176.214:8382/epay/thirdconsume/qrcode"
html = await self.oauth(url)
if not html: if not html:
return raise CookieError()
if html.url != url:
raise CookieError()
soup = BeautifulSoup(html.text, "lxml") soup = BeautifulSoup(html.text, "lxml")
try: try:
data = soup.find("input", attrs={"id": "myText"})["value"] data = soup.find("input", attrs={"id": "myText"})["value"]

View File

@ -2,6 +2,7 @@ from typing import Optional
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import cqwu import cqwu
from cqwu.errors.auth import CookieError
class GetBalance: class GetBalance:
@ -14,9 +15,12 @@ class GetBalance:
Returns: Returns:
str: 余额 str: 余额
""" """
html = await self.oauth("http://218.194.176.214:8382/epay/thirdapp/balance") url = "http://218.194.176.214:8382/epay/thirdapp/balance"
html = await self.oauth(url)
if not html: if not html:
return "" raise CookieError()
if html.url != url:
raise CookieError()
soup = BeautifulSoup(html.text, "lxml") soup = BeautifulSoup(html.text, "lxml")
try: try:
return soup.find_all("div", "weui-cell__ft")[2].next return soup.find_all("div", "weui-cell__ft")[2].next

View File

@ -1,39 +0,0 @@
from typing import List
import cqwu
from cqwu.types.order import Order
class GetOrders:
async def get_orders(
self: "cqwu.Client",
page: int = 1,
page_size: int = 10,
search: str = ""
) -> List[Order]:
"""
获取历史订单
:param page: 页码
:param page_size: 每页数量
:param search: 搜索关键字
:return: 订单列表
"""
url = "http://pay.cqwu.edu.cn/queryOrderList"
params = {
"orderno": search,
"page": page,
"pagesize": page_size,
}
if "pay.cqwu.edu.cn" not in self.init_sub_web:
await self.oauth(
"http://authserver.cqwu.edu.cn/authserver/login?service="
"http%3A%2F%2Fpay.cqwu.edu.cn%2FsignAuthentication%3Furl%3DopenPortal")
async with self.request(cookies=self.sub_cookies, follow_redirects=True) as client:
html = await client.get(url, params=params)
try:
data = html.json()["payOrderList"]
except KeyError:
return []
return [Order(**order) for order in data]

View File

@ -3,6 +3,7 @@ from bs4 import BeautifulSoup
import cqwu import cqwu
from cqwu import types from cqwu import types
from cqwu.errors.auth import CookieError
def get_value_from_soup(soup: BeautifulSoup, attr_id: str) -> Union[type(None), str, int]: def get_value_from_soup(soup: BeautifulSoup, attr_id: str) -> Union[type(None), str, int]:
@ -26,10 +27,14 @@ class GetMe:
Returns: Returns:
types.User: 个人信息 types.User: 个人信息
""" """
html = await self.oauth( url = "http://218.194.176.8/prizepunishnv/studentInfoManageStudentNV!forwardStudentInfo.action"
"http://218.194.176.8/prizepunishnv/studentInfoManageStudentNV!forwardStudentInfo.action") html = await self.oauth(url)
if not html: if not html:
return None raise CookieError()
if html.url != url:
html = await self.request.get(url)
if html.url != url:
raise CookieError()
soup = BeautifulSoup(html.text, "lxml") soup = BeautifulSoup(html.text, "lxml")
data = { data = {
"username": "detail_xh", "username": "detail_xh",

View File

@ -0,0 +1,7 @@
from .get_score import GetScore
class XG(
GetScore,
):
pass

View File

@ -0,0 +1,65 @@
import json
from typing import List
import cqwu
from cqwu import types
from cqwu.errors.auth import CookieError
class GetScore:
async def get_score(
self: "cqwu.Client",
year: int = 2022,
semester: int = 1,
) -> List["types.Score"]:
"""
获取期末成绩
Returns:
List[types.Score]: 成绩列表
"""
url = "http://xg.cqwu.edu.cn/xsfw/sys/zhcptybbapp/*default/index.do#/cjcx"
html = await self.oauth(url)
if not html:
raise CookieError()
if html.url != url:
raise CookieError()
await self.request.get(
"http://xg.cqwu.edu.cn/xsfw/sys/swpubapp/indexmenu/getAppConfig.do?appId=5275772372599202&appName=zhcptybbapp&v=046351851777942055")
query_url = "http://xg.cqwu.edu.cn/xsfw/sys/zhcptybbapp/modules/cjcx/cjcxbgdz.do"
headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Origin': 'http://xg.cqwu.edu.cn',
'Referer': 'http://xg.cqwu.edu.cn/xsfw/sys/zhcptybbapp/*default/index.do',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
'X-Requested-With': 'XMLHttpRequest',
}
query_data = [
{
"name": "XN",
"caption": "学年",
"linkOpt": "AND",
"builderList": "cbl_m_List",
"builder": "m_value_equal",
"value": str(year),
},
{
"name": "XQ",
"caption": "学期",
"linkOpt": "AND",
"builderList": "cbl_m_List",
"builder": "m_value_equal",
"value": str(semester),
}
]
data = {
'querySetting': json.dumps(query_data),
'pageSize': '100',
'pageNumber': '1',
}
html = await self.request.post(query_url, headers=headers, data=data)
data = [types.Score(**i) for i in html.json()["datas"]["cjcxbgdz"]["rows"]]
return data

View File

@ -1 +1,2 @@
from .score import Score
from .user import User from .user import User

View File

@ -1,27 +0,0 @@
import cqwu
class Order:
def __init__(
self,
orderno: str = None,
payproname: str = None,
orderamt: float = None,
createtime: str = None,
payflag: str = "",
**_
):
"""
订单
:param orderno: 订单编号
:param payproname: 缴费项目
:param orderamt: 缴费金额
:param createtime: 订单生成时间
:param payflag: 缴费状态
"""
self.order_id = orderno
self.project = payproname
self.amount = orderamt / 100
self.create_time = createtime
self.status = cqwu.enums.OrderStatus(int(payflag))

43
cqwu/types/score.py Normal file
View File

@ -0,0 +1,43 @@
from pydantic import BaseModel
class Score(BaseModel):
""" 成绩类 """
KCMC: str
XF: float
ZCJ: float
JD: str
XN: str
XN_DISPLAY: str
XQ: str
XQ_DISPLAY: str
@property
def name(self) -> str:
""" 课程名称 """
return self.KCMC
@property
def credit(self) -> float:
""" 学分 """
return self.XF
@property
def score(self) -> float:
""" 成绩 """
return self.ZCJ
@property
def grade_point(self) -> float:
""" 绩点 """
return float(self.JD)
@property
def year(self) -> int:
""" 学年 """
return int(self.XN)
@property
def semester(self) -> int:
""" 学期 """
return int(self.XQ)

View File

@ -1,6 +1,7 @@
httpx httpx==0.23.3
lxml==4.9.1 lxml==4.9.1
PyExecJS==1.5.1 PyExecJS2==1.6.1
beautifulsoup4==4.11.1 beautifulsoup4==4.11.2
qrcode==7.3.1 qrcode==7.4.2
pillow pillow
pydantic==1.10.5