- 所有账户页签在「店铺」右侧新增「总账备注」下拉:按(平台+账单项目)翻译成账单项目集合过滤,页面/列设置/导出同口径;淘系支付宝、淘系微信同页签混多平台时按店铺名前缀细分 - 每日账单脚本:胜算超出同步导出上限(url=null+taskId)时自动从「异步导出下载管理」取回本次任务全部分片并合并成一份 CSV,导入前按页面笔数核对(不一致则跳过导入) - 实测:京东 7 月异步导出两片 219158+235846=455004 笔 / 收入 17695094.84 / 支出 8035128.21,与已导入口径一致
114 lines
4.1 KiB
Python
114 lines
4.1 KiB
Python
"""把聚胜算导出的 xlsx 转成 UTF-8 CSV,并输出笔数/收入/支出核对数。
|
||
|
||
用法:
|
||
python xlsx-to-csv.py <输入.xlsx|输入.csv> [输出.csv]
|
||
python xlsx-to-csv.py --merge <输出.csv> <输入1> <输入2> ... # 多分片合并(表头取第一个文件)
|
||
|
||
输出最后一行固定为:csv=<路径> rows=<n> income=<x> expense=<y> net=<z>[ parts=<k>]
|
||
合并模式(多个输入)下,每个分片额外输出一行:part=<文件> rows=<n> income=<x> expense=<y>
|
||
"""
|
||
import csv
|
||
import os
|
||
import sys
|
||
|
||
|
||
def load(src):
|
||
"""读取一个 xlsx/csv 文件,返回 (header, rows)。"""
|
||
if src.lower().endswith(('.xlsx', '.xlsm')):
|
||
import openpyxl
|
||
|
||
wb = openpyxl.load_workbook(src, read_only=True, data_only=True)
|
||
ws = wb.worksheets[0]
|
||
iterator = ws.iter_rows(values_only=True)
|
||
header = ['' if c is None else str(c).strip() for c in next(iterator)]
|
||
rows = [['' if c is None else c for c in row] for row in iterator if row is not None]
|
||
wb.close()
|
||
return header, rows
|
||
with open(src, 'r', encoding='utf-8-sig', newline='') as fh:
|
||
reader = csv.reader(fh)
|
||
header = [h.strip() for h in next(reader)]
|
||
rows = [row for row in reader if row]
|
||
return header, rows
|
||
|
||
|
||
def amount_index(header, token, exact):
|
||
"""定位金额列:先精确匹配(收入/支出),再退化为包含匹配(收入金额/支出金额)。"""
|
||
if exact in header:
|
||
return header.index(exact)
|
||
for i, name in enumerate(header):
|
||
if token in name:
|
||
return i
|
||
return None
|
||
|
||
|
||
def tally(header, rows):
|
||
"""统计非空行数、收入合计、支出合计。"""
|
||
income_idx = amount_index(header, '收入', '收入')
|
||
expense_idx = amount_index(header, '支出', '支出')
|
||
n = 0
|
||
income = 0.0
|
||
expense = 0.0
|
||
for values in rows:
|
||
if not any(str(v).strip() for v in values):
|
||
continue
|
||
n += 1
|
||
if income_idx is not None and income_idx < len(values):
|
||
try:
|
||
income += float(values[income_idx] or 0)
|
||
except (TypeError, ValueError):
|
||
pass
|
||
if expense_idx is not None and expense_idx < len(values):
|
||
try:
|
||
expense += float(values[expense_idx] or 0)
|
||
except (TypeError, ValueError):
|
||
pass
|
||
return n, income, expense
|
||
|
||
|
||
def run(sources, dst):
|
||
total_rows = 0
|
||
total_income = 0.0
|
||
total_expense = 0.0
|
||
header = None
|
||
with open(dst, 'w', encoding='utf-8', newline='') as fh:
|
||
writer = csv.writer(fh)
|
||
for src in sources:
|
||
part_header, rows = load(src)
|
||
n, income, expense = tally(part_header, rows)
|
||
total_rows += n
|
||
total_income += income
|
||
total_expense += expense
|
||
if header is None:
|
||
header = part_header
|
||
writer.writerow(header)
|
||
elif part_header != header:
|
||
print('warning: 分片表头与首个文件不一致:%s' % src, file=sys.stderr)
|
||
for values in rows:
|
||
if not any(str(v).strip() for v in values):
|
||
continue
|
||
writer.writerow(values)
|
||
if len(sources) > 1:
|
||
print('part=%s rows=%d income=%.2f expense=%.2f' % (src, n, income, expense))
|
||
suffix = ' parts=%d' % len(sources) if len(sources) > 1 else ''
|
||
print('csv=%s rows=%d income=%.2f expense=%.2f net=%.2f%s' % (dst, total_rows, total_income, total_expense, total_income - total_expense, suffix))
|
||
|
||
|
||
def main():
|
||
args = sys.argv[1:]
|
||
if args and args[0] == '--merge':
|
||
if len(args) < 3:
|
||
print('用法:xlsx-to-csv.py --merge <输出.csv> <输入1> [输入2 ...]', file=sys.stderr)
|
||
return 2
|
||
run(args[2:], args[1])
|
||
return 0
|
||
if not args:
|
||
print('用法:xlsx-to-csv.py <输入.xlsx|输入.csv> [输出.csv]', file=sys.stderr)
|
||
return 2
|
||
dst = args[1] if len(args) > 1 else os.path.splitext(args[0])[0] + '.csv'
|
||
run([args[0]], dst)
|
||
return 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|