# -*- coding: utf-8 -*- """ 路口巡检周报自动生成脚本 (适配新数据结构版) 数据结构映射: 1. 大标题 -> data['title'] (城市 + 周数) 2. 正文/日期 -> data['date'] 和 data['part1'] 3. 表格 -> data['part2'] """ import io import logging import re 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) tabel_style = get_style_table_cell(doc) # 3. 处理表格 process_merged_table(doc, data) for table in doc.tables: if len(table.rows) > 0 and len(table.rows[0].cells) == 5: for row_index, row in enumerate(table.rows): if row_index == 0: continue columns = [table.columns[0], table.columns[1], table.columns[2]] for column in columns: for cell in column.cells: set_cell_content(cell, cell.text.replace('\n', ''), tabel_style) 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': '福州市', 'week_num': 19}, 'date': '2026年05月12日', 'part1': {'total': 3, 'normal_cross_num': 3, 'focus_cross_num': 0, 'error_cross_num': 2, 'usually_err_info': '异常最多的巡检项为机动车多次排队(2个)、路口周期过大(2个)。', 'phase_err_num': '需要优化路口方案的路口0个', 'static_org_num': '需要调整交通组织路口2个', 'static_device_num': '需要调整交通设施路口1个'}, 'part2': {'data': [{'name': '台账录入情况', 'item_data': [{'name': '录入渠化台账信息与现场不符', 'num': 0, 'item_data': []}, {'name': '录入配时方案与实际运行方案不符', 'num': 0, 'item_data': []}]}, {'name': '路口运行情况', 'item_data': [{'name': '机动车多次排队', 'num': 2, 'item_data': [{'cross_name': '康达路与清展街交叉口', 'process_func': ''}, {'cross_name': '清展街与福和路交叉口', 'process_func': '调整交通组织,其他处置措施'}]}, {'name': '停车次数较高', 'num': 0, 'item_data': []}]}, {'name': '配时方案情况', 'item_data': [{'name': '路口周期过大', 'num': 2, 'item_data': [{'cross_name': '康达路与清展街交叉口', 'process_func': ''}, {'cross_name': '清展街与福和路交叉口', 'process_func': '调整交通组织,其他处置措施'}]}, {'name': '行人过街时间不足', 'num': 0, 'item_data': []}]}, {'name': '设备设施情况', 'item_data': [{'name': '信号灯缺、损', 'num': 0, 'item_data': []}]}]}} fill_report_template( report_data ) print("✅ 报告生成完成!已适配新数据结构。")