一般来说,发送邮件有几种方法,第一种就是人工来一封封发送,第二种就是借助邮箱的IMAP/SMTP服务,第三种就是通过类似selenium、playwright这种自动化软件来控制浏览器模拟人工发送邮件,今天来说说第三种方法。
最近有个需求,需要写一个自动化发送邮件的程序,原本以为是很简单的事情,毕竟之前写过类似的gmail邮件发送程序,但是碰到某个公司的企业邮箱确实是费了点时间,特此记录如下:
先来说说,在一台电脑上批量安装一些库的快捷函数吧,借助国内源安装的速度会快很多
import subprocess
libraries_to_install = ['selenium','pyperclip','pyautogui','pywin32','Pillow']
for library in libraries_to_install:
try:
subprocess.check_call(['pip', 'install', '-i', 'https://pypi.tuna.tsinghua.edu.cn/simple', library])
print(f"Success: {library}")
except subprocess.CalledProcessError:
print(f"Failed {library}")
另外,如果你发送的文本中带有emoji等特殊符号,selenium自带的send_keys函数是无法处理的,例如输入一个符号
Message: unknown error: ChromeDriver only supports characters in the BMP
即使你使用Unicode编码,例如
element.send_keys("\U0001F604") # 这会发送一个笑脸Emoji
也是一样的结果,这时候可以使用JS输入的方式:
JS_ADD_TEXT_TO_INPUT = """
var elm = arguments[0], txt = arguments[1];
elm.value += txt;
elm.dispatchEvent(new Event('change'));
"""
browser.execute_script(JS_ADD_TEXT_TO_INPUT, element, "❤")
这样就没问题了,这种方法不仅可以用于解决符号的输入问题,还对繁体中文的输入特别友好,如果你在尝试输入部分繁体中文字符时遇到了问题,也可以考虑使用这种JS输入的方式。
如果想要在邮件中插入准备好的图片,也很简单:
import win32clipboard
from PIL import Image
from io import BytesIO
def copy_image_to_clipboard(img_path: str):
'''输入文件名,执行后,将图片复制到剪切板'''
image = Image.open(img_path)
output = BytesIO()
image.save(output, 'BMP')
data = output.getvalue()[14:]
output.close()
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
win32clipboard.CloseClipboard()
通过将准备好的图片路径通过copy_image_to_clipboard函数复制到剪贴板,再在你想要输入图片的元素下面调用Control+V的快捷键输入图片就可以了
copy_image_to_clipboard(image_path)
content_element.send_keys(Keys.CONTROL, 'v')
最后再简单说说这个邮箱自动化的事情,由于涉及到隐私,就不截图了,该网站的页面大概分为两部分,一部分是主体html页面,使用常规的xpath方法正常取值就可以了,但是右边写信的地方是一个嵌入的iframe,这一点我们通过控制台的元素路径可以看到确实如此。
因此,我们需要调用selenium的driver.switch_to.frame(iframe_element)函数来跳转到对应的iframe内容中才可以正常取值,iframe_element可以是id,也可以是选中的element元素,注意,此时web界面中的xpath等调试工具是无法使用的,或者说是你即使选中了正确的元素,xpath工具也不会返回结果。
最简单的做法就是使用浏览器自带的copy xpath元素直接获取对应节点。这里面有一个坑就是之前写惯了id获取元素,这次栽了跟头,例如,//input[@id='subject']这个元素获取输入框理论上是没有问题的对吧,毕竟ID唯一,但就是获取元素失败,而他上一级的元素//div[@class='div_txt'] 又可以获取到内容,我是如何知道上一级元素获取到了呢?通过一级级打印元素值,例如
element = WebDriverWait(driver, timeout=10).until(
EC.presence_of_element_located((By.XPATH, 'XPATH'))
)
element.get_attribute("style")
最后把二者拼接起来才成功获取到对应的元素,虽然没搞清楚具体原因,但也算是涨个姿势了。
//div[@class='div_txt']//input[@id='subject']
另外,由于该页面还存在iframe元素嵌入iframe,所以如果想要进入更深层的iframe还必须再跳转一次driver.switch_to.frame(iframe_content),如果里面的元素处理完了,返回上一级iframe元素使用driver.switch_to.parent_frame()即可,或者跳转到html主界面driver.switch_to.default_content()。