fix(gdgxk): 打通登录全链路(图形验证码 OCR + 正确换票入口)
- 登录要过 4 位图形验证码:readCaptcha 返回 data:image/png;base64,用 ddddocr 识别, 失败按登录页 #errorText 判断原因并最多重试 5 次 - 换票入口纠正:票按 ITLMS-xxx||ticketId 两段 GET /nts/loginToStoreByAccount, 之前直接带 ?ticket= 访问 courseRegister 是无效的 - requirements / pyproject 增加 ddddocr;文档同步实测结论
This commit is contained in:
@@ -62,31 +62,67 @@ def dump_cookies(s):
|
||||
|
||||
|
||||
def _sso(s):
|
||||
"""jsglpt 会话换 ITLMS 票,再把票交给 jsxx 换学习会话。"""
|
||||
"""jsglpt 会话换 ITLMS 票,票按 `ITLMS-xxx||ticketId` 两段交给 jsxx。
|
||||
|
||||
交接入口来自 jsglpt 首页那个隐藏表单:
|
||||
`GET https://jsxx.gds.edu.cn/nts/loginToStoreByAccount?ticket=&ticketId=`
|
||||
"""
|
||||
r = s.post(G + "/teacher/ssoItlmsGxkTicket.action?verificaCode=true&page=", timeout=30)
|
||||
ticket = (r.text or "").strip()
|
||||
if not ticket.startswith("ITLMS-"):
|
||||
if not ticket.startswith("ITLMS-") or "||" not in ticket:
|
||||
raise _expired("取 ITLMS 票失败:%s" % (ticket[:80] or "空响应"))
|
||||
q = s.get(X + "/uc/store/courseRegister?ticket=" + requests.utils.quote(ticket), timeout=30)
|
||||
if "showCourse" not in q.text and "我的课程" not in q.text:
|
||||
raise _expired("带票进学习平台失败,可能票已失效")
|
||||
t, tid = ticket.split("||", 1)
|
||||
q = s.get(X + "/nts/loginToStoreByAccount", params={"ticket": t, "ticketId": tid}, timeout=30)
|
||||
if "logout" not in q.text and "我的课程" not in q.text:
|
||||
raise _expired("带票进学习平台失败,票可能已失效")
|
||||
return s
|
||||
|
||||
|
||||
_OCR = None
|
||||
|
||||
|
||||
def _ocr():
|
||||
"""4 位图形验证码:ddddocr 离线识别。"""
|
||||
global _OCR
|
||||
if _OCR is None:
|
||||
try:
|
||||
import ddddocr
|
||||
except Exception as e:
|
||||
raise RuntimeError("登录要过图形验证码,需要先装 ddddocr(pip install ddddocr):%s" % e)
|
||||
_OCR = ddddocr.DdddOcr(show_ad=False)
|
||||
return _OCR
|
||||
|
||||
|
||||
def _captcha(s):
|
||||
raw = s.get(G + "/login/readCaptcha.action?d=%.6f" % time.time(), timeout=30).text
|
||||
return base64.b64decode(raw.strip().strip('"').split(",", 1)[-1])
|
||||
|
||||
|
||||
def _login_error(s):
|
||||
html = s.get(G + "/login.jsp", timeout=30).text
|
||||
m = re.search(r'id="errorText"[^>]*>([^<]*)<', html)
|
||||
return (m.group(1).strip() if m else "")
|
||||
|
||||
|
||||
def login(username, password):
|
||||
s = new_session()
|
||||
s.get(G + "/login.jsp", timeout=30)
|
||||
data = {
|
||||
"loginUser.userName": _b64(username),
|
||||
"loginUser.loginType": "normal",
|
||||
"loginUser.password": _b64(password),
|
||||
"captcha": "",
|
||||
}
|
||||
r = s.post(G + "/loginIn.do", data=data, allow_redirects=False, timeout=30)
|
||||
if r.status_code != 302 or "/default.jsp" not in (r.headers.get("location") or ""):
|
||||
raise RuntimeError("登录失败:账号或密码不对,或平台要求图形验证码(HTTP %s)" % r.status_code)
|
||||
_sso(s)
|
||||
return s, profile(s) or username
|
||||
err = ""
|
||||
for _ in range(5):
|
||||
data = {
|
||||
"loginUser.userName": _b64(username),
|
||||
"loginUser.loginType": "normal",
|
||||
"loginUser.password": _b64(password),
|
||||
"captcha": _ocr().classification(_captcha(s)),
|
||||
}
|
||||
r = s.post(G + "/loginIn.do", data=data, allow_redirects=False, timeout=30)
|
||||
if r.status_code == 302 and "/default.jsp" in (r.headers.get("location") or ""):
|
||||
_sso(s)
|
||||
return s, profile(s) or username
|
||||
err = _login_error(s)
|
||||
if "验证码" not in err: # 不是验证码的问题,重试没意义
|
||||
break
|
||||
raise RuntimeError("登录失败:%s" % (err or "账号或密码不对"))
|
||||
|
||||
|
||||
def renew_session(s):
|
||||
|
||||
Reference in New Issue
Block a user