diff --git a/邮件批量发送脚本.py b/邮件批量发送脚本.py index 4027df6..3bcb109 100644 --- a/邮件批量发送脚本.py +++ b/邮件批量发送脚本.py @@ -1,9 +1,12 @@ +import unicodedata import argparse import pandas import time from selenium import webdriver from selenium.common.exceptions import StaleElementReferenceException, TimeoutException +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait @@ -15,6 +18,7 @@ from datetime import datetime, timedelta parser = argparse.ArgumentParser(description="邮件批量发送脚本") parser.add_argument('input', nargs='?') parser.add_argument('--column-address', type=str, nargs='?', default='邮箱') +parser.add_argument('--column-name', type=str, nargs='?', default='主要联系人') parser.add_argument('--column-code', type=str, nargs='?', default='客户编号') parser.add_argument('--column-sent', type=str, nargs='?', default='已发送') parser.add_argument('-u', '--url', type=str, nargs='?', default='https://id.ionos.fr/identifier') @@ -48,6 +52,7 @@ def main(): return 1 limit = len(recipients) + names = data.setdefault(args.column_name, [None] * limit) codes = data.setdefault(args.column_code, [None] * limit) sents = data.setdefault(args.column_sent, [None] * limit) @@ -64,7 +69,7 @@ def main(): return 2 def locate(selector, condition=EC.presence_of_element_located, parent=driver) -> WebElement: - while True: + for attempt in range(args.retry): try: wait = WebDriverWait(parent, timeout=args.timeout) return wait.until(condition((By.CSS_SELECTOR, selector))) @@ -74,9 +79,10 @@ def main(): except TimeoutException: # 超时错误 raise Exception('操作超时') + raise Exception(f'无法定位元素: {selector}') - def click(selector, condition=EC.element_to_be_clickable): - element = locate(selector, condition) if isinstance(selector, str) else selector + def click(selector, condition=EC.element_to_be_clickable, parent=driver): + element = locate(selector, condition, parent) if isinstance(selector, str) else selector counter = lambda: int(element.get_attribute('taximeter') or 0) error = False @@ -99,6 +105,26 @@ def main(): except TimeoutException: continue except: break + def keyin(element: WebElement, value): + try: element.send_keys(value) + except: driver.execute_script(f"arguments[0].value = arguments[1];", element, value) + + def ready(driver, predicate): + try: + wait = WebDriverWait(driver, timeout=args.timeout) + wait.until(predicate, '操作超时') + except: + return True + wait = WebDriverWait(driver, timeout=args.timeout) + wait.until_not(predicate, '操作超时') + return True + + def contains_non_latin_alphabet(string: str): + for char in string: + if unicodedata.category(char).startswith('L') and not unicodedata.name(char, '').startswith('LATIN'): + return True + return False + try: driver.get(args.url) except TimeoutException: @@ -169,21 +195,24 @@ def main(): occurrences = {} while active and index < limit: - recipient = recipients[index] - code = codes[index] + current = index + index += 1 + + recipient = recipients[current] + name = names[current] + code = codes[current] + mark = sents[current] if not code: occurrence = [0] else: occurrence = occurrences.setdefault(code, [0]) - current = index - index += 1 - - if sents[current] is not None: + if mark is not None and str(mark).strip(): print(f'[信息] 已跳过项目 {recipient}') + occurrence[0] += 1 continue - if args.max_occurrence > 0 and occurrence[0] > args.max_occurrence: - print(f'[警告] 收件人 {recipient} 所属组织已重复出现 {occurrence} 次') + if args.max_occurrence > 0 and occurrence[0] >= args.max_occurrence: + print(f'[警告] 收件人 {recipient} 所属组织出现次数已超出限制 {occurrence}') global warnings warnings += 1 continue @@ -194,15 +223,23 @@ def main(): click("button[aria-label='Edit copy']") # 等待页面加载 - wait = WebDriverWait(driver, timeout=args.timeout) - wait.until_not(lambda x: x.find_element(By.CSS_SELECTOR, ".io-ox-busy")) + ready(driver, lambda x: x.find_element(By.CSS_SELECTOR, ".io-ox-busy")) - # 填入收件人 wrapper = locate("div.io-ox-mail-compose-window div[data-extension-id='to'] > div.mail-input") to = locate("input.token-input.tt-input[tabindex='0']", parent=wrapper) - click(wrapper) - to.send_keys(recipient) + if name is not None and (string := str(name).strip()) and not contains_non_latin_alphabet(string): + # 填入客户称呼 + firstname = string.split()[0].title() + action = ActionChains(driver).send_keys(Keys.END) + for attempt in range(args.retry): action.perform() + + ActionChains(driver, 5000).key_down(Keys.SHIFT).send_keys(Keys.LEFT).key_up(Keys.SHIFT).key_down(Keys.CONTROL).send_keys('x').key_up(Keys.CONTROL).perform() + ActionChains(driver, 5000).send_keys(Keys.SPACE).send_keys(firstname).key_down(Keys.CONTROL).send_keys('v').key_up(Keys.CONTROL).perform() + + # 填入收件人 + click(wrapper) + keyin(to, recipient) # 发送邮件 click("div.io-ox-mail-compose-window button[data-action='send']") @@ -225,22 +262,24 @@ def main(): # 关闭警告 click("div.io-ox-alert.io-ox-alert-error button[data-action='close']") - # 关闭过期邮件 - click("div.io-ox-mail-compose-window button[data-action='close']") - # 删除过期邮件 - click("div.modal-footer button[data-action='delete']") + + while mails := driver.find_elements(By.CSS_SELECTOR, "div.io-ox-mail-compose-window"): + # 关闭过期邮件 + click("button[data-action='close']", parent=mails[0]) + # 删除过期邮件 + click("div.modal-footer button[data-action='delete']") break except KeyboardInterrupt: print('[信息] 程序中断') except Exception as e: print(f'[警告] 发生错误:{e}') - key = input('[????] 重试 (R) / 跳过 (S) / 取消 (C): ') + key = input('[????] 重试 (r) / 跳过 (s) / 取消 (C): ') if key in ['R', 'r']: continue elif key in ['S', 's']: break else: active = False - if input('[????] 是否保存到文件?确定 (Y) / 取消 (N): ') in ['Y', 'y']: + if input('[????] 是否保存到文件?确定 (y) / 取消 (N): ') in ['Y', 'y']: print(f'[信息] 正在写入文件:{args.input}') try: pandas.DataFrame.from_dict(data).to_excel(args.input, index=False, sheet_name='Sheet1')