项目概要

~85%
操作时间节省
10min
优化后每日耗时
1~2h
优化前每日耗时

项目背景

公司每日需从企业银行网银下载前一日收款流水,人工清洗修改数据,手动录入 SAP 系统生成收款凭证,全程需 1~2 小时,且极易因手工操作导致录入错误,影响月末银行流水核对进度。

解决方案

通过 Python 自动化脚本实现端到端全流程自动化:自动登录网银下载流水 → 数据清洗与客户匹配 → 生成标准凭证文件 → 自动导入 SAP 批量过账 → 凭证号回填汇总表。

网银自动登录
下载流水 Excel
数据清洗匹配
生成凭证文件
SAP 批量导入
凭证号回填Excel汇总表

核心难点

网银验证码 OCR 自动识别(ddddocr,失败自动重试);SAP GUI Scripting COM 接口调用实现无人值守操作;端到端凭证号闭环回填,数据可追溯,方便查阅。

技术栈

Python Selenium SAP GUI Scripting pandas ddddocr openpyxl

项目文档

版本:v1.0 更新:2026-05 类型:财务自动化 / RPA

一、背景与目标

财务部门每日收到多笔客户汇款,财务需人工登录网银、下载流水、核对客户信息、逐条录入 SAP 凭证,全程约 1~2 小时,月均操作约 20 天,合计约 20~40 小时/月。

项目目标:将上述全流程自动化,将每日处理时间压缩至 10 分钟以内,节省操作时间约 85%,同时消除人工录入错误风险。

二、自动化流程说明

步骤脚本操作内容输出
01bank.py自动登录中信网银,OCR 识别验证码,下载昨日收款明细银行流水.xlsx
02vouchers.py清洗数据,匹配客户代码,生成标准凭证导入文件凭证导入/*.xlsx
03zfi024.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界面
SAP ZFI024 批量导入界面
汇总表
回填凭证号后的汇总表

效果展示

以下为收款自动化全流程交互演示,点击「播放」查看完整执行过程。

录屏演示

演示1
网银登录 + 流水下载演示
演示2
SAP 批量导入演示