背景
最近工作中有这样一个需求:客户反馈在浏览器操作过程中,重复流程操作太频繁,能不能让浏览器自动操作完成?
在我的认知中,浏览器就是一个用来浏览页面的工具,因此第一反应就是“不可能!绝对不可能!”。领导说,你了解过RPA吗?会后专门查阅了相关资料,在我的理解中,RPA就是一个定制化机器人(脚本流程),通过控制脚本操作显示器,来模拟用户操作的过程。
开始
一开始的时候,我在github上面找了一些开源的项目,例如:PyRPA、RPA-Python、TagUI、openrpa等,但是因为定制化比较强,后期不知道会变成怎么样,因此做的要灵活一些。但是在这些开源项目中,提供了一部分的api,但是在有些地方并不是很容易操作,感觉后面可能会不可控。
但是在我观察中发现,大多数的实现都是通过python
实现的,作为一个前端,第一反应就是,为什么不用node和js结合呢,最后反应过来,使用python可以操作整个PC,但是如果你使用node或js,只能说操作浏览器更方便一些了(其实和python差不多),只是python在使用第三方库时候,会存在一定程度的延时。
因此,我决定放弃现有的RPA的项目,自己实现一个自动化的python脚本。
语言选择
关于我为什么选择的python作为语言,其实在前面已经说过了,作为一个前端开发,其实最熟悉的莫过于javascript,另加一些nodejs,那么为什么我没有用nodejs呢?node在前端开发过程中,常常扮演着一个环境的角色,虽然也可以作为一个后端语言来写一些接口或服务(依赖于第三方库),但是很少用到DOM的操作,操作浏览器的DOM,我们通常来说,都是使用js直接操作,或使用jquery等第三方工具。如果要再直接去拦截系统的一些操作,js就不太行了。
因此,采取了python,这一门热度一直很高的语言。主要思路就是,图片的对比,获取点位,模拟用户鼠标,键盘等操作。
浏览器的启动
其实这个启动浏览器,一开始我只是单纯的打开浏览器,但是后面发现,每天第一次启动的时候,都会特别慢,查阅资料也没有找到原因,好像是缓存被清空了吧,网上找了大量的相关资料,最终找到了一个方案:加载时候启动指定驱动程序(浏览器版本
与驱动程序
需要对应),指定版本浏览器下载,新版驱动下载,114版本前驱动下载,有一部分页面需要科学上网才能访问与下载,建议大家科学上网昂。
一个启动浏览器的demo,这里的driver我在后续就不就行重复的定义了,直接使用了,例如:
1 2 3 4 5 6 7 | from selenium import webdriver from selenium.webdriver.chrome.service import Service driver = webdriver.Chrome(service = Service(r "C:\\tb2Lot\\driver\\chromedriver.exe" )) driver.maximize_window() driver.execute_script( "document.charset='utf-8';" ) driver.get( "http://www.baidu.com" ) |
这时候我们就已经可以打开浏览器,并操作浏览器调取到百度的页面了,我们想在百度的页面输入并搜索指定内容怎么办呢?这时候我们就需要获取页面的元素节点,然后进行输入了,下面我们来看一下如何在浏览器搜索吧!
在百度进行搜索的方案
首先我们先列举一下目前能够想到的两个方案:
- 从页面抓取页面
元素路径
,然后直接点击元素; - 整个页面截图,再截取需要点击的小图片,获取需要点击的
图片的坐标
,点击坐标;
这两种方案对于简单的操作都是可行的,复杂一些的,会出现一些问题,我们先看一下两种方案分别怎么进行操作,然后再来分析一下会遇见怎样的问题吧!
页面抓取元素,直接点击:
![](https://img-blog.csdnimg.cn/img_convert/ac5aa5d991fe3c8ee1c874b600119f95.jpeg)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import pyautogui import pyperclip import time from selenium.webdriver.common.by import By def cvEnter(msg): pyperclip.copy(msg) time.sleep( 1 ) pyautogui.hotkey( 'ctrl' , 'v' ) time.sleep( 1 ) pyautogui.hotkey( 'enter' ) #回车搜索 #driver的定义在本文最上面,这里没有重复写 #... cvEnter( "稀土掘金" ) driver.find_element(By.XPATH,r '//*[@id="su"]' ).click() #通过浏览器的xpath触发点击事件 time.sleep( 2 ) driver.find_element(By.XPATH,r '//*[@id="kw"]' ).clear() #清除输入框 driver.find_element(By.XPATH,r '//*[@id="kw"]' ).send_keys( '稀土掘金官网' ) #输入框输入数据 pyautogui.hotkey( 'enter' ) time.sleep( 5 ) |
在上面,其实我们已经使用了两种输入与搜索的方式:
输入
- 方式一:通过
粘贴板
,直接进行“c+v”; - 方式二:借助python浏览器的driver的
send_keys
方法,直接键入;
搜索(这里是回车和点击实现的都是搜索)
- 方式一:使用pyautogui的
键盘热键触发
; - 方式二:借助python浏览器的driver获取浏览器
元素
,触发该元素的click
事件;
通过图片的对比,获取点位进行点击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pyautogui import cv2 def getPosiXY(img): pyautogui.screenshot( './pics/screen.png' ) #显示完成之后,截图 screen = cv2.imread( './pics/screen.png' ) #加载全屏截图 current = cv2.imread(f './tb2lot_pic/{img}' ) #加载比对的图片 result = cv2.matchTemplate(screen, current, cv2.TM_CCOEFF_NORMED) #在screen中寻找current的点位 pos_start = cv2.minMaxLoc(result)[ 3 ] x = int (pos_start[ 0 ]) + int (current.shape[ 1 ] / 2 ) y = int (pos_start[ 1 ]) + int (current.shape[ 0 ] / 2 ) return x,y pointX, pointY = getPosiXY( "query_btn.png" ) pyautogui.click(pointX, pointY) |
在上面的demo中,我们借助cv2
库,实现了一个简单的大图中寻找小图的功能,这里在获取之前自动截图
并保存
到指定目录,小图
的名称作为参数,传递到封装的方法中,抓取点位信息,再点击获取到的坐标
位置。因为我们抓取的坐标点,因此如果点击的输入框,输入框粘贴数据,就需要直接通过粘贴板进行粘贴了。
为什么我们前面说抓取元素会有问题呢?
首先我们最理想的元素抓取就是,都在一个html中,没有动态的元素,没有切换界面,没有切换弹窗等,但是在实际的应用过程中,往往都是比较复杂的,例如:
vue打包的html
(元素的id不是固定的,有时候元素节点渲染的问题,嵌套可能有点区别);html嵌套了iframe
(直接切换了代码引用的窗口,后面都统一叫做句柄);选择框
的元素,展开和关闭不一致;- ...
以上的问题,都会导致直接抓取元素xpath
直接调取时候报错
,难道我们就真的不能用xpath进行操作了吗?答案当然是否定的,但是也不是完全,例如在iframe
中,我们可以切换句柄
,来达到代码引用模块的更换,例如:
1 2 3 4 5 6 | #driver的定义在本文最上面,这里没有重复写 #... iframeSrc = driver.find_element(By.XPATH,r '/html/body/div/iframe' ) driver.switch_to.frame(iframeSrc) # 切换句柄到iframe #driver.switch_to.default_content() #切换到默认的句柄 driver.find_element(By.XPATH,r '/html/body/form/div/input' ).click() |
这样看来,我们iframe也是可以抓取到元素的,不过要等待页面渲染完成
之后再去抓取,否则还是会报错的。其余的两个,因为元素是动态的,如果只是id
的问题的话,我们可以使用full xpath
进行抓取,但是如果元素嵌套都有可能变化的话,就只能采取截图比对的方式了。
此外,我们也再添加一段代码,比如开启浏览器两个标签页的时候与标签页之间的切换怎么处理:
1 2 3 | driver.get( "http://www.baidu.com" ) #打开标签1 driver.execute_script( "window.open('http://www.baidu.com');" ) #打开标签2 driver.switch_to.window(driver.window_handles[ 0 ]) # 手动切换到标签1 |
最后
其实到这里,python简单的控制浏览器就已经结束了,其实图片的对比不局限于浏览器,它可以截取PC的屏幕上的所有,因此也可以模拟APP的操作,但是我们在调取的过程中,常常会报错,但是如果打包成了exe文件,我们并不知道报的什么错,因此我们也可以在最外层加上try-except,如果检测到报错,我们可以将报错内容记录在txt中,模拟日志
的效果,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | from datetime import datetime, timedelta try : # some codes except Exception as e: now = datetime.now() date_time = now.strftime( "%Y-%m-%d %H:%M:%S" ) data_to_append = f "{date_time}\n{e}\n\n" with open ( 'C:\\logs\\error.txt' , 'a' ) as file : file .write(data_to_append) print ( "Error!" , e) driver.quit() raise ValueError( "Error!" ) |
最后打开预览报错信息,例如:
![](https://img-blog.csdnimg.cn/img_convert/03f66a9937171611f36fd319430201e4.jpeg)
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import pyautogui import pyperclip import time from selenium.webdriver.common.by import By from selenium import webdriver from selenium.webdriver.chrome.service import Service from datetime import datetime, timedelta def cvEnter(msg): pyperclip.copy(msg) time.sleep( 1 ) pyautogui.hotkey( 'ctrl' , 'v' ) time.sleep( 1 ) pyautogui.hotkey( 'enter' ) try : driver = webdriver.Chrome(service = Service(r "C:\\pyExe\\driver\\chromedriver.exe" )) #驱动写在最上面 driver.maximize_window() driver.execute_script( "document.charset='utf-8';" ) driver.get( "http://www.baidu.com" ) driver.implicitly_wait( 10 ) cvEnter( "稀土掘金" ) driver.find_element(By.XPATH,r '//*[@id="su"]' ).click() #通过浏览器的xpath触发点击事件、 time.sleep( 2 ) driver.find_element(By.XPATH,r '//*[@id="kw"]' ).clear() driver.find_element(By.XPATH,r '//*[@id="kw"]' ).send_keys( '稀土掘金官网' ) pyautogui.hotkey( 'enter' ) time.sleep( 5 ) except Exception as e: now = datetime.now() date_time = now.strftime( "%Y-%m-%d %H:%M:%S" ) data_to_append = f "{date_time}\n{e}\n\n" with open ( 'C:\\pyExe\\logs\\error.txt' , 'a' ) as file : file .write(data_to_append) print ( "Error!" , e) driver.quit() raise ValueError( "Error!" ) |
代码打包
1 2 3 4 | pyinstaller --onefile --noconsole --icon . /favorite .ico . /index .py # --noconsole 隐藏黑窗口 # --icon ./favorite.ico 打包后的应用图标(路径选对,仅支持ico) # ./index.py 需要打包的py文件路径 |
有时候打包完图标没有变化,是默认的图标,可以查看属性,属性中应该是已经有了的,这是因为应用缓存的原因,刷新一下应用程序即可。