Python问题解决--UI自动化解决二维码扫描登录

一、问题背景

公司产品Web UI自动化过程中,需要通过手机扫描二维码,然后点击确定才能登录,这样每次运行前都需要进行手动扫描一下,就比较麻烦。那么怎么解决这个问题。

环境:python 3.12.1

二、解决思路

1、第一种让web开发在登录页面提供用户和密码登录的功能,而不是只有二维码扫描,但是询问后,告诉我说他很忙,哈哈哈,此条路暂时放弃,等其他路行不通的时候再去骚扰他。(轻声吐槽一下,多个用户名和密码的入口很难吗?他是不是在忽悠我。)

2、研究了一下发现我们Web登录后,以后登录都可以免密登录,那我们如果能够找到免密登录的方法就可以了,最多在第一次登录的时候手动扫描一下,以后就可以每天自动化跑了。了解现在保持登录的方法有几种,一种是比较老的方式用Cookie机制来实现,一种是比较新的方法用localstorage数据库来实现免密登录。

3、思路有了,开始验证思路,打开浏览器,登录设备,然后F12,查看应用,发现使用的是localstorage进行的免密登录。那么接下来解决的问题是操作localstorage,将扫码登录后的localstorage数据保存下来,然后用于下一次自动化。

三、问题解决

1、实现操作localstorage的代码:localstorage就是浏览器的本地数据库,用python实现对他的增删改查即可完成基本功能。我这里提供一个别人已经封装好的代码类,大家可用,也可自己实现。但是我不喜欢重复造轮子,解决问题的思路很重要。

class LocalStorage:

    def __init__(self, driver):
        self.driver = driver

    def __len__(self):
        return self.driver.execute_script("return window.localStorage.length;")

    def all_data(self):
        return self.driver.execute_script(
            "var ls = window.localStorage, items = {}; "
            "for (var i = 0, k; i < ls.length; ++i) items[k = ls.key(i)] = ls.getItem(k); "
            "return items; "
        )

    def all_keys(self):
        return self.driver.execute_script(
            "var ls = window.localStorage, keys = []; "
            "for (var i = 0; i < ls.length; ++i) keys[i] = ls.key(i); "
            "return keys; ")

    def get(self, key):
        return self.driver.execute_script("return window.localStorage.getItem(arguments[0]);", key)

    def set(self, key, value):
        self.driver.execute_script("window.localStorage.setItem(arguments[0], arguments[1]);", key, value)

    def has(self, key):
        return key in self.all_keys()

    def remove(self, key):
        self.driver.execute_script("window.localStorage.removeItem(arguments[0]);", key)

    def clear(self):
        self.driver.execute_script("window.localStorage.clear();")

    def is_local_storage_ready(self):
        try:
            # 使用JavaScript检查localStorage是否可用
            return self.driver.execute_script("return typeof localStorage !== 'undefined';")
        except Exception as e:
            return False

    def __getitem__(self, key):
        value = self.get(key)
        if value is None:
            raise KeyError(key)
        return value

    def __setitem__(self, key, value):
        self.set(key, value)

    def __contains__(self, key):
        return key in self.all_keys()

    def __iter__(self):
        return self.all_data().__iter__()

    def __repr__(self):
        return self.all_data().__str__()


if __name__ == '__main__':
    import time
    from selenium import webdriver

    driver = webdriver.Chrome()
    driver.get("https://www.baidu.com/")
    driver.maximize_window()
    print("等待localStorage写入完成")
    time.sleep(10)

    # get the local storage
    print("开始获取->localstorage")
    storage = LocalStorage(driver)
    print("storage.all_data: ", storage.all_data())
    print("storage.all_keys: ", storage.all_keys())

    # iterate items
    print("循环打印每条数据->localstorage")
    for key, value in storage.all_data().items():
        print("%s: %s" % (key, value))

    # set an item
    print("开始设置参数->localstorage:两种方法设置都可以")
    storage["mykey_1"] = 1234
    storage.set("mykey_2", 5678)

    # get an item
    print("开始获取参数->localstorage:两种方法获取都可以")
    print(storage["mykey_1"])  # raises a KeyError if the key is missing
    print(storage.get("mykey_2"))  # returns None if the key is missing

    # delete an item
    print("开始删除参数->localstorage")
    storage.remove("mykey_1")
    storage.remove("mykey_2")

    # delete items
    print("开始清空所有参数->localstorage")
    storage.clear()

    time.sleep(30)
    driver.quit()

2、现在已经有了操作localstorage的类库,现在我们来实现我们自己的思路,第一次登录手动登录,登录完成后,保存localstorage到本地的pickle文件中;第二次自动化先进行读取本地pickle文件,填入浏览器localstorage数据库中,然后跑代码,跑完代码,根据情况是否更新本地的pickle文件数据保证下次依然可以正常自动运行。第三次自动化和第二次思路一致,如此循环。

import sys
import time
import pickle
import threading
import schedule
import datetime

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager

from sample.selenium_sample.web_localstorage import LocalStorage


