skills/financial-report-writing/scripts/document_formatter.py

378 lines
12 KiB
Python
Raw Normal View History

"""
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}")