ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Selenium自动化实战:寺庙香火钱自动分账系统实现

Selenium自动化实战:寺庙香火钱自动分账系统实现 写在前面这个故事发生在某个香火还算旺的小寺庙住持找到我时最头疼的不是佛法而是一堆功德箱的流水对不上账。我给出的方案是用 Selenium 写了一个自动分账系统把每天几百笔零散的香火钱从支付后台拉到表格里再按固定比例拆成几份每月底一键生成报表。这篇文章就是那次实操的完整记录。你可能会觉得用 Selenium 操控寺庙是个段子但它背后是一个很现实的需求功德箱二维码收到的钱全躺在支付平台里既没有自动记账也没有自动分账。我做的其实就是三件事让 Selenium 替人工去网页后台抓流水用 pandas 做分账计算再用定时任务让这套流程每天自己跑一遍。整套东西跑通之后寺院财务从月底手工对账三天变成每天睁眼就看报表省下来的时间相当可观。这套代码不复杂难点在于登录态保持、页面元素定位、异常兜底这几个环节任何一个没处理好凌晨三点定时任务炸掉第二天住持就来找你了。下面按真实推进顺序拆开讲包括我踩过的坑和最后的稳定版方案。1. 项目概述与需求拆解1.1 寺庙香火钱的真实痛点先说说需求是怎么来的。那座寺庙规模不大但位置好节假日香客多。寺里在几个大殿门口、观音像前、素斋馆收银台放了十几个功德箱每个功德箱上贴一张收款码对应商户平台里不同的收款码编号。香客扫码付款后钱进入寺院的对公账户但支付后台只显示流水不会告诉你哪笔钱是哪个功德箱收的、应该用在什么地方。到了月底财务义工要把支付后台的流水全部导出来对着 Excel 一笔一笔核对这笔是香火钱那笔是素斋收入这笔要记入功德箱甲那笔要记入功德箱乙然后还要按照寺规把总收入拆成维修基金、慈善基金、日常运营等几个科目。这种手工模式有多痛流水一多Excel 里筛选、分类、求和全是手动操作动辄干一整天二维码是固定金额还是随意金额都有有些香客一次付几百有些扫 0.1 元数据又碎又杂支付后台只有最近 30 天的明细跨月对账就得月初赶紧导出一旦错过就要找客服开权限财务报表需要留痕手工整理容易漏记、重记对账时说不清。所以住持的需求一句话就能说清能不能让系统每天自动把支付平台的流水扒下来自动标注来源再按规则把钱分好账月底直接出报表1.2 为什么选择 Selenium 而不是接 API我的第一反应其实是查支付平台的开放接口。但现实很骨感个人开发者或者小商户要拿到商户平台的交易查询 API 权限通常需要企业资质、签约某些行业类目还要走一堆申请流程。对一座小寺庙来说这套流程太重而且财务义工也不一定懂技术。API 路线走不通那就退一步用 RPA 的思路人工怎么操作网页后台就让浏览器自动怎么操作。Selenium 就是干这个的它通过 WebDriver 协议驱动真实浏览器模拟用户点击、输入、滚动、翻页最后把页面上的数据读出来。对比一下几条路线的优劣方案优点缺点适用场景官方开放 API数据最规范不受页面改版影响申请门槛高签约流程繁琐有企业资质、批量交易量大的机构Selenium 模拟操作零门槛只要有账号密码就能跑页面改版会导致脚本失效有被风控识别的风险中小商户、个人开发者、内部工具人工导出再处理最省事不涉及代码耗时耗力无法自动化月流水几十笔的场景这个项目选 Selenium 的核心原因是它在能跑起来和不用走申请流程之间找到了平衡。后续如果哪天拿到了官方接口这套分账引擎完全可以复用只要把数据源从 Selenium 抓到的 DataFrame 换成 API 返回的 DataFrame 就行。1.3 分账规则怎么定分账规则不是拍脑袋定的而是寺院管理小组开会讨论出来的。常见做法是设置几个固定科目每个科目一个比例所有收入按比例拆殿堂维修基金占 40%用于大殿翻修、瓦片更换、佛像贴金慈善救济基金占 30%用于资助困难家庭、冬季施粥、助学寺院日常运营占 20%包括水电、素斋食材、义工伙食僧众生活补助占 10%用于常住僧人的基本生活开销。这个比例不是固定的有的寺庙可能还要提留弘法基金古籍保护基金等。重要的是分账逻辑要写成配置而不是写死在代码里这样调整比例时不用改代码只改一个配置文件。我设计的分账配置是一个简单的 Python 字典ALLOCATION_RULES { 殿堂维修基金: 0.40, 慈善救济基金: 0.30, 寺院日常运营: 0.20, 僧众生活补助: 0.10, }每一笔收入按这个比例拆账后还有一个尾差处理问题比如一笔 10 元的香火钱乘 40% 是 4 元乘 30% 是 3 元乘 20% 是 2 元乘 10% 是 1 元刚好分完但如果是一笔 0.1 元的扫码付款按比例拆就会出现小数点后多位。所以我在分账引擎里专门做了尾差处理保证所有子科目之和严格等于原始金额一分不差。这部分后面详细讲。2. 技术选型与前置准备2.1 技术栈Selenium 4 Python pandas技术栈选得越简单越好最终确认的组合是Python 3.10写起来快生态成熟财务数据处理有 pandas 兜底Selenium 4.x新版 Selenium 的 API 更简洁find_element系列方法统一了用法WebDriverWait也更稳定pandas openpyxl负责数据清洗、分账计算、生成 Excel 报表schedule 库做简单的定时任务每天凌晨自动拉取前一天的流水Chrome ChromeDriverSelenium 驱动真实浏览器直观且兼容性好。这套组合不需要什么高级架构一个 Python 文件就能跑。但实际写下来代码量大概在 600 行左右分成了流水抓取、数据清洗、分账引擎、报表生成、通知推送五个模块。2.2 环境配置与 WebDriver 管理Selenium 的环境配置不复杂但有几个细节一定要处理干净不然容易在莫名其妙的地方翻车。首先是安装依赖pip install selenium pandas openpyxl schedule然后安装 Chrome 浏览器并下载对应版本的 ChromeDriver。Selenium 4.6 以上版本其实内置了 Selenium Manager会自动下载匹配的 WebDriver但国内网络环境下经常会超时所以我更推荐手动下载放在项目目录里然后用Service类显式指定路径。from selenium import webdriver from selenium.webdriver.chrome.service import Service service Service(/usr/local/bin/chromedriver) options webdriver.ChromeOptions() options.add_argument(--headlessnew) # 无头模式跑定时任务不用弹窗 options.add_argument(--no-sandbox) # Linux 服务器上必需 options.add_argument(--disable-dev-shm-usage) # 容器/小内存机器上防崩溃 options.add_argument(--window-size1920,1080) # 保证页面按 PC 端渲染 driver webdriver.Chrome(serviceservice, optionsoptions)注意--headlessnew这个参数旧版的--headless在某些新版 Chrome 上会渲染异常导致元素定位失败。我最初就是只写了--headless结果本地有头跑得好好的部署到服务器上就定位不到元素排查了半天才发现是 headless 渲染模式的问题。另外千万不要用 root 用户直接跑 Chrome否则必须加--no-sandbox而加了之后安全性会下降。实际情况是很多服务器只能用 root那就只能接受这个选项。2.3 登录态保持Cookie 复用方案自动分账系统最核心的问题不是抓数据而是每次登录都可能触发短信验证码Selenium 对验证码没有很好的自动处理能力所以我把目标定为尽量少登录最好一个月才手工登录一次。方案是 Cookie 复用第一次手动打开浏览器登录支付平台后台登录成功后把浏览器里的 Cookie 序列化保存到本地文件之后每次跑自动化脚本先启动浏览器把 Cookie 写入再刷新页面就可以保持登录态Cookie 有效期内不会触发新的验证码一旦过期脚本会发送通知提醒人工重新登录。保存 Cookie 的代码很简单import json import time def save_cookies(driver, pathcookies.json): with open(path, w, encodingutf-8) as f: json.dump(driver.get_cookies(), f, ensure_asciiFalse) def load_cookies(driver, pathcookies.json): with open(path, r, encodingutf-8) as f: cookies json.load(f) for cookie in cookies: driver.add_cookie(cookie)关键是加载 Cookie 必须在访问目标域名之后、且在目标页面加载完成之前。标准顺序是driver.get(https://merchant.example.com) time.sleep(2) load_cookies(driver) driver.refresh()为什么先访问一次页面因为 Selenium 只允许在当前域名下添加对应域名的 Cookie你还没打开这个网站浏览器没有该域名的上下文add_cookie会直接报InvalidCookieDomainException。Cookie 也不是一劳永逸。有的平台设置 Cookie 有效期 3 天有的 30 天还有的会根据设备指纹风控。我的做法是每周一凌晨定时任务跑完之后主动检查登录态如果发现跳转到登录页就立即发一条微信通知给负责的义工让他手动登录一次。这样既避免了频繁验证码又保证了系统稳定运行。3. 核心代码实现3.1 抓取流水用 Selenium 操控寺庙的手动操作流水抓取是整个系统的数据入口。目标页面是支付平台的对账单查询页需要按以下步骤操作进入查询页设置起止日期通常是昨天 00:00:00 到 23:59:59点击查询按钮等待结果表格加载完成翻页读取所有合计记录点击导出下载原始明细文件。这里最忌讳的是用固定 sleep 等待页面加载。网络波动一次后面的步骤全部失败而且不好排查。正确做法是用WebDriverWait显式等待关键元素出现from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_for_element(driver, locator, timeout30): return WebDriverWait(driver, timeout).until( EC.presence_of_element_located(locator) )比如等待查询结果表格出现table_locator (By.XPATH, //table[classtransaction-list]) table wait_for_element(driver, table_locator)比起 CSS 选择器我更喜欢用XPath 定位表格中的行和列因为支付后台的表格结构相对稳定XPath 可以直接通过文本内容定位比如rows driver.find_elements(By.XPATH, //tbody/tr) for row in rows: cells row.find_elements(By.TAG_NAME, td) if len(cells) 6: record { 订单号: cells[0].text.strip(), 交易时间: cells[1].text.strip(), 付款方式: cells[2].text.strip(), 金额: cells[3].text.strip(), 订单备注: cells[4].text.strip(), 交易状态: cells[5].text.strip(), } records.append(record)这里有一个关键细节有些金额列的文本带货币符号或者千分位逗号比如1,234.50在转成 float 之前必须清洗。另外退款记录和支付记录的状态字段不一样必须过滤掉退款成功已关闭这些非实际收款记录。3.2 数据清洗与对账把网页抓到的流水变成干净的 DataFrameSelenium 抓到的是网页文本直接从td.text拿到的字符串并不适合计算。我从页面把所有行遍历进列表之后会用 pandas 做二次加工import pandas as pd df pd.DataFrame(raw_records) # 金额清洗去掉货币符号、逗号、空格 df[金额] ( df[金额] .str.replace(, , regexFalse) .str.replace(,, , regexFalse) .str.strip() .astype(float) ) # 只保留支付成功的记录 df df[df[交易状态].isin([支付成功, 交易成功, 已入账])].copy() # 去掉退款记录通常退款单的订单备注里会带退款字样或者交易状态列单独出现退款 df df[~df[订单备注].str.contains(退款, naFalse)].copy() # 把交易时间标准化成 datetime df[交易时间] pd.to_datetime(df[交易时间])清洗之后还要做一次对账完整性校验系统抓到的总金额必须等于支付平台首页展示的昨日收入汇总。这一步是防漏抓的兜底。我的做法是在抓取明细之前先到支付平台的今日汇总页用 Selenium 获取昨日总收入数字抓完流水后把明细表金额求和两者对比expected_amount get_daily_total_from_summary_page(driver) actual_amount df[金额].sum() if abs(expected_amount - actual_amount) 0.01: raise ValueError(对账不平: 汇总页金额与明细金额不一致)这个校验非常重要。因为页面加载不全、翻页漏掉、表格懒加载等原因Selenium 偶尔会漏抓几笔。如果没有校验这几天报表就是错的月底住持一对账就出事。哪怕用 Selenium也要有不信任页面的心态。3.3 分账引擎让每一分钱都有去处分账引擎是系统的核心。它的输入是一张干净的流水表输出是一张科目-金额的汇总表以及分为若干汇总行的分账明细账。原则是每一笔收款按比例拆分尾差归入金额最大的科目或单独记入分账尾差科目保证拆分后各科目之和恒等于原金额。以一笔 10 元的香火钱为例科目比例理论金额实配金额殿堂维修基金40%4.004.00慈善救济基金30%3.003.00寺院日常运营20%2.002.00僧众生活补助10%1.001.00合计100%10.0010.00再以一笔 0.1 元的香火钱为例科目比例理论金额实配金额殿堂维修基金40%0.0400.04慈善救济基金30%0.0300.03寺院日常运营20%0.0200.02僧众生活补助10%0.0100.01合计100%0.1000.10如果按金额从大到小分配理论金额经过四舍五入后再做尾差调整最后一笔用总额减已分配的方式补齐就能确保分毫不差。代码实现如下def allocate_transaction(amount, rules): amount 是单笔金额rules 是科目名-比例 的有序字典 返回 {科目: 分配金额}各项之和恒等于 amount allocated {} remain amount total_ratio sum(rules.values()) # 先按比例计算理论值并向下取整保留两位小数 for name, ratio in rules.items(): if name list(rules.keys())[-1]: # 最后一科直接用剩余金额避免尾差 allocated[name] round(remain, 2) else: part round(amount * ratio / total_ratio, 2) allocated[name] part remain - part # 保险校验 assert abs(sum(allocated.values()) - amount) 0.005 return allocated注意代码里的一个关键细节最后一科用剩余金额赋值而不是用比例计算再四舍五入。比如某笔 20.03 元的流水按比例拆完前 3 项分别四舍五入后是 8.01、6.01、4.01剩给第四项的就是 20.03 - 8.01 - 6.01 - 4.01 2.00。如果第四项也按比例算再四舍五入可能出现总数少一分钱或多一分钱的情况。从财务角度说这种尾差处理方式是流水线分账的标准做法。如果每个月积累下来尾差比较大我们还会在报表里增加一个分账尾差科目直接把每笔子的四舍五入差额累计进去月底这个科目的余额应当趋近于零。实际上因为这个系统每笔都保证平账尾差科目余额始终为 0。3.4 报表输出与通知让住持一眼看明白分账之后的结果需要让非技术人员看懂。我生成的报表分三层第一层总览 Sheet——月度总收入、各科目分账金额、上月对比、每日趋势第二层明细 Sheet——每一笔香火钱的交易时间、金额、来源功德箱、分账结果第三层科目汇总 Sheet——按科目汇总方便财务直接导入记账软件。报表生成用 pandas openpyxlwith pd.ExcelWriter(月度香火钱分账报表.xlsx, engineopenpyxl) as writer: summary_df.to_excel(writer, sheet_name总览, indexFalse) detail_df.to_excel(writer, sheet_name流水明细, indexFalse) allocation_df.to_excel(writer, sheet_name科目汇总, indexFalse)同时每天定时任务跑完后通过企业微信机器人或者钉钉机器人推送一条消息内容是昨日香火钱总额、分账完成状态、对账是否平账。如果脚本跑挂了也会推送一条脚本异常请人工查看日志。通知配置很简单企业微信机器人本质是一个 webhookimport requests def send_wechat_notice(title, content): webhook_url https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key这里填key payload { msgtype: markdown, markdown: { content: f### {title}\n{content} } } requests.post(webhook_url, jsonpayload, timeout10)消息内容就三行昨日香火钱总额1234.56 元 分账状态完成 对账校验通过4. 踩坑记录与排查实录4.1 扫码支付与订单号问题第一个大坑来自订单号的唯一性。支付平台的订单号并不等同于实际收款记录同一次扫码付款在商户后台会显示为一条支付成功的流水但如果香客中途取消又重扫会出现多条关联记录。我一开始直接用订单号去重结果把多次尝试付款最终成功一笔的订单误判为多笔收入。排查方式是看交易备注字段支付平台的备注字段通常会带上收款码编号比如TMP20250108123456A01这里的A01就是功德箱编号。后来清洗数据时我统一提取备注字段里的功德箱编号作为分组标识而不是依赖订单号。还有一个坑是部分渠道的退款记录在列表里不显示退款字样而是显示为负金额。所以清洗逻辑里不仅要筛状态词还要把金额小于等于 0 的记录直接剔除。4.2 页面元素定位的稳定性Selenium 脚本最怕的就是页面改版。有几次支付平台后台悄悄改了 CSS 类名我原来用的classtransaction-list一下子定位不到了脚本全部报NoSuchElementException。后来我总结出几条稳定定位原则优先用 XPath 文本定位而不是属性定位。比如定位查询按钮用//button[contains(text(), 查 询) or contains(text(), 查询)]比button.btn-query抗改版能力强。多用等待元素可见而不是等待元素存在。presence_of_element_located只检查 DOM 里有节点有时元素被遮挡时点击会报ElementClickInterceptedException。我后来统一改成element_to_be_clickable。给核心元素做异常降级一个选择器定位不到时尝试备用选择器try: date_input driver.find_element(By.ID, startDate) except NoSuchElementException: date_input driver.find_element(By.CSS_SELECTOR, input.placeholder开始日期)这种多级降级虽然啰嗦但在页面改版后可以给运维留出足够的缓冲时间去改脚本。4.3 验证码与风控验证码是自动化的大敌。在 Cookie 有效期内没问题但 Cookie 过期后登录页可能出现滑块验证码。对于这种验证码我强烈不建议尝试自动破解一方面违反平台条款另一方面滑块识别代码复杂、容易被更新后的风控打回。我的方案是检测到验证码出现时立即停止自动化发送通知让人工介入。为了让检测准确我写了一个简单的页面源码特征判断def is_login_page(driver): return 登录 in driver.title or login in driver.current_url.lower() def is_captcha_visible(driver): try: captcha driver.find_element(By.XPATH, //div[contains(class,captcha)]) return captcha.is_displayed() except NoSuchElementException: return False如果发现登录页脚本不会再继续执行而是保留一个待人工处理的标记。这样系统是安全的、可控的不会触碰平台的底线。4.4 定时任务与异常恢复定时任务跑起来之后真正的问题不再是功能而是无人值守时的稳定性。我踩过的坑有某天凌晨支付平台做维护页面加载超时脚本一直等元素一直重试最后卡死。解决所有WebDriverWait都设置 timeout并在外层套一个try...except TimeoutException一旦超时立即截图并保存现场。服务器网络断开ChromeDriver 崩了残留的 chrome 进程占满内存。解决脚本开头先清理残留进程pkill -f chromedriver 2/dev/null || true pkill -f chrome 2/dev/null || true然后在每天的定时任务里给整个流程加一个总超时保护比如 30 分钟跑不完就强制退出import signal def timeout_handler(signum, frame): raise TimeoutError(脚本总执行超时) signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(1800)月度跨账期的问题如果定时任务配置的是每天凌晨 1 点跑而支付平台的数据 T1 结算凌晨 1 点可能昨天的数据还没完全入账。我改成凌晨 4 点跑同时加了一个重试第 2 次的逻辑如果当天早上 8 点发现有对账不平自动重跑。重跑时会从上次记录的最后成功日期开始抓避免重复记账。5. 从技术到制度自动分账系统的可持续性光有代码还不够分账系统要长期跑得稳还需要在制度上做几件事。第一件记账与用钱分离。自动分账系统只是把收入按规则拆到不同科目并不代表钱真的被转到不同账户。实际操作中寺院在银行开一个主账户然后定期按分账报表把相应金额划转到专门的子账户或备付金账户这需要财务负责人和住持双人确认。自动化负责把账算清楚但钱怎么动必须有人审批。我在系统里加了一个已确认标记没有人工确认的报表不会作为划款依据。第二件每月对账会签。每月的第一天系统自动生成上月完整的分账报表由三名相关人员各自登录系统查看原始流水和分账结果全部确认后归档。这听起来像公司流程但对寺庙其实更重要因为香火钱的每一分都来自信众的善心账目透明是底线。第三件日志留痕。Selenium 每次运行时都会记录日志、截图、抓取到的原始记录数、对账结果所有日志保留至少 12 个月。这么做不是为了查谁的错而是万一香客对某笔款项有疑问我们可以立刻追溯。这套逻辑做下来技术上的复杂程度其实有限真正的复杂度全在流程设计上。Selenium 只是把人做重复劳动的部分替代了但该有的人工复核、该有的审批环节一点都不能少。6. 这套系统能不能复用到其他场景文章标题虽然写的是操控寺庙但分账系统的核心逻辑其实可以迁移到很多场景。多校区培训机构的学费分账同一个对公账户收款按校区比例分配连锁门店的营业额自动归集各分店收款码混在一个商户号下自动按门店拆分多人合伙项目的收入分成每笔收入按约定比例自动分给合伙人公益组织的捐款分类按项目归集自动生成统计报表。凡是多个来源、一个账户、按规则拆分的需求都可以用这套Selenium 抓流水 pandas 分账 定时调度 消息通知的架构快速实现。差异主要在于配置文件的规则不同以及各平台页面结构不同。如果你也想做一套类似的系统我的建议是先把分账规则和页面抓取完全解耦。这两个部分分开写规则调整不动代码页面改版不动规则。我在项目里把分账规则放在一个config.json里从源头上保证了灵活性。{ allocation_rules: { 殿堂维修基金: 0.40, 慈善救济基金: 0.30, 寺院日常运营: 0.20, 僧众生活补助: 0.10 }, source_platform: merchant_backend, report_path: ./reports }代码读取这个配置文件后动态构建分账引擎。以后想加一个古籍保护基金住持说一声改配置文件就好不用动任何 Python 代码。7. 常见问题速查表写这个项目时我把运维中常见的几个排查点整理成了一张速查表在这里分享给你希望对你有用症状可能原因排查步骤解决方法找不到元素NoSuchElementException页面改版 / 未等待加载完成看报错日志中的截图检查元素是否存在改用文本 XPath 定位或增加显式等待点击按钮没反应元素被遮挡 / 页面弹窗干扰截图看当前页面状态用element_to_be_clickable等待或用 JS 点击定时任务凌晨失败网站维护 / 网络波动看异常日志中的超时时间增加重试机制与总超时保护抓取金额与汇总不一致漏翻页 / 懒加载未触发比对汇总页总数先做对账校验不一致时自动重跑Cookie 失效平台登录态过期检查是否跳转登录页发送通知人工登录重新保存 CookieChromeDriver 崩溃版本不匹配 / 内存不足检查服务日志手动下载匹配版本清理残留进程加--disable-dev-shm-usage金额尾差几分钱四舍五入方式不对检查分账引擎尾差处理最后一科用剩余金额填充保证恒等重复记账页面重新点击查询导致重复读取查看订单号是否有重复按订单号加交易时间联合去重或维护最后抓取日期这个表我打印出来贴在服务器旁边每次出了问题先照表查能省去大半排查时间。收尾的几句大实话回想这整个项目我最想说的是自动化不是目的省心才是目的。用 Selenium 抓寺庙香火钱这件事技术含量确实不算高真正难的是把账要算得平数据要可追溯流程要有人复核这些原则落到了代码里。我也踩了几个让我印象深刻的坑比如 ChromeDriver 版本不匹配导致第二天一早住持问我报表为什么没生成比如 Cookie 过期后脚本在凌晨三点卡在登录页整个任务队列全部停滞。这些坑都成了我后来做自动化项目时的肌肉记忆——永远不要假设外部系统稳定永远要把异常当作正常流程的一部分来设计。如果你也打算给自己的小项目做一套类似的自动分账工具我的建议是从最小的闭环开始先手工登录保存 Cookie再写一个只抓昨天流水的脚本跑通然后加上分账逻辑最后才上定时任务。每一步都确认无误后再走下一步比一次性写一大坨代码靠谱得多。这套系统上线至今已经跑了半年多每个月的报表都是平的住持很满意。当然我也知道Selenium 永远是个临时的、脆弱的方案如果哪天支付平台放出更开放的接口或者改用更严的风控这套脚本就得跟着调整。但至少现在它让一座小寺庙的财务从手工 Excel 里解放了出来——这本身就是一件挺酷的事。
返回列表