版本:v2.1.0
这份 README 同时面向 终端用户(直接用 exe 的人)和 开发者(看代码、改打包的人)。
- 如果你只想用 exe 看前 7 章就够了
- 第 8 章开始是源码、算法、Excel 格式等开发者内容
- 业务内容与桌面 README 保持一致;桌面那份是 exe 旁说明,本文件是项目仓库说明
两个筛选都默认关闭。正常使用仍然选择Excel并生成对方科目。需要筛选时,先勾选对应选项,再选择文件;程序会用这份文件的列名和科目显示设置窗口,每个批次设置一次。
选择账里实际记录日期或时间的那一列,然后勾选要查的条件。夜间按你填写的正常工作起止时间判断,开始时刻包含在工作时段内,结束时刻不包含;也支持跨午夜的工作时段。
程序按每条记录的年份读取中国大陆放假调休日历。随程序提供2024、2025、2026年数据,补班的周六、周日按工作日处理。法定节假日与假期连休或调休分别标明。普通筛选不用联网,设置窗口的“更新放假调休日历”可获取本项目维护的日历文件。更新失败保留原文件,不上传序时账。
缺少某年的日历时,该年的周末调休和节假日检查会注明未做,不猜测未来安排。只有日期、没有真实时分秒时,跳过夜间检查。日期型Excel单元格不会因软件补了午夜时间而被误选。所选字段的含义仍以原表为准,记账日期不能证明实际操作时刻。
输出“入账时间筛选”和“筛选说明”,保留原始记录、来源文件、工作表、行号及选中原因。同一行命中多个条件只列一次。
日历依据为国务院办公厅的2024年通知、2025年通知、2026年通知,法定日期结合全国年节及纪念日放假办法区分。后续年度由项目维护者核对官方通知后更新 src/中国节假日.json,使用者不用逐年登记日期。
例如要查银行付款是否被拆成几笔以绕过10万元审批,设置时选择银行存款科目和贷方,填入审批限额100000、候选最低金额90000。程序读取原始贷方金额,选出大于等于90000、严格小于100000的记录。9.8万元借贷各记一行时,只取所选方向的9.8万元,不相加为19.6万元。
可以多选科目。所选科目按该列原值匹配,先选择科目列,再从列表选择,不要求手填科目编码。审批限额与原有“异常金额阈值”分别设置。
需要把记录放在一起看时,再选择日期列和交易对方列。程序按同账套、同日、同对方归集候选,显示各笔及同组金额;没有这些资料就只列金额候选。金额来自原始业务行,不把生成对方科目时拆出的行再加一遍。红字保留在原始数据中并单独说明,核查时应结合冲销记录,不取绝对值凑限额。
输出“拆分审批筛选”和“筛选说明”。“同组金额”会在组内各行重复展示,不能再次相加。这里的多笔是原始记录,不自动证明存在多张付款单或同一项业务,更不直接认定违规。
主程序和独立CLI使用相同参数。下面以独立CLI为例,源码和随附 ledger-cli.exe 均支持。精简模式在明确启用筛选时,也会追加对应筛选表。
ledger-cli.exe 输入.xlsx 结果.xlsx --screen-date 记账时间 --screen-weekend --screen-holiday --screen-night --work-start 08:00 --work-end 18:00
ledger-cli.exe 输入.xlsx 结果.xlsx --split-subject-column 一级科目 --split-subject 银行存款 --split-direction 贷方 --approval-limit 100000 --split-minimum 90000 --split-date 记账时间 --split-party 对方单位
多选科目可重复指定 --split-subject。启用后参数不全时,原有分析继续,筛选表和说明明确记为未检查。桌面设置窗口也可以选择“本次只做原有分析”。
你给它一张序时账表格,它帮你自动填上每一行分录的"对方科目",并告诉你哪笔分录有问题。
举个例子:
原始序时账(只有借贷两行):
| 会计月 | 凭证编号 | 一级科目 | 借方发生额 | 贷方发生额 |
|---|---|---|---|---|
| 1月 | 记-001 | 银行存款 | 10000 | 0 |
| 1月 | 记-001 | 应收账款 | 0 | 10000 |
处理后:
| 会计月 | 凭证编号 | 一级科目 | 借方发生额 | 贷方发生额 | 对方科目 | 匹配类型 |
|---|---|---|---|---|---|---|
| 1月 | 记-001 | 银行存款 | 10000 | 0 | 应收账款 | 标准模板 |
| 1月 | 记-001 | 应收账款 | 0 | 10000 | 银行存款 | 标准模板 |
工具会自动补出两个新列:
- 对方科目:这一行的钱是和哪个科目来往的。
- 匹配类型:这笔对方科目是怎么匹配上的(可信度高不高)。
除了补对方科目外,工具还会额外生成几张分析表,帮你找出可疑分录、判断数据是否被人为修改过。
| 人群 | 用途 |
|---|---|
| 审计人员 | 快速分析被审计单位的分录合理性 |
| 会计人员 | 检查自己做的账有没有问题 |
| 财务分析人员 | 分析资金流向和业务模式 |
| 税务稽查人员 | 发现可疑的异常分录 |
直接双击桌面上的 序时账分析器.exe,弹出主窗口后:
- 点 "选择文件",选你的序时账 Excel
- (可选)调整"异常金额阈值",默认 10000 元
- 点 "开始处理"
- 等进度条走完,结果文件会自动生成
如果你想批量跑或者不弹窗口,按住 Shift 右键桌面 → "在此处打开 PowerShell 窗口",输入:
序时账分析器.exe 输入文件.xlsx 输出文件.xlsx --no-gui
常用参数:
| 参数 | 作用 | 默认 |
|---|---|---|
--threshold 50000 |
异常分录筛选阈值(元) | 10000 |
--no-gui |
不弹窗口,纯命令行模式 | — |
--log-level DEBUG |
日志详细程度 | INFO |
除了上面带界面的版本,项目 cli 文件夹里还有一个纯命令行版(序时账分析器命令行版.py),专门给 AI agent、自动化脚本调用,不依赖图形界面库。
打包好的 exe 在 cli\ledger-cli.exe,复制到任何 Windows 电脑都能直接跑,不用装 Python。
基本用法:
cli\ledger-cli.exe 输入文件.xlsx 输出文件.xlsx
cli\ledger-cli.exe 输入文件.xlsx 输出文件.xlsx --threshold 50000 --mode full
参数说明:
| 参数 | 作用 | 默认 |
|---|---|---|
输入文件 |
输入 Excel 路径(必填) | - |
输出文件 |
输出 Excel 路径(必填) | - |
--threshold 50000 |
异常分录筛选阈值(元) | 10000 |
--mode summary |
只输出一张"生成结果"表(精简,适合程序读取) | summary |
--mode full |
输出全部 sheet(和原版一样,但无美化无图表) | - |
-i / --interactive |
列识别不出时弹命令行选单逐列选 | 不加则缺列直接报错 |
--log-level DEBUG |
日志详细程度 | INFO |
两种输出模式:
| 模式 | 输出内容 | 适合谁 |
|---|---|---|
summary(默认) |
只一张"生成结果"表(含对方科目、匹配类型列),普通 Excel 格式 | agent、脚本批量处理 |
full |
全部 sheet(生成结果/原始数据/异常分录明细/异常分录/班福分析),普通格式无图表 | 需要完整分析结果时 |
和带界面版的区别:
- 不弹窗口、不依赖图形界面库,适合服务器/批处理
- 默认只输出生成结果表(精简),加
--mode full才输出全部 - 输出用普通 Excel 格式(无深海蓝美化、无班福图表),文件更小、程序更好读
summary和full模式生成的每个工作表首行表头均水平居中- 列识别不出时,默认直接报错退出(agent 友好);加
-i可以在命令行一列一列手动选
重新打包 exe(开发者):
pyinstaller --noconfirm cli_build.spec
打包配置在 cli_build.spec(已排除 GUI 库,产物约 31MB)。验证通过后,将 dist\ledger-cli.exe 复制到项目的 cli 文件夹即可。
| 列名 | 作用 | 例子 |
|---|---|---|
| 会计月 | 第几月的账 | 1月、1、2024-01、一月都行 |
| 凭证编号 | 这笔分录的凭证号 | 1、001、记-001 |
| 一级科目 | 会计科目名称 | 银行存款、应收账款、管理费用 |
| 借方发生额 | 借方金额 | 10000、5000.5 |
| 贷方发生额 | 贷方金额 | 10000、5000.5 |
| 列名 | 什么时候要 | 例子 |
|---|---|---|
| 凭证种类 | 你想区分"记/收/付/转"时 | 记、收、付、转 |
| 账套 | 一个 Excel 里有多家公司数据时 | A公司、B公司 |
列名不必完全一致。 程序会按关键词自动识别。如果识别不到,会弹窗让你手动选择列对列。
可识别的列名关键词:
| 程序需要的列 | 能识别的列名 |
|---|---|
| 会计月 | 月、日期、期间、制单日期 |
| 凭证编号 | 编号、凭证号 |
| 一级科目 | 科目、一级、名称 |
| 借方发生额 | 借方、借 |
| 贷方发生额 | 贷方、贷 |
| 账套 | 账套、公司、核算主体 |
🔴 重要提示:原始序时账表几乎都不能直接用,必须先做数据清洗,否则处理结果会失真或报错。
| 常见问题 | 后果 |
|---|---|
| 没有"一级科目"列(只有明细科目) | 对方科目匹配准确度大幅下降 |
| 表头有多行(合并单元格、空行、单位行混在表头里) | 程序找不到表头,列识别错误 |
| 借贷两列同时有数 | 报警告,部分分录被丢弃 |
| 金额列混进文字(如"一百元"、"测试") | 直接报错停止 |
| 多公司数据没加账套列 | 跨公司凭证被错误合并 |
实务中拿到的序时账常常只有明细科目(如"银行存款-工商银行-XX支行"),没有"一级科目"列。手工补太慢,标准做法是用 Excel 公式 + 科目余额表匹配:
步骤:
- 拿到科目余额表(一般企业财务系统都能导出),里面有一级科目代码对应一级科目名称
- 在序时账的明细科目列右边插入一列,假设原列叫 C 列、余额表中代码列叫 D 列、名称列叫 E 列
- 在新列里用 LEFT 函数取明细科目代码前 4 位:
=LEFT(C2, 4) - 用 VLOOKUP 到科目余额表里匹配一级科目名称:
=VLOOKUP(LEFT(C2,4), 余额表!D:E, 2, FALSE) - 公式下拉填充整列
- 把这一列重命名为"一级科目"
- 留几行检查匹配结果,大部分成功即可;有匹配不上的少数行,手工补一下
注意事项:
- 不同企业的科目编码长度可能不是 4 位,请根据实际的科目余额表调整 LEFT 的位数(常见有 4 位、6 位、8 位等)
- 同一个一级科目代码在余额表里只能对应一个名称
- 如果代码和名称都不规律、匹配不上,那就只能用筛选+手工归类的笨办法
常见表头问题: 第一行是单位名(如"XX 公司"),第二行是标题(如"2024 年序时账"),第三行才是列名;或者表头合并了单元格。
清洗目标:让第一行就是规范的列名行。
具体操作:
- 打开 Excel,按 Ctrl+End 看一下末尾
- 把表头以上的所有"多余行"(单位行、标题行、空行)整行删除
- 把表头里合并的单元格取消合并(开始 → 合并后居中 → 取消合并单元格)
- 表头每个单元格填上明确的列名(会计月、凭证编号、一级科目、借方发生额、贷方发生额)
- 检查表头是否只有一行,把多出来的空行删掉
提示: 处理完之后,第一行往下就是数据,列名清晰规范,可以保存后用本工具处理。
| 提示 | 说明 |
|---|---|
| 借贷不要同时有数 | 一行要么借方有数,要么贷方有数,不要两列都填。如果某行借贷都有数,要么是数据错误,要么是红字冲销(确认后再处理) |
| 合并的数据单元格 | 程序会自动向下填充空值,不用手工处理 |
| 文件格式 | 仅支持 .xlsx、.xls,不要把 .csv 改名成 .xlsx,先在 Excel 里打开另存为 .xlsx |
| 金额格式 | 支持 10000、1,000、1,000.50;混进文字(如"一百元")会直接报错停止——不要指望程序自动当成 0 |
| 表内不要有空行 | 数据中间有空行会导致分组错乱,删掉 |
| 不要带小计行 | 小计/合计行会让"对方科目匹配"和"异常分录检测"误报,删掉 |
一个 Excel 含多公司时,加一列"账套":
| 账套 | 会计月 | 凭证编号 | 一级科目 | 借方发生额 | 贷方发生额 |
|---|---|---|---|---|---|
| A公司 | 1月 | 记-001 | 银行存款 | 10000 | 0 |
| A公司 | 1月 | 记-001 | 应收账款 | 0 | 10000 |
| B公司 | 1月 | 记-001 | 银行存款 | 20000 | 0 |
| B公司 | 1月 | 记-001 | 应付账款 | 0 | 20000 |
为什么不加账套列会出错? A 公司的"1月记-001"和 B 公司的"1月记-001"会被当成同一张凭证合并处理,对方科目匹配就会错。
结果文件命名为:原文件名 + "生成对方科目" + 原扩展名。
例如:2024年序时账.xlsx → 2024年序时账生成对方科目.xlsx
文件里包含 6 张工作表:
所有工作表第一行的非空表头均水平居中。
| 工作表 | 里面是什么 | 怎么用 |
|---|---|---|
| 生成结果 | 原始数据 + 自动填的"对方科目"列 + "匹配类型"列 | 这是主表,所有分析都基于它 |
| 原始数据 | 你原来上传的 Excel 内容备份 | 对照原始凭证用 |
| 异常分录明细 | 金额大且不符合常规的分录 | 看第 6.2 节 |
| 异常分录 | 异常分录按类型汇总 | 看第 6.2 节 |
| 班福分析 | 数据造假分析(含折线图) | 看第 6.3 节 |
| 失败分组 | 处理失败的凭证组 | 出错时排查用 |
做数据透视分析。 现在每笔分录都有明确的去向,可以像查账单一样查账:
- 某月"银行存款"流入了多少?流向哪些对手方?
- 某月"管理费用"主要付给了谁?
- 某客户/供应商全年交易额是多少?
匹配类型是质量信号:
| 匹配类型 | 可信度 | 说明 |
|---|---|---|
| 标准模板 | ⭐⭐⭐⭐⭐ | 命中内置的 256 条标准会计分录规则,可直接采信 |
| 结转损益规则 | ⭐⭐⭐⭐ | 识别为期末损益结转分录 |
| 结转损益规则(拆分) | ⭐⭐⭐⭐ | 损益结转中利润分配科目被拆到各行 |
| 结转损益规则(调整) | ⭐⭐⭐ | 损益结转时的微小误差调整行 |
| 算法生成 | ⭐⭐⭐ | 金额配对成功,但科目关系是算法推的,建议抽查 |
| 算法生成(兜底拆分) | ⭐⭐ | 复杂分录的兜底拆分,建议复核 |
| 算法生成(异常剩余) | ⭐ | 实在匹配不上,必须人工核对 |
实务建议:⭐⭐⭐ 及以上的可以直接信任;⭐⭐⭐ 以下的建议人工复核。
什么是异常分录?
同时满足以下条件的分录:
- 金额超过阈值(默认 10000 元)
- 没命中标准模板规则
- 没命中结转损益规则
为什么要关注? 这些可能是:非常规业务、错误分录、造假痕迹。
怎么看?
- 打开 "异常分录明细" 工作表
- 按"金额"从大到小排序
- 重点看金额最大的那几笔
- 对照原始凭证核实
怎么调阈值? 不同规模公司建议不同:
| 公司规模 | 建议阈值 |
|---|---|
| 小公司 | 5000 |
| 中型公司 | 10000(默认) |
| 大公司 | 50000 或更高 |
阈值越高,列出的异常分录越少、越聚焦大额可疑交易。
这是什么? 班福定律(Benford's Law)是一个数学规律:自然产生的一组数字,首位数的分布不是随机的。
| 首位数 | 理论概率 |
|---|---|
| 1 | 30.1% |
| 2 | 17.6% |
| 3 | 12.5% |
| 4 | 9.7% |
| 5 | 7.9% |
| 6 | 6.7% |
| 7 | 5.8% |
| 8 | 5.1% |
| 9 | 4.6% |
简单说:以"1"开头的金额应该最多(30%+),以"9"开头的应该最少(不到 5%)。
为什么能检测造假? 人编的数字往往不符合这个规律(要么均匀分布,要么偏好某些数字)。如果实际分布和理论分布差很多,数据可能被修改过。
怎么看? 打开 "班福分析" 工作表,看折线图:
- 蓝线 = 实际数据分布
- 橙线 = 理论分布
- 两线越接近,数据越自然;偏差越大,越可疑
注意: 班福分析只是辅助证据,不是定罪铁证。有些真实业务本身就符合正态分布而非班福分布。
工具自动填上"对方科目"列后,每一个一级科目都能看清它的钱从哪来、去了哪。这就是会计上说的"科目来源去去向分析",是审计和分析最常用的视角。
下面按科目类别列出常见的分析套路:
核心思路:损益类科目(收入、成本、费用)的对方科目,主要应该是现金、银行存款、应收应付、相关资产类科目。如果看到异常的对方科目,就要重点核查。
| 你关注的科目 | 正常应该对应的对方科目 | 如果出现这些对方科目 → 可能有问题 |
|---|---|---|
| 主营业务收入、其他业务收入 | 银行存款、应收账款、应收票据、预收账款(红字)、现金 | 出现往来对抵(其他应收/其他应付)、关联方挂账等异常挂账科目 |
| 主营业务成本、其他业务成本 | 库存商品、原材料、银行存款、应付账款 | 出现"在建工程""研发支出"等非正常结转 |
| 销售费用、管理费用、研发费用 | 银行存款、库存现金、应付职工薪酬、累计折旧、其他应付款 | 出现关联方往来(如其他应收款-某股东)、长期资产科目 |
| 财务费用 | 银行存款、应付利息、长期借款 | 出现非利息相关科目(如营业外支出) |
| 折旧与摊销 | 管理费用、制造费用、销售费用、生产成本、研发支出 | 看折旧摊销到底有没有按用途归集到对应成本费用科目,没归集就是异常 |
| 职工薪酬 | 管理费用、制造费用、生产成本、销售费用、研发支出、应付职工薪酬、银行存款 | 看薪酬有没有按部门/用途归集到对应成本费用科目 |
核心思路:成本结转类科目(生产成本、制造费用等)的对方科目应该形成清晰的"投入-产出"链条。
| 你关注的科目 | 正常应该对应的对方科目 | 分析重点 |
|---|---|---|
| 生产成本 | 原材料、应付职工薪酬、制造费用、银行存款(投入端) / 库存商品、主营业务成本(产出端) | 看生产成本的来源主要是哪些科目(是不是正常的料工费投入),又结转到了哪些科目(最终是不是到了库存商品、主营业务成本) |
| 制造费用 | 累计折旧、应付职工薪酬、水电费相关科目(银行存款、应付账款)、原材料 | 看制造费用归集是否完整,结转到生产成本时金额是否合理 |
| 研发支出-费用化支出 | 银行存款、原材料、应付职工薪酬、累计折旧 | 资本化条件是否符合会计准则 |
核心思路:往来类科目(应收/应付)的对方科目变动必须符合业务实质。增加要有合理的来源,减少要有合理的去向,否则就是异常的"对抵"或挂账。
| 你关注的科目 | 正常增加时(借方)的对方科目 | 正常减少时(贷方)的对方科目 | 异常信号 |
|---|---|---|---|
| 应收账款 | 主营业务收入、其他业务收入、应收票据(背书转入)、银行存款(收到预收款转货款) | 银行存款(实际收款)、应收票据、坏账准备(核销) | 减少时对方是其他应付款、其他应收款、预付账款、长期挂账等 → 高度可疑,可能是走账、对抵、虚假回款 |
| 应付账款 | 原材料、库存商品、应付票据、银行存款(预付转应付)、相关成本费用 | 银行存款(实际付款)、应付票据、其他货币资金 | 增加时对方是其他应收款、其他应付款、银行存款(非真实付款)→ 异常挂账;减少时对方是往来类而非货币资金 → 可能是对抵 |
| 预收账款 | 银行存款、库存现金(收到预收款冲销时) | 主营业务收入、其他业务收入(确认收入时) | 长期挂账不确认收入,或冲销时对方科目异常 |
| 预付账款 | 银行存款、库存现金(付款时) | 原材料、库存商品、相关成本费用(收到货物/服务时) | 长期挂账,或冲销到非货品/非费用科目 |
| 其他应收款 | 银行存款、库存现金(备用金、押金、垫付款) | 银行存款、相关成本费用(收回或冲销时) | 频繁与"其他应付款""应收账款""应付账款"对抵 → 高风险信号,可能隐藏关联方占款或资金体外循环 |
| 其他应付款 | 银行存款(收到押金等)、相关收入(应付款转收入) | 银行存款、库存现金(实际支付时) | 频繁与"其他应收款""应付账款""应收账款"对抵 → 高风险信号 |
| 你关注的科目 | 重点看的对方科目 |
|---|---|
| 银行存款、库存现金 | 对方科目应该覆盖所有业务场景。重点看是否有大量"银企对抵":减少方是"应付账款"或"其他应付款",增加方又是"应收账款"或"其他应收款"——这种没有实际过账的转账是虚假回款的典型手法 |
| 其他货币资金(保证金、专户存款等) | 保证金是否对应真实的合同/保函业务 |
- 导出"生成结果"工作表,按"一级科目"分组,对每个科目查看"对方科目"列的金额分布
- 重点关注大额科目:金额大的科目,对方科目稍有异常就是大问题
- 关注同一对方科目反复出现:比如某笔款往来方反复出现且金额相同,可能是循环转账
- 关注时间集中度:短期内大量同向同金额交易,要警觉
- 结合"异常分录明细":金额大、没匹配上的分录,加上对方科目异常,是审计重点
先看"匹配类型"列。 如果大量显示 ⭐⭐ 以下:
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 大量"异常剩余" | 数据借贷本身不平衡 | 检查原始凭证 |
| "兜底拆分"很多 | 复杂多借多贷 | 抽查原始凭证,确认业务是否真实 |
| 部分匹配不上 | 科目特殊、金额有微小差异 | 微调阈值或检查科目命名 |
打开 "失败分组" 工作表,可以直接看到处理失败的凭证组有哪些。
问:双击 exe 弹出黑窗口一闪就没了?
答:这是因为程序需要依赖库(pandas/openpyxl 等)但当前电脑没装。本工具是已经打包好的单文件 exe,正常情况下双击会直接弹出主窗口。如果一闪而过,说明杀毒软件拦截了,尝试关闭杀软后重试,或右键 → "以管理员身份运行"。
问:处理时报"读取 Excel 失败"?
答:检查文件是不是被打开着(关闭 Excel 再处理)、文件格式是不是 .xlsx 或 .xls、文件有没有损坏。
问:金额列里有"一百元"、"测试"这种文字,程序会怎么处理?
答:直接报错停止运行,告诉你哪一列第几行有问题。不会偷偷当成 0,避免结果被污染。改干净原始数据再处理。
问:借贷两列都有数会怎样?
答:这种情况会计上不合理,程序会报警告。建议改原始数据,确保一行只有一列有数。
问:处理速度慢怎么办?
答:先关掉其他程序释放内存。如果数据量巨大(几十万行以上),可以用命令行模式 --no-gui 跑,省去界面开销。
问:结果文件能直接发给领导看吗?
答:可以。生成结果采用专业排版格式(深海蓝表头、千分位数字、冻结首行),可直接交付。
本项目为模块化架构,目录结构如下:
序时账分析器/
├── main.py 主程序入口(自动选择 GUI 或命令行模式)
├── make_excel.py Excel 输出美化模块(deep-navy 摩根系标准格式)
├── requirements.txt 依赖列表
├── LICENSE 开源许可证
├── cli/ 纯命令行版(给 agent / 脚本调用,详见 3.3 节)
│ ├── 序时账分析器命令行版.py
│ └── ledger-cli.exe 可直接运行的纯命令行版
├── tests/ 冗余约束与 Excel 输出回归检查
└── src/
├── core/ 核心算法层
│ ├── precision.py 金融级精度引擎
│ ├── algorithms.py 折半枚举算法
│ └── rules.py 256条会计分录规则库
├── pipeline/ 处理流水线层
│ ├── group_processor.py 凭证分组处理器
│ ├── orchestrator.py 流水线编排器
│ ├── anomaly.py 异常分录检测 + 班福定律分析
│ └── validator.py 数据完整性校验
├── io/ 输入输出层
│ ├── reader.py 数据读取与预处理
│ └── writer.py 结果输出(含图表)
├── gui/ 图形界面层
│ ├── app.py 主窗口(含列映射配置)
│ ├── widgets.py 界面组件适配器
│ ├── progress.py 进度消息队列
│ └── log_redirector.py 日志重定向
└── utils/ 工具层
└── logger.py 统一日志模块
| 层级 | 模块 | 职责 |
|---|---|---|
| 核心层 | precision + algorithms + rules | 精度引擎、折半枚举算法、规则库 |
| 流水线层 | group_processor + orchestrator + anomaly + validator | 分组处理、流水线编排、异常检测、数据校验 |
| 输入输出层 | reader + writer | 读取预处理、结果输出(含图表) |
| 界面层 | app + widgets + progress + log_redirector | 主窗口、列映射、组件适配器、进度条、日志重定向 |
| 工具层 | logger | 统一日志模块 |
- 每层职责单一:修改某个功能只需要改一个文件
- 模块解耦:数据加载与界面完全解耦,通过参数注入列映射对话框
- 进度通知统一:从全局变量改为回调函数参数传递
- 入口清晰:
main.py供日常 GUI/命令行使用,cli目录保留专用自动化入口
问题: 电脑用二进制存储小数,有些十进制小数是无限循环的,导致 0.1+0.2 不等于 0.3。
解决方案: 把所有金额乘以 10000,转成整数"厘"(万分之一元)来计算。
| 原金额(元) | 转换后(厘) |
|---|---|
| 1 | 10000 |
| 0.1 | 1000 |
| 0.01 | 100 |
| 0.0001 | 1 |
好处:
- 精确计算:整数运算不会丢精度
- 速度快:整数运算比小数快
- 可靠:不会出现 0.1+0.2 ≠ 0.3
容差设置: 默认容差 100 厘 = 0.01 元 = 1 分,两个金额差一分钱以内就算匹配成功。
问题: 一笔借方对应多笔贷方时,要凑数字。比如借方 10000 元,贷方有 3000、5000、2000 三笔,要找出哪些加起来 = 10000。
朴素穷举(不做任何优化)的组合数是 2^N(N 条候选每条选/不选两种),N=20 时就有 100 万种,N=36 时接近 700 亿种——绝对不可能在合理时间内跑完。
所以必须用剪枝(Pruning,提前砍掉不可能的分支)和递归(Recursion,分而治之)来把搜索空间压到可接受范围。
思路: 把大问题拆成小问题,小问题的解可以组合出大问题的解。
折半枚举的两步递归:
-
左半区预计算(递归穷举所有组合)
- 假设左半区有 N 条候选,递归枚举所有可能的组合(每个元素选或不选)
- 对每个组合计算"金额总和",存入列表 L
- 把 L 按金额从小到大排序
- 例:左半区
[a, b, c]→ 组合有[]、[a]、[b]、[c]、[a,b]、[a,c]、[b,c]、[a,b,c],共 2³=8 个金额
-
右半区生成并二分查找
- 递归枚举右半区的每个组合 R,计算金额总和
sum_R - 目标:在左半区金额列表 L 中,找一个金额
sum_L满足sum_L + sum_R ≈ target(在容差范围内) - 用二分查找在已排序的 L 中找
target - sum_R,时间复杂度 O(log M)(M 是左半区组合数)
- 递归枚举右半区的每个组合 R,计算金额总和
合并过程: 左半区每个组合都能和右半区每个组合拼接,所以理论上所有候选的组合都被覆盖了,但计算量从 O(2^N) 降到 O(2^(N/2) × log(2^(N/2)))——对 N=36 来说,大约 2^18 = 26 万次运算,几十毫秒搞定。
光靠分治还不够,必须在递归过程中尽早排除不可能成功的路径。本算法有 5 重剪枝:
| # | 剪枝策略 | 怎么做 | 效果 |
|---|---|---|---|
| 1 | 排序剪枝 | 候选金额从大到小排序,大的先尝试,更容易快速逼近目标 | 平均分支数减半 |
| 2 | 和超出剪枝 | 当前累计金额 > 目标 + 容差,立刻放弃这条路径 | 砍掉约 30% 分支 |
| 3 | 剩余不足剪枝 | 当前累计金额 + 剩余所有候选的总额 < 目标 - 容差,放弃这条路径 | 砍掉约 20% 分支 |
| 4 | 重复值剪枝 | 相同金额的候选只尝试一次(避免重复路径) | 砍掉大量重复分支 |
| 5 | 目标为 0 短路 | 目标金额本身为 0,直接返回空组合,不需要递归 | 边界情况秒返回 |
def find_combination(candidates, target, tolerance):
"""
candidates: 候选金额列表(已从大到小排序)
target: 目标金额
tolerance: 容差
返回:满足 sum ≈ target 的子集
"""
candidates.sort(reverse=True) # 排序剪枝 #1
result = []
def dfs(index, current_sum, chosen):
# 剪枝 #5:刚好在容差范围内
if abs(current_sum - target) <= tolerance:
result.append(chosen[:])
return True # 找到一个就返回
# 递归终止:候选用完
if index >= len(candidates):
return False
# 剪枝 #2:当前和已超出目标
if current_sum > target + tolerance:
return False
# 剪枝 #3:当前和 + 剩余最大可能 < 目标
remaining_max = sum(candidates[index:])
if current_sum + remaining_max < target - tolerance:
return False
# 剪枝 #4:相同金额只试一次
seen_values = set()
for i in range(index, len(candidates)):
val = candidates[i]
if val in seen_values: # 剪枝 #4
continue
seen_values.add(val)
chosen.append(i)
if dfs(i + 1, current_sum + val, chosen):
chosen.pop()
return True
chosen.pop()
return False
dfs(0, 0, [])
return result| 维度 | 旧版回溯法 | 新版折半枚举 |
|---|---|---|
| 计算方式 | 全部穷举 | 左右分治 + 二分查找 + 5 重剪枝 |
| 最大有效深度 | 约 20~25 条 | 稳定支持 36 条 |
| 长尾凑单场景 | 容易超时假死 | 毫秒级响应 |
| N=20 耗时 | ~1 秒 | ~10 毫秒 |
| N=36 耗时 | 数分钟(实际无法完成) | ~100 毫秒 |
保护机制:
- 最大迭代次数:300000 次(防止单边计算过大导致内存溢出)
- 最大候选池:36 条(单边 18 条,最大组合数约 26 万,数十毫秒完成)
每张凭证按以下顺序尝试匹配:
识别条件:
- 科目以"本年利润"开头,或
- 科目以"利润分配"开头
排除条件(不会误判):
- 如果科目包含"费用"、"成本"、"折旧"、"减值"、"摊销",就不是损益结转
比如"利润中心折旧费"不会被误判为损益结转。
内置 256 条常见会计分录规则:
| 借方科目 | 贷方科目 | 业务含义 |
|---|---|---|
| 银行存款 | 主营业务收入 | 销售收款 |
| 管理费用 | 银行存款 | 支付费用 |
| 库存商品 | 主营业务成本 | 结转成本 |
匹配顺序:先 1 对 1(金额相等、科目符合规则)→ 再 1 对多(凑数字)→ 最后多对 1(凑数字)。
标准模板没匹配上时,用纯金额匹配,不管科目。
匹配顺序:先 1 对 1 → 再 1 对多 → 最后多对 1。
把剩余的借方和贷方按顺序配对,金额按较小的那个拆分。
例如:剩余借方 50000、剩余贷方 30000,拆成 借方 30000 ↔ 贷方 30000(匹配成功)+ 借方剩余 20000(标记"未找到匹配")。
| 情况 | 处理方式 |
|---|---|
| 借方是正数 | 正常作为借方处理 |
| 贷方是正数 | 正常作为贷方处理 |
| 借方是负数 | 转成正数,作为贷方处理 |
| 贷方是负数 | 转成正数,作为借方处理 |
输出采用 deep-navy 摩根系标准格式,由 make_excel.py 提供:
| 格式要素 | 说明 |
|---|---|
| 数据起始位置 | 从 B2 单元格开始写入 |
| 表头样式 | 深海蓝背景 #1F4E79、白字粗体、Arial 11、水平居中 |
| 行高 | 统一 18 磅 |
| 数字格式 | 千分位分隔,数字列自动蓝色字体 |
| 框线 | 上下粗框线 + 中间虚线,无竖线 |
| 网格线 | 隐藏 Excel 默认网格线 |
| 冻结 | 首行冻结,方便滚动查看 |
| 班福图表 | 正常保留 |
降级策略: make_excel.py 不可用时,程序自动回退到 to_excel 普通保存,不报错。
- 删除未被调用的旧列映射文件、终端进度条、重复日志通道和内部兼容包装,既有程序源码净减少 736 行
- 保留双 GUI、独立 CLI、主说明文档和已发布 EXE,不改变核心匹配算法、入口参数及退出码
- 主程序、专用 CLI 源码及
cli\ledger-cli.exe生成的每个工作表首行表头统一水平居中 - 重建
cli\ledger-cli.exe,体积由 32,368,746 字节降至 32,346,279 字节 - 新增可重复的结构测试和 Excel 工作簿回归检查;逐格验证除首行水平对齐外无其他输出差异
本次修复了结转损益凭证(摘要为"结转当前损益")在"编码\名称"式科目下全部无法识别、以及表格末尾"总计"汇总行混入凭证的问题。
- 问题:判断是否损益结转时,只认科目名称以"本年利润"或"利润分配"开头的开头匹配;而账套科目多为"4103\本年利润"这类"编码\名称"格式,开头是数字,永远匹配不上,导致所有结转凭证被当成普通凭证乱配金额。
- 修复:识别逻辑从"开头匹配"改为"包含匹配",只要科目名含有"本年利润"或"利润分配"即识别为损益结转科目;原有的排除项(含"费用/成本/折旧/减值/摊销"的不算)保持不变。
- 问题:很多账套导出文件末尾有一行"总计/合计/小计"汇总行(无凭证号、无科目名),读取时空凭证号被向下填充成上一个凭证号,导致该行被算进最后一张凭证,变成超大金额假分录。
- 修复:读取后自动检测并剔除摘要为"总计/合计/小计"的汇总行,并在日志提示剔除行数;同时给处理环节加保险,科目名称为空的行直接跳过保留,绝不参与配对。
- 「未找到匹配」行数:17 → 0
- 「对方科目」为空/NaN 行数:17 → 0
- 输出中「总计」行数:18 → 0
- 「结转损益规则」行数:0 → 354
- 结转凭证全部 40 条分录对方科目均正确指向"本年利润"
新增独立的纯命令行版本(cli/序时账分析器命令行版.py),专为 AI agent 和自动化脚本设计,复用现有核心算法、不修改任何现有代码:
--mode summary(默认,只输出生成结果表)/--mode full(全部 sheet)--threshold控制异常分录阈值-i / --interactive列识别不出时弹命令行选单逐列选- 普通格式输出(无美化、无图表),打包成独立 exe(
dist/序时账分析器命令行版.exe,约 72MB)
本次对全部源码做了一轮完整审查(含合成数据实测),修复了 3 个会导致"结果悄悄出错"的隐患和一批中小问题:
- 分组键空值静默丢行:凭证编号/凭证种类为空且无法向下填充时,这些行会被分组功能静默丢弃。修复方式:
groupby改用dropna=False,空值行强制纳入处理。 - 生成结果行序随机:多进程并行按完成先后写入结果,行序每次都乱。修复方式:每行携带原始行号(
_orig_idx),输出前按原始顺序稳定排序。 - 同行借贷双非零丢金额:某行借贷双方同时有金额时,贷方金额被静默丢弃且警告发在子进程里用户看不到。修复方式:拆成借/贷两个独立条目分别参与匹配(禁止自我配对),警告随结果回传主进程展示。
- 版本号混乱:
main.py、GUI 标题(ctk/tk)、界面角落标签统一为v2.0.8。 --no-gui参数失效:此前定义了但从未生效,现在不带文件路径使用时会明确报错提示。- CLI 退出码:处理失败(读文件失败/保存失败)时退出码为 1,成功为 0,方便批量脚本判断;保存链路全程回传成功状态。
- GUI 旧文件误报成功:此前只判断输出文件"是否存在",上次遗留的旧文件会被误报为本次成功;现在同时校验流水线返回状态和文件修改时间。
- 账套列误判:列名恰好叫"公司"等短名称时会被误当账套列参与分组。修复方式:账套列检测改为单向包含匹配。
- 空"异常分录"sheet 丢失:无异常数据时该 sheet 被静默跳过。修复方式:写入提示行保证 sheet 一定存在。
- 班福图表回退错位:
make_excel不可用走to_excel回退时图表引用列偏移错误。修复方式:按实际写入路径动态计算起始列。 - make_excel 健壮性:NaN/NA/NaT 统一清洗为空再写入(pd.NA 会直接写崩);行写入从
iterrows换为itertuples,大文件写出更快;writer.py导入make_excel增加项目根目录回退,打包环境也能找到。 - 金额容差统一:子集和求解默认容差从 0.005 元统一为 0.01 元,与其他环节一致。
- 空凭证编号显式警告:凭证编号向下填充时升级为 WARNING,提示"若非合并单元格所致,这些行将被并入上一张凭证"。
- 小数据免多进程:分组数 ≤200 时走单进程串行,省去进程池启动开销(实测小文件秒出结果)。
- 清理死代码:移除未被引用的
column_mapper.py、CTkProgressBar、CustomInputDialog,以及误入仓库的ers27651DocumentsCode垃圾文件。
- 全量代码复核:审查全部 16 个业务代码文件,确认无致命 Bug
- 异常处理、精度处理、算法逻辑、数据流转均已完善
- Excel 输出使用 excel-master 技能最新版 make_excel,deep-navy 主题
输出从裸 to_excel 升级为深海蓝摩根系标准格式,专业美观。writer.py 从 ExcelWriter + to_excel 改为调用 make_excel(theme='deep-navy'),美化模块随项目分发。
代码中多处硬编码 v2.0.0,README 已更新到 v2.0.4 但源代码未同步,用户无法从界面或命令行帮助确认实际运行的版本。修复方式:main.py、src/gui/app.py 中所有版本号统一更新为 v2.0.5。
_make_label 适配器函数在 CTK 模式下未清理 tkinter 专属的 fg 参数。修复方式:CTK 分支增加 kw.pop('fg', None) 和 kw.pop('anchor', None) 清理逻辑。
原始数据中某行同时存在借方和贷方非零金额(属于数据异常)时,旧代码的 if-elif 链会静默丢弃其中一方的金额,用户无法察觉。修复方式:_prepare_data 中增加检测,借贷双方同时非零时通过 logger 发出警告。
_match_fallback 中的 set_split_amount 内部函数在 while 循环内重复定义,每次迭代都创建新函数对象。修复方式:函数定义移到循环外。
_process_profit_loss 中计算出的差额 diff 直接用浮点数赋值,可能因浮点误差输出极小非零金额(如 1e-12)。修复方式:差额赋值前通过 PrecisionEngine.to_decimal() 清洗为 2 位小数。
"异常分录"汇总工作表中同一笔异常分录金额被算成 2 倍(借方行和贷方行都加一遍)。修复方式:金额改为 max(借方金额, 贷方金额),只在借方行记录聚合。
科目名称本身包含逗号时被误判为"多个对方科目"。修复方式:校验规则改为匹配"分隔符后跟非空白字符"模式。
红字分录处理后借贷方合计产生等额差额,旧版本会弹"严重警告"。修复方式:差额一致时降为 INFO 级别,明确说明系红字分录处理所致。
删除 对方科目生成.py(已摒弃的单文件版),统一使用模块化版本。
修复了"异常分录"汇总工作表金额翻倍的问题(详见 11.3 同名条目),以及多对方科目校验误判的低优先级修复。
旧版本会把无法识别的金额(如 abc、测试、一百元)偷偷当成 0 处理,带来金额失真、匹配错误、问题难排查三重风险。
修复方式:程序先检查金额列的每个值,非空但不能识别为数字时立刻报错并停止处理,报错信息明确指出哪一列、第几行有问题。
1234.56正常支持1,234.56千分位逗号正常支持- 空白单元格仍按原规则处理为 0
- 非法金额不再被静默转成零
- 千分位金额格式仍可正常解析
旧版本在"结果校验"这一步会因默认存在借方/贷方/对方科目列而崩溃。修复方式:校验前先补齐空结果表需要的关键列。
同一凭证里多条本年利润/利润分配相关分录时,旧版本会错误地都挂到第一条。修复方式:检测到多条损益类分录时不再强行套用特殊规则,回退到通用匹配流程。
v2.0.0 模块化重构后的紧急修复:
| 缺陷 | 严重程度 | 修复内容 |
|---|---|---|
| 图形界面导入路径错误 | 致命 | 修复了两个不存在的模块导入 |
| 命令行模式缺少入口函数 | 致命 | 补充了命令行模式的完整入口函数 |
| 图形界面跳过数据预处理 | 致命 | 改用标准预处理流程 |
| 进度条不更新 | 中等 | 修复进度回调函数未传递 |
| 图形界面日志不显示 | 中等 | 新增 GuiLogHandler |
| 版本号不统一 | 低 | 统一所有位置的版本号 |
从零开始的架构重构,把原来 2315 行的单文件拆分成 5 层模块化架构。
| 层级 | 模块 | 负责 |
|---|---|---|
| 核心层 | precision + algorithms + rules | 精度引擎、折半枚举算法、规则库 |
| 流水线层 | group_processor + orchestrator + anomaly + validator | 分组处理、流水线编排、异常检测、数据校验 |
| 输入输出层 | reader + writer | 读取预处理、结果输出 |
| 界面层 | app + column_mapper + widgets + progress + log_redirector | 主窗口、列映射、组件适配器、进度条、日志重定向 |
| 工具层 | logger | 统一日志模块 |
旧版用 print,新版用标准 logging,支持调试/信息/警告/错误四个级别,带时间戳和级别标记。
旧版直接写 if/else 判断用哪种界面库,新版用适配器模式,统一组件工厂函数。
旧版手动判断 sys.argv,新版用 argparse,支持 --threshold、--no-gui、--log-level,自带 --help。
进度通知从全局变量改为回调参数;数据加载与界面完全解耦,通过参数注入列映射对话框。
| 指标 | 旧版(单文件) | 新版(模块化) |
|---|---|---|
| 文件数量 | 1 个 | 16 个 |
| 最大单文件行数 | 2315 行 | 约 320 行 |
| 架构层级 | 无 | 5 层清晰分层 |
- 折半枚举算法:核心匹配算法从全部穷举升级为左右分治 + 二分查找
- 最大有效计算深度从 20~25 条提升到 36 条
- 候选池超过 36 条时自动截断
- 自动识别账套列,按账套 + 月份 + 凭证号分组
- 支持 7 种账套列名变体
- 输出文件中账套列放在第一列
- 1.6.6:界面更加紧凑,列映射直接嵌入主界面,异常阈值直接在主界面设置
- 1.6.3:修复未安装增强界面库时程序无法运行的问题,修复进度条不显示
- 1.6.2:引入分组处理器类,规则库从 423 条去重到 256 条
- 1.6.0:全新现代化界面,卡片式布局
模块化设计,移除未使用的依赖库,进度条轻量化。
异常分录智能检测、班福定律分析、交互式列映射配置、多进程并行计算、高速读取引擎。
禁用日期智能解析避免月份合并问题,增强损益结转识别避免误判。
多进程并行计算,算法优化引入缓存,数据遍历加速。
合并单元格支持(自动向下填充),依赖库完善。
本项目采用 GNU GPL v3.0 许可证,详情见 LICENSE 文件。