NJUlogin 3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- NJUlogin/QRlogin.py +133 -0
- NJUlogin/__init__.py +3 -0
- NJUlogin/base.py +101 -0
- NJUlogin/captchaOCR/__init__.py +1 -0
- NJUlogin/captchaOCR/charsets.py +8214 -0
- NJUlogin/captchaOCR/common.onnx +0 -0
- NJUlogin/captchaOCR/ocr.py +134 -0
- NJUlogin/captchaOCR/utils.py +14 -0
- NJUlogin/pwdLogin.py +98 -0
- NJUlogin/utils/__init__.py +1 -0
- NJUlogin/utils/clear.py +5 -0
- NJUlogin/utils/config.py +9 -0
- NJUlogin/utils/get_post.py +21 -0
- NJUlogin/utils/urls.py +10 -0
- njulogin-3.3.dist-info/LICENSE +21 -0
- njulogin-3.3.dist-info/METADATA +65 -0
- njulogin-3.3.dist-info/RECORD +18 -0
- njulogin-3.3.dist-info/WHEEL +4 -0
NJUlogin/QRlogin.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import numpy as np
|
|
3
|
+
import time
|
|
4
|
+
from PIL import Image
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
import os
|
|
7
|
+
from lxml import etree
|
|
8
|
+
from user_agents import parse
|
|
9
|
+
|
|
10
|
+
from .utils import config, urls, get_post
|
|
11
|
+
from .utils.clear import clear
|
|
12
|
+
from .base import baseLogin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QR(object):
|
|
16
|
+
def __init__(self, session: requests.Session, getTimeout: int):
|
|
17
|
+
self.session = session
|
|
18
|
+
self.getTimeout = getTimeout
|
|
19
|
+
|
|
20
|
+
def getQR(self) -> np.ndarray:
|
|
21
|
+
"""获取二维码图片,返回numpy数组"""
|
|
22
|
+
self.ts = int(time.time() * 1000)
|
|
23
|
+
url = urls.QRid % self.ts
|
|
24
|
+
QRid = get_post.get(self.session, url, timeout=self.getTimeout).text
|
|
25
|
+
self.QRid = QRid
|
|
26
|
+
url = urls.QRimg % QRid
|
|
27
|
+
QRdata = get_post.get(self.session, url, timeout=self.getTimeout).content
|
|
28
|
+
qr = Image.open(BytesIO(QRdata)).convert("L")
|
|
29
|
+
return np.array(qr)[6:-6, 6:-6]
|
|
30
|
+
|
|
31
|
+
def printQR(self):
|
|
32
|
+
"""打印二维码至终端,同时也会保存一份QR.png到当前目录"""
|
|
33
|
+
QRimg = self.getQR()
|
|
34
|
+
Image.fromarray(QRimg).save("QR.png")
|
|
35
|
+
char_full = "\u2588"
|
|
36
|
+
char_up = "\u2580"
|
|
37
|
+
char_down = "\u2584"
|
|
38
|
+
for i in range(0, QRimg.shape[0] - 3, 6):
|
|
39
|
+
for j in range(0, QRimg.shape[1], 3):
|
|
40
|
+
if QRimg[i, j] < 128 and QRimg[i + 3, j] < 128:
|
|
41
|
+
print(char_full, end="")
|
|
42
|
+
elif QRimg[i, j] < 128 and QRimg[i + 3, j] >= 128:
|
|
43
|
+
print(char_up, end="")
|
|
44
|
+
elif QRimg[i, j] >= 128 and QRimg[i + 3, j] < 128:
|
|
45
|
+
print(char_down, end="")
|
|
46
|
+
else:
|
|
47
|
+
print(" ", end="")
|
|
48
|
+
print("")
|
|
49
|
+
for j in range(0, QRimg.shape[1], 3):
|
|
50
|
+
if QRimg[-1, j] < 128:
|
|
51
|
+
print(char_up, end="")
|
|
52
|
+
else:
|
|
53
|
+
print(" ", end="")
|
|
54
|
+
print("")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class QRlogin(baseLogin):
|
|
58
|
+
def __init__(self, loginTimeout: int = config.loginTimeout, *args, **kwargs):
|
|
59
|
+
"""
|
|
60
|
+
QRlogin(loginTimeout: int = config.loginTimeout, headers: dict = config.headers, getTimeout: int = config.getTimeout)
|
|
61
|
+
@description:
|
|
62
|
+
二维码登录
|
|
63
|
+
-------
|
|
64
|
+
@param:
|
|
65
|
+
loginTimeout: int, 登录超时时间,即二维码有效时间
|
|
66
|
+
headers: dict, 请求头
|
|
67
|
+
getTimeout: int, 请求超时时间,即在getTimeout秒内未获取到响应则抛出TimeoutError
|
|
68
|
+
-------
|
|
69
|
+
"""
|
|
70
|
+
super().__init__(*args, **kwargs)
|
|
71
|
+
assert parse(self.session.headers["User-Agent"]).is_pc, "扫码登录只支持PC端"
|
|
72
|
+
self.loginTimeout = loginTimeout
|
|
73
|
+
|
|
74
|
+
def getStatus(self, qr: QR) -> str:
|
|
75
|
+
"""等候扫码,返回扫码状态"""
|
|
76
|
+
url = urls.status % (qr.ts, qr.QRid)
|
|
77
|
+
status = self.get(url, timeout=self.getTimeout).text
|
|
78
|
+
return status
|
|
79
|
+
|
|
80
|
+
def waitingLogin(self, qr: QR) -> bool:
|
|
81
|
+
"""等候登录,返回登录状态"""
|
|
82
|
+
# 0: 未扫码, 1: 登录成功, 2: 已扫码未确认登录
|
|
83
|
+
first0, first2 = False, False
|
|
84
|
+
for _ in range(self.loginTimeout):
|
|
85
|
+
status = self.getStatus(qr)
|
|
86
|
+
try:
|
|
87
|
+
status = int(status)
|
|
88
|
+
if status not in [0, 1, 2]:
|
|
89
|
+
raise ValueError
|
|
90
|
+
except ValueError:
|
|
91
|
+
raise ValueError("未知状态,代码可能需要维护")
|
|
92
|
+
if status == 0 and not first0:
|
|
93
|
+
print("微信或南京大学APP扫码登录")
|
|
94
|
+
first0 = True
|
|
95
|
+
elif status == 2 and not first2:
|
|
96
|
+
print("扫描成功,请在手机上『确认登录』")
|
|
97
|
+
first2 = True
|
|
98
|
+
elif status == 1:
|
|
99
|
+
clear()
|
|
100
|
+
return True
|
|
101
|
+
time.sleep(1)
|
|
102
|
+
clear()
|
|
103
|
+
print("登录超时")
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def login(self, dest: str = None) -> requests.Session:
|
|
107
|
+
if dest is not None:
|
|
108
|
+
url = urls.login % dest
|
|
109
|
+
else:
|
|
110
|
+
url = urls.login.split("?")[0]
|
|
111
|
+
html = self.get(url, timeout=self.getTimeout).text
|
|
112
|
+
qr = QR(self.session, self.getTimeout)
|
|
113
|
+
clear()
|
|
114
|
+
qr.printQR()
|
|
115
|
+
if not self.waitingLogin(qr):
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
selector = etree.HTML(html)
|
|
119
|
+
data = {
|
|
120
|
+
"lt": selector.xpath('//input[@name="lt"]/@value')[1],
|
|
121
|
+
"uuid": qr.QRid,
|
|
122
|
+
"dllt": selector.xpath('//input[@name="dllt"]/@value')[1],
|
|
123
|
+
"execution": selector.xpath('//input[@name="execution"]/@value')[1],
|
|
124
|
+
"_eventId": selector.xpath('//input[@name="_eventId"]/@value')[1],
|
|
125
|
+
"rmShown": selector.xpath('//input[@name="rmShown"]/@value')[1],
|
|
126
|
+
}
|
|
127
|
+
res = self.post(url, data=data, timeout=self.getTimeout)
|
|
128
|
+
if self.judge_not_login(res, url):
|
|
129
|
+
print("登录失败")
|
|
130
|
+
return None
|
|
131
|
+
os.remove("QR.png")
|
|
132
|
+
self.response = res
|
|
133
|
+
return self.session
|
NJUlogin/__init__.py
ADDED
NJUlogin/base.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
from lxml import etree
|
|
4
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
5
|
+
from cryptography.hazmat.primitives import hashes
|
|
6
|
+
from cryptography.hazmat.backends import default_backend
|
|
7
|
+
from cryptography.fernet import Fernet
|
|
8
|
+
import base64
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from .utils import config, get_post, urls
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class baseLogin(object):
|
|
15
|
+
def __init__(
|
|
16
|
+
self, headers: dict = config.headers, getTimeout: int = config.getTimeout
|
|
17
|
+
):
|
|
18
|
+
self.session = requests.Session()
|
|
19
|
+
self.session.headers.update(headers)
|
|
20
|
+
self.getTimeout = getTimeout
|
|
21
|
+
|
|
22
|
+
def get(self, url: str, **kwargs) -> requests.Response:
|
|
23
|
+
return get_post.get(self.session, url, **kwargs)
|
|
24
|
+
|
|
25
|
+
def post(self, url: str, data: dict, **kwargs) -> requests.Response:
|
|
26
|
+
return get_post.post(self.session, url, data, **kwargs)
|
|
27
|
+
|
|
28
|
+
def judge_not_login(self, html: requests.Response, loginurl: str) -> bool:
|
|
29
|
+
"""判断是否登录成功"""
|
|
30
|
+
return (
|
|
31
|
+
html is None or html.url == loginurl or html.url.endswith("security_check")
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def logout(self) -> None:
|
|
35
|
+
"""退出登录"""
|
|
36
|
+
self.get(urls.logout, timeout=self.getTimeout)
|
|
37
|
+
|
|
38
|
+
def logout_all(self) -> None:
|
|
39
|
+
"""退出所有设备的登录"""
|
|
40
|
+
html = self.get(urls.onlineList, timeout=self.getTimeout)
|
|
41
|
+
selector = etree.HTML(html.text)
|
|
42
|
+
OnlineList = selector.xpath('//input[@value="踢出"]/@onclick')
|
|
43
|
+
for item in OnlineList:
|
|
44
|
+
tokenId = item.split("'")[1]
|
|
45
|
+
self.post(urls.logoutOthers, data={"tokenId": tokenId})
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def available(self) -> bool:
|
|
49
|
+
"""判断是否登录成功"""
|
|
50
|
+
html = self.get(urls.index, timeout=self.getTimeout)
|
|
51
|
+
return html.url == urls.index
|
|
52
|
+
|
|
53
|
+
def export(self, filename: str, password: str = None) -> None:
|
|
54
|
+
"""导出登录信息"""
|
|
55
|
+
if not self.available:
|
|
56
|
+
raise ValueError("未登录,无法导出登录信息")
|
|
57
|
+
if password is None:
|
|
58
|
+
with open(filename, "w") as f:
|
|
59
|
+
json.dump(self.session.cookies.get_dict(), f, indent=2)
|
|
60
|
+
else:
|
|
61
|
+
cookies = json.dumps(self.session.cookies.get_dict(), indent=2).encode()
|
|
62
|
+
salt = os.urandom(16)
|
|
63
|
+
kdf = PBKDF2HMAC(
|
|
64
|
+
algorithm=hashes.SHA256(),
|
|
65
|
+
length=32,
|
|
66
|
+
salt=salt,
|
|
67
|
+
iterations=100000,
|
|
68
|
+
backend=default_backend(),
|
|
69
|
+
)
|
|
70
|
+
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
|
71
|
+
fernet = Fernet(key)
|
|
72
|
+
enc_cookie = fernet.encrypt(cookies)
|
|
73
|
+
with open(filename, "wb") as f:
|
|
74
|
+
f.write(enc_cookie + salt)
|
|
75
|
+
|
|
76
|
+
def load(self, filename: str, password: str = None) -> None:
|
|
77
|
+
"""加载登录信息"""
|
|
78
|
+
if password is None:
|
|
79
|
+
try:
|
|
80
|
+
with open(filename, "r") as f:
|
|
81
|
+
cookies = json.load(f)
|
|
82
|
+
except UnicodeDecodeError:
|
|
83
|
+
raise RuntimeError("请提供密码")
|
|
84
|
+
else:
|
|
85
|
+
with open(filename, "rb") as f:
|
|
86
|
+
enc_cookie = f.read()
|
|
87
|
+
salt = enc_cookie[-16:]
|
|
88
|
+
enc_cookie = enc_cookie[:-16]
|
|
89
|
+
kdf = PBKDF2HMAC(
|
|
90
|
+
algorithm=hashes.SHA256(),
|
|
91
|
+
length=32,
|
|
92
|
+
salt=salt,
|
|
93
|
+
iterations=100000,
|
|
94
|
+
backend=default_backend(),
|
|
95
|
+
)
|
|
96
|
+
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
|
97
|
+
fernet = Fernet(key)
|
|
98
|
+
cookies = json.loads(fernet.decrypt(enc_cookie))
|
|
99
|
+
self.session.cookies.update(cookies)
|
|
100
|
+
if not self.available:
|
|
101
|
+
raise ValueError("登录信息已失效,请重新登录")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .ocr import CaptchaOCR
|