Sources: extracted from two upstream archives (skill-repo, skills-main),
merged with the following policy:
- 15 broken symlinks (pointing to /Users/jameslee/.cc-switch/skills or
../../.agents/skills on a foreign machine) discarded
- 3 real name collisions with identical content (ai-pair, ifind-http-api,
zhipu-websearch) kept as one copy
- Functional overlaps deduped keeping the strongest variant:
- docx family: kept docx (official, full toolchain) + docx-cn
(GB/T 9704 Chinese official-document constants),
dropped docx_writer (no scripts, name collided with docx)
- humanizer family: kept humanizer-zh (6 zh reference docs),
dropped humanizer (en, redundant for CN workflow)
- Skills that only ran in a foreign environment removed:
ablemind-ops, app-publish, hlb-design-system, openclaw-adj-skill,
claude-driver
- alphapai excluded from this public repo because its SKILL.md hard-coded
live credentials
Result: 25 skills, 572 files, ~7.5 MB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
378 lines
12 KiB
Python
378 lines
12 KiB
Python
"""
|
||
Word文档格式化模块
|
||
按照GB/T 9704-2012标准格式化金融研究报告
|
||
"""
|
||
|
||
from docx import Document
|
||
from docx.shared import Pt, Mm, RGBColor
|
||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
||
from docx.oxml.ns import qn
|
||
from docx.oxml import OxmlElement
|
||
|
||
|
||
class GBT9704Formatter:
|
||
"""
|
||
GB/T 9704-2012党政机关公文格式化器
|
||
"""
|
||
|
||
# 字体映射(号数 -> 磅值)
|
||
FONT_SIZES = {
|
||
'三号': 16,
|
||
'四号': 14,
|
||
'小四': 12,
|
||
'五号': 10.5
|
||
}
|
||
|
||
def __init__(self, doc: Document):
|
||
"""
|
||
初始化格式化器
|
||
|
||
Args:
|
||
doc: Document对象
|
||
"""
|
||
self.doc = doc
|
||
self._setup_page()
|
||
|
||
def _setup_page(self):
|
||
"""设置页面格式"""
|
||
for section in self.doc.sections:
|
||
# 页面边距(单位:毫米)
|
||
section.top_margin = Mm(37)
|
||
section.bottom_margin = Mm(35)
|
||
section.left_margin = Mm(28)
|
||
section.right_margin = Mm(26)
|
||
|
||
def set_font(self, run, font_name: str, size: int = None, bold: bool = None, color: RGBColor = None):
|
||
"""
|
||
设置字体
|
||
|
||
Args:
|
||
run: Run对象
|
||
font_name: 字体名称(中文字体)
|
||
size: 字号(磅值),如 16, 14, 12
|
||
bold: 是否加粗
|
||
color: 字体颜色
|
||
"""
|
||
run.font.name = font_name
|
||
run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
|
||
|
||
if size is not None:
|
||
run.font.size = Pt(size)
|
||
if bold is not None:
|
||
run.font.bold = bold
|
||
if color is not None:
|
||
run.font.color.rgb = color
|
||
|
||
def add_heading(self, text: str, level: int = 1, font_name: str = '黑体', size: int = 16):
|
||
"""
|
||
添加标题
|
||
|
||
Args:
|
||
text: 标题文本
|
||
level: 标题级别(1-3)
|
||
font_name: 字体名称
|
||
size: 字号
|
||
|
||
Returns:
|
||
Paragraph对象
|
||
"""
|
||
if level == 1:
|
||
heading = self.doc.add_heading(text, level=1)
|
||
run = heading.runs[0]
|
||
self.set_font(run, font_name, size, bold=True)
|
||
heading.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||
heading.paragraph_format.space_before = Pt(0)
|
||
heading.paragraph_format.space_after = Pt(0)
|
||
return heading
|
||
else:
|
||
# 二级、三级标题使用"一、"、"(一)"等格式
|
||
prefix_map = {1: '一、', 2: '(一)', 3: '1.'}
|
||
prefix = prefix_map.get(level, '')
|
||
|
||
paragraph = self.doc.add_paragraph(f"{prefix}{text}")
|
||
run = paragraph.runs[0]
|
||
self.set_font(run, font_name, size, bold=True)
|
||
|
||
# 段落格式
|
||
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||
paragraph.paragraph_format.space_before = Pt(0)
|
||
paragraph.paragraph_format.space_after = Pt(0)
|
||
paragraph.paragraph_format.line_spacing = Pt(25)
|
||
paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
|
||
|
||
return paragraph
|
||
|
||
def add_paragraph(self, text: str,
|
||
font_name: str = '仿宋',
|
||
size: int = 14,
|
||
first_line_indent: bool = True,
|
||
alignment: str = 'justify'):
|
||
"""
|
||
添加正文段落
|
||
|
||
Args:
|
||
text: 段落文本
|
||
font_name: 字体名称
|
||
size: 字号(磅值)
|
||
first_line_indent: 是否首行缩进
|
||
alignment: 对齐方式('left', 'center', 'right', 'justify')
|
||
|
||
Returns:
|
||
Paragraph对象
|
||
"""
|
||
paragraph = self.doc.add_paragraph(text)
|
||
run = paragraph.runs[0]
|
||
self.set_font(run, font_name, size)
|
||
|
||
# 段落格式
|
||
alignment_map = {
|
||
'left': WD_ALIGN_PARAGRAPH.LEFT,
|
||
'center': WD_ALIGN_PARAGRAPH.CENTER,
|
||
'right': WD_ALIGN_PARAGRAPH.RIGHT,
|
||
'justify': WD_ALIGN_PARAGRAPH.JUSTIFY
|
||
}
|
||
paragraph.paragraph_format.alignment = alignment_map.get(alignment, WD_ALIGN_PARAGRAPH.JUSTIFY)
|
||
|
||
if first_line_indent:
|
||
# 首行缩进2字符(28pt)
|
||
paragraph.paragraph_format.first_line_indent = Pt(28)
|
||
|
||
# 行距固定值28pt
|
||
paragraph.paragraph_format.line_spacing = Pt(28)
|
||
paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
|
||
|
||
# 段前段后距为0
|
||
paragraph.paragraph_format.space_before = Pt(0)
|
||
paragraph.paragraph_format.space_after = Pt(0)
|
||
|
||
return paragraph
|
||
|
||
def add_bullet_list(self, items: List[str], font_name: str = '仿宋', size: int = 14):
|
||
"""
|
||
添加项目符号列表
|
||
|
||
Args:
|
||
items: 列表项
|
||
font_name: 字体名称
|
||
size: 字号
|
||
"""
|
||
for item in items:
|
||
paragraph = self.doc.add_paragraph(item, style='List Bullet')
|
||
run = paragraph.runs[0]
|
||
self.set_font(run, font_name, size)
|
||
paragraph.paragraph_format.line_spacing = Pt(28)
|
||
paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
|
||
paragraph.paragraph_format.space_before = Pt(0)
|
||
paragraph.paragraph_format.space_after = Pt(0)
|
||
|
||
def add_three_line_table(self, data: List[List[str]],
|
||
headers: List[str],
|
||
font_name: str = '仿宋',
|
||
size: int = 12):
|
||
"""
|
||
添加三线表
|
||
|
||
Args:
|
||
data: 表格数据(二维列表)
|
||
headers: 表头
|
||
font_name: 字体名称
|
||
size: 字号
|
||
|
||
Returns:
|
||
Table对象
|
||
"""
|
||
# 创建表格
|
||
table = self.doc.add_table(rows=len(data) + 1, cols=len(headers))
|
||
self._set_three_line_table_borders(table)
|
||
|
||
# 设置表头
|
||
header_cells = table.rows[0].cells
|
||
for i, header in enumerate(headers):
|
||
cell = header_cells[i]
|
||
self._set_cell_font(cell, header, font_name, size, bold=True)
|
||
cell.paragraphs[0].paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
||
# 设置数据行
|
||
for row_idx, row_data in enumerate(data, start=1):
|
||
cells = table.rows[row_idx].cells
|
||
for i, cell_data in enumerate(row_data):
|
||
cell = cells[i]
|
||
self._set_cell_font(cell, str(cell_data), font_name, size)
|
||
cell.paragraphs[0].paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
||
return table
|
||
|
||
def _set_three_line_table_borders(self, table):
|
||
"""设置三线表边框"""
|
||
tblPr = table._tbl.tblPr
|
||
tblBorders = OxmlElement('w:tblBorders')
|
||
|
||
# 顶底边框 1.5pt (sz=18)
|
||
for pos in ['top', 'bottom']:
|
||
border = OxmlElement(f'w:{pos}')
|
||
border.set(qn('w:val'), 'single')
|
||
border.set(qn('w:sz'), '18')
|
||
border.set(qn('w:color'), 'auto')
|
||
tblBorders.append(border)
|
||
|
||
# 内部横线 0.75pt (sz=9)
|
||
for pos in ['insideH']:
|
||
border = OxmlElement(f'w:{pos}')
|
||
border.set(qn('w:val'), 'single')
|
||
border.set(qn('w:sz'), '9')
|
||
border.set(qn('w:color'), 'auto')
|
||
tblBorders.append(border)
|
||
|
||
# 无左右和竖线
|
||
for pos in ['left', 'right', 'insideV']:
|
||
border = OxmlElement(f'w:{pos}')
|
||
border.set(qn('w:val'), 'none')
|
||
tblBorders.append(border)
|
||
|
||
tblPr.append(tblBorders)
|
||
|
||
def _set_cell_font(self, cell, text: str, font_name: str, size: int, bold: bool = False):
|
||
"""设置单元格字体"""
|
||
cell.text = ''
|
||
run = cell.paragraphs[0].add_run(text)
|
||
self.set_font(run, font_name, size, bold=bold)
|
||
|
||
def add_page_number_footer(self):
|
||
"""添加页码页脚"""
|
||
for section in self.doc.sections:
|
||
footer = section.footer
|
||
|
||
# 添加页码段落
|
||
p = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
|
||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
||
# 页码格式:— 1 —
|
||
self.add_footer_run(p, '— ', font_name='仿宋', size=12)
|
||
|
||
# 添加页码字段
|
||
self.add_page_number_field(p)
|
||
|
||
self.add_footer_run(p, ' ', font_name='仿宋', size=12)
|
||
self.add_footer_run(p, '—', font_name='仿宋', size=12)
|
||
|
||
def add_footer_run(self, paragraph, text: str, font_name: str, size: int):
|
||
"""添加页脚run"""
|
||
run = paragraph.add_run(text)
|
||
self.set_font(run, font_name, size)
|
||
|
||
def add_page_number_field(self, paragraph):
|
||
"""添加页码字段"""
|
||
fldChar1 = OxmlElement('w:fldChar')
|
||
fldChar1.set(qn('w:fldCharType'), 'begin')
|
||
|
||
instrText = OxmlElement('w:instrText')
|
||
instrText.set(qn('xml:space'), 'preserve')
|
||
instrText.text = "PAGE"
|
||
|
||
fldChar2 = OxmlElement('w:fldChar')
|
||
fldChar2.set(qn('w:fldCharType'), 'end')
|
||
|
||
run = paragraph.add_run()
|
||
run._r.append(fldChar1)
|
||
run._r.append(instrText)
|
||
run._r.append(fldChar2)
|
||
|
||
self.set_font(run, '仿宋', 12)
|
||
|
||
def add_cover_page(self, title: str, organization: str, date_str: str):
|
||
"""
|
||
添加封面页
|
||
|
||
Args:
|
||
title: 报告标题
|
||
organization: 机构名称
|
||
date_str: 日期
|
||
"""
|
||
# 标题
|
||
paragraph = self.doc.add_paragraph()
|
||
run = paragraph.add_run(title)
|
||
self.set_font(run, '黑体', 22, bold=True)
|
||
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
paragraph.paragraph_format.space_before = Pt(100)
|
||
paragraph.paragraph_format.space_after = Pt(50)
|
||
|
||
# 机构名称
|
||
paragraph = self.doc.add_paragraph()
|
||
run = paragraph.add_run(organization)
|
||
self.set_font(run, '黑体', 16)
|
||
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
paragraph.paragraph_format.space_after = Pt(20)
|
||
|
||
# 日期
|
||
paragraph = self.doc.add_paragraph()
|
||
run = paragraph.add_run(date_str)
|
||
self.set_font(run, '黑体', 16)
|
||
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||
|
||
# 添加分页符
|
||
self.doc.add_page_break()
|
||
|
||
def add_data_source_footnote(self, text: str = "数据来源:Wind资讯"):
|
||
"""
|
||
添加数据来源注脚
|
||
|
||
Args:
|
||
text: 注脚文本
|
||
"""
|
||
paragraph = self.doc.add_paragraph()
|
||
run = paragraph.add_run(text)
|
||
self.set_font(run, '仿宋', 12)
|
||
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||
|
||
|
||
# 便捷函数
|
||
def create_formatted_document() -> Tuple[Document, GBT9704Formatter]:
|
||
"""
|
||
创建格式化的Word文档
|
||
|
||
Returns:
|
||
(Document, GBT9704Formatter) 文档对象和格式化器
|
||
"""
|
||
doc = Document()
|
||
formatter = GBT9704Formatter(doc)
|
||
return doc, formatter
|
||
|
||
|
||
if __name__ == '__main__':
|
||
# 测试代码
|
||
doc, formatter = create_formatted_document()
|
||
|
||
# 添加封面
|
||
formatter.add_cover_page(
|
||
'某某股票投资研究报告',
|
||
'某某证券研究所',
|
||
'2026年2月6日'
|
||
)
|
||
|
||
# 添加核心提要
|
||
formatter.add_heading('核心提要', level=1)
|
||
formatter.add_bullet_list([
|
||
'该公司2025年三季度净资产收益率(ROE)为15.2%,盈利能力较强',
|
||
'当前市盈率(TTM)为25.3倍,略高于行业平均水平',
|
||
'技术面显示股价呈现震荡上行趋势,建议关注回调机会',
|
||
'给予"买入"评级,目标价格35元'
|
||
])
|
||
|
||
# 添加投资评级表格
|
||
formatter.add_heading('一、投资评级', level=1)
|
||
data = [
|
||
['买入', '35.00元', '2026年12月31日', '中风险']
|
||
]
|
||
formatter.add_three_line_table(
|
||
data,
|
||
['投资建议', '目标价格', '有效期', '风险等级']
|
||
)
|
||
|
||
# 添加数据来源
|
||
formatter.add_data_source_footnote()
|
||
|
||
# 保存文档
|
||
output_path = 'test_report.docx'
|
||
doc.save(output_path)
|
||
print(f"测试报告已保存到: {output_path}")
|