人行模拟登陆服务部署

上一篇仅仅是实现了模拟登陆的单个实现
本篇则更进一步,介绍登陆程序的任务调度、服务实现、以及程序性能的优化

1 服务部署

由于windows控件的限制,代码只能部署到Windows服务器上
用flask实现接口,接受http请求的账户、密码登陆的任务
api部分:

from flask import Flask, jsonify, request

# 应用实例
app = Flask(__name__)

@app.route('/login', methods=['GET', 'POST'])
def login():
    loginname = request.values.get('loginname')
    passwd = request.values.get('passwd')

    if not all([loginname, passwd]):
        return jsonify(return_code=10001, msg='请求参数不全', data='')

    login = rh_Login()
    code, msg= login.add_task(loginname, passwd)
    if code != 10000:
        return jsonify(code=code, msg=msg, data='')

    return jsonify(code=code, msg='', data=msg)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=False, threaded=True)

对外的接口获取到账户密码,然后实例化登陆调度器rh_Login
把任务发送到调度器,并获取调度器的执行结果
注意app.run方法中的参数,debug调试关闭,如果开启的话,默认会开两个进程,这里先关闭,并开启多线程接收任务
整体流程:

img.png

2. 调度器实现

api以及登陆模块比较简单,实现完成之后,调度器就是核心逻辑了

这里谈谈设计模式,从事编码行业的或多或少都会听到设计模式这种说法,乍听比较复杂,诸如享元模式、单例模式、生产者消费者模式,这些都是业界前辈总结出来的规律,只是概念抽象枯燥,理解起来麻烦
难归难,了解还是要的
当你开发久了,不知不觉中就会把这些模式融入到你的程序设计中
本次调度器中用到的就是生产者消费者模式
让我们先忘掉这些花里胡哨的名字,先从逻辑看起:

在调度器中,先不停的从api获取登陆任务放到调度器的任务数据池中,这就是生产
然后调度器中的消费者不停的从任务池中获取任务,接着执行任务,得到结果再放到另外的结果数据池中
调度器不停的从自己的结果数据池中拿出结果返回给api
这就是整套的工作流程[蓝—>橙—>黑—>绿]

img.png

具体服务实现策略:
在服务器上提前开启n个浏览器,并保存浏览器的打开时间,设计好浏览器刷新阈值,有登陆任务的时候取出其中一个浏览器,检查打开时间,如果超过阈值,则刷新浏览器,然后调用登陆方法,登陆后保存结果

每个浏览器就是一个selenium的driver对象
每个浏览器上都有控件框,此时driver要和控件框对应起来,这里用字典来映射

控件框:
可以使用工具来查看控件框,这里使用的查看工具是:Spy++


image.png

每个窗口都有类名,通过类名来定位控件框

import win32gui
这里可能要了解win32gui库常见接口,以及windows系统相关句柄的一些原理了,感兴趣的同学可以去了解一下,这里贴上获取控件句柄的代码:

 def map_driver_hwnd(self, driver):
     hwndList = []
     win32gui.EnumWindows(lambda hWnd, param: param.append(hWnd), hwndList)
     for hWnd in hwndList:
         title = win32gui.GetWindowText(hWnd)
         if title == '个人信用信息服务平台 - Internet Explorer':
             nh = win32gui.FindWindowEx(hWnd, None, 'Frame Tab', None)
             if nh > 0:
                 ny = win32gui.FindWindowEx(nh, None, 'TabWindowClass', None)
                 # ATL:Edit
                 ny = win32gui.FindWindowEx(ny, None, 'Shell DocObject View', None)
                 ny = win32gui.FindWindowEx(ny, None, 'Internet Explorer_Server', None)
                 ny = win32gui.FindWindowEx(ny, None, None, None)
                 ny = win32gui.FindWindowEx(ny, None, 'ATL:Edit', None)
                 if not ny:
                     continue
                 if ny not in self.hwnd_driver_list:
                     # print('找到控件准备添加')
                     self.hwnd_driver_map[driver] = ny
                     self.hwnd_driver_list.append(ny)
                     break

简单说明吧,传入的参数是driver浏览器对象,用来和浏览器中的句柄对应起来
首先枚举初当前系统中的所有句柄,递归并找出句柄标题为'个人信用信息服务平台 - Internet Explorer'的句柄,并在当前句柄向下查找,定位到句柄的类名为ATL:Edit,即获取到了控件输入框的句柄
每次打开一个driver浏览器,就调用本方法一次,这样n个浏览器就能对应上n个控件的句柄了

初始化调度器对象:

    thread_io_lock = RLock()

    def __init__(self):
        self.driver_q = []
        self.hwnd_driver_map = {}
        self.driver_time_map = {}
        self.data = {}
        self.hwnd_driver_list = []
        self._raw_tasks = []
        self.pool = ThreadPoolExecutor(max_workers=6)

driver_q代表所有浏览器的列表
hwnd_driver_map浏览器以及控件句柄映射
driver_time_map浏览器的打开时间映射
data数据池,用来保存登陆结果
hwnd_driver_list当前等待输入任务的控件句柄列表
_raw_tasks任务池
pool线程池
thread_io_lock线程锁

