起名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 '未知原因,下线失败!'

这个是登出代码,同样提交登出请求后管理系统会马上回复登出成功,但这里采用获取不到用户信息时才认为是下线成功。


下面是图形界面:

mian_1.png

main_2.png

change_id_pw.png

代码如下,使用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文件夹拷贝到生成文件夹下。