weekly-report
端到端自动生成兴银理财多资产投资部产品运作周报。完整流程包括:(1)更新config日期配置,(2)运行Python数据处理管线(移动文件、AI产品分类、生成结果表、计算基准、收益预测),(3)用openpyxl更新Excel链接工作簿中的外部文件路径和公式引用,(4)将Excel表格截图并按书签顺序插入Word模版生成最终docx/pdf周报。当用户说"生成XX日期的周报"、"生成本周周报"、"
SKILL.mdskills/weekly-report/SKILL.md
GitHub多资产投资部产品运作周报 - 端到端自动化
前置条件
- Wind终端已登录 (业绩基准计算需要)
- Python环境 - pandas, numpy, openpyxl, openai, pyarrow, WindPy, playwright, lxml
- LibreOffice已安装 -
brew install --cask libreoffice(PDF生成需要) - 数据文件已就位 - 本周的产品净值、业绩指标、运作概览等原始文件已下载到
~/Downloads或对应目录
执行流程概览
步骤1: 更新config日期 → 步骤2: Python数据处理管线(5个脚本)
→ 步骤3: 更新Excel链接工作簿 → 步骤4: Excel表格截图(含VLOOKUP解析)
→ 步骤5: 组装Word文档 + 生成PDF
步骤 1: 解析日期并更新配置
从用户输入中提取报告日期 (格式: YYYYMMDD)。
更新 config.py:
# /Users/fanshengxia/Desktop/周报V2/config.py 中第46行左右
YUNZUOGAILAN_DATE = 'YYYYMMDD' # 改为用户指定的日期
其他日期 (LASTW, LASTM, LASTY, WEEK_MON 等) 由 Config 类从此日期自动计算,无需手动改。
验证: 改完后用 Python 快速验证日期计算:
cd /Users/fanshengxia/Desktop/周报V2 && python -c "from config import Config; print(f'报告日期:{Config.YUNZUOGAILAN_DATE}, 上周:{Config.LASTW}, 上月:{Config.LASTM}')"
步骤 2: 运行 Python 数据处理管线
按顺序执行5个脚本。可使用主控脚本一键运行,也可逐个执行:
方式 A: 一键执行 (推荐)
cd /Users/fanshengxia/Desktop/周报V2
echo "y" | python run_weekly_report.py
方式 B: 逐步执行
cd /Users/fanshengxia/Desktop/周报V2
# 2.1 移动数据源文件到正确目录
python move_files.py
# 2.2 AI产品分类 (新产品自动归类)
python update_new_product_catgoery.py
# 2.3 生成主结果表 + AI文字总结 (最核心的步骤,约5-15分钟)
python generate_report.py
# 2.4 计算业绩基准表现 (需要Wind API)
python calculate_all_benchmarks.py --date {ZHOUBAO_DATE}
# 2.5 收益预测 (合享/悦动稳享系列)
python yield_forecast.py
输出文件: 输出结果/输出结果{DATE}.xlsx (含40+个sheet)
验证: 确认输出文件存在且包含关键sheet:
python -c "
import openpyxl
wb = openpyxl.load_workbook('/Users/fanshengxia/Desktop/周报V2/输出结果/输出结果{DATE}.xlsx', read_only=True)
print(f'Sheet数量: {len(wb.sheetnames)}')
print('关键Sheet:', [s for s in wb.sheetnames if '总结文字' in s or '资产大表' in s or '分渠道' in s])
wb.close()
"
步骤 3: 更新 Excel 链接工作簿
目标: 替代原来的VBA宏操作。用openpyxl直接更新 周报生成工具_加权_调整兴动合兴系列.xlsm 中所有外部文件引用路径。
核心原理: 工作簿中68个sheet通过VLOOKUP公式引用外部Excel文件。需要把公式中的旧日期替换为新日期。
3.1 读取链接数据表获取新旧文件路径
import openpyxl
WORKBOOK_PATH = '/Users/fanshengxia/Desktop/周报V2/周报生成工具_加权_调整兴动合兴系列.xlsm'
wb = openpyxl.load_workbook(WORKBOOK_PATH, keep_vba=True)
# 从 链接数据表 sheet 读取旧路径(B列)和新路径(C列)
link_sheet = wb['链接数据表']
for row in link_sheet.iter_rows(min_row=2, max_col=3, values_only=False):
old_path = row[1].value # B列:旧文件路径
new_path = row[2].value # C列:新文件路径
if old_path and new_path:
# 更新B列为C列的值
row[1].value = new_path
3.2 批量替换公式中的日期引用
遍历所有数据sheet,将公式中的旧日期替换为新日期:
OLD_DATE = '20260129' # 上一期日期 (从链接数据表旧路径中提取)
NEW_DATE = '20260205' # 本期日期
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
for row in ws.iter_rows():
for cell in row:
if isinstance(cell.value, str) and OLD_DATE in cell.value:
cell.value = cell.value.replace(OLD_DATE, NEW_DATE)
3.3 更新周报生成流程 sheet 中的日期
ws_flow = wb['周报生成流程']
# 更新B2 (当前日期) 和 B3 (上期日期)
ws_flow['B2'] = NEW_DATE
ws_flow['B3'] = OLD_DATE
保存时必须 keep_vba=True 以保留.xlsm中的VBA代码:
wb.save(WORKBOOK_PATH)
步骤 4: Excel 表格截图 (已验证可用)
核心脚本: /tmp/generate_excel_screenshots_v2.py (~800行)
该脚本从VBA工作簿读取59个数据sheet,生成高保真HTML(保留Excel原始格式、合并单元格、颜色样式),解析VLOOKUP/IF/SUM等公式从外部文件获取数据,最后通过Playwright截图为PNG。
运行方式
python /tmp/generate_excel_screenshots_v2.py
关键技术实现
4.1 外部文件映射 (externalLink顺序)
重要: [1], [2]等编号对应xlsm zip中 xl/externalLinks/ 的顺序,不是链接数据表行号:
EXTERNAL_FILES = {
1: '数据/合享承诺收益率.xlsx', # [1] = 合享承诺收益率 (静态文件)
2: f'输出结果/输出结果{DATE}.xlsx', # [2] = Python输出结果
3: f'数据/产品运作概览数据-母子产品/产品运作概览信息表增加指标变化_{DATE}.xlsx', # [3] = 运作概览
4: f'数据/产品业绩指标数据/内部产品业绩_{DATE}.xlsx', # [4] = 产品业绩
5: f'数据/收益测算数据/丰利合享底表-{DATE}.xlsx', # [5] = 合享底表
6: f'数据/收益测算数据/悦动稳享底表-{DATE}.xlsx', # [6] = 悦动稳享底表
}
4.2 openpyxl 关键注意事项
绝对不要用 read_only=True! 它会创建 EmptyCell 对象,没有 .row/.column 属性,导致所有外部数据加载静默失败:
# 错误 - EmptyCell没有row/column属性会导致崩溃
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
# 正确
wb = openpyxl.load_workbook(path, data_only=True)
4.3 公式解析 (覆盖 ~30K/33K 公式)
支持的公式模式(按频率排序):
IF($A2="","",VLOOKUP(...))— 14,536个VLOOKUP(lookup, '[2]Sheet'!$A:$Z, col, 0)— 8,040个 (含全列引用)IF([2]Sheet!$A4="","",[2]Sheet!$A4)— 3,744个 (外部单元格引用)SUM(B3:B10)— 1,507个IFERROR(VALUE(VLOOKUP(...)),fallback)— 534个- 简单算术
+,-,*,/— 212个
多轮解析: 公式之间有依赖关系(SUM依赖VLOOKUP结果),需要5轮迭代解析。
4.4 HTML生成与截图
- 从openpyxl读取单元格格式(字体、颜色、对齐、合并)
- 生成精确匹配Excel样式的HTML table
- Playwright异步截图59个HTML文件 → 59个PNG (~30秒)
输出:
- HTML文件:
/tmp/weekly_report_html/(59个) - PNG截图:
/tmp/weekly_report_screenshots/(59个)
截图填充率说明
部分sheet因公式复杂度限制,数据填充不完整(属正常现象):
- 高填充率 (>90%): 产品规模、系列产品业绩、指数收益、投资经理维度等核心表格
- 中等填充率 (50-90%): 分渠道余额、最短持有期、狭义客户周期等
- 低填充率 (<50%): 到期情况表、中收监控、持仓明细等(含SUMPRODUCT等未解析公式)
步骤 5: 组装 Word 文档并生成 PDF (已验证可用)
核心脚本: /tmp/assemble_word_report.py (~500行)
关键技术要点
5.1 python-docx 无法打开 .docm 文件
python-docx 打开 .docm 会报 ValueError: content type is 'application/vnd.ms-word.document.macroEnabled.main+xml'。
解决方案: 用 zipfile 方式将 .docm 转换为 .docx:
import zipfile, re
def convert_docm_to_docx(docm_path, docx_path):
"""Strip VBA from .docm to create valid .docx"""
vba_files = {'word/vbaProject.bin', 'word/vbaData.xml', 'word/_rels/vbaProject.bin.rels'}
with zipfile.ZipFile(docm_path, 'r') as zin:
with zipfile.ZipFile(docx_path, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.namelist():
if item in vba_files:
continue
data = zin.read(item)
if item == '[Content_Types].xml':
content = data.decode('utf-8')
content = content.replace(
'application/vnd.ms-word.document.macroEnabled.main+xml',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml')
content = re.sub(r'<Override[^>]*vbaData[^>]*/>', '', content)
content = re.sub(r'<Default[^>]*Extension="bin"[^>]*/>', '', content)
data = content.encode('utf-8')
elif item == 'word/_rels/document.xml.rels':
content = data.decode('utf-8')
content = re.sub(r'<Relationship[^>]*vbaProject[^>]*/>', '', content)
data = content.encode('utf-8')
zout.writestr(item, data)
5.2 书签操作 - 用 lxml 直接操作 OOXML
所有66个书签都是空书签(bookmarkStart和bookmarkEnd相邻),需要在两者之间插入内容。
文字书签插入:
from lxml import etree
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
# 构建 <w:r><w:t>text</w:t></w:r> 元素
run_xml = f'<w:r xmlns:w="..."><w:rPr><w:rFonts w:hint="eastAsia"/><w:sz w:val="21"/></w:rPr><w:t xml:space="preserve">{text}</w:t></w:r>'
run_elem = etree.fromstring(run_xml.encode())
bm_start.addnext(run_elem)
图片书签插入:
# 1. 将PNG文件添加到docx zip的 word/media/ 目录
# 2. 在 word/_rels/document.xml.rels 添加 Relationship
# 3. 构建 <w:r><w:drawing><wp:inline>...</wp:inline></w:drawing></w:r> XML
# 4. 插入到 bookmarkStart 后面
# 图片尺寸: 按96 DPI计算EMU (1 EMU = 1/914400 inch)
emu_per_px = 914400 / 96 # = 9525
cx = int(px_width * emu_per_px)
cy = int(px_height * emu_per_px)
# 缩放到页面宽度 (A4: 11906 twips - 720*2 margins = 10466 twips content)
MAX_WIDTH_EMU = 10466 * 635 # ~6,645,910 EMU
if cx > MAX_WIDTH_EMU:
scale = MAX_WIDTH_EMU / cx
cx = MAX_WIDTH_EMU
cy = int(cy * scale)
多行文字处理: 规模文字、中收文字、到期文字含换行符。第一行插入当前段落,后续行创建新段落(复制原段落样式但排除sectPr)。
5.3 文字书签内容来源
| 书签名 | 内容来源 | 说明 |
|---|---|---|
| 首页日期 | 固定格式 | 2026年02月05日 |
| 到期日期 | 固定格式 | 2026年02月05日 |
| 规模文字 | 输出结果.xlsx → 总结文字 sheet B4 | AI生成的规模总结 |
| 中收文字 | 输出结果.xlsx → 总结文字 sheet B3 | AI生成的中收总结 |
| 到期文字 | 输出结果.xlsx → 总结文字 sheet B2 | AI生成的到期总结 |
| 市场文字 | 留空 | 通常需手动填写 |
| 目标盈 | 留空 | 通常需手动填写 |
5.4 PNG content type
必须在 [Content_Types].xml 中添加PNG的Default:
<Default Extension="png" ContentType="image/png"/>
5.5 生成 PDF
# LibreOffice headless (推荐,最可靠)
soffice --headless --convert-to pdf --outdir "输出结果/" "输出结果/多资产投资部产品周报-{DATE}.docx"
注意: Word for Mac 的 AppleScript save as 不工作(返回 -1708 错误)。JXA exportAs 可以执行但不一定生成文件。LibreOffice headless 是最可靠的方案。
运行方式
python /tmp/assemble_word_report.py
输出:
- DOCX:
输出结果/多资产投资部产品周报-{DATE}.docx(~13MB) - PDF:
输出结果/多资产投资部产品周报-{DATE}.pdf(~12MB, ~57页)
项目路径
| 路径 | 说明 |
|---|---|
/Users/fanshengxia/Desktop/周报V2/ |
项目根目录 |
config.py |
配置文件 (日期、参数) |
run_weekly_report.py |
Python主控脚本 |
周报生成工具_加权_调整兴动合兴系列.xlsm |
Excel VBA工作簿 (68 sheets) |
多资产投资部产品周报-模版.docm |
Word模版 (66个书签) |
输出结果/ |
所有输出文件目录 |
数据/ |
所有源数据目录 |
/tmp/generate_excel_screenshots_v2.py |
步骤4截图脚本 (~800行) |
/tmp/assemble_word_report.py |
步骤5组装脚本 (~500行) |
关键数据文件
| 文件 | 说明 |
|---|---|
输出结果/输出结果{DATE}.xlsx |
Python输出 (40+ sheets) |
数据/产品运作概览数据-母子产品/产品运作概览信息表增加指标变化_{DATE}.xlsx |
产品运作概览 |
数据/产品业绩指标数据/内部产品业绩_{DATE}.xlsx |
业绩指标 |
数据/收益测算数据/丰利合享底表-{DATE}.xlsx |
合享收益预测 |
数据/收益测算数据/悦动稳享底表-{DATE}.xlsx |
悦动稳享收益预测 |
数据/合享承诺收益率.xlsx |
合享承诺收益率 (静态文件) |
最终输出 (成功标志)
| 文件 | 格式 | 大小 |
|---|---|---|
输出结果/多资产投资部产品周报-{DATE}.docx |
Word文档 | ~13MB |
输出结果/多资产投资部产品周报-{DATE}.pdf |
PDF文档 (~57页) | ~12MB |
错误处理
| 错误 | 解决方案 |
|---|---|
| Wind未连接 | 跳过步骤2.4(业绩基准),后续手动补充 |
| config日期格式错误 | 必须为YYYYMMDD格式,如20260205 |
| 数据文件缺失 | 检查Downloads中文件是否已下载,运行move_files.py |
| NAV缓存异常 | python manage_nav_cache.py clear 清除缓存后重试 |
| AI摘要生成失败 | 检查DeepSeek API key和网络连接 |
| openpyxl公式替换失效 | 确认旧日期字符串正确匹配 |
| openpyxl read_only=True崩溃 | 绝对不要用read_only=True,EmptyCell没有row/column属性 |
| python-docx打不开.docm | 用zipfile方式剥离VBA后转为.docx再处理 |
| Word AppleScript save as失败 | 用LibreOffice headless: soffice --headless --convert-to pdf |
| LibreOffice未安装 | brew install --cask libreoffice |
| 截图公式数据缺失 | 部分复杂公式(SUMPRODUCT等)无法解析,属正常现象 |
参考文件
- Excel Sheet→Word书签完整映射:
references/bookmark_mapping.md - PDF报告结构 (目录/章节/表格): 参考最近的PDF
输出结果/多资产投资部产品周报-{LAST_DATE}.pdf
示例
用户: "生成20260205的周报"
执行:
- 更新 config.py:
YUNZUOGAILAN_DATE = '20260205' - 运行
run_weekly_report.py(5个步骤) - 用 openpyxl 更新 Excel 链接工作簿日期引用
- 运行
/tmp/generate_excel_screenshots_v2.py截图59个表格 - 运行
/tmp/assemble_word_report.py组装Word (文字+图片→书签) soffice --headless --convert-to pdf导出PDF
报告:
已生成 20260205 周报
输出文件:
- DOCX: 输出结果/多资产投资部产品周报-20260205.docx (13MB)
- PDF: 输出结果/多资产投资部产品周报-20260205.pdf (12MB, 57页)
执行摘要:
- Python管线: 5/5 步骤成功
- Excel链接更新: 68 sheets, 6个外部文件引用已更新
- 表格截图: 59/59 完成 (含VLOOKUP公式解析)
- Word书签: 64/66 已填充 (市场文字和目标盈留空)
- AI文字总结: 规模/中收/到期 3段已生成