存数据的curd:

    def save_data(self, name, key, data):
        self.data[name] = {key: data}

    def get_data(self, name, key):
        try:
            result = self.data[name].get(key)
        except KeyError:
            result = ''
        return result

    def del_key(self, name):
        try:
            del self.data[name]
        except KeyError:
            pass

打开、刷新浏览器的实现、切入句柄接口的调用

    def open_driver(self):
        # 初始化浏览器
        driver = Driver()
        self.fresh_driver(driver)

    def fresh_driver(self, driver):
        # 刷新浏览器
        driver.start()
        self.driver_q.append(driver)
        self.driver_time_map[driver] = time.time()
        self.map_driver_hwnd(driver)

    @staticmethod
    def switch_to_hwnd(hwnd):
        assert type(hwnd) == int
        u32.SwitchToThisWindow(hwnd, True)

注意刷新浏览器做的初始化,参数是driver对象,上篇文章中已经实现
浏览器列表添加新的driver对象
浏览器时间映射表添加映射
调用map_driver_hwnd方法,做获取控件句柄的操作
这里贴出上篇文章中Driver的start方法:

def start(self):
    self.driver.get('https://ipcrs.pbccrc.org.cn/page/login/loginreg.jsp')
    img = 'pictures/{}.png'.format(time.time())
    self.driver.save_screenshot(img)
    element = self.driver.find_element_by_xpath('//img[@class="yzm_img"]')
    left = element.location['x']
    top = element.location['y']
    right = element.location['x'] + element.size['width']
    bottom = element.location['y'] + element.size['height']

    im = Image.open(img)
    im = im.crop((left + 23, top + 1, right, bottom))
    im.save(img)
    self.img = img
    # self.hand = self.driver.current_window_handle
    self.captch = ''

调度器通过账户密码登陆的实现:

    def driver_input(self, name, passwd):
        while True:
            # 拿出浏览器列表的浏览器
            driver = self.driver_q.pop()
            # 获取浏览器对应的时间映射
            last_time = self.driver_time_map[driver]
            if time.time() - last_time > 600:
                # 超时后取出句柄
                hwnd = self.hwnd_driver_map[driver]
                # 删除句柄浏览器映射的句柄
                del self.hwnd_driver_map[driver]
                # 删除句柄列表中的句柄
                self.hwnd_driver_list.remove(hwnd)
                # 刷新浏览器
                self.fresh_driver(driver)
            try:
                hwnd = self.hwnd_driver_map[driver]
            except:
                # 产生异常的话刷新浏览器
                hwnd = self.hwnd_driver_map[driver]
                self.hwnd_driver_list.remove(hwnd)
                self.fresh_driver(driver)
                self.save_data(name, 'data', {"msg": '登陆控件句柄异常'})
                return

            # 请求控件输入线程锁
            self.thread_io_lock.acquire()
            self.switch_to_hwnd(hwnd)
            result = driver.form_submit_control(name, passwd)
            # 释放控件输入线程锁
            self.thread_io_lock.release()

            coo = driver.coo
            del self.hwnd_driver_map[driver]
            self.hwnd_driver_list.remove(hwnd)
            if result == 'success':
                self.save_data(name, 'data', {"cookie": coo})
            else:
                self.save_data(name, 'data', {"msg": result})
            self.fresh_driver(driver)
            return

在输入控件的时候加锁了,保证同一时间只有一个输入控件操作,注意刷新浏览器以及完成登陆的数据结构操作,一旦忘记及时清理响应的数据结构,将会产生未知的调度错误,甚至内存溢出(假设任务运行的够久)

调度器的发布任务并返回的方法:

    def add_task(self, name, passwd):
        self._raw_tasks.append((name, passwd))
        result = ''
        while not result:
            time.sleep(0.2)
            # result = redis_manage.get_data(name, 'data')
            result = self.get_data(name, 'data')
        self.del_key(name)
        logger.info(result)
        return result

调度器从接口获取任务,并发布到任务池中,然后不断轮询数据池,通过用户名查找登陆结果,一旦登陆成功,则返回

消费者的方法:

    def worker(self):
        while True:
            if not len(self._raw_tasks):
                pass
            else:
                task = self._raw_tasks.pop()
                logger.info('开始任务:\n{}'.format(task))
                name, passwd = task
                self.del_key(name)
                self.driver_input(*task)
            time.sleep(0.2)

消费者是一个无限循环,后面会用线程来包装,循环不停的从任务队列_raw_tasks中获取账户密码,调用driver_input方法登陆并把结果保存到数据池中,为了防止数据池过大以及产生结果混淆,登陆之前要清除账户之前的登陆数据记录

提前打开n个浏览器:

    def open_drivers_before_task_start(self):
        while len(self.driver_q) < 2:
            self.open_driver()

这里提前打开了2个浏览器,具体打开数量可以自己调试

整体调度入口方法:

    def run(self):
        self.open_drivers_before_task_start()
        self.pool.submit(self.worker)
        self.pool.submit(self.worker)
        self.pool.submit(self.worker)

整体调度逻辑就相对简单了,提前打开若干浏览器,然后线程池开启三个worker消费线程,不停的监听、消费登陆任务

这里推荐一个很好用的线程池包:

from concurrent.futures import ThreadPoolExecutor

简洁,好用
至此,整个调度器实现完成
调度器中的各个数据结构一定要厘清,切勿混淆...

你可能感兴趣的:(人行模拟登陆服务部署)