上个月我被一个破活儿折磨得不行。每天下午四点,要从三个不同的系统里导出数据,然后手动拼成一个Excel报表,再发给老板。数据量不大,但步骤烦得要死——先登录系统A,选日期范围,点导出,等下载,再登录系统B... 每个步骤大概两分钟,整套流程下来十五分钟。但问题是,中间但凡有人找我聊个天、回个消息,我就忘了做到哪一步,经常搞到快下班才发现少导了一个表。
这种低水平重复劳动,干一次是苦力,干一百次就是对自己智商的侮辱。我决定写个脚本把它自动化掉。
先说技术选型。我用的Python 3.10.6,主要依赖三个库:selenium 4.15.0做浏览器自动化,openpyxl 3.1.2处理Excel,schedule 1.2.0做定时任务。为什么没用requests?因为这三个系统都不是纯API接口,有些数据是通过JavaScript动态加载的,直接抓HTTP请求太麻烦。selenium虽然慢,但胜在稳定,而且我只需要每天跑一次,慢点无所谓。
先写核心逻辑。第一步是登录系统A。系统A是个老古董,连验证码都没有,纯用户名密码登录。但坑爹的是,它的登录按钮是个div,不是标准的submit,用selenium的click方法有时候没反应。我查了一下,发现是页面用了某个版本的React,click事件绑定的方式比较特殊。解决办法是先用ActionChains移动鼠标到按钮上,再执行click,或者直接执行JavaScript:
```python
from selenium.webdriver.common.action_chains import ActionChains
login_btn = driver.find_element(By.CSS_SELECTOR, 'div.login-button')
ActionChains(driver).move_to_element(login_btn).click().perform()
```
或者更简单粗暴:
```python
driver.execute_script("arguments[0].click();", login_btn)
```
后者成功率更高,我最后用的这个。
登录之后要导航到报表页面。系统A的URL是带token的,每次登录token都不一样,没法直接硬编码URL。我用了WebDriverWait,等页面加载完某个特定元素出现,再点击菜单项。这里有个坑——系统A的菜单是hover展开的,不是点击。所以我得先hover到父菜单,等子菜单出现,再点击子菜单项。
```python
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
parent_menu = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, 'menu-report'))
)
ActionChains(driver).move_to_element(parent_menu).perform()
child_menu = WebDriverWait(driver, 5).until(
EC.visibility_of_element_located((By.LINK_TEXT, '日报表'))
)
child_menu.click()
```
选日期范围这块,系统A用的是个日期选择器,直接填input框不行,它绑定了change事件。我试了send_keys然后触发事件,发现不行。最后直接改input的value属性,然后手动触发一个change事件:
```python
date_input = driver.find_element(By.NAME, 'startDate')
driver.execute_script("arguments[0].value = '2024-01-15'", date_input)
driver.execute_script("arguments[0].dispatchEvent(new Event('change'))", date_input)
```
这一步搞了我将近两个小时才试出来。所以有时候直接操作DOM比模拟用户操作更可靠。
导出按钮倒是很老实,点击后直接下载一个CSV文件。但selenium有个问题——它没法直接控制浏览器下载对话框。默认下载路径是浏览器的设置,我用了ChromeOptions指定下载目录:
```python
from selenium.webdriver.chrome.options import Options
chrome_options = Options()
chrome_options.add_experimental_option('prefs', {
'download.default_directory': '/tmp/report_downloads',
'download.prompt_for_download': False,
'download.directory_upgrade': True
})
driver = webdriver.Chrome(options=chrome_options)
```
系统B和系统C的登录方式完全不同。系统B用的是OAuth2.0,跳转到第三方认证页面,输入账号密码后授权。这个流程里有个iframe,需要先切换进去:
```python
iframe = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.TAG_NAME, 'iframe'))
)
driver.switch_to.frame(iframe)
# 然后在iframe里填用户名密码
```
系统C最恶心,它用了图形验证码。我一开始想用OCR,但那个验证码扭曲得太厉害,tesseract识别率不到30%。后来发现系统C有个隐藏的API,验证码其实是后端生成的base64图片,但如果你在请求里带一个固定的session_id,验证码就不会变。这明显是开发偷懒留下的后门。我试了一下,果然能用。所以我就直接绕过验证码了,这种安全漏洞不是我的问题,我只是利用它而已。
三个系统的数据都下载好后,我用pandas读进来,合并,再用openpyxl写成一个格式好看的Excel。这里有个细节——老板要求某些列要标颜色,某些行要冻结。openpyxl支持这些操作,但代码有点啰嗦:
```python
from openpyxl.styles import PatternFill, Font, Alignment
from openpyxl.utils import get_column_letter
wb = Workbook()
ws = wb.active
# 表头样式
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_font = Font(bold=True, color='FFFFFF')
for col_idx, col_name in enumerate(merged_data.columns, 1):
cell = ws.cell(row=1, column=col_idx, value=col_name)
cell.fill = header_fill
cell.font = header_font
# 冻结首行
ws.freeze_panes = 'A2'
```
最后一步是发邮件。我用了smtplib,连接公司Exchange服务器。公司邮箱用的是Office 365,需要TLS加密,端口587。注意密码不能用明文写死在代码里,我用了环境变量:
```python
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import encoders
smtp_server = 'smtp.office365.com'
smtp_port = 587
username = os.environ.get('EMAIL_USER')
password = os.environ.get('EMAIL_PASS')
msg = MIMEMultipart()
msg['Subject'] = '日报表 - 自动生成'
msg['From'] = username
msg['To'] = 'boss@company.com'
with open('/tmp/report.xlsx', 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment', filename='日报表.xlsx')
msg.attach(part)
with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls()
server.login(username, password)
server.send_message(msg)
```
定时任务我用的是schedule库,配合一个while循环。因为我的脚本需要运行在公司的Windows服务器上,没法用cron,schedule是最轻量的方案:
```python
import schedule
import time
def job():
try:
run_automation()
print(f'{datetime.now()} - 报表已生成并发送')
except Exception as e:
print(f'{datetime.now()} - 出错: {str(e)}')
# 发个通知给自己
send_error_notification(str(e))
schedule.every().day.at('16:00').do(job)
while True:
schedule.run_pending()
time.sleep(60)
```
第一次跑的时候,崩了。原因是系统A的页面在凌晨做了维护,CSS选择器变了。我加了个重试机制,如果某个元素找不到,就截图保存,然后等五分钟再试。截图功能用selenium自带的:
```python
driver.save_screenshot(f'/tmp/error_{datetime.now().strftime("%Y%m%d_%H%M%S")}.png')
```
跑了一个星期,成功率大概90%。剩下的10%要么是网络超时,要么是系统B的OAuth页面改版了。我加了个异常处理,如果失败三次就不重试了,直接发邮件告诉我手动处理。
现在每天不用再花十五分钟做这个破事了。脚本跑一次大概三分钟,大部分时间浪费在selenium等待页面加载上。我试了用headless模式,速度差不多,但稳定性差一点,有些页面元素在headless下渲染不出来。最后我还是保留了有头模式,但把窗口最小化到任务栏,眼不见心不烦。
踩坑总结:
第一,selenium的click不一定靠谱。遇到点击没反应的情况,先试试ActionChains或者execute_script。后者几乎不会失败。
第二,日期选择器这种交互组件,直接改DOM属性比模拟用户操作靠谱。前提是你得知道它绑定了什么事件。
第三,验证码能绕过就绕过。不是所有验证码都值得用深度学习去破解。找找有没有API后门、固定的session、或者测试账号的万能验证码。
第四,定时任务要考虑异常处理。截图是调试利器,比日志直观得多。另外不要忘了处理节假日——报表只在工作日需要。我后来加了个判断,如果是周末或者法定节假日,直接跳过。
第五,密码和敏感信息永远不要硬编码。用环境变量,或者用python-dotenv加载.env文件。公司服务器上有人共享账号,明文密码就是给自己挖坑。
现在这个脚本已经跑了三个月,只手动干预过两次。一次是系统C的API后门被堵上了,我不得不改用OCR方案。另一次是公司换了Exchange服务器,SMTP配置变了。
整个过程最耗时的不是写代码,而是调试各种反人类的前端交互。但搞定之后,那种每天省下十几分钟的快感,值了。
📎 延伸阅读
看完这篇,如果你想:
- 直接拿工具 → 回复"13",我把跨境获客工具包发给你
- 系统学习 → 点击菜单"AI训练营",从0开始跑通AI变现
本文由猫哥AI助手自动发布 🐱