自动化飞书邮箱网页版应该有不少例子,可以使用Clicknium、Selenium,Playwright这些工具实现。这次使用[Clicknium](https://www.clicknium.com/)来介绍一下桌面客户端的自动化,进一步讲解如果利用Python实现桌面端自动化。想要写一个能稳定运行的自动化脚本,并不容易,过程中需要不停修正定位数据。很多时候,我们看到脚本的结果,但很少有教程描述,脚本是怎么一步一步写出来的。
首先,我们要明确自动化的需求。举一个最简单的场景:群发邮件。与过年群发祝福消息类似,想要别人不知道这则消息是群发的,就需要在消息中带上对方独特的属性,比如名字等。现在不少提供模板方式群发邮件的服务都是收费的。其实我们使用Python+Clicknium的方式可以很快实现一个根据模板群发邮件的程序。
有了需求,我们需要理清楚实现自动化的基本步骤:
1. 材料:邮件模板
2. 材料:邮箱列表和模板填充数据
3. 操作:将模板填充数据,填入邮件模板中
4. 操作:用飞书客户端将填充数据后的模板,根据邮箱地址发送出去
下面是简单的样例:
邮件模板(sample.txt):
Hi {name},
Welcome! Do you like our product?Regards,
Kay
其中{name}就是一个占位符(placeholder),在实际邮件中会被详细信息中的userName替换掉。
邮箱列表(user.csv):
userName | |
---|---|
xxx@gmail.com | Jack |
xxx@qq.com | HuaHua Wong |
准备环境:
- Windows 7 SP1+
- VS Code
- Clicknium: 参考 quick start , 不在此详述
- 飞书
下面我们详细描述怎么把这个sample一步一步做出:
1. Python启动飞书:
import subprocessdef main():process = subprocess.Popen("C:\\Users\\kay\\AppData\\Local\\Feishu\\Feishu.exe") #replace with your feishu.exe path这里我们利用subprocess将飞书启动起来。
2. 点击邮箱按钮,进入飞书邮箱,新建邮件:
在这一步,为了方便我将邮箱从更多列表中拖入左侧任务栏中,并放置在第五个。
利用抓取按钮,点击上图的邮箱图标,抓取按键。
抓取后我们可以在locator store中看到刚抓取的UI元素
从上图中可以看到,Clicknium利用上面的属性来定位元素。大概看一下,其中最重要的应该是第九行。 index=6 最终定位了这个按钮的位置。还可以得到的信息是,获取元素位置采用了UIA技术,按钮的name属性为空,意味着飞书团队并没有给改按钮命名,我们无法通过name标识这个按钮。index并不是固定的,随意拖动邮箱图标都会导致index位置变化。但是这个方式比较高效,如果该脚本需要给别人使用,我们可以采用图像识别的方式。可以通过上图的Validation确定能不能定位到元素,通过Action尝试点击操作是否能成功。
补充上面的代码:
from clicknium import clicknium as cc, locator
import subprocessdef main():process = subprocess.Popen("C:\\Users\\kay\\AppData\\Local\\Feishu\\Feishu.exe") #replace with your feishu.exe pathcc.find_element(locator.feishu.mailbutton).click()if __name__ == "__main__":main()
这部分代码中,引入了clicknium库,通过locator.feishu.mailbutton可以定位到邮箱图标,对其点击操作。clicknium通过将locator传入find_element()函数就找到对应的UI元素,然后直接对其进行操作。通过F5运行,看是否能够成功执行。运行成功飞书会打开一个新的窗口,如下图:
第一步,抓取收件人输入框。Recorder在auto配置下得到下图的locator:
在auto档捕获技术下使用的是UIA技术,点击validation可以找到元素,点击action-->set_text失败。看来UIA技术还需要调优,我们点击Recapture尝试使用IA技术。
切换IA技术后得到上图的locator,确认Validate和Action都能成功运行。同样的方式,我们抓取下图的主题\正文和发送按钮。
文本处理
邮件模板放在项目根目录中的sample.txt文件中,简单用Python直接读取数据即可。
f = open(r".\sample.txt")mailtxt = f.read()
然后读取csv文件中的邮箱和用户名信息:
with open('user.csv') as f:f_csv = csv.DictReader(f)
定一个发送邮件的函数,接受模板内容、邮箱和用户名:
from clicknium import clicknium as cc, locator,UiElement
import subprocess
import csvdef main():process = subprocess.Popen("C:\\Users\\kay\\AppData\\Local\\Feishu\\Feishu.exe") #replace with your feishu.exe pathcc.find_element(locator.feishu.mailbutton).click()f = open(r".\sample.txt")mailtxt = f.read()with open('user.csv') as f:f_csv = csv.DictReader(f)for u in f_csv:#sendMai(u['email'],u['username'],mailtxt)print("email:",u['email'],"username:",u['username'])if __name__ == "__main__":main()
实现sendMail方法
发送有邮件的函数步骤:
1. 用户名替换模板placeholder(文本处理)
2. 点击写新邮件的按钮(click)
3. 填写收件人邮箱地址(click)
4. 填写邮件主题(set_text)
5. 填写邮件内容(set_text)
6. 点击发送按钮(click)
实现:
def sendMai(mail, name, text):username = getName(name)mailcontent = text.replace("{name}",username)cc.find_element(locator.feishu.newmailbutton).click()cc.find_element(locator.feishu.recipient).set_text(mail, by='sendkey-after-click')cc.find_element(locator.feishu.title_text).set_text("Welcome to Clicknium", by='sendkey-after-click') paste_text(cc.find_element(locator.feishu.content_ia),mailcontent)cc.find_element(locator.feishu.send_button).click()
def sendMai(mail, name, text):username = getName(name)mailcontent = text.replace("{name}",username)cc.find_element(locator.feishu.newmailbutton).click()cc.find_element(locator.feishu.recipient).set_text(mail, by='sendkey-after-click')cc.find_element(locator.feishu.title_text).set_text("Welcome to Clicknium", by='sendkey-after-click') paste_text(cc.find_element(locator.feishu.content_ia),mailcontent)cc.find_element(locator.feishu.send_button).click()
第一行对用户名做一个简单的处理,去掉特殊字符,首字母大写等。第二行用文本替换,处理模板占位符。
之后点击飞书客户端写新邮件的按钮 之后我们填写收件人:
cc.find_element(locator.feishu.recipient).set_text(mail, by='sendkey-after-click')
点击查看set_text函数,或者查函数文档
从描述中可知,clicknium提供三种输入文本的方法。set-text是系统调用,性能是最好的,直接将一段文本设置上去,但不是所有场景都可用。sendkey-after-click和sendkey-after-focus是采用模拟键盘输入的方法,主要解决的就是点击或设置关注后才激活的问题,简单说来就是模拟鼠标和键盘,所以字母也是一个一个敲进去的,消耗的实际就比较长。这里我们采用sendkey-after-click。
在设置完邮件主题后,发现邮件这个弹框上的文本会根据主题变化,会对定位弹框有影响,我们检查locator store中的locator对其进行一定的泛化处理。
打开邮件文本的locator:
通过locator中的配置信息, 可以看到发件人, 邮件主题,收件人这些信息都用来帮助定位元素了。这就意味着,其中任何一项信息发生变化,很可能就无法定位元素了。邮件主题,发件人, 收件人发生变化就无法使用的话,泛化能力就太差了,自动化脚本的可用性和稳定性都会大大降低。Clicknium Locator中的配置信息是可以直接修的,也提供了一些方式解决泛化的能力:
-
每一项配置前都有一个check box。取消勾选,这项配置就不参与定位。对于特殊元素,如果在定位中不起决定性作用,可以尝试取消勾选,使用validation和action测试是否依然有效。
-
如果配置中只有部分内容参与定位,尤其是位于配置的头尾,我们可以直接选中,在右侧修改配置信息。删除特殊内容,测试是否有效。
-
通配符:可在配置中使用通配符
-
正则表达式:可在配置中使用正则表达式
-
变量 :可在配置中使用变量 可以在文档中查看parametric locator章节
去掉与邮件主题,发件人,收件人相关的配置,就能提高泛化能力,使用不同账号给不同的人发送邮件了。 邮件主题可以采用变量的方式配置。
在邮件正文locator的配置中,我们发现其定位到的是一个group,并不是edit,我们模拟鼠标键盘输入的方式,一个按键一个按键点击的速度是比较慢的 所以我们采用复制张贴的方式,将文本内容写入剪贴板,然后利用粘贴功能写入文本。这里需要利用pyperclip库,使用pip install pyperclip安装利用下面的函数使用张贴功能:
def paste_text(webEle:UiElement,text:str):webEle.send_hotkey('^a',preaction="click")webEle.send_hotkey('^x',preaction="click")pyperclip.copy(text)webEle.send_hotkey('^v',preaction="click")
最后点击发送按钮即可。
下面是完整代码:
Github: Feishu Mail
from clicknium import clicknium as cc, locator,UiElement
import subprocess
import csv
import pyperclipdef main():process = subprocess.Popen("C:\\Users\\kay\\AppData\\Local\\Feishu\\Feishu.exe") #replace with your feishu.exe pathcc.find_element(locator.feishu.mailbutton).click()f = open(r".\sample.txt")mailtxt = f.read()with open('user.csv') as f:f_csv = csv.DictReader(f)for u in f_csv:sendMai(u['email'],u['username'],mailtxt)def sendMai(mail, name, text):username = getName(name)mailcontent = text.replace("{name}",username)print("email:",mail," name:",username)cc.find_element(locator.feishu.newmailbutton).click()cc.find_element(locator.feishu.edit_ia).set_text(mail, by='sendkey-after-click')cc.find_element(locator.feishu.title_text).set_text("Welcome to Clicknium", by='sendkey-after-click') paste_text(cc.find_element(locator.feishu.content_ia),mailcontent)cc.find_element(locator.feishu.send_button).click()def getName(userName):return userName.split(",")[0].split("@")[0].split(" ")[0].split(".")[0].capitalize()def paste_text(webEle:UiElement,text:str):webEle.send_hotkey('^a',preaction="click")webEle.send_hotkey('^x',preaction="click")pyperclip.copy(text)webEle.send_hotkey('^v',preaction="click")if __name__ == "__main__":main()