起名V2.0是因为这次改动真的好大,花了一天摸索QT做出了一个图形界面,将以前的登录代码修改为面向对象接口,总的来说效果还是很不错的。
先贴代码,首先接口代码:
import urllib.request
import urllib.parse
import json
import time
import os
import base64
login_url = 'http://p.nju.edu.cn/portal_io/login' # 登录post的URL
logout_url = 'http://p.nju.edu.cn/portal_io/logout' # 登出post的URL
info_url = 'http://p.nju.edu.cn/portal_io/getinfo' # 获取用户信息URL
info_name = 'info.ini' # 存储账号密码
class NJU_Login():
def __init__(self): # 获取保存的账号密码,若文件不存在则创建文件
if os.path.exists(info_name):
with open(info_name, 'r') as f:
self.id = self.decrypt(f.readline().strip())
self.pw = self.decrypt(f.readline().strip())
else:
with open(info_name, 'w') as f:
pass
self.id = ''
self.pw = ''
def set_id_pw(self, id, pw): # 修改账号密码并存入文件
self.id = id
self.pw = pw
with open(info_name, 'w') as f:
f.write('%s\n%s' % (self.encrypt(id), self.encrypt(pw)))
def login(self): # 登录接口
data = urllib.parse.urlencode({ # 转换post的form并编码
'username': self.id,
'password': self.pw
}).encode('utf-8')
post = urllib.request.urlopen(url=login_url, data=data)
return json.loads(post.read().decode('utf-8')) # 获得登录信息
def encrypt(self, s): # 加密账号密码
return base64.b32encode(base64.b64encode(s.encode('utf-8'))).decode('utf-8')
def decrypt(self, s): # 解密账号密码
return base64.b64decode(base64.b32decode(s)).decode('utf-8')
def logout(self): # 登出接口
post = urllib.request.Request(url=logout_url, method='POST')
con = urllib.request.urlopen(post)
return json.loads(con.read().decode('utf-8'))
def get_info(self): # 获取用户信息
# urlopen()不能指定post,这里用Request()
post = urllib.request.Request(url=info_url, method='POST')
con = urllib.request.urlopen(post)
info = json.loads(con.read().decode('utf-8')) # 获得用户信息
if info['reply_code'] == 0:
text = ('姓名:%s\n'
'学号:%s\n'
'接入区域:%s\n'
'服务类别:%s\n'
#'累计上网时长:%s\n'
'网费余额:%.2f元\n')
text_real = (info['userinfo']['fullname'], # 与text配合
info['userinfo']['username'],
info['userinfo']['area_name'],
info['userinfo']['service_name'],
# info['userinfo']['acctstarttime'],
info['userinfo']['balance'] / 100)
return text % text_real
else:
return -1
def first_time_login(self): # 登录后至获取用户信息存在2秒左右延时,需要单独处理
time.sleep(1) # 延时1秒
count = 0 # 重复尝试计数
while 1:
info = self.get_info()
if info == -1:
count += 1
time.sleep(0.5) # 延时0.5秒再次尝试
if count == 4: # 重复尝试4次,若仍获取不到,则放弃
return "无法获取登录信息!"
else:
return info
def connect(self): # 登录
ln = self.login()
if ln['reply_code'] == 1: # 首次登录成功
return ln['reply_msg'] + '\n' + self.first_time_login()
elif ln['reply_code'] == 6: # 已经登录成功,但重复尝试登录
return ln['reply_msg'] + '\n\n' + self.get_info()
else: # 其他情况,提示错误
return ln['reply_msg']
def disconnect(self): # 登出
count = 0 # 重复尝试计数
while 1:
logout = self.logout()
if self.get_info() == -1: # 如果不能获取到用户信息,则登出成功
return logout['reply_msg']
else:
count += 1
time.sleep(0.5)
if count == 6: # 重复尝试6次
return '未知原因,下线失败!'
if __name__ == '__main__': # 测试代码
nju = NJU_Login()
print(nju.get_info())
下面说每一部分功能:
def __init__(self): # 获取保存的账号密码,若文件不存在则创建文件
if os.path.exists(info_name):
with open(info_name, 'r') as f:
self.id = self.decrypt(f.readline().strip())
self.pw = self.decrypt(f.readline().strip())
else:
with open(info_name, 'w') as f:
pass
self.id = ''
self.pw = ''
当class产生实例时,即从文件中获取登录账号密码;如果文件不存在,则创建文件,并将账号密码设为空,这样直接提交登录会返回错误。
def set_id_pw(self, id, pw): # 修改账号密码并存入文件
self.id = id
self.pw = pw
with open(info_name, 'w') as f:
f.write('%s\n%s' % (self.encrypt(id), self.encrypt(pw)))
存储修改后的账号密码,这样会同步更新此实例的账号密码,并加密存储到文件
def login(self): # 登录接口
data = urllib.parse.urlencode({ # 转换post的form并编码
'username': self.id,
'password': self.pw
}).encode('utf-8')
post = urllib.request.urlopen(url=login_url, data=data)
return json.loads(post.read().decode('utf-8')) # 获得登录信息
def logout(self): # 登出接口
post = urllib.request.Request(url=logout_url, method='POST')
con = urllib.request.urlopen(post)
return json.loads(con.read().decode('utf-8'))
用来提交登录登出的接口,返回值为json格式。
def encrypt(self, s): # 加密账号密码
return base64.b32encode(base64.b64encode(s.encode('utf-8'))).decode('utf-8')
def decrypt(self, s): # 解密账号密码
return base64.b64decode(base64.b32decode(s)).decode('utf-8')
加密解密账号密码。
def get_info(self): # 获取用户信息
# urlopen()不能指定post,这里用Request()
post = urllib.request.Request(url=info_url, method='POST')
con = urllib.request.urlopen(post)
info = json.loads(con.read().decode('utf-8')) # 获得用户信息
if info['reply_code'] == 0:
text = ('姓名:%s\n'
'学号:%s\n'
'接入区域:%s\n'
'服务类别:%s\n'
#'累计上网时长:%s\n'
'网费余额:%.2f元\n')
text_real = (info['userinfo']['fullname'], # 与text配合
info['userinfo']['username'],
info['userinfo']['area_name'],
info['userinfo']['service_name'],
# info['userinfo']['acctstarttime'],
info['userinfo']['balance'] / 100)
return text % text_real
else:
return -1
获取用户信息的接口,这里挺好玩的,似乎登录后管理系统记录的是网卡地址或者IP地址,所以只需要对获取信息URL发送空的POST请求,即可获得用户信息。当reply_code为0时是正常获得用户信息,当为其他值时代表获取不到,所以可以用这个来判断是否下线。
def first_time_login(self): # 登录后至获取用户信息存在2秒左右延时,需要单独处理
time.sleep(1) # 延时1秒
count = 0 # 重复尝试计数
while 1:
info = self.get_info()
if info == -1:
count += 1
time.sleep(0.5) # 延时0.5秒再次尝试
if count == 4: # 重复尝试4次,若仍获取不到,则放弃
return "无法获取登录信息!"
else:
return info
当首次登录(区别于已登录但重复提交登录请求)时,从管理系统发回登录成功提示,到获取到用户信息会存在一定延时,这里就需要单独处理了。总的来说就是有限次尝试获取用户信息。
def connect(self): # 登录
ln = self.login()
if ln['reply_code'] == 1: # 首次登录成功
return ln['reply_msg'] + '\n' + self.first_time_login()
elif ln['reply_code'] == 6: # 已经登录成功,但重复尝试登录
return ln['reply_msg'] + '\n\n' + self.get_info()
else: # 其他情况,提示错误
return ln['reply_msg']
这个是正真用来登录的代码,区别出三种情况,返回值均为字符串。
def disconnect(self): # 登出
count = 0 # 重复尝试计数
while 1:
logout = self.logout()
if self.get_info() == -1: # 如果不能获取到用户信息,则登出成功
return logout['reply_msg']
else:
count += 1
time.sleep(0.5)
if count == 6: # 重复尝试6次
return '未知原因,下线失败!'
这个是登出代码,同样提交登出请求后管理系统会马上回复登出成功,但这里采用获取不到用户信息时才认为是下线成功。
下面是图形界面:



