结合pychrom与selenium实现页面自动登录
缘起
一直在浏览器里用Katalon插件录制一些常用的流程,以减少重复操作,也就自然而然想自己搞搞自动化测试,但无奈登录一关跨不过去,就无法串起来。(不想让开发添加万能验证码的功能)首先想到的是识别验证码。用selenium模拟登录时,验证码一关实在过不了。无论怎么处理验证码图片,tesseract识别率还是太低,完全不可用。看到有机器学习提高验证码识别率的例子,但觉得实在太麻烦,就没有研究,搁置了一段时间。最近发现在登陆时,后台发起了2个请求。先是Get_VerifyCode,获得verifyCode和backEndKey,然后是Post_Authenticate,并传入用户名,md5加密后的密码,verifyCode和backEndKey。登录成功后,后台会返回token。登录后访问页面,请求头中就都有Authorization字段,后台会根据id和authorization发给权限。
我就想如果能先用Get_VerifyCode获取verifyCode和backEndKey,然后把加上其它信息,对Post_Authenticate发起请求,这样不就能登录成功了吗?先写好用到的方法,然后再发起用requests发起请求吧。结果是不成功。验证码虽然能获得,但Post_Authenticate总是返回500错误。原因是什么呢?第一次是猜测,是不是参数不对,用response.url查看响应的url,怎么参数没在里面?查询后知道,要给requests.post()传参数,得用params(keyword argument),不能用data。修改后再尝试,还是不行。再查询,可能是请求头不对,就用Chrome dev tools查看请求头。在请求Post_Authenticate时,给加上了几个。结果不行。再查,有人说一定要把请求头给加全了,一个都别缺,下面也有人回复有用。难道不能偷懒,不能只在请求头里加user-agent?补齐请求头后,还是不行。容我想想。是不是在获取验证码时就得加上,不然拿到的backEndKey也是无效的,试了,不行。观察下请求头吧。获取验证码时,有个cookie字段,是不是应该拿到cookie,然后放到2个请求的请求头中?怎么获取cookie呢?我发现打开登录页面后,发起的请求里就有cookie字段了。我想到的第一个办法是,用selenium模拟登录,用get_cookies()方法,试了,可以,但Post_Authenticate还是不通。第二个办法是,用requests发起请求后,也能在返回值里取到cookie,试了,可以,但最后还是一样的问题。请求走不通,难道是requests库的问题,于是又试了下urllib库。一样的结果。这么一圈下来还是解决不了,想想能能不能换个思路。能不能在页面上登录,但又用Get_VerifyCode获取验证码呢?试了,不行,根本就不是一次请求,获得了验证码也不可用。因为backendky不一致。那能不能selenium登录浏览器后,执行js脚本发起请求,就好像在页面上点击了验证码,也能更新验证码呢?奈何行不通。还是因为backendkey不一致。
在selenium模拟登录后,页面上点击验证码图片,在去监控网络请求,拿到Get_verifyCode的返回值,这样也能解决问题。于是就去搜索selenium capture net traffic,翻来拣去,总算是看到相关的。可以让selenium在启动后,打开一个dev tools,不过主要能用来观测浏览器性能。又看到chrome devtool protocol,似乎可行,就安装了python的对它的实现pychrome,想执行例子试试,但有报错。按最后给出的错误信息,有说关闭防火墙,有说把把代理设置改为自动,有说用cmd的命令行执行,有说是pycharm的问题,一一试了试,无一可行。
错误信息往前追溯,看到是urllib3报错,搜索后还是没解决。再往前看是socket问题,还是不行。
让我想想,之前有人遇到的类似问题是,服务端没有开启监听,所以客户端访问时,就会提示目标计算机拒绝了请求。
由于pychrome库,实例化Browser对象后,监听的是9222端口,会不会是9222端口的服务端问题呢,也就是浏览器设置的问题呢?这会用baidu吧,搜索“9222端口”,第一个就是
设置Chrome远程调试接口,是给前端打断点调试用的。试试看吧,总算可行,pychrome库能用了。
又了解了下chrome dev protocol,尝试了pychrome里的各种方法,试图捕捉到Get_verifyCode请求的返回值
试了多个方法,看了github仓库中提供的例子,总算弄好了
接下来的问题就变成了,怎么把selenium和pychrome结和起来,这样既能让selenium来做自动化操作,
又能使用pychrome监控请求。分开使用时,两个库都是会生成各自的浏览器实例,互不相干,
莫名其妙,执行写好的代码就不能用了,会提示chrome unreachable。
以为是chrome自动升级了,导致webriver不匹配,下载了新的webdriver,不行。
更新chrome时发现,原来自己早把chrome自动更新给停了,就更新到了75,又换了chromedriver,不行。
再查,有人说是hosts的问题,没有填入127.0.0.1和localhost的映射,打开hosts查看,已经有了,不是这个问题。
再查,有人说是代码中的webdriver配置问题,改了后不行。中间还遇到常用的js代理走不通,谷歌访问助手不能用的情况,又调了下这个问题。
再查,有人说防火墙问题,仍然不行。
停下俩想想吧,是不是浏览器的问题呢?以remote-debugging模式打开chrome,访问不到9222端口。用telnet local 9222命令,也是不通。
可能是浏览器问题吧。实在是查不到相关信息了,只能想到浏览器重装大法。查询中,记得看到过chrome canary的信息,就查了下,原来是类似nightly的版本,
就装了下,是77版。以远程调试模式,打开chrome canary后,发现可以访问到9222端口。再打开chrome stable版,发现居然也可以了。
试着卸载了canary版本,又不行了,只得再装回来。可是网络突然又不好了,无论如何都下载不成功。第二天到了公司,重装了下,总算原来的代码可以用了
我真的很困惑,问题不知道为什么就发生了,也不知道为什么就解决了。
卡在某处,就想一次性解决,不拖到下次。可是总会遇到各种各样的问题。要解决一个问题,查询后发现得先解决另一个问题,预置了前提,而前提又未必一定导向
原本的问题,于是就有了两个问题。以此类推,问题像锁链一样,一环套一环。卸掉一环,未必就能离原本的问题近一点,但却只能如此。
代码
1 from selenium import webdriver 2 import pychrome 3 import json 4 import subprocess 5 import time 6 from data import * 7 from commons import EventHandler, Task 8 import os 9 from action import Action 10 from action import * 11 12 13 class ChromeClient(object): 14 options = webdriver.ChromeOptions() 15 options._debugger_address = "localhost:9222" 16 # options.add_argument('--remote-debugging-port=9222') 17 18 def __init__(self): 19 # 原来用os.popen时不时就报错,打开的chrome一直无响应,改为用subprocess后就没报错了 20 21 # os.popen(CHROME_CMD) 22 23 subprocess.Popen(CHROME_CMD) 24 self.browser = pychrome.Browser() 25 self.driver = webdriver.Chrome(executable_path=r'C:\Python36\chromedriver.exe', chrome_options=ChromeClient.options) 26 self.driver.implicitly_wait(30) 27 self.tab = None 28 self.event = None 29 self.token = None 30 31 # self.driver.get('about:blank') 32 33 def monitor(self): 34 self.tab = self.browser.list_tab()[0] 35 self.tab.start() 36 self.tab.Page.enable() 37 self.tab.Network.enable() 38 # self.tab.Page.enable() 39 self.event = EventHandler() 40 self.tab.Network.requestWillBeSent = self.event.on_request_will_be_sent 41 self.tab.Network.responseReceived = self.event.on_response_received 42 print('----Requests are being monitored....') 43 return True 44 45 def naivigate(self, url): 46 self.tab.Page.navigate(url=url) 47 time.sleep(2.5) 48 print('----Navigate to ', url) 49 50 def get_code_from_request(self, requestId): 51 try: 52 _response = self.tab.Network.getResponseBody(requestId=requestId) 53 _response_content = _response.get('body') 54 _response_dict = json.loads(json.loads(_response_content)) 55 _verifyCode_str = _response_dict.get('data').get('verifyCode') 56 print("----Verifycode ", _verifyCode_str) 57 return _verifyCode_str 58 except Exception as e: 59 return False 60 61 def get_token_from_request(self, requestId): 62 time.sleep(2) 63 _response = self.tab.Network.getResponseBody(requestId=requestId) 64 print(_response) 65 _response_content = _response.get('body') 66 _response_dict = json.loads(json.loads(_response_content)) 67 _token = _response_dict.get('data').get('tokenID') 68 self.token = ''.join(['BasicAuth ', _token]) 69 print("----Token ", self.token) 70 return self.token 71 72 def login(self, username, password, verifycode): 73 _driver = self.driver 74 _username_input = _driver.find_element_by_name("username") 75 _password_input = _driver.find_element_by_name("password") 76 _verifycode_input = _driver.find_element_by_name("verificationCode") 77 78 Task.delete_text(_username_input) 79 _username_input.send_keys(username) 80 Task.delete_text(_password_input) 81 _password_input.send_keys(password) 82 _verifycode_input.send_keys(verifycode) 83 84 for _element in _driver.find_elements_by_css_selector('span'): 85 if "登录" in _element.text: 86 _driver.execute_script("arguments[0].click();", _element) 87 # self.tab.wait(2.5) 88 # _driver.execute_script('alert(1);') 89 # _driver.execute_script("arguments[0].click();", login_span) 90 91 92 if __name__ == '__main__': 93 host = 'ppm-test' 94 role = 'la' 95 no = 0 96 97 client = ChromeClient() 98 client.monitor() 99 client.naivigate(url=LOGIN_URLS[host]) 100 verifycode = client.get_code_from_request(client.event.request_id) 101 client.login(username=USERS[role][no][0], password=USERS[role][no][1], verifycode=verifycode) 102 token = client.get_token_from_request(client.event.request_id)
感想
即便是花了很多时间把登录搞好了,之后各类流程却又是个问题。各个业务用到的接口数据,复杂繁多,而且有诸多限制。况且录制的代码,常常无法直接变成selenium代码,嵌在项目里,往往还有经过多轮调试,还不一定能用。UI自动化测试收益果然不高。瞎折腾实在是费心费力。
参考文章
https://div.io/topic/1464
https://testerhome.com/topics/16526
https://testerhome.com/topics/16222