class LoginFirst:
    def __init__(self, url, localstorage_file, time_out, xpath=None):
        self.url = url
        self.localstorage_file = localstorage_file
        self.time_out = time_out
        self.user_input = None
        self.xpath_dic = xpath

    def run(self):
        print("start login")
        service = ChromeService(executable_path=ChromeDriverManager().install())
        driver = webdriver.Chrome(service=service)
        driver.maximize_window()
        driver.get(self.url)
        storage = LocalStorage(driver)
        # 尝试读取本地保存的localstorage数据
        localstorage_data = self.load_localstorage_from_file()
        if localstorage_data:
            # 重要:等待浏览器页面稳定后写入本地数据
            print("正在写入localstorage数据")
            try:
                WebDriverWait(driver, self.time_out).until(
                    ec.presence_of_element_located((By.XPATH, self.xpath_dic['qr-code']))
                )
                WebDriverWait(driver, self.time_out).until(lambda x: storage.is_local_storage_ready())
                time.sleep(5)
            except Exception as e:
                print("页面稳定超时或发生异常:", e)
                driver.quit()
                sys.exit(0)
            # 设置localstorage
            for key, value in localstorage_data.items():
                storage.set(key, value)
            driver.refresh()
            
            # 判断登录是否成功
            if self.is_login_success(driver):
                # 进行其他操作
                self.send_text_schedule(driver)
                # 更新localstorage数据到本地
                self.save_localstorage_to_file(storage)

            else:
                # 登录失败重新扫码
                self.scan_qr_code()
                # 更新localstorage数据到本地
                self.save_localstorage_to_file(storage)
        else:
            # 本地localstorage数据读取失败,扫码登录,保存数据,执行其他操作。
            self.scan_qr_code()
            self.save_localstorage_to_file(storage)
            self.send_text_schedule(driver)
        driver.quit()

    def send_text_schedule(self, driver):
        print("登录成功,可以进行其他操作了")

    def is_login_success(self, driver):
        start_time = time.time()
        while True:
            result = ec.visibility_of_element_located((By.XPATH, self.xpath_dic['contacts']))
            try:
                end_time = time.time()
                if end_time - start_time > self.time_out:
                    print("登录失败,{}s内未刷新出xpath元素。请手动扫码登录".format(self.time_out))
                    return False

                if result(driver):
                    print("登录成功,找到对应xpath元素。")
                    return True
            except:
                time.sleep(1)

    def load_localstorage_from_file(self):
        try:
            with open(self.localstorage_file, "rb") as f:
                data = pickle.load(f)
            return data
        except FileNotFoundError:
            return None

    def save_localstorage_to_file(self, storage):
        data_raw = storage.all_data()
        while True:
            time.sleep(10)
            data_new = storage.all_data()
            if data_new == data_raw:
                print("localstorage数据写入完成")
                break
            else:
                data_raw = data_new
        with open(self.localstorage_file, "wb") as f:
            pickle.dump(data_new, f)

    def input_thread(self, exit_event):
        print("登录过期或第一次登录,需要{}s内手动扫描二维码登录和确认登录情况".format(self.time_out))
        while not exit_event.is_set():
            self.user_input = input("扫描后,需要手动输入Y表示登录成功,N表示登录失败:\n")
            if self.user_input == "Y" or self.user_input == "N":
                break

    def scan_qr_code(self):
        exit_event = threading.Event()
        thread_1 = threading.Thread(target=self.input_thread, args=(exit_event,))
        thread_1.daemon = True
        thread_1.start()
        start_time = time.time()
        while not exit_event.is_set():
            end_time = time.time()
            if self.user_input == "Y":
                print("手动反馈登录成功,请等待,正在进行localstorage数据保存处理。")
                break
            if self.user_input == "N":
                print("手动反馈登录失败,已退出程序。按回车键结束程序。")
                sys.exit()
            if end_time - start_time > self.time_out:
                print("超时{}s无手动反馈,判断登录失败,已超时退出程序。".format(self.time_out))
                exit_event.set()
                sys.exit()
            time.sleep(1)
        thread_1.join()


if __name__ == '__main__':

    def job():
        URL_1 = "https://www.baidu.com"
        localstorage_file = "localstorage.pickle"
        time_out = 300
        # 用于确定页面稳定和成功的元素
        ele_xpath = {
            'qr-code': '//*[@id="app"]/div/div/div[1]/div[3]',
            'contacts': '//*[@id="chatMenu"]/div[1]/div',
        }
        lf = LoginFirst(URL_1, localstorage_file, time_out, ele_xpath)
        lf.run()

    # # 循环按照计划时间运行
    # schedule.every().day.at("00:00").do(job)
    # schedule.every().day.at("04:00").do(job)
    # schedule.every().day.at("10:00").do(job)
    # while True:
    #     schedule.run_pending()
    #     time.sleep(30)

    job()

这是一个应用的例子,根据自己的环境进行修改适配即可。

四、注意事项

1、如果有token的时间校验怎么办

解决办法:一般token的有效期为一个礼拜或更长时间,如果有token校验,且有时间限制;那么每次运行完自动化代码重新保存一遍localstorage数据到本地,每次自动化运行间隔不超过token有效期就可以长期运行了。

2、如果按照思路实现后还是有问题

解决办法:第一点,确认自己的程序是否用的localstorage数据进行的校验;第二点,在保存和填入localstorage的时候需要一些等待时间或者加一些校验,保证所有的localstorage数据都保存下来或填入成功。

好好学习,天天向上

  • 30
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值