cross_doctor/tool/cross_monitor_week_report.py

346 lines
13 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""
路口巡检周报自动生成脚本 (适配新数据结构版)
数据结构映射
1. 大标题 -> data['title'] (城市 + 周数)
2. 正文/日期 -> data['date'] data['part1']
3. 表格 -> data['part2']
"""
import io
from datetime import datetime
from docx import Document
from docx.shared import Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL
from docx.oxml.ns import qn
# ================= 1. 样式定义函数 =================
from tool.qcos_func import g_cos_bucket, g_cos_root
from tool.export_cross_monitor_task_file import folder_manager, cos_client
def get_style_body(doc):
"""正文样式:宋体,小四"""
style_name = '自定义正文'
try:
return doc.styles[style_name]
except KeyError:
style = doc.styles.add_style(style_name, 1)
style.font.name = '宋体'
style.font.size = Pt(12) # 小四
style.font._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
# 设置行距为1.5倍,看起来更舒服
pf = style.paragraph_format
pf.line_spacing = 1.5
return style
def get_style_title(doc):
"""大标题样式:宋体,小二,加粗"""
style_name = '自定义大标题'
try:
return doc.styles[style_name]
except KeyError:
style = doc.styles.add_style(style_name, 1)
style.font.name = '宋体'
style.font.size = Pt(18) # 小二
style.font.bold = True
style.font._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
pf = style.paragraph_format
pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
pf.space_before = Pt(12) # 稍微加点段前距
pf.space_after = Pt(6)
return style
def get_style_table_cell(doc):
"""表格样式:宋体,五号,无缩进"""
style_name = '自定义表格内容'
try:
return doc.styles[style_name]
except KeyError:
style = doc.styles.add_style(style_name, 1)
style.font.name = '宋体'
style.font.size = Pt(10.5) # 五号
style.font._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
pf = style.paragraph_format
pf.first_line_indent = Cm(0)
pf.left_indent = Cm(0)
pf.right_indent = Cm(0)
pf.space_before = Pt(0)
pf.space_after = Pt(0)
pf.line_spacing = 1.0
pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
return style
# ================= 2. 业务逻辑函数 =================
def set_title(doc, data):
"""
处理大标题
逻辑在文档开头插入或替换第一行作为标题
"""
title_style = get_style_title(doc)
title_text = f"{data['title']['city']}市第{data['title']['week_num']}周路口巡检周报"
# 检查文档第一行是否为空或包含旧标题,这里简单处理:直接替换第一行
if len(doc.paragraphs) > 0:
doc.paragraphs[0].text = title_text
doc.paragraphs[0].style = title_style
else:
# 如果文档为空(极少见),添加一个段落
doc.add_paragraph(title_text, style=title_style)
def replace_basic_placeholders(doc, data):
"""
替换正文占位符 (日期 + 第一部分数据)
应用样式自定义正文 (宋体, 小四)
"""
body_style = get_style_body(doc)
# 组合所有需要替换的键值对
# 注意:日期单独处理,其他数据来自 part1
placeholders = {
'{{date}}': str(data.get('date', '')),
'{{total}}': str(data.get('part1', {}).get('total', '')),
'{{normal_cross_num}}': str(data.get('part1', {}).get('normal_cross_num', '')),
'{{focus_cross_num}}': str(data.get('part1', {}).get('focus_cross_num', '')),
'{{error_cross_num}}': str(data.get('part1', {}).get('error_cross_num', '')),
'{{usually_err_info}}': str(data.get('part1', {}).get('usually_err_info', '')),
'{{phase_err_num}}': str(data.get('part1', {}).get('phase_err_num', '')),
'{{static_org_num}}': str(data.get('part1', {}).get('static_org_num', '')),
'{{static_device_num}}': str(data.get('part1', {}).get('static_device_num', ''))
}
# 从第二个段落开始遍历(跳过标题)
for paragraph in doc.paragraphs[1:]:
contains_placeholder = any(key in paragraph.text for key in placeholders.keys())
if contains_placeholder:
for key, value in placeholders.items():
if key in paragraph.text:
paragraph.text = paragraph.text.replace(key, value)
# 强制应用正文样式
paragraph.style = body_style
def set_cell_content(cell, text, style):
"""写入单元格并应用样式"""
cell.text = str(text)
paragraph = cell.paragraphs[0]
paragraph.style = style
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
def merge_cells_in_column(table, start_row, end_row, col_index):
if start_row >= end_row:
return
start_cell = table.cell(start_row, col_index)
end_cell = table.cell(end_row, col_index)
start_cell.merge(end_cell)
def add_merged_table_rows(table, table_data, cell_style):
"""
填充表格数据最终适配你的 dict 结构版
"""
merge_list = []
current_row_idx = 1 # 0是表头数据从索引1开始
for item in table_data:
item_name = item.get('name', '') # 一级类目
item_data_list = item.get('item_data', [])
item_start_row = current_row_idx
item_total_rows = 0
for a in item_data_list:
a_name = a.get('name', '') # 二级类目
a_num = a.get('num', '')
a_item_data = a.get('item_data', []) # 路口列表
# 兜底逻辑:如果没有路口数据,给一个默认行
if not a_item_data:
a_item_data = [{'cross_name': '-', 'process_func': '-'}]
a_start_row = current_row_idx
a_total_rows = len(a_item_data)
for idx, b in enumerate(a_item_data):
row = table.add_row()
cells = row.cells
# 填充最右侧两列(路口名称、处理措施)
set_cell_content(cells[3], b.get('cross_name', ''), cell_style)
set_cell_content(cells[4], b.get('process_func', ''), cell_style)
# 填充中间两列(二级类目、异常数)- 只有该组的第一行填
if idx == 0:
set_cell_content(cells[1], a_name, cell_style)
set_cell_content(cells[2], a_num, cell_style)
else:
set_cell_content(cells[1], '', cell_style)
set_cell_content(cells[2], '', cell_style)
# 填充最左侧列(一级类目)- 只有整个大类的第一行填
if item_total_rows == 0 and idx == 0:
set_cell_content(cells[0], item_name, cell_style)
else:
set_cell_content(cells[0], '', cell_style)
current_row_idx += 1
# 记录二级类目的合并范围
if a_total_rows > 1:
merge_list.append((1, a_start_row, current_row_idx - 1))
merge_list.append((2, a_start_row, current_row_idx - 1))
item_total_rows += a_total_rows
# 记录一级类目的合并范围
if item_total_rows > 1:
merge_list.append((0, item_start_row, current_row_idx - 1))
# 从下往上执行合并(防止索引错乱)
merge_list.sort(key=lambda x: x[1], reverse=True)
for col_idx, start_idx, end_idx in merge_list:
try:
top_cell = table.cell(start_idx, col_idx)
bottom_cell = table.cell(end_idx, col_idx)
top_cell.merge(bottom_cell)
except Exception as e:
print(f"合并单元格失败: {e}")
def process_merged_table(doc, data):
table_data = data.get('part2', {}).get('data', [])
table_cell_style = get_style_table_cell(doc)
for table in doc.tables:
# 锁定目标表格
if len(table.rows) > 0 and len(table.rows[0].cells) == 5:
header_texts = [cell.text.strip() for cell in table.rows[0].cells]
if '巡检类型' in header_texts and '巡检项' in header_texts:
# 1. 清空模板里的示例行(只保留表头)
while len(table.rows) > 1:
table._tbl.remove(table.rows[1]._tr)
# 2. 填充数据并合并
add_merged_table_rows(table, table_data, table_cell_style)
# 3. 恢复样式
table.style = 'Table Grid'
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
if paragraph.text.strip() not in ['巡检类型', '巡检项', '异常路口数', '异常路口', '处理措施']:
paragraph.style = table_cell_style
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
break
def fill_report_template(data):
doc = Document('./tool/XX市第XX周路口巡检周报模板.docx')
# 1. 设置大标题
set_title(doc, data)
# 2. 替换正文和日期
replace_basic_placeholders(doc, data)
# 3. 处理表格
process_merged_table(doc, data)
city_name = data.get('title', {}).get('city', '')
week_num = data.get('title', {}).get('week_num', '')
file_stream = io.BytesIO()
doc.save(file_stream)
file_stream.seek(0)
doc.save('./text.docx')
cos_path = f'user/cross_doctor/task_file'
folder_manager.ensure_folder(cos_path)
cos_key = f"{cos_path}/{city_name}{week_num}周路口巡检周报.docx"
cos_client.put_object(Bucket=g_cos_bucket, Key=cos_key, Body=file_stream)
download_url = f'{g_cos_root}/{cos_key}'
print(download_url)
return download_url
# ================= 3. 测试运行 =================
if __name__ == "__main__":
# 使用您提供的新数据结构
report_data = {
'title': {
'city': 'XX',
'week_num': 15,
},
'date': '2024年4月8日-2024年4月14日',
'part1': {
'total': 120,
'normal_cross_num': 85,
'focus_cross_num': 35,
'error_cross_num': 8,
'usually_err_info': '信号灯故障3起',
'phase_err_num': '相位异常2起',
'static_org_num': '标志标线问题2起',
'static_device_num': '设备损坏1起',
},
'part2': {
'data': [
{
'name': '信号系统',
'item_data': [
{
'name': '信号灯',
'num': 3,
'item_data': [
{'cross_name': '中山路-解放路', 'process_func': '已更换LED模块'},
{'cross_name': '人民路-建设路', 'process_func': '已修复电源线路'},
{'cross_name': '光明路-新华路', 'process_func': '已调整配时方案'}
]
},
{
'name': '相位控制',
'num': 2,
'item_data': [
{'cross_name': '胜利路-和平路', 'process_func': '已重新配置相位'},
{'cross_name': '东风路-红旗路', 'process_func': '已优化绿信比'}
]
}
]
},
{
'name': '静态设施',
'item_data': [
{
'name': '交通标志',
'num': 2,
'item_data': [
{'cross_name': '文化路-教育路', 'process_func': '已更换破损标志'},
{'cross_name': '体育路-健康路', 'process_func': '已补充缺失标线'}
]
},
{
'name': '设备设施',
'num': 1,
'item_data': [
{'cross_name': '科技路-创新路', 'process_func': '已维修检测设备'}
]
}
]
}
]
}
}
fill_report_template(
report_data
)
print("✅ 报告生成完成!已适配新数据结构。")