Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ledgerlite

微信 / 支付宝 / 招商银行 / 中国银行信用卡四类账单解析后幂等入库 SQLite,剔除内部流转、 跨账单去重,并提供一个常驻 web 站点用来看消费明细和资产负债。

  • 消费明细:收入/支出流水,按任意字段排序过滤,支出类型占比,人工分类与剔除/恢复。
  • 资产负债:手工登记各项资产/负债,按日期取最新值算净值曲线。
  • 邮件监控(可选):后台轮询邮箱,发现账单邮件后一键下载解析入库。依赖一个不在本仓库内的 外部模块,见下方「邮件监控」一节。

快速开始

python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt

# 把导出的账单原始文件放进 raw/(见下方「目录」),然后:
export LEDGERLITE_CMB_PASSWORD=xxxxxx   # 招行账单 zip 的解压密码,登录招行 App 查看
cd scripts && python combine.py         # 首次批量灌库,产出 data/bill.db

cd ../web && python app.py --port 5057  # 启动网站,浏览器打开 http://localhost:5057

无测试、无 lint 配置。验证靠重跑 combine.py 后核对网页展示 / data/meta.json 的 warnings。

目录

raw/      原始账单文件(.gitignore,需自己放进去)
scripts/  解析/入库/合并/邮件监控脚本(核心)
data/     bill.db(SQLite,唯一账单存储)+ meta.json + 人工覆盖 JSON(.gitignore)
web/      Flask 常驻站点

raw/ 下期望的文件(combine.py 用 glob 按这些模式查找,找不到会报错提示):

