python 制作复杂表格报告
python 制作复杂表格报告
最近用odoo集成检测系统,有一复杂表格报告需要处理,即要用到数据库中详细实验信息,检测项格式也不统一,在word中需要有宣然,有列合并,有行合并等
用Python生成Word表格的问题,特别是处理行合并和行高计算。用户提供的test_data结构比较复杂,每一行有多个字段,需要根据aa、bb、cc列的内容进行合并,同时还要处理跨列合并,比如cc列与xm列的合并。此外,行高的计算需要根据内容长度和合并情况动态调整,确保表格在Word中正确显示,尤其是跨页的情况。
使用python-docx库,因此我需要熟悉该库的表格操作,特别是合并单元格和调整行高的方法。可能的问题点包括如何动态合并行和列,如何计算每行的最大行高,以及如何处理分页。
关于合并行和列,需要先遍历数据,确定哪些行需要合并。例如,对于前三列,需要检查当前行与前后行的aa、bb、cc是否相同,如果相同则标记需要合并。这里可能需要维护一个变量来记录合并的起始和结束位置。
行高的计算是关键。要根据内容长度和合并的列数来计算。例如,如果aa列合并了n行,则行高为该列内容长度除以n,但实际中可能需要考虑字体大小、列宽等因素。此外,当cc和xm列合并时,内容占据两列,行高计算需考虑两列的宽度总和。可能需要估算每列能容纳的字符数,再根据内容长度计算所需行数,然后乘以行高系数。
每页单独一个表格,这意味着当数据超过一页容量时,需要分割数据到不同的表格中。这可能需要先计算每页能容纳的行数,根据合并后的行高总和是否超过页面高度来决定分页点。
预处理数据时,可以创建一个辅助结构,记录每一行的合并信息。例如,对于每一行,记录前三列是否需要合并,以及合并的行数。这可能涉及到比较当前行与前一行和后一行的值。
计算行高时,可能需要估算每个单元格的文本所占的行数。例如,假设列宽已知,可以计算每列能容纳的字符数,然后根据实际内容的长度除以每列字符数,得到所需的行数。合并列的情况下,列宽相加,所以字符数也相加。例如,合并cc和xm列,总列宽为两列之和,因此能容纳的字符数更多,所需行数可能更少。
然而,python-docx的行高设置可能需要具体的高度值,或者自动调整。可能需要设置行高为至少某个值,或者让Word自动处理,但用户希望精确控制,所以需要手动计算。
分页处理可能需要计算每页的高度,假设每页的高度固定(如A4纸的高度),减去页边距,得到可用高度。然后累加每行的行高,直到超过可用高度,此时分页。
但在实际中,这可能比较复杂,因为行高可能动态变化,且Word的渲染可能有自己的逻辑。用户可能希望每个表格单独在一页,因此可能需要根据合并后的行数判断是否分页,或者简单地将每个表格放在新的一页,根据数据量拆分多个表格。
以下是一个详细的Python代码实现,使用python-docx库处理表格合并和行高计算。代码包含详细注释说明每个步骤的作用:
from docx import Document
from docx.shared import Cm, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL
from docx.oxml.shared import qn
import mathdef set_font(run, font_name='宋体', font_size=Pt(12), bold=False):"""强制设置中文字体""""""安全设置中文字体和字号"""# 设置字体run.font.name = font_namerun._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)# 设置字号run.font.size = font_sizerun.bold = boldclass SmartPaginationReport:def __init__(self,mbfile, filename):# 模板file 没有self.doc = Document(mbfile)# DocxTemplate(mbfile) ##Document()self.mbfile= mbfileself.filename = filenameself._setup_document()# 页面尺寸参数(单位:厘米)self.USABLE_PAGE_HEIGHT = 300 ##self.PAGE_HEIGHT - self.PAGE_TOP_MARGIN - self.PAGE_BOTTOM_MARGIN# 初始化分页参数self.current_page_height = 0self.current_table = Noneself.zongye =1self.current_page = 1self.yyhangshu =[0]def _setup_document(self):"""初始化文档设置"""section = self.doc.sections[0]section.page_width = Cm(20.5)section.left_margin = Cm(1.5)section.right_margin = Cm(1.5)section.top_margin = Cm(1.5)section.bottom_margin = Cm(1.5)# 设置全局样式(小四号/12磅)style = self.doc.styles['Normal']style.font.name = '宋体'style.font.size = Pt(10)style._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')self.mydata=[{'yeshu':1,'yhangshu':0,'xhs':0,'weihid':0,'weihny':''}]def _set_changm_taitou(self):# 添加公司标题(小三号/14磅)company = self.doc.add_paragraph()company_run = company.add_run("河南豫质信检验检测有限公司\n检验检测报告")company_run.bold = Trueset_font(company_run, font_size=Pt(20)) # 标题字号 16三号,18小二号,company.alignment = WD_ALIGN_PARAGRAPH.CENTERp = self.doc.add_paragraph()num_run = p.add_run("№: 20250423005")# p.add_run("№: 20250423005").bold = Trueset_font(num_run, font_size=Pt(10.5))p.add_run(" " * 65 + "共 "+str(self.zongye)+" 页 第 "+str(self.current_page)+" 页")def _set_biaotou(self,table, ypm, ggxh="/"):# 合并样品名称行cell = table.cell(0, 0)cell.merge(table.cell(0, 1))paragraph = cell.paragraphs[0]# 添加第一个文本块(宋体+加粗)run1 = paragraph.add_run("样品名称")set_font(run1, font_name='宋体', font_size=Pt(10))# run1.font.size = Pt(10)# 添加第二个文本块(楷体+常规)run2 = paragraph.add_run("\nSample")set_font(run2, font_name='Times New Roman', font_size=Pt(11), bold=True)cell = table.cell(0, 2)cell.merge(table.cell(0, 5))cell.text = ypm# 应用对齐设置self._set_cell_alignment(cell, "center", "center") # 左右居左,上下居中cell = table.cell(0, 6)cell.merge(table.cell(0, 7))paragraph = cell.paragraphs[0]run1 = paragraph.add_run("规格型号\n")set_font(run1, font_name='宋体', font_size=Pt(10))# 添加第二个文本块(楷体+常规)run2 = paragraph.add_run("Model")set_font(run2, font_name='Times New Roman', font_size=Pt(11), bold=True)cell = table.cell(0, 8)# cell.merge(table.cell(0, 5))cell.text = ggxhself._set_cell_alignment(cell, "center", "center")# 构建复杂表头header_data = [(1, 0, 0, "序号\n", "№"),(1, 1, 3, "检验项目\n", "Items"),(1, 4, 4, "单位\n", "Unit"),(1, 5, 5, "检验方法依据\n", "Standards"),(1, 6, 6, "标准要求\n", "Specification"),(1, 7, 7, "检验结果\n", "Test Data"),(1, 8, 8, "单项结论\n", "Conclusion")]# 设置表头合并for merge_info in header_data:row, col_start, col_end, text, text2 = merge_infocell = table.cell(row, col_start)if col_end != col_start:cell.merge(table.cell(row, col_end))# cell.text = textparagraph = cell.paragraphs[0]run1 = paragraph.add_run(text)set_font(run1, font_name='宋体', font_size=Pt(10))# 添加第二个文本块(楷体+常规)run2 = paragraph.add_run(text2)set_font(run2, font_name='Times New Roman', font_size=Pt(11), bold=True)cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTERdef _set_cell_alignment(self,cell, horizontal="left", vertical="center"):"""设置单元格对齐方式horizontal: left/center/rightvertical: top/center/bottom"""# 设置垂直对齐vertical_map = {"top": WD_ALIGN_VERTICAL.TOP,"center": WD_ALIGN_VERTICAL.CENTER,"bottom": WD_ALIGN_VERTICAL.BOTTOM}cell.vertical_alignment = vertical_map[vertical]# 设置水平对齐horizontal_map = {"left": WD_ALIGN_PARAGRAPH.LEFT,"center": WD_ALIGN_PARAGRAPH.CENTER,"right": WD_ALIGN_PARAGRAPH.RIGHT}for paragraph in cell.paragraphs:paragraph.alignment = horizontal_map[horizontal]def _check_page_break(self, row_height):"""检查是否需要分页"""# 已用高度 + 新行高 + 表头高 > 可用高度header_height = 150 # 表头估算高度(磅)total_required = self.current_page_height + row_height + header_heightreturn total_required > self.USABLE_PAGE_HEIGHTdef _create_new_page(self):"""创建新页"""if self.current_table is not None:self.doc.add_page_break()self._set_changm_taitou()# 创建新表格self.current_table = self.doc.add_table(rows=2, cols=9)self.current_table.style = 'Table Grid'self.current_table.autofit = False# 设置表格总宽度(填满页面可用宽度) 页面可以宽度,每个单元格是不是还要加线宽# 设置表格总宽度(A4横向约26.7厘米)available_width_cm = 18# self.current_table.width = Cm(available_width_cm)# self.current_table.width = self.available_width # 约26.7cm(A4横向)# 设置列宽比例(根据实际需求调整)column_ratios = [1.5, 1, 3, 3, 2, 4, 4, 3, 3.2]total_ratio = sum(column_ratios)# 按比例分配列宽(确保列宽不低于Word最小限制)for col_idx, ratio in enumerate(column_ratios):col = self.current_table.columns[col_idx]# 计算列宽(厘米)col_width_cm = max(available_width_cm * (ratio / total_ratio), 0.63) # 强制最小宽度 0.63cmcol.width = Cm(col_width_cm)# 同时设置列中所有单元格的宽度(确保兼容性)for cell in col.cells:cell.width = Cm(col_width_cm)# 添加表头# self._create_table_header() # 没合并前多页时,也增加了表头,处理麻烦self._set_biaotou(self.current_table, '要子第三方尽快')self.current_page_height = 0 # 重置当前页高度def preprocess_data(self,data):processed = []# 确定前三列的合并范围merge_ranges = self.find_merge_ranges(data)print(merge_ranges)# 计算每行最大行高for i, row_data in enumerate(data):# 获取当前行的合并信息aa_range = merge_ranges['aa'][i]bb_range = merge_ranges['bb'][i]hang = merge_ranges['hang'][i]xh = merge_ranges['xh']if len(row_data['aa']) == 0: ##空前两例 只有项目没目录 前三列宽,aa_height = 1bb_height = 1ccxm_height = self.calc_cell_height(row_data['xm'], col_width=2 * 3) # 合并参列,宽主参elif len(row_data['bb']) == 0: ##一级目录bb为空,merged_rows = aa_range[1] - aa_range[0] + 1if merged_rows > 0:## 第一列上下行合并,第一列列宽不变,后面二,三列合为检项用aa_height = self.calc_cell_height(row_data['aa'], col_width=2 * 1,merged_rows=aa_range[1] - aa_range[0] + 1) # 合并两列,宽关键bb_height = 1ccxm_height = self.calc_cell_height(row_data['xm'], col_width=2 * 2) # 只计算xm占两列正常行高else: ## 第一 ,二列合并aa_height = self.calc_cell_height(row_data['aa'], col_width=2 * 2, merged_rows=2)bb_height = 1ccxm_height = self.calc_cell_height(row_data['xm'], col_width=2 * 1) # 只计算xm占一列正常行高else: # 两目录,aa,bb 均有值,# 计算各列所需高度aa_height = self.calc_cell_height(row_data['aa'], col_width=2, merged_rows=aa_range[1] - aa_range[0] + 1)bb_height = self.calc_cell_height(row_data['bb'], col_width=2, merged_rows=bb_range[1] - bb_range[0] + 1)ccxm_height = self.calc_cell_height(row_data['xm'], col_width=2 * 1, ) # 只计算xm占一列正常行高# 取最大行高max_h = max(aa_height, bb_height, ccxm_height)processed.append({'data': row_data,'aa_merge': aa_range,'bb_merge': bb_range,'hang_merge': hang,'max_row_height': max_h,'xh':xh})return processeddef find_merge_ranges(self,data):# 为每个列找连续相同的区间,每列中上下相同的起始点,{'aa': {0: (0, 2), 1: (0, 2), 2: (0, 2), 3: (3, 4), 4: (3, 4)}, 'bb':ranges = {'aa': {}, 'bb': {}, 'hang': {},'xh':{}}for col in ['aa', 'bb']:start = 0for i in range(1, len(data)):# print(data[i][col],len(data[i][col]))if data[i][col] != data[i - 1][col]:if len(data[i - 1][col]) == 0:for j in range(start, i):ranges[col][j] = (j, j)else:for j in range(start, i):ranges[col][j] = (start, i - 1)start = i# print(ranges[col])# 处理最后一组for j in range(start, len(data)):# print('最后行',data[i][col], len(data[i][col]))ranges[col][j] = (start, len(data) - 1)# 计算行合并for i in range(0, len(data)):if len(data[i]['aa']) == 0: ##空前两例 只有项目没目录 前三列宽,ranges['hang'][i] = (0, 2)elif len(data[i]['bb']) == 0: ## 第一 ,二列合并 此处估计应加上前两列合并的情况if ranges['aa'][i][0] == ranges['aa'][i][1]: #前一列没重复的,直接前两列合并ranges['hang'][i] = (0, 1)else:ranges['hang'][i] = (1, 2)else:ranges['hang'][i] = (0, 0)# 填加序号xhs =0for i, dd in enumerate(ranges['aa']):print(dd)if ranges['aa'][dd][0] == i:xhs =xhs+1ranges['xh'][i] = xhs# data[i]['id']=xhsprint('ranges',ranges['xh'])return rangesdef calc_cell_height(self, text, col_width, merged_rows=1):# 假设每列字符容量:1英寸≈6字符(根据实际调整)char_per_line = col_width * 6lines = math.ceil(len(text) / char_per_line)# 行高=基础高度×行数 / 合并行数(平均分配)base_height = 15 # 每行基础高度(磅)return max(base_height, base_height * lines / merged_rows)def _add_table_row(self, row_data):"""添加表格行并更新高度"""row = self.current_table.add_row()for col, kny in enumerate(row_data['data']):cell = row.cells[col]cell.text = str(row_data['data'][kny])self._set_cell_alignment(cell,'center','center')self.current_page_height += row_data['max_row_height']def table_merge(self,table, data):# 单元格因前面没考虑到序号列,所以列数加了1,行数因为表头占两行,所以要加2print('需合并数据',data)for row_idx, p in enumerate(data):# 合并aa列# print(p)start1, end1 = p['aa_merge']start =start1-self.yyhangshu[self.current_page-2]end = end1 - self.yyhangshu[self.current_page-2]if row_idx == start:if end > start:# print(end, start)tempcell=table.cell(start+2,1)tempt = tempcell.text# print('aa列',tempt)tempcell.merge(table.cell(end+2, 1))tempcell.text = temptself._set_cell_alignment(tempcell, 'center', 'center')# 合并序号temcellxh = table.cell(start + 2, 0)temptxh = str(p['xh'][row_idx])temcellxh.merge(table.cell(end + 2, 0))temcellxh.text = temptxhself._set_cell_alignment(tempcell, 'center', 'center')self._set_cell_alignment(temcellxh, 'center', 'center')else:table.cell(start + 2, 0).text=str(p['xh'][row_idx])self._set_cell_alignment(table.cell(start + 2, 0), 'center', 'center')# 合并BB列start, end = p['bb_merge']if row_idx == start:if end > start:# print('合B', end, start)tempt = table.cell(start+2, 2).texttable.cell(start+2, 2).merge(table.cell(end+2, 2))table.cell(start+2, 2).text = temptself._set_cell_alignment(table.cell(start+2, 2), 'center', 'center')for row_idx, p in enumerate(data):# # 合并行start, end = p['hang_merge']# print('ppp', end, start)# print('rowdx', row_idx)if end > start:if end - start == 2:tempt = table.cell(row_idx+2, 3).text# print('取不到值 ',tempt)mergcell = table.cell(row_idx+2, 1)else: # if end -start ==1:if start ==0:tempt = table.cell(row_idx+2, 1).textmergcell=table.cell(row_idx+2, 1)else:tempt = table.cell(row_idx+2, 3).textmergcell = table.cell(row_idx+2, 2)print('合ew B', tempt)mergcell.merge(table.cell(row_idx+2, end+1))mergcell.text = temptself._set_cell_alignment(mergcell, 'center', 'center')def generate_report(self,test_data):"""生成带智能分页的报告"""# 预处理:确定合并范围和计算行高processed = self.preprocess_data(test_data)myhs =0for i,row_data in enumerate(processed):# 估算当前行高度row_height = row_data['max_row_height']#self._estimate_row_height(row_data)# print('表格最大行高', row_height)# 分页条件判断if self.current_table is None:# print('新建页吧')self.zongye +=1self.current_page +=1self._create_new_page()meiyshuju= []if self._check_page_break(row_height):print('新建页吧',self.current_page)self.table_merge(self.current_table, meiyshuju)self.current_table = Noneprint('合并页后第',self.current_page)self.zongye +=1self.current_page +=1self.yyhangshu.append(myhs)meiyshuju = []self._create_new_page()# print(self.current_table.width)# 添加行数据self._add_table_row(row_data)meiyshuju.append(row_data)myhs += 1# 保存文档self.doc.save(self.filename)# 模拟不同高度的测试数据
test_data =[ {'id':'','aa':"外观性能",'bb':"",'xm':'车消部件','dw':'/','jyffyj':'GBsdfds5564','bzyq':'格含多行内容的单元','jcjg':'','dxjl':'合格'},{'id':'','aa':"外观性能",'bb':"",'xm':'零部件','dw':'/','jyffyj':'GBsdfds5564','bzyq':'含多行内多行格含多行内容的单元','jcjg':'','dxjl':'合格'},{'id':'','aa':"外观性能",'bb':"刚制件",'xm':'55零部件','dw':'/','jyffyj':'GBsdfds5564','bzyq':'需要跨页的非常长的描述内容需描述内容需要跨页的非常长的描述内容','jcjg':'','dxjl':'合格'},{'id':'','aa':"",'bb':"",'xm':'55零部件','dw':'/','jyffyj':'GBsdfds5564','bzyq':'aaaaaaaaaa需要跨页的非内容需要跨页的非常长的描述内容','jcjg':'','dxjl':'合格'},{'id':'','aa':"主要尺寸",'bb':"纯森",'xm':'55零部件','dw':'/','jyffyj':'GBsdfds5564','bzyq':'需要跨页的非常长的描述内容需要跨页的非常长的描述内容需要跨页的非常长的描述内容','jcjg':'','dxjl':'合格'}]*5if __name__ == '__main__':mbfile = r'd:\od172406\常用文件\word操作\家具报告模板3.docx'report = SmartPaginationReport(mbfile,"A打开文件添分页数据.docx")report.generate_report(test_data)
生成实际效果图