项目概要
~85%
操作时间节省
10min
优化后每日耗时
1~2h
优化前每日耗时
项目背景
公司每日需从企业银行网银下载前一日收款流水,人工清洗修改数据,手动录入 SAP 系统生成收款凭证,全程需 1~2 小时,且极易因手工操作导致录入错误,影响月末银行流水核对进度。
解决方案
通过 Python 自动化脚本实现端到端全流程自动化:自动登录网银下载流水 → 数据清洗与客户匹配 → 生成标准凭证文件 → 自动导入 SAP 批量过账 → 凭证号回填汇总表。
网银自动登录
→
下载流水 Excel
→
数据清洗匹配
→
生成凭证文件
→
SAP 批量导入
→
凭证号回填Excel汇总表
核心难点
网银验证码 OCR 自动识别(ddddocr,失败自动重试);SAP GUI Scripting COM 接口调用实现无人值守操作;端到端凭证号闭环回填,数据可追溯,方便查阅。
技术栈
项目文档
一、背景与目标
财务部门每日收到多笔客户汇款,财务需人工登录网银、下载流水、核对客户信息、逐条录入 SAP 凭证,全程约 1~2 小时,月均操作约 20 天,合计约 20~40 小时/月。
项目目标:将上述全流程自动化,将每日处理时间压缩至 10 分钟以内,节省操作时间约 85%,同时消除人工录入错误风险。
二、自动化流程说明
| 步骤 | 脚本 | 操作内容 | 输出 |
|---|---|---|---|
| 01 | bank.py | 自动登录中信网银,OCR 识别验证码,下载昨日收款明细 | 银行流水.xlsx |
| 02 | vouchers.py | 清洗数据,匹配客户代码,生成标准凭证导入文件 | 凭证导入/*.xlsx |
| 03 | zfi024.py | 调用 SAP GUI Scripting,批量导入凭证并回填凭证号 | 汇总表.xlsx(含凭证号) |
三、异常处理机制
验证码识别失败自动重试,最多 15 次;SAP 导入失败的凭证行标记"凭证错误",不中断整体流程;所有执行结果写入汇总表,方便事后人工核查。
四、使用说明
每日运行 python main.py,程序自动完成全流程,完成后在汇总表查看结果及异常项,对异常项人工补录即可。
源码
main.py — 主入口,按序调用三个脚本
import subprocess
import sys
# 按顺序执行三个脚本
# 任意一个失败(returncode != 0)立即停止,不继续执行后续脚本
scripts = [
"bank.py", # 第一步:登录网银,下载昨日流水,移动到目标文件夹
"vouchers.py", # 第二步:清洗流水数据,按模板批量生成凭证文件,追加汇总表
"zfi024.py", # 第三步:SAP批量导入凭证,回填凭证号到汇总表J列
]
for script in scripts:
print(f"\n===== 开始运行:{script} =====")
result = subprocess.run([sys.executable, script], check=False)
if result.returncode != 0:
print(f" {script} 运行失败,中止后续流程")
break
print(f" {script} 完成")
print("\n全部完成")
input("按回车键关闭...") # 防止窗口一闪而过bank.py — 网银自动登录 + 流水下载
import subprocess
import time
import requests
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
import ddddocr
from datetime import datetime, timedelta
import os
import glob
import shutil
from selenium.webdriver.support.ui import WebDriverWait
from selenium import webdriver
#========================基础配置==============================
URL = "https://corp.bank.ecitic.com/cotb/electronic/login.html"#网址
DOWNLOAD_folder = r"C:\Users\DELL\Desktop" #下载目录
TARGET_folder = r"D:\银行流水\2026年*月" #目标目录
# ========================结束==================================
def get_captcha_text(driver):
"""
从验证码元素获取图片字节流并识别
"""
# 等待验证码图片元素可见且 src 属性不为空
captcha_img = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "verifyCode"))
)
# 确保图片加载完成(Selenium 无法直接判断,建议短暂 sleep 或等待属性)
time.sleep(0.5) # 给图片一点加载时间
img_url = captcha_img.get_attribute("src")
print(f"验证码图片URL: {img_url}")
# 获取当前浏览器的 cookies 用于下载
cookies = {c["name"]: c["value"] for c in driver.get_cookies()}
# 下载图片
response = requests.get(img_url, cookies=cookies, timeout=10)
if response.status_code != 200:
raise Exception(f"下载验证码图片失败,状态码:{response.status_code}")
# 使用 ddddocr 识别
ocr = ddddocr.DdddOcr(show_ad=False)
result = ocr.classification(response.content)
print(f"识别结果:{result}")
return result
# ===========================网页操作=================================
# 关闭所有 Chrome 进程 防止其他进程干扰 (可选,会丢失已有会话)
subprocess.run('taskkill /f /im chrome.exe', shell=True, capture_output=True)
time.sleep(2)
# 等待浏览器完全打开
chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
user_data_dir = r"D:\selenium_profile"
cmd = f'"{chrome_path}" --remote-debugging-port=9222 --user-data-dir="{user_data_dir}"'
subprocess.Popen(cmd, shell=True)
# time.sleep(3)
# 连接到调试浏览器
chrome_options = Options()
chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver = webdriver.Chrome()
# 打开登录页面
driver.get(URL)
print("当前页面标题:", driver.title)
time.sleep(3)
# 自动填写用户名
Username = driver.find_element(By.NAME, "userCode")
Username.send_keys("账号")
# 等待用户输入密码
time.sleep(15)
# ==============================验证码处理==========================================
MAX_TRIES = 15
for attempt in range(1, MAX_TRIES + 1):
print(f"第 {attempt} 次尝试登录...")
# 1. 获取并识别验证码
captcha_text = get_captcha_text(driver)
# 2. 输入验证码
captcha_input = driver.find_element(By.ID, "verifyCodeInput")
captcha_input.clear()
captcha_input.send_keys(captcha_text)
# 3. 点击登录按钮
login_btn = driver.find_element(By.XPATH, "//button[@data-i18n-key='i0008']")
login_btn.click()
# 4. 判断登录结果
try:
WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.XPATH, "//a[@class='first-menu']"))
)
print(" 登录成功!")
break
except:
# 登录失败,检查是否有“关闭”按钮(验证码错误弹窗)
try:
close_btn = WebDriverWait(driver, 3).until(
EC.element_to_be_clickable((By.XPATH, "//button[text()='关闭']"))
)
close_btn.click()
print("验证码错误,弹窗已关闭,准备重试...")
# 关键:刷新验证码图片(可点击图片触发刷新)
captcha_img = driver.find_element(By.ID, "verifyCode")
captcha_img.click()
time.sleep(1)
except:
print("未找到关闭按钮,可能账号密码错误或其他原因,停止重试")
break
else:
print(f"达到最大尝试次数 {MAX_TRIES},登录失败。")
driver.quit()
exit()
# ========================账户余额及交易明细查询=============
# 点击账户明细
menu = driver.find_element(By.XPATH, "(//a[@class='first-menu'])[2]")
menu.click()
# 选择账号
# 1. 点击只读输入框,触发下拉面板
input_box = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "bsnAcc"))
)
input_box.click()
# 2. 等待下拉选项出现 选择网银账户 option = driver.find_element(By.XPATH, "//li[@data-value='123456789']")
option = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//li[contains(text(), '请输入网银账户')]"))
)
option.click()
# 处理日期选择器 选择昨日银行流水
# 第一步:算出目标日期
today = datetime.now()
yesterday = today - timedelta(days=1)
# 日历的 data-ymd 格式是 2026-4-13(月和日没有补零)
# 所以格式化时用 %-m 和 %-d(Linux),Windows 用 %#m 和 %#d
date_str = f"{yesterday.year}-{yesterday.month}-{yesterday.day}"
print(date_str) # 2026-4-13
# 第二步:点击输入框弹出日历
start_input = driver.find_element(By.NAME, "startDate")
start_input.click()
# 第三步:处理开始日期
wait = WebDriverWait(driver, 10)
wait.until(EC.visibility_of_element_located((By.ID, "jedatebox")))
# 第四步:直接找到目标日期的 li 并点击
target = driver.find_element(By.XPATH, f"//li[@data-ymd='{date_str}']")
target.click()
# 第五步:点确定
start_input = driver.find_element(By.NAME, "endDate")
start_input.click()
# 处理截止日期
wait = WebDriverWait(driver, 10)
wait.until(EC.visibility_of_element_located((By.ID, "jedatebox")))
# 直接找到目标日期的 li 并点击
target = driver.find_element(By.XPATH, f"//li[@data-ymd='{date_str}']")
target.click()
# 点击查询按钮
qry = driver.find_element(By.XPATH, "//button[@ng-click='qry()']")
qry.click()
time.sleep(3)
# 点击下载
download = driver.find_element(By.XPATH, "//a[text()='全部下载']")
download.click()
time.sleep(3)
# 选择Excel下载
download_select = driver.find_element(By.XPATH, "//a[text()='Excel下载']")
download_select.click()
time.sleep(3)
# 确保目标文件夹存在
os.makedirs(TARGET_folder, exist_ok=True)
# 获取原文件路径
old_path = glob.glob(os.path.join(DOWNLOAD_folder, "COBP*.xlsx"))[0]
# 复制到目标文件夹并重命名为日期.xlsx
new_path = os.path.join(TARGET_folder, f"{date_str}.xlsx")
shutil.move(old_path, new_path)
# =================================================================================================vouchers.py — 数据清洗 + 凭证批量生成
import os
import pandas as pd
from openpyxl import load_workbook
import shutil
# =====================================基础配置===============================
# 数据导入
total_path = r'C:\Users\Yuyu\Desktop\收款\汇总表.xlsx'
bank_path = r'C:\Users\Yuyu\Desktop\收款\银行流水.xlsx'
customer = pd.read_excel(total_path,sheet_name='客户代码')
total = pd.read_excel(total_path,sheet_name='汇总数据')
bank = pd.read_excel(bank_path,sheet_name='SHEET1')
output_dir = r'C:\Users\Yuyu\Desktop\收款\凭证导入'
os.makedirs(output_dir, exist_ok=True)
# =======================================结束==================================
# =============================银行数据清洗生成凭证明细表==========================
# 正文修正 筛选去除表头银行数据汇总等不必要行
bank = bank.iloc[12:]
# 筛选收款发生额不为0的数据(排除付款的流水项目)
bank = bank[bank.iloc[:, 10].notna()]
# 筛选付款账户名称为公司的项目(排除个人收款)
bank = bank[bank.iloc[:, 5].astype(str).str.contains('公司', na=False)]
# 银行流水表里只提取需要数据 按位置提取 A (0), F (5), K (10) 三列数据
bank = bank.iloc[:, [0, 5, 10]]
# 重新构建标题行 方便匹配客户代码
bank.columns = ['交易日期', '客户名称', '收款发生额']
# 将客户名称匹配上对应SAP系统里的客户代码(类似xlookup)
bank_selected = pd.merge(
bank,
customer,
on='客户名称', # 按按户名称匹配
how='left' # 左连接,保留所有记录
)
# 转换格式 修正客户代码浮点转字符串 收款转换为浮点数格式
bank_selected['客户代码']=bank_selected['客户代码'].astype(int).astype(str)
bank_selected['收款发生额']=bank_selected['收款发生额'].astype(float)
# 增加列项 设置需要列便于后续生成凭证
bank_selected['回款性质']="试剂款"
bank_selected['现金流指定']="A01"
bank_selected['银行代码']="10021031"
bank_selected['特别总账标志']="A"
bank_selected['凭证号']="A"
bank_selected['凭证文本']=bank_selected['客户代码'] + "/"+bank_selected['客户名称'] + "/"+bank_selected['回款性质']
# 构建行标题
column_order = [
'交易日期', '客户代码', '客户名称', '收款发生额',
'回款性质', '银行代码', '凭证文本',
'特别总账标志', '现金流指定'
]
# 标题修正
bank_selected = bank_selected[column_order]
print(f"凭证明细表 {bank_selected}")
# =================================结束===================================
# ==================================按模板批量生成凭证========================
# 删除凭证导入文件夹里的文件(避免后续重复导入)
shutil.rmtree(output_dir, ignore_errors=True)
os.makedirs(output_dir, exist_ok=True)
# 读取模板文件数据
template_path = r'C:\Users\Yuyu\Desktop\收款\汇总表.xlsx'
# 读取凭证明细表数据
Batch_voucher = bank_selected
# 遍历每一行数据 按照模板批量生成凭证
for index, row in Batch_voucher.iterrows():
# 读取模版工作表
wb = load_workbook(template_path)
ws = wb['模板']
ws.title = "Sheet1"
# 指定要保留的工作表名称
target_sheet_name = 'Sheet1'
# 获取当前所有工作表名称(复制一份列表,避免遍历时修改)
all_sheets = wb.sheetnames[:]
# 删除除目标工作表以外的所有工作表(因为汇总表这个表里还有别的sheet表 不需要复制过去)
for sheet_name in all_sheets:
if sheet_name != target_sheet_name:
wb.remove(wb[sheet_name])
# wb 中只剩 '模板' 这一个工作表
ws = wb[target_sheet_name]
# 处理凭证日期过账日期 将格式改成为8位字符串
voucher_date = str(row['交易日期'])
posting_date = voucher_date
# ------------------------数据填充部分---------------------------------
# 填充抬头部分(第三行)
ws['B3'] = voucher_date # 凭证日期
ws['C3'] = posting_date # 过账日期
ws['E3'] = '收款' # 参照
ws['F3'] = row['客户代码'] # 公司名称
ws['G3'] = 'DA' # 凭证类型
ws['H3'] = 1019 # 公司代码
ws['I3'] = 'CNY' # 货币
# 第一个行项目(第四行)
ws['B4'] = 40
ws['C4'] = row['银行代码']
ws['E4'] = row['收款发生额']
ws['S4'] = row['凭证文本']
ws['T4'] = row['现金流指定']
# 第二个行项目(第五行)
ws['B5'] = 19
ws['C5'] = row['客户代码']
ws['D5'] = row['特别总账标志']
ws['E5'] = row['收款发生额']
ws['S5'] = row['凭证文本']
# 生成文件名(替换特殊字符)
client_code = str(row['客户代码'])
client_name = row['客户名称'].replace('/', '_').replace('\\', '_')
filename = f"{client_code}_{client_name}.xlsx"
filepath = os.path.join(output_dir, filename)
# 保存文件
wb.save(filepath)
# -------------------------------------------------结束-----------------------------------
# 读取原有的汇总数据 将提取的数据追加到汇总表
original_total = pd.read_excel(total_path, sheet_name='汇总数据')
# 拼接新旧数据
new_total = pd.concat([original_total, bank_selected], ignore_index=True)
# 写回 Excel(覆盖 sheet)
with pd.ExcelWriter(total_path, engine='openpyxl', mode='a', if_sheet_exists='replace') as writer:
new_total.to_excel(writer, sheet_name='汇总数据', index=False)
print(bank_selected)
# ==========================================结束===========================================
# ============================SAP操作项目===================================zfi024.py — SAP GUI 批量导入凭证
import win32com.client, time
from pathlib import Path
import pandas as pd
# ===================================基础配置========================================
# region 基础配置
path = Path(r"C:\Users\DELL\Desktop\销售会计1")
total_path = r'C:\Users\Yuyu\Desktop\收款\汇总表.xlsx'
total = pd.read_excel(total_path, sheet_name='汇总数据')
# endregion
#===================================结束=========================================
# ==================================连接SAP=============================================
# region 连接SAP
# 连接SAP,拿到session 选择第一个账号第二个页面
session = win32com.client.GetObject("SAPGUI").GetScriptingEngine.Children(0).Children(1)
# 最大化窗口
session.findById("wnd[0]").maximize()
# endregion
# ===================================结束===============================================
#自定义导航回ZFI024初始页面(开始前和每次处理完都要调一次,防止页面状态残留) 多次使用定义函数方便调用
def zfi024(session):
# 输入事务码/nzfi024
session.findById("wnd[0]/tbar[0]/okcd").text = "/nzfi024"
session.findById("wnd[0]/tbar[0]/btn[0]").press()
time.sleep(1)
# 取消勾选测试运行复选框
session.findById("wnd[0]/usr/chkP_CB1").selected = False
# ==================================进入zfi024页面导入凭证=================================
# region 进入zfi024页面导入凭证
#调用自定义函数先进一次初始页面
zfi024(session)
#创建一个列表存储数据 凭证结果
results = []
# endregion
# -----------------------------凭证提交操作-------------------------------------------------
#排序遍历文件夹里每个xls文件
for file in sorted(path.glob("*.xls")):
# 打开文件选择弹窗
session.findById("wnd[0]").sendVKey(4)
time.sleep(0.3)
# 弹窗里点确认
session.findById("wnd[1]/tbar[0]/btn[0]").press()
time.sleep(0.3)
# 点进路径输入框 再按F4进入文件浏览
session.findById("wnd[1]/usr/ctxtDY_PATH").setFocus()
session.findById("wnd[1]").sendVKey(4)
time.sleep(0.3)
# 填入文件夹路径 填入文件名
session.findById("wnd[2]/usr/ctxtDY_PATH").text = str(file.parent)
session.findById("wnd[2]/usr/ctxtDY_FILENAME").text = file.name
time.sleep(0.5)
# 确认路径和文件名
session.findById("wnd[2]/tbar[0]/btn[0]").press()
time.sleep(0.3)
# 确认选择对号
session.findById("wnd[1]/tbar[0]/btn[0]").press()
time.sleep(0.3)
# 点执行按钮提交按钮
session.findById("wnd[0]/tbar[1]/btn[8]").press()
time.sleep(3)
# ------------------------------------凭证返回信息提取-----------------------------------------
# 1. 首先,定位到目标网格对象
grid = session.findById("wnd[0]/usr/cntlGRID1/shellcont/shell")
# 2. 获取第一条消息(消息在 "MESSAGE" 列,行索引为 0)
row_index = 0
column_name = "MESSAGE"
# 3.凭证提交异常处理 zfi024凭证提交后会出现返回空值即提交失败
try:
#获取SAP返回对应单元格内容
message_content = grid.GetCellValue(row_index, column_name)
print(f"获取到的消息内容: {message_content}")
except Exception as e:
message_content = "凭证未提交成功"
print(f"获取单元格值时出错: {e}")
# ----------------------------------------返回信息判断回传-----------------------------------------
# message_content 情况1: 凭证过帐成功: BKPFF 180000139410192026 PRDCLNT800
if "凭证过帐成功" in message_content:
#提取9位凭证号
result_text = message_content.split("BKPFF ")[1].split()[0][:9]
# 数据追加到列表
results.append(result_text)
# message_content 情况2: 凭证错误: BKPFF $ PRDCLNT800
elif "凭证错误" in message_content:
# 直接返回凭证错误 客户冻结情况
result_text = "凭证错误 客户冻结"
results.append(result_text)
else:
# 没有获取到massage 返回为空 即凭证提交失败
result_text = "凭证错误 未提交成功"
results.append(result_text)
print(f"完成:{file.name}")
# 处理完回到初始页面,准备下一个文件
zfi024(session)
# ===================================结束===============================================
# =================================凭证状态回填到凭证汇总表格==========================================
# region 凭证状态回填到凭证汇总表格
# 筛选出汇总表凭证号为空的列内容
empty_indices = total.index[total["凭证号"].isna() | (total["凭证号"] == "")]
# 将获取到的凭证返回信息传回数据表
for i, cert in zip(empty_indices, results):
if cert:
total.at[i,"凭证号"] = cert
with pd.ExcelWriter(total_path, engine='openpyxl', mode='a', if_sheet_exists='replace') as writer:
total.to_excel(writer, sheet_name='汇总数据', index=False)
print("全部完成")
# endregion
# ===================================结束======================================================
示例文件
以下为项目运行过程中涉及的关键文件截图及示例。(注:此文件中的数据均已进行脱敏及适应性修改,与公司业务无直接关联,仅用于演示或测试用途)
原始银行流水 Excel 格式
生成的凭证导入文件格式
SAP ZFI024 批量导入界面
回填凭证号后的汇总表
效果展示
以下为收款自动化全流程交互演示,点击「播放」查看完整执行过程。
录屏演示
网银登录 + 流水下载演示
SAP 批量导入演示