一、问题背景
公司产品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数据都保存下来或填入成功。
好好学习,天天向上