做Web自动化这行时间久了会收到各种奇怪的需求。有人找我帮忙抢课有人想给内部表单做自动填报还有一次需求来自一座小寺庙——用 Selenium 给香火钱做一套自动分账系统。说实话刚听到的时候我也愣了一下觉得这组合有点反差但仔细一琢磨这其实是一个特别典型的网页自动化落地场景。事情是一位熟人转介绍过来的。那座寺庙的线上功德系统能看到每一笔捐赠金额、时间、留言都能显示但就是没有导出按钮也没有开放接口。管理员每周都要对着后台逐条复制到一个 Excel 里再按比例拆成修缮、慈善、日常运营三本账。周末香客多一天一百多笔手一抖账就不平。他们早就想搞自动化了可问了一圈没人愿意接这种小活。我听完反而觉得挺有意思老系统、无接口、纯页面操作、重复劳动、数据准确性要求高这不就是 Selenium 最擅长的场景吗。于是花了两周做了一个香火钱自动分账系统Selenium 负责自动登录、抓取流水、翻页解析业务层负责清洗数据、按配置分账最后生成报表等管理员确认。这篇文章把整个系统的设计思路、关键代码和上线后的坑完整拆一遍适合正在学 Selenium 的朋友参考也适合所有想把重复劳动交给程序的人。1. 这个需求是怎么来的不是技术难题而是账本难题先说清楚问题本身。那座寺庙的功德金分两块线下现金有人每天清点走线上渠道的直接进到功德系统后台里后台能按日期查询流水也能翻页查看就是不支持导出。管理员每周做一次汇总把页面上的每一笔记录手工复制到 Excel再按规则拆成几份。这个活不难但极度枯燥而且抄错一行就要从头对账经常一弄就是大半天。不少人会问都这么痛苦了为什么不换一个系统或者找供应商开个导出接口1.1 线上功德系统的现状数据都在就是出不来我实际上手之后发现这个后台就是典型的老式管理系统顶部一排查询条件中间一张表格底部翻页按钮功能只有“增删改查”没有任何报表或导出能力。线上支付通道都是提前绑定的换系统意味着要重新对接支付渠道、二维码、语音播报这些事情对一座小庙来说成本太高。找供应商更不现实。这套系统是很多年前外包给某家公司做的中间经历了好几轮人员变动连客服联系方式都换了想找人开接口根本无从下手。所以在“不侵入现有系统”这个前提下Selenium 反而成了最务实的方案它不需要对方提供任何东西只需要用管理员账号在页面上做和人一样的操作。这个选择也提醒了我一件事做自动化项目先搞清楚为什么非要自动化比急着写爬虫更重要。如果系统有 API、有导出功能压根不需要 Selenium直接写脚本调接口就行。正是因为所有正规通道都堵死了页面自动化才成了“最合适的方案”而不是“最好的方案”。1.2 目标拆解把三件事分开做项目开始前我把目标拆成了三层避免一上来就陷入“写爬虫”的思维第一层每个周期默认每天把最新流水抓下来落库保存。第二层按配置好的分账规则自动拆账生成“待复核明细”。第三层管理员确认后把明细固化为“已生效账目”导出 Excel。全程保留日志与截图。这三层对应三个模块抓取模块、分账模块、复核模块。后面所有设计都是围绕这三层展开的尤其是第三层的人工确认虽然多了一步但对“涉及钱”的系统来说非常关键。自动化不是为了消灭人的监督而是把人的精力从无效抄写里解放出来。2. 分账规则建模先把“钱”变成“数据”我以前也犯过一上来就写代码的毛病但这个项目让我不得不先坐下来理规则。因为分账这件事业务规则一旦定错金额对不上后面所有自动化都是白搭。2.1 分账规则比想象中多两条支线管理师兄把规则整理给我看起来只有三条主线无备注的普通功德金按比例拆到三个户头。比如日常运营 40%、修缮 35%、慈善 25%。有指定用途的捐赠备注里写了“助学”“助医”这类字样这笔钱不分100% 进入对应专项账户。退款或异常流水不参与分账标记为“异常待查”。但用了两周发现现实远比这复杂来自不同渠道的流水手续费不一样有些大额捐赠在后台分两行显示偶尔会有名为“测试”的捐赠混进来需要过滤。所以建表时不能只存“金额”和“用途”必须把原始渠道、备注、支付流水号、抓取批次全部留下来否则后续对不上账就没法追溯。2.2 表结构设计金额一律以“分”为单位我设计的核心表有两张一张存流水一张存分账记录。流水表的关键字段如下字段名类型说明donation_idvarchar(64)主键由“渠道流水号日期”生成amount_fenint金额单位是分绝对不用浮点数channelvarchar(32)渠道公众号、小程序、扫码remarkvarchar(255)捐赠备注pay_timedatetime支付时间statustinyint0 待分账1 已分账2 异常fetch_batchvarchar(32)抓取批次号这里最核心的决策是金额用“分”存整数。很多人写金额计算直接浮点数相乘等遇到精度问题就晚了。这个项目里哪怕差一分钱管理员那边都会非常紧张所以从一开始就必须用整数或者 Decimal不接受任何浮点误差。分账记录表则在 donation_id 上建了唯一索引同时记录每一笔钱进了哪个基金、分了多少、用的是哪个比例版本。有了这个索引后续的幂等设计才有了抓手。2.3 比例不写死配置表替代硬编码分账比例不是一成不变的。比如那段时间佛堂大修修缮占比临时从 35% 调到了 50%过几个月可能又调整回来。如果比例写死在代码里改一次比例就要改代码、发一次版本既不灵活也容易出错。我的做法是把它做成了配置表带生效起始日生效起始日运营占比修缮占比慈善占比2024-01-0140%35%25%2024-06-0140%50%10%分账时系统根据流水的支付时间找到当前生效的比例配置。将来管理员调整比例只需要在后台配置页面上改一行程序本身不用动。这样设计还有一个好处历史账目可以复算因为每一期的分账都记录了当时用的比例版本。2.4 幂等设计防止“跑两次就翻倍”定时任务有个天然问题某天早上脚本跑到一半超时了手动重跑一次会不会把同一批流水又分了一次账如果不做幂等重复分账会直接导致账目翻倍这是最严重的线上事故之一。我的方案是三件套配合流水表给 donation_id 建唯一索引分账前先按 donation_id 查一遍已存在就跳过分账插入用事务提交并利用“受影响行数”判断是否重复插入。SQL 大概长这样INSERT INTO allocation (donation_id, target_fund, amount_fen, ratio_version, created_at) VALUES (?, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE id id;这个写法的妙处在于重复触发时不会报错也不会静默覆盖而是直接跳过保证了只要流水不重复账目就不会翻倍。2.5 抓取批次后来排查问题的救命稻草每次自动化运行系统会生成一个 fetch_batch比如20240607_0700。当天抓到的所有流水都带上这个批次号。设计阶段我觉得这只是个日志字段直到后来线上发现某天数据差异靠批次号一点一点定位到具体是哪次运行出了问题才意识到这个字段有多重要。如果你也在做类似系统建议从一开始就保留这个字段别等出事再补。3. Selenium抓取实现登录态、元素定位与翻页细节说到具体实现为什么选 Selenium 而不是 Playwright 或者 Puppeteer我的判断是这套功德系统是很老的管理端页面Selenium 对老控件、iframe、alert 这类东西的兼容处理更成熟网上能查到的踩坑案例也更多部署时 Chrome 的 headless 模式稳定而且我自己更熟悉这套 API。项目目标就一个字稳不是追新。3.1 登录态手动登录一次cookies 用起来很多后台系统登录时会带短信验证码或扫码二次验证纯自动登录极不稳定。我的方案是“半自动”管理员手动登录一次程序把登录后的 cookies 保存到本地文件后续运行直接注入。流程是这样的启动时检查本地 cookie 文件是否存在存在则注入然后访问后台首页判断是否跳回登录页如果没跳回说明 cookie 有效直接进入查询页面如果失效则启动“人工登录模式”用有头浏览器打开页面等管理员手动登录登录成功后自动保存新 cookies。代码骨架大致如下from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import json options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--no-sandbox) driver webdriver.Chrome(optionsoptions) driver.get(https://donate.example.com/admin/login) with open(cookies.json) as f: cookies json.load(f) for c in cookies: driver.add_cookie(c) driver.refresh() try: WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, flow-table)) ) print(登录态有效) except: print(cookie 失效需要人工登录)cookie 有明显弱点会过期。但账务系统的登录有效期通常按天计算每天跑任务时检查一次就算失效也就是一次人工扫码比每次自动登录撞验证码省事太多。3.2 元素定位优先稳定属性少用深层 XPath老系统有个特点class 命名乱、层级深、各种隐藏遮罩层一堆。定位策略上我的原则是优先用 id、name 这类业务属性其次用相对稳定的 CSS 选择器比如容器 class 子元素结构尽量别用深层 XPath页面改版最容易挂的就是它关键操作都封装一个显式等待函数避免用固定 sleep。def wait_for(driver, by, selector, timeout15): return WebDriverWait(driver, timeout).until( EC.presence_of_element_located((by, selector)) ) start_input wait_for(driver, By.ID, start_date) end_input wait_for(driver, By.ID, end_date) search_btn wait_for(driver, By.CLASS_NAME, btn-search) start_input.clear() start_input.send_keys(day_start) end_input.clear() end_input.send_keys(day_end) search_btn.click() wait_for(driver, By.CSS_SELECTOR, #flow-table tbody tr)有一点特别提醒查询按钮经常会被某个 loading 遮罩盖住元素存在不等于可以点击。如果直接 click可能会点在遮罩上导致查询没触发。所以我习惯在点击前显式判断遮罩是否消失或者判断按钮是否可点击否则很难排查。3.3 翻页抓取随机等待、边界判断、防重复翻页逻辑相对机械但有两个细节很关键。第一个是节奏控制。每次点击“下一页”之后程序随机等 0.8~1.5 秒再解析避免固定间隔看起来太像脚本。这倒不是为了对抗什么风控而是不给老系统增加压力账务类系统要是因为高频访问把 IP 限制了影响的是寺庙日常对账不能因小失大。第二个是翻页终止条件。不能只看“下一页”按钮是否存在因为到最后一页按钮还在只是变成了 disabled 状态。我一般按两个条件判断按钮的 disabled 属性以及当前页第一条流水号是否和上一页最后一条流水号重复。后者尤其重要——如果某一页因为渲染慢导致重复抓取靠这个条件可以立刻发现。还有一点抓取日期范围不要一次性拉一年。老系统后端扛不住大查询也容易因为超时丢数据。我改成按天循环查询每天的数据量通常几页就能抓完稳定很多。3.4 数据清洗金额、时间、备注一个都不能放过页面表格解析还算直接麻烦的是文本清洗。后台显示的金额是“1,280.00”必须去掉货币符号和千分位再转成整数分时间可能是“2024-06-07 12:33:22”也可能是“昨天”“06月07日”这种相对写法备注里还混着全角空格和换行。这些都要在入库前统一处理。from decimal import Decimal def parse_amount(text: str) - int: # 1,280.00 - 128000分 cleaned text.replace(, ).replace(,, ).strip() yuan Decimal(cleaned) return int(yuan * 100)另外退款状态有时候是单独的列不能只看备注。有一回测试阶段漏了退款行导致那个月统计口径偏了排查了好久。现在我的清洗逻辑会把整行所有字段都读出来再综合判断状态而不会只盯某一个字段。4. 定时调度与异常自愈从脚本到能长期跑的系统脚本能跑通是一回事能长期没人管地稳定跑是另一回事。这套系统上线后我把大量时间花在了“异常情况下该怎么办”上。4.1 抓取节奏为什么是早上7点抓取任务不需要实时也不该实时。实时意味着频繁登录、频繁刷新对老系统是不小的负担而且晚上 8 点到 10 点是香客捐赠高峰那个时段后台页面慢得出奇容易超时。我把定时任务设在两个时间点早上 7 点抓前一天的完整流水下午 2 点补抓一次处理上午系统延迟入账的情况。选 7 点是因为夜里流水少查询快也不干扰管理员白天用系统下午 2 点则是交易平稳期正好补漏。实测下来两轮就能覆盖 99% 的流水。4.2 定时调度与 headless 模式服务器上跑的是 Linux 环境我用 APScheduler 的 CronTrigger 做调度而不是直接写 crontab原因很简单APScheduler 可以在应用内统一处理日志、异常回调和主流程耦合得更紧密。from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger scheduler BlockingScheduler() scheduler.add_job(main_job, CronTrigger(hour7, minute0)) scheduler.add_job(main_job, CronTrigger(hour14, minute0)) scheduler.start()服务器没有图形界面浏览器必须用 headless 模式。这个我一开始就装了但后来遇到一个坑headless 下某些老系统报表控件不输出数据页面整块空白。排查发现是浏览器特性检测的问题。解决办法是用 Xvfb 跑一个虚拟显示有头模式跑任务。这个组件装上之后问题就消失了代价只是多占一点内存。4.3 三层异常处理不是“报个错”就行脚本最怕的不是报错而是坏了没人发现。我把异常处理分成三个级别单次运行失败保留当前抓取结果不清理浏览器直接把错误堆栈和截图发到通知群。连续失败连续 3 天失败会升级告警因为账务延迟时间越长月底对账越麻烦。数据异常比如某天流水总额比上周同一天低 50% 以上也会触发告警这种情况大概率是页面改版导致漏抓。告警渠道用的是通用 Webhook 机器人服务器上不用额外装客户端。通知内容一定包含运行批次号、失败原因摘要、截图链接。管理师兄虽然不懂代码但看到“批次号”和“截图”基本能判断问题严重程度。4.4 对账闭环自动分账之外保留人工确认钱的事情不适合“全自动”。我的设计是两层复核系统每天把抓取流水总额和分账后各账户合计做一次平衡校验不等就终止入账分账明细生成后先写入待复核状态管理员在后台点“确认入账”后才从待复核变为已生效。起初我觉得这一步是多余的但实践下来人工确认环节让管理员对系统的信任度提高了很多。自动化的意义不是取代人而是把人从复制粘贴里解放出来把精力放在核对和决策上。5. 实测中踩过的坑改版、验证码与重复分账上线两个多月踩过的坑挺多挑几个最典型的详细说说都是真实经历过的教训。5.1 页面改版一夜之间所有选择器失效功德系统做了一次升版表面上看变化不大但某些 class 从.btn-search改成了.btn-query表格 id 也从#flow-table变成了#list-table。升级当天的任务立刻抓不到数据。排查过程大概是这样先看日志浏览器正常打开、登录态有效、没有报错但返回行数是 0再看截图查询按钮点击没生效页面上多了一个 loading 遮罩遮罩的层级比按钮高click 点在了遮罩上用开发者工具手动确认后才定位到是 CSS 变化导致的问题。修复方式是把定位器统一改成新 class同时在点击前增加等待遮罩消失的逻辑。经历过这次之后我加了一条铁律每次抓取开始和结束各截一张图日常运行不用看但排查问题全靠它。截图体积也不大保留 30 天硬盘负担可以忽略。5.2 验证码决定不硬刚做人肉介入有一次系统临时加了图片验证码登录页和查询页都有。我认真评估过用识别模型绕过去这条路但最终决定不做。原因不复杂这是账务系统账号是寺庙的验证码本身就是一道安全机制。代码绕过验证码轻则账号进风控重则可能惹上系统安全方面的责任完全不划算。我把“检测到验证码”当成一种业务异常处理程序发现验证码元素存在就停止操作给管理师兄发通知让他手动处理一次。自动化的可靠边界不是“什么都能自动”而是“知道什么时候不该自动”。这个原则放到很多场景都适用。5.3 重复分账凌晨重跑引发的账目翻倍上线初期有天下半夜服务器自动重启定时任务被重复调度了两次。第一次跑完还没提交事务第二次又开始了结果同一批流水被插入了两遍。虽然我建了唯一索引但当时分账逻辑先读流水再逐条插入没有把“插入结果受影响行数”作为判断条件导致部分幂等逻辑失效。修复后在代码里加了判断cursor.execute(insert_sql, params) if cursor.rowcount 0: # 说明这条流水已存在跳过 continue同时把所有“读流水—分账”的过程放进一个数据库事务里配合唯一索引和受影响行数判断重复调度也只会产生一份分账记录。这个案例让我长了一个记性幂等不能只靠数据库约束代码层面的判断同样不能少。5.4 金额精度和“昨天”的边界两个小坑单独说一下。第一个是金额精度。一开始写分账比例直接用浮点数运行两周后发现某日几个账户合计差了几分钱排查下来就是浮点误差。后来把代码里所有金额字段全部改成整数“分”分账时用比例乘整数再四舍五入账目才完全对上。第二个是日期归属。夜里 11 点到凌晨 1 点之间的流水如果按“抓取时间”分类会把今天早上抓到的数据归到前一天导致日汇总漂移。我的处理方式是完全以功德系统页面显示的交易时间为准绝不用本机时间干预。谁定义了“交易日”谁就定义了账目这个边界在项目一开始就要说清楚。问题根因解决方案选择器失效页面升版改动 class/id截图留证、定位器集中管理、等待遮罩消失验证码系统临时加码检测到即停止转人工通知重复分账幂等逻辑漏洞唯一索引 受影响行数判断 事务金额误差浮点数计算全部改为整数“分”日期漂移用本机时间判断以业务系统时间为准6. 部署交付让管理员用最简单的界面闭环系统做出来不是给自己玩的是要交给完全不懂技术的人日常使用的。所以部署和交付阶段我把重点放在了“怎么让管理员放心用”上。6.1 部署形态容器 定时任务整个项目部署在一台 Linux 小服务器上用一个 Docker 容器同时跑 Chrome 和 Python容器外部挂载了数据、日志、截图目录重启容器数据不丢。docker-compose 简化版大致是这样services: donate-bot: build: . restart: unless-stopped volumes: - ./data:/app/data - ./logs:/app/logs environment: - TZAsia/Shanghai时区环境变量一定要设置否则定时任务的“早上 7 点”跟北京时间对不上这种低级错误会让你调很久的“生物钟”。6.2 管理员的日常操作只有两件事交付给管理师兄的时候他只需要学会两件事每天查看通知群里收到的日报汇总金额不对就在后台标记偶尔收到“cookie 失效”或“验证码出现”的告警时打开浏览器扫码登录一次。所有配置都放在后台页面上比如分账比例、流水查询、异常列表。他不需要碰任何脚本文件。为了让管理员放心后台只提供一个只读账号不开放任何修改功德系统的入口。6.3 权限、备份与安全最后的安全措施提几句。抓取用的浏览器账号是管理员分配的只读子账号只读取流水和金额看不到完整的个人用户隐私字段数据库每天自动备份到另一个目录保留 7 天告警内容里不包含敏感信息只有批次号和截图链接落库的明细字段做了掩码处理捐赠人姓名只显示首尾字符完整数据只有管理员手动导出时才显示。这个项目上线到现在稳定跑了三个多月。我最大的体会是自动化的难点从来不在自动化本身而在“业务规则是否清晰”和“异常发生时怎么处理”。代码好写规则难定账目上的信任更难建立。如果再往后扩展这套系统还可以加上定期向捐赠人发送回执的功能、年度统计报表甚至一个公开的收支公示页面。只要业务规则不变Selenium 配置表 定时任务 人工复核这套模式就能一直稳稳跑下去。