代码如下,使用PyQt5:
from PyQt5.QtWidgets import *
from PyQt5 import QtGui, QtCore
import sys
import nju_login_2
class ChangeInfo(QDialog): # 修改账号密码界面
def __init__(self, parent=None):
super(ChangeInfo, self).__init__(parent)
usr = QLabel("账号:")
pwd = QLabel("密码:")
self.usrLineEdit = QLineEdit()
self.pwdLineEdit = QLineEdit()
self.pwdLineEdit.setEchoMode(QLineEdit.Password)
gridLayout = QGridLayout()
gridLayout.addWidget(usr, 0, 0, 1, 1)
gridLayout.addWidget(pwd, 1, 0, 1, 1)
gridLayout.addWidget(self.usrLineEdit, 0, 1, 1, 2)
gridLayout.addWidget(self.pwdLineEdit, 1, 1, 1, 2)
okBtn = QPushButton("确定")
cancelBtn = QPushButton("取消")
gridLayout.addWidget(okBtn, 2, 0, 1, 1)
gridLayout.addWidget(cancelBtn, 2, 1, 1, 1)
aboutBtn = QPushButton("关于作者")
gridLayout.addWidget(aboutBtn, 2, 2, 1, 1)
self.setLayout(gridLayout)
okBtn.clicked.connect(self.ok)
aboutBtn.clicked.connect(self.about)
cancelBtn.clicked.connect(self.reject)
self.setWindowTitle("修改账号密码")
self.setWindowIcon(QtGui.QIcon('favicon.ico'))
#self.resize(300, 200)
self.usrLineEdit.setText(nju.id)
self.pwdLineEdit.setText(nju.pw)
def ok(self): # 提交修改账号密码
nju.set_id_pw(self.usrLineEdit.text(), self.pwdLineEdit.text())
super(ChangeInfo, self).accept()
def about(self): # 关于作者
QMessageBox.about(
self, "关于作者", "如果你有任何意见或建议\n欢迎联系我:zhantong1994@163.com")
class LoginDlg(QWidget): # 主界面
def __init__(self, parent=None):
super(LoginDlg, self).__init__(parent)
self.browser = QPlainTextEdit()
gridLayout = QGridLayout()
gridLayout.addWidget(self.browser, 0, 0, 1, 2)
self.conBtn = QPushButton("连接")
changeinfoBtn = QPushButton("修改账号密码")
gridLayout.addWidget(self.conBtn, 1, 0, 1, 1)
gridLayout.addWidget(changeinfoBtn, 1, 1, 1, 1)
cancelBtn = QPushButton("关闭程序")
cancelBtn.setDefault(True)
gridLayout.addWidget(cancelBtn, 2, 0, 1, 2)
self.setLayout(gridLayout)
self.setWindowTitle("南京大学|校园网自动登录")
self.setWindowIcon(QtGui.QIcon('favicon.ico'))
cancelBtn.setFocus()
cancelBtn.clicked.connect(self.close)
changeinfoBtn.clicked.connect(self.show_info)
self.conBtn.clicked.connect(self.connect)
self.browser.setReadOnly(True)
self.conBtn.clicked.emit(True)
def show_info(self): # 打开修改账号密码界面
cinfo = ChangeInfo()
cinfo.show()
cinfo.exec_()
def connect(self): # 连接/断开
if self.conBtn.text() == '连接':
self.browser.setPlainText(nju.connect())
else:
self.browser.setPlainText(nju.disconnect())
if nju.get_info() == -1:
self.conBtn.setText('连接')
else:
self.conBtn.setText('断开')
if __name__ == '__main__':
nju = nju_login_2.NJU_Login()
app = QApplication(sys.argv)
dlg = LoginDlg()
dlg.show()
sys.exit(app.exec_())
这个没有什么好说的,而且我也是胡乱弄出的界面,更没有什么好说的了。需要注意的是最好将接口和窗体代码分离,这样逻辑更清晰,而且修改也更迅速。
接下来是生成.exe代码:
"""
使用py2exe将.py文件转换为.exe文件。
需要首先安装py2exe,可以直接pip install py2exe
"""
from distutils.core import setup
import py2exe
import sys
file_dir = 'window.py'
sys.argv.append('py2exe') # 巧妙避免了用到cmd
options = {
'py2exe': {
'compressed': 1, # 压缩
'bundle_files': 1, # 是否打包到一个.exe文件
'includes': ['sip'] # PyQt打包成exe的错误修复
}
}
setup(options=options,
zipfile=None, # 是否将.zip文件也打包进.exe文件
windows=[{ # windows参数可以隐藏运行时的cmd框
'script': file_dir,
'icon_resources': [(1, "favicon.ico")] # 加入图标
}])
注意当应用到PyQt后,打包成.exe复杂了许多,上面的代码已经做出了错误修复,但实际上打包后的.exe想要运行的话还需要将PyQt文件夹下的platform文件夹拷贝到生成文件夹下。