渠道 期望路径/模式 说明
支付宝 raw/ex_alipay/*.csv 支付宝导出的交易明细 CSV(GBK 编码)
微信 raw/ex_wechat*/*.xlsx 微信导出的账单 xlsx;可放多份不同时间范围的,自动选覆盖最宽的一份
招行 raw/*招商银行*.zip 招行 App 导出的交易流水 zip(内含加密 PDF)
中行 raw/*中国银行*.PDF 中行信用卡账单 PDF

环境变量

全部可不设,用合理默认值;需要自定义部署路径/接入自己的账单细节时按需覆盖(见 scripts/paths.py):

变量 用途 默认值
LEDGERLITE_HOME 项目根目录 代码所在目录的上一级
LEDGERLITE_RAW_DIR 原始账单目录 <HOME>/raw
LEDGERLITE_DATA_DIR 数据目录 <HOME>/data
LEDGERLITE_CMB_PASSWORD 招行账单 zip 解压密码 无默认值,combine.py 批量灌库必须设置
LEDGERLITE_OWNER_NAME 你在账单里显示的姓名,用于识别"转给自己另一账户"的自转账 空(不做这项剔除)
LEDGERLITE_CMB_CARD_TAG 支付宝/微信"支付方式"字段里银行卡的文本,用于识别哪些支出会在招行流水里重复出现 招商银行(只写银行名,不含卡号;多卡场景可设成含卡号尾号的完整文本以精确匹配)
LEDGERLITE_URL_PREFIX 整站挂载的子路径(反向代理场景),如 /ledgerlite 空(直接挂根路径)
OUTLOOK_SKILL_DIR 邮件监控功能依赖的外部模块所在目录 空(不设则邮件监控功能报错,不影响解析本地文件这条主流程)

存储模型(SQLite)

data/bill.db

  • transactions:每条成交记录一行,主键 natural_key(渠道|时间|金额|对方|单号|备注;招行同日同额同对方同余额的真重复加 #n 后缀,既不误并又对重复导入幂等)。raw 列由解析器落库,派生列(spend_category/amount_cny/excluded/exclude_reason/dup_of)由 combine.derive()全量重算后回写——故跨源覆盖与去重与入库顺序无关。
  • detected_bills:邮件监控发现的账单邮件(主键 message_id,幂等),状态 detected/ingested/error/ignored。
  • kv:扫描状态(last_scan_at / last_scan_error)等。

data/snapshot.db(资产负债,独立于账单库):asset/debt 两张"更新式"表,每条记录 (date, item, amount, liquid) 表示在某日把某项资产/负债更新为某值;当前持仓 = 按 item 取 日期最大的一条。

脚本职责

  • scripts/parsers.py —— 四类异构账单 → 统一 schema(UNIFIED_COLUMNS)。
    • 支付宝:GBK CSV,表头以"交易时间"开头。
    • 微信:xlsx,表头在数据区前若干行。
    • 招行:约百页文字版 PDF,按词坐标(extract_words)分列 + 最近锚点重建。对手信息单元格围绕金额行垂直居中、换行片段在金额行上一行/下一行,且摘要列长文本会串列,故不能按 extract_text 逐行拼;按 x 分列(对手列 x≥412)、把无日期片段按 top 距离归到最近交易锚点,能正确拼回换行对手名。
    • 中行:信用卡 PDF,extract_tables(),分人民币段(CNY)/日元段(JPY)。
  • scripts/db.py —— SQLite 存储层。upsert_raw(INSERT OR IGNORE on natural_key,幂等)、load_raw/write_derivedload_all(供 web)、detected_bills/kv CRUD。assign_natural_keys 给招行同键真重复加 #n 后缀。
  • scripts/combine.py —— derive(df):对一份原始记录做全量派生(剔除标注→跨源去重→个人转账镜像去重→主观分类→折算),无窗口、纯函数、入库后重算用。rebuild_derived():读 DB 全量→derive→回写+刷新 meta。main():批量灌库(解析四源→窗口过滤→upsert→rebuild)。
    • 剔除口径(每条都带 exclude_reason,写入 transactions 表可审计):支付宝/微信"不计收支"(退款/理财/提现)、转给本人自己的转账(账户互转,需设 LEDGERLITE_OWNER_NAME);招行 朝朝宝转入转出 / 赎回 / 申购 / 银证转账 / 转账汇款(本人) / 信用卡自动还款 / ATM取现 / 购付汇 / 快捷退款冲正 / 贷款;中行 还款/购汇。转给同事/家人/朋友的个人转账保留为真实收支,不再一刀切剔除;其经招行卡产生的"微信转账"镜像由去重剔除,保证每笔只计一次。
    • 跨账单去重:支付宝/微信里走银行卡(LEDGERLITE_CMB_CARD_TAG)的支出,会在招行"快捷支付/银联快捷支付"里重复出现;按金额+日期(±3天,见 DEDUP_DAYS)匹配,保留平台明细、剔除招行重复条(招行条 dup_of 指回平台行)。
  • scripts/overrides.py + data/category_overrides.json + data/manual_exclusions.json —— 人工覆盖层(按"自然键 source|trade_time|amount|counterparty|order_id|note"持久化,combine 与 web 都套用、跨重跑保留):
    • 分类覆盖:网页"分类"彩色 badge 点击弹单选下拉改分类 → POST /api/category(badge 带 ✎ 标记)。
    • 手动剔除/恢复:每行"剔除"按钮 + 表格上方"查看已剔除"切换里每行"恢复"按钮 → POST /api/exclude {action:exclude|restore}effective_excluded(auto, manual):manual=exclude→剔除,=include→保留,否则按自动。
  • scripts/categorize.py —— 把每笔支出映射到主观分类 餐饮/出行/数码/日用/娱乐/运动/居住/医疗/礼物/美容/服饰/其他(收入标"收入"),写入 spend_category 列。
    • 判定模型:一张有序过滤器表 FILTERS,每个过滤器是 谓词(rec)->bool(rec=整条成交记录,可看 counterparty/description/category/note 等任意字段);拿每条记录从上往下依次匹配,第一个命中的过滤器决定分类(first-match-wins)。仓库里这份是通用示例规则,真正好用的规则(家人朋友姓名、常去的本地商户等私有信息)建议放进 scripts/categorize_local.py(已 .gitignore,定义 EXTRA_FILTERS 列表,会被自动加载并排在最前面优先命中),具体格式见 categorize.py 顶部注释。
  • scripts/fx.py —— JPY→CNY 汇率,三源互备(frankfurter / er-api / jsdelivr)。全部失败抛 FxUnavailableError,不回退常量;由 combine 捕获并写入 meta 告警。
  • combine.py 产出 data/meta.json:记录汇率状态(fx.ok/rate)、时间窗口、未折算外币笔数、warnings。汇率全失败时该币种 amount_cny 留空(NaN)。
  • scripts/mail_monitor.py —— 邮件监控。scan_inbox(lookback_hours) 取近 N 小时邮件、按发件人特征(SENDER_TO_SOURCE)识别四类账单、登记进 detected_bills(幂等),刷新 kv 扫描状态。scan_safe 捕获异常写 last_scan_error(后台线程不崩)。
  • scripts/ingest.py —— 账单入库。ingest_message(message_id, password):从邮箱下载附件(微信无附件→正文下载链接抓 zip)→ 按来源解压/解析 → upsert_rawcombine.rebuild_derived() → 更新 detected_bills 状态。ingest_file 供本地补录/测试。失败标 error 并抛错(接口回 400+原因)。
  • web/app.py + web/static/app.js + web/templates/index.html —— 常驻站点。
    • 消费明细:读 bill.db + meta.json,前端过滤/排序/占比(无外部 CDN);占比饼图按 spend_category;汇率失败顶部黄条告警;amount_cny 为 NaN 时 API 输出 null。
    • 资产负债:GET/POST /api/networth*,手工登记 → 净值曲线。
    • 邮件监控:GET /api/mail/status(扫描状态+已发现邮件列表)、POST /api/mail/scan(立即扫描,可带 hours)、POST /api/mail/ingest {message_id,password}(入库)。启动时拉起后台扫描线程(每 600s,回看 24h);加密账单在行内填解压密码。
    • app.py --no-scan 关后台线程;deploy/ledgerlite-web.service 是 systemd 常驻单元模板(路径需按你的部署改)。

邮件监控(可选功能)

自动发现/下载账单邮件依赖一个外部模块 outlook_graph(本仓库不包含实现),需要你自己提供并 通过环境变量 OUTLOOK_SKILL_DIR 指向它所在目录。该模块需暴露以下接口(对照微软 Graph API 的 邮件读取 + MSAL 静默令牌续期):

def load_credentials(): ...                          # 返回给 acquire_token_silent 用的凭据对象
def acquire_token_silent(creds) -> str | None: ...    # 静默换取访问令牌,无缓存/过期返回 None
def list_recent_messages(token: str, lookback_hours: int) -> list[dict]: ...
    # 返回最近 N 小时的邮件列表,每条至少含 id / from.emailAddress.address /
    # receivedDateTime / hasAttachments / subject
def save_attachments(token: str, message_id: str, out_dir: str) -> list[str]: ...
    # 下载某封邮件的附件到 out_dir,返回保存的文件路径列表

不需要这个功能(比如只想手动导出账单跑 combine.py)可以完全忽略这一节;OUTLOOK_SKILL_DIR 不设置时,网站其余功能(消费明细/资产负债)不受影响,只有点"邮件监控"相关操作会报错提示。

已知口径与取舍

  • 招行/中行账单只有交易日期、没有具体时间(导出口径如此);支付宝/微信有完整时间。
  • 招行对手名过长换行时只取首行(尾部可能截断),但不会被相邻行文本污染。
  • 退款一律按"不计收支"剔除(与支付宝/微信平台口径一致),不做与原支出的轧差。
  • 招行直接刷卡消费(未经支付宝/微信)category 原始只有交易摘要(如"快捷支付"),但已由 spend_category 按对方商户名重新归类,占比图用后者。
  • 微信红包/转账/群收款这类对外转账:转给同事/家人/朋友的保留为真实收支并按对方名归类(需要在 categorize_local.py 里配置这些人名对应的分类,否则归"其他");只有转给本人自己(LEDGERLITE_OWNER_NAME)的按账户互转剔除。
  • 房租:招行"转账汇款"给房屋租赁/物业公司保留为支出归"居住";给本人/他人的大额转账仍按账户间转账剔除。
  • 跨账单去重保留支付宝/微信那条,因平台记录带完整交易时间(招行/中行只有日期)。
  • 时间窗口默认只在首次批量灌库时限制(combine.py --start/--end);DB 长期累积,之后的邮件入库不再受窗口限制。

License

MIT,见 LICENSE

About

Personal bill aggregator (WeChat/Alipay/CMB/BOC) + net worth tracker: dedup into SQLite, browse via a small Flask site.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages