今天遇到问题了:该行已经属于另一个表。解决方法:TB.ImportRow(R)

import logging,time,re import numpy as np from collections import defaultdict from itertools import zip_longest from kotei_omp.data import DocumentBlockObject from kotei_omc.comparers.picture_comparer import PictureComparer, GraphicComparer from kotei_omc.comparers.base_comparer import BaseComparer from kotei_omc.comparers.plugins import register_plugin from kotei_omc.data.diff import DiffItem from kotei_omp.data import TextObject, GraphicObject, PictureObject, StyleObject, RunObject from kotei_omp.data.table import CellObject, RowObject, TableObject from kotei_omc.settings import settings from kotei_omc.utils.type_checker import is_instance_of from kotei_omc.middlewares.table_middlewares import CustomTableStrategyMiddleware logger = logging.getLogger("req_diff") @register_plugin("table") class TableComparer(BaseComparer): def get_block_resource(self, block, belong_to='block'): return self.do_get_block_resource(block, belong_to, 'tables', TableObject) def compare(self, block_name, base, target, belong_to=None): t0 = time.time() # 格匹配 logger.info(f'start match table, block_name: {block_name}, base_num: {len(base)}, target_num: {len(target)}') match_func = CustomTableStrategyMiddleware(self._path_base).match if settings.MATCH_WITH_CHAPTER: tb_delete_list, tb_add_list, old_new_tb_matched = self.do_match_with_chapter(base, target,match_func) else: tb_delete_list, tb_add_list, old_new_tb_matched = self.do_match_normal(base, target,match_func) logger.info('finish match table') # 格新增删除 ls_tb_delete, ls_tb_add = self.process_delete_add_diff(block_name, 'table', tb_delete_list, tb_add_list, belong_to=belong_to) # 格差分 ls_tb_update = [] for old_table, new_table in old_new_tb_matched: # 要求废止特殊处理 old_table, new_table = self.pre_process_require(old_table, new_table) # 格位置差分 if not old_table.is_same_pos(new_table): ls_tb_update.append(DiffItem('update', 'table', sub_type='table', block_name=block_name, old=old_table, new=new_table, belong_to=belong_to,diff_point='coordinate_desc')) # 对匹配的每个格进行对比 part_delete, part_add, part_update = self.compare_table(block_name, old_table, new_table,belong_to=belong_to) ls_tb_delete.extend(self.row_del_add_after(part_delete,category='delete')) ls_tb_add.extend(self.row_del_add_after(part_add,category='add')) ls_tb_update.extend(self.cell_update_after(part_update)) t1 = time.time() logger.info(f'Time Cost:table diff {block_name} {t1 - t0}') return {'add': ls_tb_add, 'delete': ls_tb_delete, 'update': ls_tb_update} @staticmethod def copy_table_attrs(to_table, from_table): for attr_name in ('layout', 'style', 'border', 'coordinate', 'data_id'): setattr(to_table, attr_name, getattr(from_table, attr_name)) @staticmethod def fill_visual_merged_cells(table): num_rows = len(table.rows) if num_rows == 0: return num_cols = max([len(row.cells) for row in table.rows]) if num_cols == 0: return # 判断是否有边界 def is_bordered(side): return side.border_style is not None for col in range(num_cols): row_ptr = 0 while row_ptr < num_rows: cell = table.rows[row_ptr].cells[col] top_border_exists = is_bordered(cell.border.border_top) if row_ptr == 0 or top_border_exists: start_row = row_ptr end_row = start_row while end_row < num_rows: current_cell = table.rows[end_row].cells[col] bottom_border_exists = is_bordered(current_cell.border.border_bottom) # import ipdb;ipdb.set_trace() if bottom_border_exists or end_row == num_rows - 1: break else: end_row += 1 block_text = None block_content = None for r in range(start_row, end_row + 1): val = table.rows[r].cells[col].text if val is not None and str(val).strip() != "": block_text = val block_content = table.rows[r].cells[col].content break if block_text is not None: merged_ranges = [start_row, col, end_row, col] for r in range(start_row, end_row + 1): val = table.rows[r].cells[col].text if val is None or str(val).strip() == "": table.rows[r].cells[col].content = block_content table.rows[r].cells[col].text = block_text # 添加 merged_ranges 属性 if not table.rows[r].cells[col].merged_ranges: table.rows[r].cells[col].merged_ranges = merged_ranges row_ptr = end_row + 1 else: row_ptr += 1 def compare_table(self, block_name, old_table, new_table, belong_to): logger.info(f"start compare table, old_data_id: {old_table.data_id}, new_data_id: {new_table.data_id}") # 使格内列数一致 self.align_table_col(old_table, new_table) # 格中存在大量视觉上merge但是实际未合并的空格,需要将空格赋值为正确的文本,防止影响相似度匹配 self.fill_visual_merged_cells(old_table) self.fill_visual_merged_cells(new_table) if old_table.head_type == new_table.head_type == 'horizontal': old_col_table, new_col_table = self.transpose_table(old_table, new_table) else: if old_table.head_type == 'vertical': new_table.head_list = old_table.head_list new_table.head_type = 'vertical' elif new_table.head_type == 'vertical': old_table.head_list = new_table.head_list old_table.head_type = 'vertical' old_col_table, new_col_table = old_table, new_table # 列匹配 del_cols, add_cols = old_col_table.rows, new_col_table.rows col_matched = CustomTableStrategyMiddleware(self._path_base).match_row(del_cols, add_cols,is_col=True, head_indexes=[old_table.head_list,new_table.head_list]) if col_matched: matched_old_cols, matched_new_cols = list(zip(*list(col_matched))) del_cols = [old_col for old_col in old_col_table.rows if old_col not in matched_old_cols] add_cols = [new_col for new_col in new_col_table.rows if new_col not in matched_new_cols] sub_type = 'col' if old_table.head_type == 'horizontal' else 'row' ls_col_delete, ls_col_add = self.process_delete_add_diff(block_name, sub_type, del_cols, add_cols, belong_to=belong_to, head_type=old_table.head_type) # 根据matched的列组合新的,得到列一致的两个 if col_matched: old_col_indexes,new_col_indexes =[],[] for old_col, new_col in col_matched: old_col_indexes.append(old_col_table.rows.index(old_col)) new_col_indexes.append(new_col_table.rows.index(new_col)) old_equal_col_table = self.choice_cols(old_table, old_col_indexes) new_equal_col_table = self.choice_cols(new_table, new_col_indexes) else: return ls_col_delete, ls_col_add, [] # 行匹配 del_rows, add_rows = old_equal_col_table.rows, new_equal_col_table.rows row_matched = CustomTableStrategyMiddleware(self._path_base).match_row(del_rows, add_rows, is_col=False) if row_matched: matched_old_rows, matched_new_rows = list(zip(*list(row_matched))) del_rows_indexes = [idx for idx, old_row in enumerate(old_equal_col_table.rows) if old_row not in matched_old_rows] add_rows_indexes = [idx for idx, new_row in enumerate(new_equal_col_table.rows) if new_row not in matched_new_rows] # 使用没有重组前的,横头直接处理,竖头需要转置 if old_table.head_type == new_table.head_type == 'horizontal': del_rows = [old_table.rows[idx] for idx in del_rows_indexes] add_rows = [new_table.rows[idx] for idx in add_rows_indexes] else: old_transpose_table = self.choice_cols(old_table, list(range(len(old_table.rows)))) new_transpose_table = self.choice_cols(new_table, list(range(len(new_table.rows)))) del_rows = [old_transpose_table.rows[idx] for idx in del_rows_indexes] add_rows = [new_transpose_table.rows[idx] for idx in add_rows_indexes] sub_type = 'row' if old_table.head_type == 'horizontal' else 'col' ls_row_delete, ls_row_add = self.process_delete_add_diff(block_name, sub_type, del_rows, add_rows, belong_to=belong_to, head_type=old_table.head_type) # 根据matched的行组合新的,得到行一致的两个 if row_matched: old_equal_row_table, new_equal_row_table = TableObject(), TableObject() old_equal_row_table.rows = list(matched_old_rows) old_equal_row_table.head_type = old_table.head_type self.copy_table_attrs(old_equal_row_table, old_table) new_equal_row_table.rows = list(matched_new_rows) new_equal_row_table.head_type = new_table.head_type self.copy_table_attrs(new_equal_row_table, new_table) # 查找行变更、列变更、单元格变更 ls_row_update, ls_col_update, ls_cell_update = self.compare_ordered_tables(block_name,old_equal_row_table, new_equal_row_table,belong_to=belong_to) else: ls_row_update, ls_col_update, ls_cell_update = [], [], [] part_delete = ls_row_delete + ls_col_delete part_add = ls_row_add + ls_col_add part_update = ls_row_update + ls_col_update + ls_cell_update logger.info(f"finish compare table, old_data_id: {old_table.data_id}, new_data_id: {new_table.data_id}") return part_delete, part_add, part_update def transpose_table(self, old_table, new_table): """ 将格进行转置操作,即将行转换为列,列转换为行。 Args: old_table (TableObject): 原始格对象 new_table (TableObject): 目标格对象 Returns: tuple: 返回转置后的两个格对象 (old_col_table, new_col_table) """ # 创建新的格对象用于存储转置后的数据 old_col_table, new_col_table = TableObject(), TableObject() # 对原始格的行进行转置操作 old_col_table.rows = self.transpose_table_rows(old_table.rows) # 根据原始格的头类型,设置转置后的头类型 old_col_table.head_type = 'vertical' if old_table.head_type == 'horizontal' else 'horizontal' # 复制原始格的属性到转置后的格 self.copy_table_attrs(old_col_table, old_table) # 对目标格的行进行转置操作 new_col_table.rows = self.transpose_table_rows(new_table.rows) # 根据目标格的头类型,设置转置后的头类型 new_col_table.head_type = 'vertical' if new_table.head_type == 'horizontal' else 'horizontal' # 复制目标格的属性到转置后的格 self.copy_table_attrs(new_col_table, new_table) # 返回转置后的两个格对象 return old_col_table, new_col_table def compare_ordered_tables(self, block_name, old_table_obj, new_table_obj, belong_to): row_updates, col_updates, cell_updates = [], [], [] # 获取新旧行数据 old_rows = getattr(old_table_obj, 'rows', []) new_rows = getattr(new_table_obj, 'rows', []) old_cells_list = [row.cells for row in old_rows] new_cells_list = [row.cells for row in new_rows] # 获取内容用于对比 old_content_cells_list = self.get_cell_content_list(old_cells_list, settings.DIFF_ATTR) new_content_cells_list = self.get_cell_content_list(new_cells_list, settings.DIFF_ATTR) # 删除完全一样的匹配 for row_index in range(len(old_content_cells_list) - 1, -1, -1): # 如果新旧行内容相同,则删除该行 # 之后可以在这里增加原子操作逻辑,避免删除不同步 if old_content_cells_list[row_index] == new_content_cells_list[row_index]: old_content_cells_list.pop(row_index) new_content_cells_list.pop(row_index) old_cells_list.pop(row_index) new_cells_list.pop(row_index) #原子一致性检查 flag = False if len(old_content_cells_list) ==len(new_content_cells_list)==len(old_cells_list) == len(new_cells_list): flag = True if not flag: logger.warning(f"{block_name} old_table_obj: {old_table_obj}, new_table_obj: {new_table_obj}; delete operator is not atomic; all the cells list will involved in finding differences computation") if not old_content_cells_list: return [], [], [] # 查找差异 diff_type, row_diffs, col_diffs, cell_diffs, cell_diff_points, cell_diff_values, \ row_diff_idx, col_diff_idx, graphic_diff, picture_diff = self.find_differences( old_content_cells_list, new_content_cells_list, old_cells_list, new_cells_list) # 抽取单元格内的图形图像差分 for item in graphic_diff + picture_diff: if item: cell_updates.extend(item) # 处理单元格差分 for idx, (cell_diff_idx, diff_point, diff_value) in enumerate( zip(cell_diffs, cell_diff_points, cell_diff_values)): try: # old = self.get_element_by_index(old_cells_list, cell_diff_idx) # new = self.get_element_by_index(new_cells_list, cell_diff_idx) old, new = old_cells_list, new_cells_list for cell_idx in cell_diff_idx: old = old[cell_idx] new = new[cell_idx] except IndexError: continue # 忽略非法索引 cell_diff_obj = DiffItem( 'update', 'table', 'cell', block_name=block_name, old=old, new=new, belong_to=belong_to, diff_point=diff_point, diff_values=diff_value ) cell_updates.append(cell_diff_obj) # 处理行差分 # if diff_type == 'row': # for row_idx, row_diff_col_idx in zip(row_diffs, row_diff_idx): # try: # old_row = [old_cells_list[row_idx][cell_idx] for cell_idx in row_diff_col_idx] # new_row = [new_cells_list[row_idx][cell_idx] for cell_idx in row_diff_col_idx] # except IndexError: # continue # # row_diff_item = DiffItem( # 'update', 'table', 'row', # block_name=block_name, # old=self.merge_cells_to_row(old_row), # new=self.merge_cells_to_row(new_row), # belong_to=belong_to) # row_updates.append(row_diff_item) # 处理列差分 # elif diff_type == 'col': # for col_idx, col_diff_col_idx in zip(col_diffs, col_diff_idx): # try: # old_col = [old_cells_list[cell_idx][col_idx] for cell_idx in col_diff_col_idx] # new_col = [new_cells_list[cell_idx][col_idx] for cell_idx in col_diff_col_idx] # except IndexError: # continue # # col_diff_item = DiffItem( # 'update', 'table', 'col', # block_name=block_name, # old=self.merge_cells_to_row(old_col), # new=self.merge_cells_to_row(new_col), # belong_to=belong_to # ) # col_updates.append(col_diff_item) return row_updates, col_updates, cell_updates def choice_cols(self, table_obj, col_indexes): if table_obj.head_type == 'horizontal': rows = [] for row_obj in table_obj.rows: cells = [] for cel_idx in col_indexes: cells.append(row_obj.cells[cel_idx]) rows.append(cells) else: rows = [[] for _ in range(len(table_obj.rows[0].cells))] for cel_idx in col_indexes: for idx, cell in enumerate(table_obj.rows[cel_idx].cells): rows[idx].append(cell) res_table_obj = TableObject() for cell_list in rows: row_obj = RowObject() if cell_list: row_obj.cells = cell_list row_obj.coordinate = cell_list[0].coordinate # 对cell_obj的layout.parent_ref进行判断,有值在进行赋值 if cell_list[0].layout.parent_ref: row_obj.layout = cell_list[0].layout.parent_ref.layout row_obj.style = cell_list[0].layout.parent_ref.style row_obj.border = cell_list[0].layout.parent_ref.border row_obj.row_index = cell_list[0].row_index row_obj.data_id = cell_list[0].data_id # res_table_obj.rows.append(self.merge_cells_to_row(cell_list)) res_table_obj.rows.append(row_obj) self.copy_table_attrs(res_table_obj, table_obj) return res_table_obj @staticmethod def process_delete_add_diff(block_name, sub_type, delete_tables, add_tables, belong_to, head_type=None): def process_graphic_objects(action, cell_list): """ 辅助函数:处理单元格中的图形对象和图片对象。 action: 操作类型('delete' 或 'add') cells_list: 单元格列 """ diff_items = [] all_merged_ranges = [] for cell_obj in cell_list: if cell_obj.merged_ranges: # 合并单元格只处理一次 if cell_obj.merged_ranges not in all_merged_ranges: all_merged_ranges.append(cell_obj.merged_ranges) else: continue for item_obj in cell_obj.content: if is_instance_of(item_obj, GraphicObject) or is_instance_of(item_obj, PictureObject): # 检查是否是图形或图片对象 diff_items.append( DiffItem(action, item_obj._type, sub_type=item_obj._type, block_name=block_name, old=item_obj if action == 'delete' else None, new=None if action == 'delete' else item_obj, belong_to=belong_to) ) return diff_items # filter_duplicate_cells 过滤在一行或者一列中因合并单元格引起的重复 # 相关代码暂时先不启用,可以在后续使用者启用查看是否会引起漏差分的问题在决定是否启用 # 如果在解析端可以处理合并单元格,则不需要过滤,避免冗余处理而降低效率 def filter_duplicate_cells(item,sub_type): """ 根据text和merged_ranges过滤掉cells_list中的合并单元格 Args: item: RowObject or TableObject """ if sub_type != 'table': seen_contents = defaultdict(list) for i in range(len(item.cells) - 1, -1, -1): cell = item.cells[i] cell_merged_ranges = cell.merged_ranges if not cell_merged_ranges: continue cell_text = cell.text if cell_merged_ranges == seen_contents[cell_text]: del item.cells[i] continue seen_contents[cell_text] = cell_merged_ranges else: for row in item.rows: seen_contents = defaultdict(list) for i in range(len(row.cells) - 1, -1, -1): cell = row.cells[i] cell_merged_ranges = cell.merged_ranges if not cell_merged_ranges: continue cell_text = cell.text if cell_merged_ranges == seen_contents[cell_text]: del row.cells[i] continue seen_contents[cell_text] = cell_merged_ranges return item ls_tb_add, ls_tb_delete = [], [] for tb_base_item in delete_tables: # 过滤(行、列)合并单元格 tb_base_item = filter_duplicate_cells(tb_base_item,sub_type) diff_obj = DiffItem('delete', 'table', sub_type=sub_type, block_name=block_name, old=tb_base_item, new=None, belong_to=belong_to) setattr(diff_obj, 'head_type', head_type) ls_tb_delete.append(diff_obj) # 如果是格、行或列,处理单元格中的内容 if sub_type in ('row', 'col', 'table'): cells_list = tb_base_item.cells if sub_type != 'table' else [ cell for row in tb_base_item.rows for cell in row.cells] ls_tb_delete.extend(process_graphic_objects('delete', cells_list)) for tb_target_item in add_tables: # 过滤(行、列)合并单元格 tb_target_item = filter_duplicate_cells(tb_target_item,sub_type) diff_obj = DiffItem('add', 'table', sub_type=sub_type, block_name=block_name, old=None, new=tb_target_item, belong_to=belong_to) setattr(diff_obj, 'head_type', head_type) ls_tb_add.append(diff_obj) # 如果是格、行或列,处理单元格中的内容 if sub_type in ('row', 'col', 'table'): cells_list = tb_target_item.cells if sub_type != 'table' else [ cell for row in tb_target_item.rows for cell in row.cells] ls_tb_add.extend(process_graphic_objects('add', cells_list)) return ls_tb_delete, ls_tb_add def transpose_table_rows(self, rows): """ 将格的行进行转置操作,即将行转换为列,列转换为行。 Args: rows (list): 原始格的行列,每个元素是一个RowObject对象 Returns: list: 返回转置后的行列,每个元素是一个RowObject对象 """ # 创建新的行对象列,数量等于原始格的最大列数 max_cell_count = 0 for row in rows: if len(row.cells) > max_cell_count: max_cell_count = len(row.cells) t_rows = [RowObject() for _ in range(max_cell_count)] # 遍历原始格的每一行 for row in rows: # 遍历每一行的单元格 for idx, cell in enumerate(row.cells): # 将单元格添加到转置后的对应行中 t_rows[idx].cells.append(cell) # 为转置后的每一行设置属性 for row in t_rows: # 设置行的坐标为第一个单元格的坐标 row.coordinate = row.cells[0].coordinate # 设置行的数据ID为第一个单元格的数据ID row.data_id = row.cells[0].data_id # 设置行的布局为第一个单元格的布局 row.layout = row.cells[0].layout # 如果第一个单元格有列索引,则设置行的列索引 if isinstance(row.cells[0].col_index, int): row.col_index = row.cells[0].col_index # 如果第一个单元格有行索引,则设置行的行索引 if isinstance(row.cells[0].row_index, int): row.row_index = row.cells[0].row_index # 返回转置后的行列 return t_rows def find_differences(self, array1: list, array2: list, old_items, new_items, diff_mode='normal'): if isinstance(array1, list): array1 = np.array(array1) if isinstance(array2, list): array2 = np.array(array2) # 确保两个ndarray的shape相同 if array1.shape != array2.shape: raise ValueError("两个ndarray的shape必须相同") diff_type = diff_mode if diff_type == 'normal': # 计算行差异数 row_diff_count = np.sum(~np.all(array1 == array2, axis=1)) # 计算列差异数 col_diff_count = np.sum(~np.all(array1 == array2, axis=0)) # 根据差异数选择差异类型 diff_type = 'col' if col_diff_count < row_diff_count else 'row' # 找出所有行和列的差异项 row_diffs, col_diffs, cell_diffs, cell_diff_points, cell_diff_values, row_diff_idx, col_diff_idx, graphic_diffs, picture_diffs = [ [] for _ in range(9)] if diff_type == 'row': for i in range(array1.shape[0]): if not np.all(array1[i] == array2[i]): # if np.sum(array1[i] != array2[i]) < settings.T_MERGE_MULTI_CELL_UPDATE_TO_ROW_UPDATE_MIN_CELL_COUNT: # 如果该行只有一个数值不一致,则将这个差异项改为单元格的差异项 for j in range(array1.shape[1]): if array1[i, j] == array2[i, j]: continue diff_point, diff_values, graphic_diff, picture_diff = self.get_cell_not_equal_attrs( old_items[i][j], new_items[i][j], settings.CELL_COMPARE_ATTRS) if diff_point: cell_diffs.append((i, j)) cell_diff_points.append(' '.join(diff_point)) cell_diff_values.append(diff_values) if graphic_diff: graphic_diffs.append(graphic_diff) if picture_diff: picture_diffs.append(picture_diff) # else: # row_diffs.append(i) # row_diff_idx.append(np.where(array1[i] != array2[i])[0].tolist()) else: for j in range(array1.shape[1]): if not np.all(array1[:, j] == array2[:, j]): # if np.sum(array1[:, j] != array2[:,j]) < settings.T_MERGE_MULTI_CELL_UPDATE_TO_ROW_UPDATE_MIN_CELL_COUNT: # 如果该列只有一个数值不一致,则将这个差异项改为单元格的差异项 for i in range(array1.shape[0]): if array1[i, j] == array2[i, j]: continue diff_point, diff_values, graphic_diff, picture_diff = self.get_cell_not_equal_attrs( old_items[i][j], new_items[i][j], settings.CELL_COMPARE_ATTRS) if diff_point: cell_diffs.append((i, j)) cell_diff_points.append(' '.join(diff_point)) cell_diff_values.append(diff_values) if graphic_diff: graphic_diffs.append(graphic_diff) if picture_diff: picture_diffs.append(picture_diff) # else: # col_diffs.append(j) # col_diff_idx.append(np.where(array1[:, j] != array2[:, j])[0].tolist()) # 返回所有差异类型对应的单元格索引 return diff_type, row_diffs, col_diffs, cell_diffs, cell_diff_points, cell_diff_values, row_diff_idx, col_diff_idx, graphic_diffs, picture_diffs @staticmethod def get_cell_chars(cell_obj): chars = [] for text_obj in cell_obj.content: if not is_instance_of(text_obj, TextObject): continue chars.extend(text_obj.get_chars()) return chars def _compare_cell_diff(self, base, target, data_type, block_name=''): """ 对比单元格图像的方法 """ result = [] if data_type == 'graphic': cp_obj = GraphicComparer(self._base_block_mapping, self._target_block_mapping, self._path_base, self._path_target) else: cp_obj = PictureComparer(self._base_block_mapping, self._target_block_mapping, self._path_base, self._path_target) item_result = cp_obj.compare(block_name, base, target, 'cell', True) result.extend(item_result['add']) result.extend(item_result['delete']) result.extend(item_result['update']) return result @staticmethod def _get_graphic_picture_obj(old_cell, new_cell): base_graphic = [] target_graphic = [] base_picture = [] target_picture = [] for base_item in old_cell.content: if is_instance_of(base_item, GraphicObject): base_graphic.append(base_item) elif is_instance_of(base_item, PictureObject): base_picture.append(base_item) for new_item in new_cell.content: if is_instance_of(new_item, GraphicObject): target_graphic.append(new_item) elif is_instance_of(new_item, PictureObject): target_picture.append(new_item) return [(base_graphic, target_graphic), (base_picture, target_picture)] def _get_cell_graphic_picture_diff(self, old_cell, new_cell): """ 对比单元格图形图像的方法 """ graphic_diff = [] picture_diff = [] block = old_cell while not isinstance(block, DocumentBlockObject) and block and hasattr(block, 'layout'): block = block.layout.parent_ref block_name = block.name if block else '' graphic_obj, picture_obj = self._get_graphic_picture_obj(old_cell, new_cell) if graphic_obj[0] or graphic_obj[1]: graphic_diff = self._compare_cell_diff(graphic_obj[0], graphic_obj[1], 'graphic', block_name) if picture_obj[0] or picture_obj[1]: picture_diff = self._compare_cell_diff(picture_obj[0], picture_obj[1], 'picture', block_name) return graphic_diff, picture_diff def get_cell_not_equal_attrs(self, old_cell, new_cell, compare_attrs): diff_attrs = [] diff_values = [] if getattr(old_cell, 'auto_number', None) and getattr(new_cell, 'auto_number', None): return [], [], [], [] if old_cell.text != new_cell.text: diff_attrs.append('text') diff_values.append((old_cell.text, new_cell.text)) else: # 直接在对象上取值的属性 # direct_attr = ['style.background_color', 'border.border_top.border_style', 'style.background_color', # 'border.border_bottom.border_style', 'border.border_left.border_style', # 'border.border_right.border_style', 'style.background_style'] direct_attr = ['style.background_color', 'style.background_style'] attrs, values = self.get_not_equal_attrs(old_cell, new_cell, direct_attr) diff_attrs.extend(attrs) diff_values.extend(values) for old_char, new_char in zip_longest(self.get_cell_chars(old_cell), self.get_cell_chars(new_cell), fillvalue=None): if old_char is None or new_char is None: diff_attrs.append('text') diff_values.append((str(old_char), str(new_char))) else: attrs, values = self.get_not_equal_attrs(old_char, new_char, compare_attrs) diff_attrs.extend(attrs) diff_values.extend(values) # 单元格增加图形图像的比较 graphic_diff, picture_diff = self._get_cell_graphic_picture_diff(old_cell, new_cell) unique_diff_attrs = list(set(diff_attrs)) unique_not_equal_values = [diff_values[diff_attrs.index(v)] for v in unique_diff_attrs] return unique_diff_attrs, unique_not_equal_values, graphic_diff, picture_diff def get_cell_content_list(self, cell_obj_lists, with_attr=False): content_lists = [] processed_merged_ranges = set() for cell_obj_list in cell_obj_lists: row_content_list = [] for cell_obj in cell_obj_list: # 检查是否是合并单元格且已经处理过 if hasattr(cell_obj, 'merged_ranges') and cell_obj.merged_ranges: # 创建一个基于合并范围和内容的唯一键 merged_key = (tuple(cell_obj.merged_ranges), str(getattr(cell_obj, 'text', ''))) if merged_key in processed_merged_ranges: # 如果已经处理过,设置为空字符串 row_content_list.append('') # 直接添加空字符串到结果中 continue else: # 如果是合并单元格但未处理过,标记为已处理 processed_merged_ranges.add(merged_key) cell_contents = [f'text:{cell_obj.text}'] if with_attr: attr_list = settings.CELL_COMPARE_ATTRS for attr in attr_list: if attr == "text": continue attr_value = self.get_nest_attr(cell_obj, attr) if attr_value not in (None, ''): cell_contents.append(f'{attr}:{str(attr_value)}') row_content_list.append('🙉'.join(cell_contents)) content_lists.append(row_content_list) return content_lists def get_nest_attr(self, obj, nest_attr): if is_instance_of(obj, CellObject): result_attr = [] # 特殊处理单元格背景色 # if nest_attr in ('style.background_color', 'border.border_top.border_style', # 'border.border_bottom.border_style', 'border.border_left.border_style', # 'border.border_right.border_style', 'style.background_style'): if nest_attr in ('style.background_color', 'style.background_style'): return self.get_target_attr(obj, nest_attr) if nest_attr == 'font_background_color': nest_attr = 'style.background_color' for item_obj in obj.content: if is_instance_of(item_obj, GraphicObject) and nest_attr == 'graphic': for item_attr in settings.PICTURE_COMPARE_ATTRS: run_attr = self.get_target_attr(item_obj, item_attr) if run_attr and str(run_attr) not in result_attr: result_attr.append(str(run_attr)) graphic_text_obj = getattr(item_obj, 'text_obj', None) if graphic_text_obj and graphic_text_obj.text: text_attr_list = [] for text_attr in settings.TEXT_COMPARE_ATTRS: for run_obj in graphic_text_obj.runs: attr_val = self.get_target_attr(run_obj, text_attr) if attr_val and str(attr_val) not in text_attr_list: text_attr_list.append(str(attr_val)) if text_attr_list: result_attr.extend(text_attr_list) elif is_instance_of(item_obj, PictureObject) and nest_attr == 'picture': for item_attr in settings.PICTURE_COMPARE_ATTRS: run_attr = self.get_target_attr(item_obj, item_attr) if run_attr and str(run_attr) not in result_attr: result_attr.append(str(run_attr)) else: if is_instance_of(item_obj, TextObject): for run_obj in item_obj.runs: run_attr = self.get_target_attr(run_obj, nest_attr) if run_attr and str(run_attr) not in result_attr: result_attr.append(str(run_attr)) elif is_instance_of(item_obj, RunObject): run_attr = self.get_target_attr(item_obj, nest_attr) if run_attr and str(run_attr) not in result_attr: result_attr.append(str(run_attr)) return "".join(result_attr) elif is_instance_of(obj, TextObject): result_attr = [] for run_obj in obj.runs: run_attr = self.get_target_attr(run_obj, nest_attr) if run_attr and str(run_attr) not in result_attr: result_attr.append(str(run_attr)) else: return self.get_target_attr(obj, nest_attr) @staticmethod def get_target_attr(obj, nest_attr): nest_attrs = nest_attr.split('.') attr_str = nest_attrs.pop(0) base_attr = getattr(obj, attr_str, None) while base_attr and nest_attrs: attr_str = nest_attrs.pop(0) base_attr = getattr(base_attr, attr_str, None) return base_attr # @staticmethod # def merge_cells_to_row(cell_list): # row_obj = RowObject() # # if cell_list: # row_obj.cells = cell_list # row_obj.coordinate = cell_list[0].coordinate # # 对cell_obj的layout.parent_ref进行判断,有值在进行赋值 # if cell_list[0].layout.parent_ref: # row_obj.layout = cell_list[0].layout.parent_ref.layout # row_obj.style = cell_list[0].layout.parent_ref.style # row_obj.border = cell_list[0].layout.parent_ref.border # row_obj.row_index = cell_list[0].row_index # row_obj.data_id = cell_list[0].data_id # return row_obj @staticmethod def align_table_col(base_table, target_table): base_max_col_count = max([len(row.cells) for row in base_table.rows]) target_max_col_count = max([len(row.cells) for row in target_table.rows]) for base_row in base_table.rows: if len(base_row.cells) != base_max_col_count: # 匹配行的列数不一致,补齐缺失的cell add_col_count = abs(len(base_row.cells) - base_max_col_count) base_row.cells.extend([CellObject() for _ in range(add_col_count)]) for target_row in target_table.rows: if len(target_row.cells) != target_max_col_count: # 匹配行的列数不一致,补齐缺失的cell add_col_count = abs(len(target_row.cells) - target_max_col_count) target_row.cells.extend([CellObject() for _ in range(add_col_count)]) def cell_update_after(self, update_cells): """ 单元格的变更后处理, 只有在content和merged_ranges都一样的情况下才过滤重复项 :return: """ if not update_cells: return update_cells result = [] custom_cells_merged_ranges = list() seen_cells = defaultdict(list) def normalize_content(cell): """标准化单元格内容用于比较""" if not cell: return "" # 获取文本内容并标准化 content_text = str(cell.text) if hasattr(cell, 'text') else "" # 标准化换行符 normalized = content_text.strip().replace('\r\n', '\n').replace('\r', '\n') return normalized def get_cell_key(item): """生成用于比较的键""" old_cell = getattr(item, 'old', None) new_cell = getattr(item, 'new', None) # 获取内容键 old_content = normalize_content(old_cell) new_content = normalize_content(new_cell) content_key = f"{old_content}|{new_content}" # 获取合并范围键 old_range = tuple(old_cell.merged_ranges) if old_cell and hasattr(old_cell, 'merged_ranges') and old_cell.merged_ranges else () new_range = tuple(new_cell.merged_ranges) if new_cell and hasattr(new_cell, 'merged_ranges') and new_cell.merged_ranges else () range_key = f"{old_range}|{new_range}" return f"{content_key}||{range_key}" def get_is_custom_cell(cell_obj): for c_obj in cell_obj.get_heads(): if c_obj.text == settings.SPECIAL_CELL_CONTENT3: return True for item in update_cells: # 如果不是单元格更新或者没有old/new对象,直接添加到结果中 if (item.type != 'update' or item.data_type != 'table' or item.sub_type != 'cell' or (not item.old or not item.old.merged_ranges) and (not item.new or not item.new.merged_ranges)): result.append(item) continue current_old_range = getattr(item.old, 'merged_ranges', []) if item.old else [] current_new_range = getattr(item.new, 'merged_ranges', []) if item.new else [] # 特殊定制的格累加处理 if item.old and get_is_custom_cell(item.old): if current_old_range not in custom_cells_merged_ranges: custom_cells_merged_ranges.add(current_old_range) result.append(item) else: existing_idx = custom_cells_merged_ranges.index(current_old_range) result[existing_idx].old.text += item.old.text result[existing_idx].old.content.extend(item.old.content) elif item.new and get_is_custom_cell(item.new): if current_new_range not in custom_cells_merged_ranges: custom_cells_merged_ranges.append(current_new_range) result.append(item) else: existing_idx = custom_cells_merged_ranges.index(current_new_range) result[existing_idx].new.text += item.new.text result[existing_idx].new.content.extend(item.new.content) # 检查是否只有单侧有合并范围, 如果只有单侧有合并范围,则不视为重复 # elif len(current_old_range) <4 or len(current_new_range)<4: # result.append(item) else: # 处理普通单元格 - 进行去重检查 # 生成用于比较的键 cell_key = get_cell_key(item) # 检查是否已经存在相同的键 is_duplicate = False for existing_idx in seen_cells[cell_key]: existing_item = result[existing_idx] # 获取当前和已存在项目的合并范围 existing_old_range = getattr(existing_item.old, 'merged_ranges', []) if existing_item.old else [] existing_new_range = getattr(existing_item.new, 'merged_ranges', []) if existing_item.new else [] # 只有当merged_ranges完全相同时才认为是重复 if (current_old_range == existing_old_range and current_new_range == existing_new_range): is_duplicate = True break if not is_duplicate: seen_cells[cell_key].append(len(result)) result.append(item) # 如果是重复项,则忽略(不添加到结果中) return result def row_del_add_after(self, part, category='add'): """ 根据 category 参数处理新增或删除的行对象,判断行中的单元格是否有 merged_ranges 属性。 如果行中的任意一个单元格没有 merged_ranges 属性,则添加到结果列中。 同时过滤具有相同 merged_ranges 的重复行对象,仅保留第一个出现的行。 注意:对于两列(行)中至少共享一个合并单元格,同时两列(行)内容完全相同,依然有可能会被误删除 解决方案:需要解析提供所有的单元格范围,之后综合计算整列(行)的范围进行判断, 若整列(行)都是因合并单元格而造成的冗余则进行过滤,否则(如只共享一(多)个合并单元格)则保留 :param part: 行对象列 :param category: 操作类型,'add' 或 'delete' :return: 处理后的结果列 """ # 使用列来保存拼接后的列 result = [] if not part: return part if category not in ('add', 'delete'): return part # 根据 category 决定处理新增还是删除的行对象 merged_rows = [] for row in part: # 检查是否是行对象;(会有PictureObject和GraphicObject)如不是则直接加入结果中 if category == 'add' and not is_instance_of(row.new, RowObject): result.append(row) continue if category == 'delete' and not is_instance_of(row.old, RowObject): result.append(row) continue # 获取要检查的单元格列 cells = row.new.cells if category == 'add' else row.old.cells # 检查行中的每个单元格是否有 merged_ranges 属性 has_merged_ranges = any(hasattr(cell, 'merged_ranges') for cell in cells) # 如果行中的任意一个单元格没有 merged_ranges 属性,则添加到结果中 if not has_merged_ranges: result.append(row) else: merged_rows.append(row) # 处理具有 merged_ranges 的行,过滤重复项 if merged_rows: seen_contents = defaultdict(list) def remove_timestamp(text): return re.sub(r'\d{4}[-/]\d{2}[-/]\d{2}.*?(?=\t|\n|$)', '', text) for index, row in enumerate(merged_rows): # 获取当前行的内容 content = getattr(row, 'new_content' if category == 'add' else 'old_content', None) if content: #确保在处理可能包含非UTF-8编码字符的文本时不会出现解码错误 if isinstance(content, bytes): content = content.decode('utf-8', errors='replace') elif not isinstance(content, str): content = str(content) # 标准化 content cleaned_content = remove_timestamp(content) normalized_content = cleaned_content.strip().replace('\r\n', '\n').replace('\r', '\n') seen_contents[normalized_content].append(index) duplicates_row = {item: indices for item, indices in seen_contents.items() if len(indices) > 1} removed_rows_indices = [] for _, indices in duplicates_row.items(): seen_merged_ranges = set() for i in indices: # 获取要检查的单元格列 cells = merged_rows[i].new.cells if category == 'add' else merged_rows[i].old.cells for cell in cells: if cell.merged_ranges: merged_range_tuple = tuple(cell.merged_ranges) if merged_range_tuple not in seen_merged_ranges: seen_merged_ranges.add(merged_range_tuple) break else: removed_rows_indices.append(i) break # 添加未被移除的行到结果中 for index, row in enumerate(merged_rows): if index not in removed_rows_indices: result.append(row) return result def pre_process_require(self, old_table, new_table): base_resources = [old_table] target_resources = [new_table] changes_dict = {} # 存储变更信息的字典 # 合并格并编号(0=变更前,1=变更后) for table_idx, table in enumerate(base_resources + target_resources): col_list = table.get_col_list(col_name=settings.SPECIAL_COLUMN) #'要求廃止' # 在循环外部初始化计数器 be_counter = 1 af_counter = 1 if col_list: for row_index, cell in enumerate(col_list): cell_text = getattr(cell, 'text', '') if cell_text == settings.SPECIAL_CELL_CONTENT2: #'レ' if table_idx < len(base_resources): table_key = f"be_{be_counter:02d}" be_counter += 1 else: table_key = f"af_{af_counter:02d}" af_counter += 1 # 获取列索引 col_index = cell.col_index # 存储变更位置信息 changes_dict[table_key] = (row_index, col_index) if any(changes_dict): # 分离变更前和变更后的数据 be_changes = {k: v for k, v in changes_dict.items() if k.startswith('be_')} af_changes = {k: v for k, v in changes_dict.items() if k.startswith('af_')} # 处理变更前的数据 # 记录需要清理的key(分前后) be_clear_keys = [] af_clear_keys = [] if be_changes: for table_key, (row_idx, col_idx) in be_changes.items(): next_col_idx = col_idx + 1 if next_col_idx < len(base_resources[0].rows[row_idx].cells): if self._tbl_find_unique(base_resources[0], target_resources[0], row_idx, col_idx + 1): be_clear_keys.append(table_key) # 处理变更后的数据 if af_changes: for table_key, (row_idx, col_idx) in af_changes.items(): next_col_idx = col_idx + 1 if next_col_idx < len(target_resources[0].rows[row_idx].cells): if self._tbl_find_unique(target_resources[0], base_resources[0], row_idx, col_idx + 1): af_clear_keys.append(table_key) # 统一清理(分操作) for table_key in be_clear_keys: row_idx, col_idx = changes_dict[table_key] # 清理前(base_resources) for cell in base_resources[0].rows[row_idx].cells: cell.text = '' cell.content = [] cell.style = StyleObject() for table_key in af_clear_keys: row_idx, col_idx = changes_dict[table_key] # 清理后(target_resources) for cell in target_resources[0].rows[row_idx].cells: cell.text = '' cell.content = [] cell.style = StyleObject() return base_resources[0], target_resources[0] @staticmethod def _tbl_find_unique(base_tbl, target_tbl, row_idx, col_idx): """校验指定单元格是否满足唯一性条件: 1. 在 base_tbl 对应列中不存在相同值 2. 在 target_tbl 当前列中唯一(排除自己) 返回:是否需要清理(True/False) """ if not target_tbl or not base_tbl: return False target_rows = target_tbl.rows if row_idx >= len(target_rows) or col_idx >= len(target_rows[row_idx].cells): return False compare_text = str(target_rows[row_idx].cells[col_idx].text) # 条件1:检查 base_tbl 对应列是否存在相同值 if base_tbl.rows and col_idx < len(base_tbl.rows[0].cells): for row in base_tbl.rows: if col_idx < len(row.cells) and str(row.cells[col_idx].text) == compare_text: return True # 需要清理 # 条件2:检查 target_tbl 当前列是否有重复(排除自己) for i, row in enumerate(target_rows): if i == row_idx: continue if col_idx < len(row.cells) and str(row.cells[col_idx].text) == compare_text: return True # 需要清理 return False # 无需清理 详细讲解格匹配的代码以及流程
最新发布
11-05
import pandas as pd import tkinter as tk from tkinter import ttk, messagebox, filedialog import os import json import openpyxl from openpyxl.utils.dataframe import dataframe_to_rows from tkinter.font import Font import traceback import logging import sys import io # 配置日志系统 logging.basicConfig( filename='app.log', level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger() class ScrollableFrame(ttk.Frame): """自定义可滚动框架实现""" def __init__(self, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs) # 创建Canvas和滚动条 self.canvas = tk.Canvas(self, highlightthickness=0) self.scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview) self.scrollable_frame = ttk.Frame(self.canvas) # 配置Canvas self.canvas.configure(yscrollcommand=self.scrollbar.set) self.canvas_frame = self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") # 布局 self.canvas.pack(side="left", fill="both", expand=True, padx=0, pady=0) self.scrollbar.pack(side="right", fill="y") # 绑定事件 self.scrollable_frame.bind("<Configure>", self.on_frame_configure) self.canvas.bind("<Configure>", self.on_canvas_configure) self.canvas.bind_all("<MouseWheel>", self.on_mousewheel) def on_frame_configure(self, event): """当内部框架大小改变时更新滚动区域""" self.canvas.configure(scrollregion=self.canvas.bbox("all")) def on_canvas_configure(self, event): """当Canvas大小改变时调整内部框架宽度""" self.canvas.itemconfig(self.canvas_frame, width=event.width) def on_mousewheel(self, event): """鼠标滚轮滚动支持""" self.canvas.yview_scroll(int(-1*(event.delta/120)), "units") class ExcelControlPanel: def __init__(self, master): self.master = master master.title("功能点确认系统") master.geometry("1280x800") master.configure(bg="#f0f2f5") # 设置全局样式 self.set_styles() # 加载配置 self.config = self.load_config() # 初始化多列确认配置 self.confirmation_columns = self.config.get("confirmation_columns", []) self.column_order = self.config.get("column_order", []) # 创建界面元素 self.create_widgets() # 初始化数据 self.excel_path = "" self.df = None self.check_states = {} # 存储每个功能点的复选框状态 self.current_sheet = "" self.header_row = 0 # 更新列选择器 self.update_col_selector() def set_styles(self): """设置全局样式和字体""" style = ttk.Style() style.theme_use('clam') # 自定义字体 self.title_font = Font(family="Microsoft YaHei", size=16, weight="bold") self.subtitle_font = Font(family="Microsoft YaHei", size=10) self.normal_font = Font(family="Microsoft YaHei", size=9) # 配置样式 style.configure("TFrame", background="#f0f2f5") style.configure("TLabel", font=self.normal_font, background="#f0f2f5", foreground="#333") style.configure("TButton", font=self.normal_font, padding=8) # Treeview样式 style.configure("Treeview.Heading", font=self.subtitle_font, background="#4a76b5", foreground="white") style.configure("Treeview", font=self.normal_font, rowheight=40, background="white", fieldbackground="white") # 状态栏样式 style.configure("Status.TFrame", background="#e0e0e0") style.configure("Status.TLabel", font=self.normal_font, background="#4a76b5", foreground="#ffffff", padding=5) # 卡片样式 style.configure("Card.TFrame", background="white", borderwidth=0, relief="solid", padding=10, bordercolor="#e1e4e8") style.configure("Card.TLabelframe", background="white", borderwidth=1, relief="solid", padding=10, bordercolor="#e1e4e8") style.configure("Card.TLabelframe.Label", font=self.subtitle_font, foreground="#2c3e50", background="white") # 按钮样式 style.map("Primary.TButton", background=[("active", "#3a66a5"), ("pressed", "#2a5685")], foreground=[("active", "white"), ("pressed", "white")]) style.configure("Primary.TButton", background="#4a76b5", foreground="white", font=self.subtitle_font, borderwidth=0) style.map("Success.TButton", background=[("active", "#28a745"), ("pressed", "#218838")], foreground=[("active", "white"), ("pressed", "white")]) style.configure("Success.TButton", background="#28a745", foreground="white", font=self.subtitle_font, borderwidth=0) style.map("Danger.TButton", background=[("active", "#dc3545"), ("pressed", "#c82333")], foreground=[("active", "white"), ("pressed", "white")]) style.configure("Danger.TButton", background="#dc3545", foreground="white", font=self.subtitle_font, borderwidth=0) # 输入框样式 style.configure("Custom.TEntry", fieldbackground="#f8f9fa", bordercolor="#ced4da") def load_config(self): """加载配置文件""" config_path = "excel_config.json" default_config = { "id_col": "No.", "desc_col": "レビュー観点(CHN)", "status_col": "レビュー結果", "sheet_name": "", "header_row": 9, "last_dir": os.getcwd(), "custom_status": "OK", "confirmation_columns": [], "column_order": ["selection", "id", "description", "status"] } if os.path.exists(config_path): try: with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) # 确保所有键都存在 for key in default_config: if key not in config: config[key] = default_config[key] return config except Exception as e: logger.error(f"加载配置文件失败: {str(e)}") return default_config return default_config def save_config(self): """保存配置文件""" config_path = "excel_config.json" try: with open(config_path, 'w', encoding='utf-8') as f: json.dump(self.config, f, ensure_ascii=False, indent=2) except Exception as e: logger.error(f"保存配置文件失败: {str(e)}") def create_widgets(self): """创建现代化界面元素""" # 主容器 main_container = ttk.Frame(self.master, style="Card.TFrame") main_container.pack(fill="both", expand=True, padx=20, pady=20) # 标题栏 title_frame = ttk.Frame(main_container, style="Title.TFrame") title_frame.pack(fill="x", pady=(0, 20)) ttk.Label(title_frame, text="功能点确认系统", font=self.title_font, background="#4a76b5", foreground="white", padding=10).pack(side="left", fill="x", expand=True) # 主内容区域 content_frame = ttk.Frame(main_container) content_frame.pack(fill="both", expand=True) # 左侧控制面板 - 可滚动 control_container = ttk.Frame(content_frame, width=350) control_container.pack(side="left", fill="y", padx=(0, 20)) # 创建自定义可滚动框架 scrollable_frame = ScrollableFrame(control_container, width=350) scrollable_frame.pack(fill="both", expand=True) # 获取内部框架 inner_frame = scrollable_frame.scrollable_frame # 美化控制面板 control_card = ttk.LabelFrame(inner_frame, text="控制面板", style="Card.TLabelframe") control_card.pack(fill="both", expand=True, padx=10, pady=10) # 文件选择区域 file_frame = ttk.LabelFrame(control_card, text="Excel文件设置") file_frame.pack(fill="x", padx=10, pady=(0, 15)) ttk.Label(file_frame, text="Excel文件路径:").pack(anchor="w", pady=(0, 5), padx=10) path_frame = ttk.Frame(file_frame) path_frame.pack(fill="x", pady=5, padx=10) self.path_entry = ttk.Entry(path_frame, width=30, style="Custom.TEntry") self.path_entry.pack(side="left", fill="x", expand=True, padx=(0, 5)) ttk.Button(path_frame, text="浏览", command=self.browse_file, width=8).pack(side="left") ttk.Button(file_frame, text="加载数据", command=self.load_data, style="Primary.TButton").pack(fill="x", pady=10, padx=10) # Sheet选择区域 sheet_frame = ttk.LabelFrame(control_card, text="工作设置") sheet_frame.pack(fill="x", padx=10, pady=(0, 15)) ttk.Label(sheet_frame, text="当前Sheet:").pack(anchor="w", padx=10, pady=(10, 0)) self.sheet_var = tk.StringVar(value=self.config.get("sheet_name", "未选择")) sheet_display = ttk.Label(sheet_frame, textvariable=self.sheet_var, font=self.subtitle_font) sheet_display.pack(anchor="w", pady=(0, 10), padx=10) ttk.Button(sheet_frame, text="选择工作", command=self.select_sheet, style="Primary.TButton").pack(fill="x", padx=10, pady=(0, 10)) # 头行设置 header_frame = ttk.LabelFrame(control_card, text="头设置") header_frame.pack(fill="x", padx=10, pady=(0, 15)) ttk.Label(header_frame, text="头所在行号:").pack(anchor="w", padx=10, pady=(10, 0)) self.header_var = tk.IntVar(value=self.config.get("header_row", 9)) ttk.Entry(header_frame, textvariable=self.header_var, width=10, style="Custom.TEntry").pack(anchor="w", pady=5, padx=10) # 列名配置区域 col_frame = ttk.LabelFrame(control_card, text="列名配置") col_frame.pack(fill="x", padx=10, pady=(0, 15)) # ID列 ttk.Label(col_frame, text="ID列名:").pack(anchor="w", padx=10, pady=(10, 0)) self.id_col_var = tk.StringVar(value=self.config["id_col"]) ttk.Entry(col_frame, textvariable=self.id_col_var, style="Custom.TEntry").pack(fill="x", pady=(0, 10), padx=10) # 功能点列 ttk.Label(col_frame, text="功能点列名:").pack(anchor="w", padx=10, pady=(0, 0)) self.desc_col_var = tk.StringVar(value=self.config["desc_col"]) ttk.Entry(col_frame, textvariable=self.desc_col_var, style="Custom.TEntry").pack(fill="x", pady=(0, 10), padx=10) # 状态列 ttk.Label(col_frame, text="状态列名:").pack(anchor="w", padx=10, pady=(0, 0)) self.status_col_var = tk.StringVar(value=self.config["status_col"]) ttk.Entry(col_frame, textvariable=self.status_col_var, style="Custom.TEntry").pack(fill="x", pady=(0, 10), padx=10) # 自定义状态 ttk.Label(col_frame, text="自定义确认状态:").pack(anchor="w", padx=10, pady=(0, 0)) self.custom_status_var = tk.StringVar(value=self.config.get("custom_status", "OK")) ttk.Entry(col_frame, textvariable=self.custom_status_var, style="Custom.TEntry").pack(fill="x", pady=(0, 10), padx=10) # 多列确认设置 multi_col_frame = ttk.LabelFrame(control_card, text="多列确认设置") multi_col_frame.pack(fill="x", padx=10, pady=(0, 15)) # 列选择器 ttk.Label(multi_col_frame, text="选择要确认的列:").pack(anchor="w", padx=10, pady=(10, 0)) col_selector_frame = ttk.Frame(multi_col_frame) col_selector_frame.pack(fill="x", pady=5, padx=10) self.col_selector = ttk.Combobox(col_selector_frame, state="readonly", width=15) self.col_selector.pack(side="left", fill="x", expand=True, padx=(0, 5)) # 添加/移除按钮 ttk.Button(col_selector_frame, text="添加", command=self.add_confirmation_column, width=8).pack(side="left") # 已选列列 ttk.Label(multi_col_frame, text="已选确认列:").pack(anchor="w", padx=10, pady=(10, 0)) self.selected_cols_listbox = tk.Listbox(multi_col_frame, height=3, font=self.normal_font, bg="#f8f9fa", highlightthickness=0) self.selected_cols_listbox.pack(fill="x", pady=5, padx=10) # 加载已配置的确认列 for col in self.confirmation_columns: self.selected_cols_listbox.insert(tk.END, col) # 移除按钮 remove_btn = ttk.Button(multi_col_frame, text="移除选中列", command=self.remove_confirmation_column) remove_btn.pack(fill="x", pady=(0, 10), padx=10) # 操作按钮区域 btn_frame = ttk.Frame(control_card) btn_frame.pack(fill="x", padx=5, pady=10) ttk.Button(btn_frame, text="保存配置", command=self.save_current_config, style="Success.TButton").pack(fill="x", pady=5) ttk.Button(btn_frame, text="确认选中项", command=self.confirm_selected, style="Primary.TButton").pack(fill="x", pady=5) ttk.Button(btn_frame, text="全选", command=self.select_all, style="Primary.TButton").pack(side="left", fill="x", expand=True, pady=5) ttk.Button(btn_frame, text="取消全选", command=self.deselect_all, style="Danger.TButton").pack(side="left", fill="x", expand=True, pady=5) ttk.Button(btn_frame, text="保存到Excel", command=self.save_to_excel, style="Success.TButton").pack(fill="x", pady=5) # 数据显示区域 data_card = ttk.LabelFrame(content_frame, text="功能点列", style="Card.TLabelframe") data_card.pack(side="right", fill="both", expand=True) # Treeview容器 tree_frame = ttk.Frame(data_card) tree_frame.pack(fill="both", expand=True, padx=5, pady=5) # 创建Treeview self.tree = ttk.Treeview(tree_frame, columns=[], show="headings", height=20) # 滚动条设置 vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) # 使用grid布局管理器 self.tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") # 配置网格权重 tree_frame.columnconfigure(0, weight=1) tree_frame.rowconfigure(0, weight=1) # 添加标签样式 self.tree.tag_configure("confirmed", background="#d4edda") self.tree.tag_configure("pending", background="#f8d7da") # 绑定列调整事件 self.tree.bind("<Configure>", self.on_tree_configure) # 绑定点击事件处理复选框 self.tree.bind("<Button-1>", self.on_tree_click) # 绑定选择事件 self.tree.bind("<<TreeviewSelect>>", self.on_tree_select) # 详情区域 detail_frame = ttk.LabelFrame(data_card, text="功能点详情", style="Card.TLabelframe") detail_frame.pack(fill="x", padx=5, pady=(0, 5)) self.detail_text = tk.Text(detail_frame, wrap="word", font=self.normal_font, height=3, bg="#f8f9fa", relief="solid", borderwidth=1) scroll_detail = ttk.Scrollbar(detail_frame, command=self.detail_text.yview) self.detail_text.config(yscrollcommand=scroll_detail.set) # 使用grid布局文本框和滚动条 self.detail_text.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) scroll_detail.grid(row=0, column=1, sticky="ns", pady=5) # 配置网格权重 detail_frame.columnconfigure(0, weight=1) detail_frame.rowconfigure(0, weight=1) self.detail_text.config(state="disabled") # 状态栏 self.status_var = tk.StringVar() self.status_var.set("就绪 - 请选择Excel文件开始") # 创建状态栏容器框架 status_container = ttk.Frame(self.master, style="Status.TFrame") status_container.pack(side="bottom", fill="x", padx=0, pady=0) # 状态标签 status_bar = ttk.Label( status_container, textvariable=self.status_var, style="Status.TLabel", anchor="w", padding=(10, 5, 10, 5) ) status_bar.pack(side="bottom", fill="x", expand=False) # 初始化Treeview列 self.update_treeview_columns() def update_treeview_columns(self): """更新Treeview列""" # 基础列 base_columns = ["selection", "id", "description", "status"] # 所有列 = 基础列 + 确认列 all_columns = base_columns + self.confirmation_columns # 如果列顺序未设置或长度不匹配,则重置 if not self.column_order or len(self.column_order) != len(all_columns): self.column_order = all_columns # 重新配置Treeview self.tree.configure(columns=self.column_order) # 设置列标题 column_titles = { "selection": "选择", "id": "ID", "description": "功能点", "status": "状态" } for col in self.column_order: if col in column_titles: self.tree.heading(col, text=column_titles[col]) else: self.tree.heading(col, text=col) # 设置列属性 if col == "selection": self.tree.column(col, width=50, anchor="center") elif col == "id": self.tree.column(col, width=100, anchor="center") elif col == "description": self.tree.column(col, width=400, anchor="w", stretch=True) elif col == "status": self.tree.column(col, width=100, anchor="center") else: # 确认列 self.tree.column(col, width=100, anchor="center") # 调整列宽 self.adjust_columns() def on_tree_configure(self, event): """Treeview大小改变时调整列宽""" self.adjust_columns() def on_tree_select(self, event): """当Treeview中选中行时,显示功能点详情""" selected_items = self.tree.selection() if not selected_items: return item = selected_items[0] # 获取功能点描述 values = self.tree.item(item, "values") # 找到描述列的索引 desc_idx = None for idx, col in enumerate(self.tree["columns"]): if self.tree.heading(col, "text") == "功能点": desc_idx = idx break if desc_idx is not None and desc_idx < len(values): desc = values[desc_idx] self.detail_text.config(state="normal") self.detail_text.delete(1.0, tk.END) self.detail_text.insert(tk.END, desc) self.detail_text.config(state="disabled") def on_tree_click(self, event): """处理Treeview点击事件,切换复选框状态""" region = self.tree.identify("region", event.x, event.y) if region == "cell": column = self.tree.identify_column(event.x) item = self.tree.identify_row(event.y) if not item: return # 获取列索引 col_idx = int(column[1:]) - 1 columns = self.tree["columns"] # 确保索引有效 if col_idx >= len(columns): return # 获取列标识符 col_id = columns[col_idx] # 只处理"选择"列 if self.tree.heading(col_id, "text") == "选择": # 获取当前状态 values = list(self.tree.item(item, "values")) current_state = values[col_idx] # 切换状态 if current_state == "☐": new_state = "☑" else: new_state = "☐" # 更新Treeview values[col_idx] = new_state self.tree.item(item, values=values) # 更新状态存储 self.check_states[item] = (new_state == "☑") # 更新行的整体状态显示 self.update_row_status(item) def update_row_status(self, item_id): """根据复选框状态更新行状态显示""" values = list(self.tree.item(item_id, "values")) is_checked = self.check_states.get(item_id, False) # 找到状态列的位置 status_col_idx = None for idx, col_id in enumerate(self.tree["columns"]): if self.tree.heading(col_id, "text") == "状态": status_col_idx = idx break if status_col_idx is not None and status_col_idx < len(values): if is_checked: values[status_col_idx] = "✓ 待确认" self.tree.item(item_id, tags=("confirmed",)) else: values[status_col_idx] = "✗ 未确认" self.tree.item(item_id, tags=("pending",)) self.tree.item(item_id, values=values) def adjust_columns(self, event=None): """根据窗口大小自动调整列宽""" if not self.tree.winfo_exists(): return width = self.tree.winfo_width() if width < 100: # 防止宽度过小 return # 计算可用宽度 available_width = width - 20 # 减去滚动条宽度 # 设置列宽比例 column_weights = { "selection": 0.05, "id": 0.15, "description": 0.55, # 增加功能点列的比例 "status": 0.1 } # 设置基础列宽 for col, weight in column_weights.items(): if col in self.tree["columns"]: col_width = int(available_width * weight) self.tree.column(col, width=col_width) # 设置确认列宽 if self.confirmation_columns: confirm_col_width = int(available_width * 0.08) # 减小确认列宽度 for col in self.confirmation_columns: if col in self.tree["columns"]: self.tree.column(col, width=confirm_col_width) def update_col_selector(self): """更新列选择器""" if self.df is not None: self.col_selector["values"] = list(self.df.columns) def add_confirmation_column(self): """添加确认列""" col = self.col_selector.get() if col and col not in self.confirmation_columns: self.confirmation_columns.append(col) self.selected_cols_listbox.insert(tk.END, col) self.update_treeview_columns() def remove_confirmation_column(self): """移除确认列""" selection = self.selected_cols_listbox.curselection() if selection: index = selection[0] self.confirmation_columns.pop(index) self.selected_cols_listbox.delete(index) self.update_treeview_columns() def browse_file(self): initial_dir = self.config.get("last_dir", os.getcwd()) file_path = filedialog.askopenfilename( initialdir=initial_dir, filetypes=[("Excel文件", "*.xlsx;*.xls")] ) if file_path: self.path_entry.delete(0, tk.END) self.path_entry.insert(0, file_path) # 更新最后访问目录 self.config["last_dir"] = os.path.dirname(file_path) self.save_config() def select_sheet(self): """选择工作""" file_path = self.path_entry.get() if not file_path or not os.path.exists(file_path): messagebox.showerror("错误", "请先选择有效的Excel文件") return try: # 获取所有sheet名称 xl = pd.ExcelFile(file_path) sheet_names = xl.sheet_names # 创建选择对话框 sheet_dialog = tk.Toplevel(self.master) sheet_dialog.title("选择工作") sheet_dialog.geometry("400x300") sheet_dialog.transient(self.master) sheet_dialog.grab_set() sheet_dialog.configure(bg="#f5f7fa") ttk.Label(sheet_dialog, text="请选择工作:", font=self.subtitle_font, background="#f5f7fa").pack(pady=10) # 使用Treeview显示工作 sheet_tree = ttk.Treeview(sheet_dialog, columns=("名称",), show="headings", height=8) sheet_tree.heading("名称", text="工作名称") sheet_tree.column("名称", width=350) sheet_tree.pack(fill="both", expand=True, padx=20, pady=5) for name in sheet_names: sheet_tree.insert("", "end", values=(name,)) # 按钮框架 btn_frame = ttk.Frame(sheet_dialog) btn_frame.pack(fill="x", padx=20, pady=10) def on_select(): selected = sheet_tree.selection() if selected: self.current_sheet = sheet_tree.item(selected[0], "values")[0] self.sheet_var.set(self.current_sheet) # 保存工作名称到配置 self.config["sheet_name"] = self.current_sheet self.save_config() sheet_dialog.destroy() ttk.Button(btn_frame, text="取消", command=sheet_dialog.destroy).pack(side="right", padx=5) ttk.Button(btn_frame, text="确定", command=on_select, style="Primary.TButton").pack(side="right") except Exception as e: messagebox.showerror("错误", f"读取Excel失败: {str(e)}") logger.error(f"选择工作失败: {str(e)}") def load_data(self): file_path = self.path_entry.get() if not file_path or not os.path.exists(file_path): messagebox.showerror("错误", "无效的文件路径") return # 在状态栏显示加载中 self.status_var.set("正在加载数据...") self.master.update() # 强制更新界面 # 清空Treeview for item in self.tree.get_children(): self.tree.delete(item) self.check_states = {} # 获取当前配置 id_col = self.id_col_var.get().strip() desc_col = self.desc_col_var.get().strip() status_col = self.status_col_var.get().strip() sheet_name = self.sheet_var.get() or None header_row = self.header_var.get() - 1 # pandas header是0-based索引 # 直接调用加载任务 self.load_task(file_path, id_col, desc_col, status_col, sheet_name, header_row) def load_task(self, file_path, id_col, desc_col, status_col, sheet_name, header_row): try: # 确保使用正确的sheet_name if not sheet_name and self.config.get("sheet_name"): sheet_name = self.config["sheet_name"] # 从配置获取工作名 # 读取Excel文件 if sheet_name: self.df = pd.read_excel( file_path, sheet_name=sheet_name, header=header_row ) else: # 如果没有指定sheet,尝试读取第一个sheet self.df = pd.read_excel( file_path, header=header_row ) # 尝试获取第一个sheet的名称 xl = pd.ExcelFile(file_path) if xl.sheet_names: self.current_sheet = xl.sheet_names[0] self.sheet_var.set(self.current_sheet) self.excel_path = file_path # 检查列是否存在 missing_cols = [] if id_col not in self.df.columns: missing_cols.append(f"ID列 '{id_col}'") if desc_col not in self.df.columns: missing_cols.append(f"功能点列 '{desc_col}'") if missing_cols: # 提供更详细的错误信息 available_cols = "\n".join(self.df.columns) error_msg = ( f"以下列不存在: {', '.join(missing_cols)}\n\n" f"可用列名:\n{available_cols}\n\n" "请检查头行设置是否正确(默认为第9行)" ) raise ValueError(error_msg) # 如果状态列不存在,则创建 if status_col not in self.df.columns: self.df[status_col] = "否" # 默认未确认 # 更新列选择器 self.update_col_selector() # 更新Treeview列 self.update_treeview_columns() # 修复ID列显示问题 - 正确处理NaN值 if id_col in self.df.columns: # 处理所有可能的缺失值示 self.df[id_col] = self.df[id_col].fillna("缺失ID") # 处理可能的浮点数问题 if self.df[id_col].dtype == float: # 将浮点数转换为整数再转字符串 self.df[id_col] = self.df[id_col].astype(int).astype(str) else: # 确保是字符串类型 self.df[id_col] = self.df[id_col].astype(str) # 处理描述列显示问题 - 自动换行 if desc_col in self.df.columns: self.df[desc_col] = self.df[desc_col].fillna("无描述").astype(str) # 添加换行符使长文本自动换行 max_length = 100 # 每100个字符换行 self.df[desc_col] = self.df[desc_col].apply( lambda x: '\n'.join([x[i:i+max_length] for i in range(0, len(x), max_length)]) ) # 添加数据到Treeview for i, row in self.df.iterrows(): status_value = row.get(status_col, "否") # 使用图标示状态 status_icon = "✓" if status_value in ["是", "Y", "y", "Yes", "yes", "OK", "确认"] else "✗" status_text = f"{status_icon} {status_value}" tag = "confirmed" if status_icon == "✓" else "pending" # 构建行数据字典 row_data = { "selection": "☐", "id": str(row[id_col]) if id_col in row else "缺失ID", "description": row[desc_col] if desc_col in row else "无描述", "status": status_text } # 添加多列确认数据 for col in self.confirmation_columns: row_data[col] = row[col] if col in row else "" # 按列顺序构建值列 ordered_values = [row_data.get(col, "") for col in self.column_order] # 插入行 item_id = self.tree.insert("", "end", values=ordered_values, tags=(tag,)) # 存储复选框状态 self.check_states[item_id] = (status_icon == "✓") # 每100行更新一次界面 if i % 100 == 0: self.tree.update() self.master.update_idletasks() # 更新状态 self.status_var.set(f"成功加载: {len(self.df)} 条记录") # 调整列宽并刷新界面 self.adjust_columns() except Exception as e: # 显示详细的错误信息 error_msg = f"读取Excel失败: {str(e)}" self.status_var.set("加载失败") messagebox.showerror("加载错误", error_msg) logger.error(f"加载数据失败: {error_msg}\n{traceback.format_exc()}") def confirm_selected(self): """确认选中的功能点""" selected_items = [] for item_id in self.tree.get_children(): if self.check_states.get(item_id, False): selected_items.append(item_id) if not selected_items: messagebox.showinfo("提示", "请先选择功能点") return custom_status = self.custom_status_var.get().strip() or "OK" for item_id in selected_items: values = list(self.tree.item(item_id, "values")) # 找到状态列的位置 status_col_idx = None for idx, col_id in enumerate(self.tree["columns"]): if self.tree.heading(col_id, "text") == "状态": status_col_idx = idx break # 更新状态列 if status_col_idx is not None and status_col_idx < len(values): values[status_col_idx] = f"✓ {custom_status}" # 更新多列确认 if self.confirmation_columns and self.df is not None: row_idx = self.tree.index(item_id) for col in self.confirmation_columns: if col in self.df.columns: # 找到列在Treeview中的位置 col_idx = self.column_order.index(col) if col_idx < len(values): values[col_idx] = custom_status self.df.at[row_idx, col] = custom_status # 更新Treeview self.tree.item(item_id, values=values, tags=("confirmed",)) self.status_var.set(f"已确认 {len(selected_items)} 个功能点") # 自动保存 self.auto_save() def select_all(self): """全选功能点""" for item_id in self.tree.get_children(): values = list(self.tree.item(item_id, "values")) # 找到选择列的位置 select_col_idx = None for idx, col_id in enumerate(self.tree["columns"]): if self.tree.heading(col_id, "text") == "选择": select_col_idx = idx break # 更新选择列 if select_col_idx is not None and select_col_idx < len(values): values[select_col_idx] = "☑" self.tree.item(item_id, values=values) self.check_states[item_id] = True self.update_row_status(item_id) self.status_var.set("已全选所有功能点") def deselect_all(self): """取消全选功能点""" for item_id in self.tree.get_children(): values = list(self.tree.item(item_id, "values")) # 找到选择列的位置 select_col_idx = None for idx, col_id in enumerate(self.tree["columns"]): if self.tree.heading(col_id, "text") == "选择": select_col_idx = idx break # 更新选择列 if select_col_idx is not None and select_col_idx < len(values): values[select_col_idx] = "☐" self.tree.item(item_id, values=values) self.check_states[item_id] = False self.update_row_status(item_id) self.status_var.set("已取消全选所有功能点") def save_current_config(self): """保存当前配置""" self.config["id_col"] = self.id_col_var.get().strip() self.config["desc_col"] = self.desc_col_var.get().strip() self.config["status_col"] = self.status_col_var.get().strip() self.config["sheet_name"] = self.sheet_var.get() self.config["header_row"] = self.header_var.get() self.config["custom_status"] = self.custom_status_var.get().strip() self.config["confirmation_columns"] = self.confirmation_columns self.config["column_order"] = self.column_order self.save_config() messagebox.showinfo("成功", "配置已保存") def auto_save(self): """自动保存功能""" if self.df is None or not self.excel_path: return try: # 获取当前配置 id_col = self.id_col_var.get().strip() desc_col = self.desc_col_var.get().strip() status_col = self.status_col_var.get().strip() # 更新DataFrame中的确认状态 for i, item_id in enumerate(self.tree.get_children()): # 获取Treeview中的状态值 values = self.tree.item(item_id, "values") # 找到状态列的位置 status_col_idx = None for idx, col_id in enumerate(self.tree["columns"]): if self.tree.heading(col_id, "text") == "状态": status_col_idx = idx break if status_col_idx is not None and status_col_idx < len(values): status_value = values[status_col_idx] if status_value.startswith(("✓", "✗")): status_value = status_value[2:].strip() self.df.at[i, status_col] = status_value # 保存回Excel wb = openpyxl.load_workbook(self.excel_path) if self.current_sheet in wb.sheetnames: del wb[self.current_sheet] ws = wb.create_sheet(self.current_sheet) # 写入数据 for r, row in enumerate(dataframe_to_rows(self.df, index=False, header=True), 1): ws.append(row) wb.save(self.excel_path) self.status_var.set("数据已自动保存") except Exception as e: self.status_var.set("自动保存失败") logger.error(f"自动保存失败: {str(e)}") def save_to_excel(self): if self.df is None: messagebox.showerror("错误", "没有加载的数据") return try: # 执行保存 self.auto_save() messagebox.showinfo("成功", f"数据已保存到:\n{self.excel_path}") self.status_var.set("数据保存成功") except Exception as e: messagebox.showerror("保存错误", f"写入Excel失败: {str(e)}\n请确保文件未被其他程序打开") logger.error(f"保存失败: {str(e)}") def main(): root = tk.Tk() # 设置应用图标 try: # 创建一个简单的蓝色方块作为图标 icon_data = """ R0lGODlhEAAQAIAAAP///wAAACH5BAEAAAAALAAAAAAQABAAAAIOhI+py+0Po5y02ouzPgUAOw== """ icon_img = tk.PhotoImage(data=icon_data) root.tk.call('wm', 'iconphoto', root._w, icon_img) except Exception as e: logger.warning(f"设置应用图标失败: {str(e)}") # 添加全局异常处理 def handle_exception(exc_type, exc_value, exc_traceback): error_msg = f"发生未捕获的异常:\n{exc_type.__name__}: {exc_value}" tb_str = "".join(traceback.format_tb(exc_traceback)) logger.error(f"未捕获的异常: {error_msg}\n{tb_str}") messagebox.showerror("严重错误", f"{error_msg}\n\n请查看日志获取详细信息") root.destroy() sys.excepthook = handle_exception app = ExcelControlPanel(root) root.mainloop() if __name__ == "__main__": # 强制使用UTF-8编码 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') main() 还是显示缺失ID,外功能列中的字符还是没有全部显示,最后希望能在每一行与上一行做一个区分,要不然视觉上很混乱
08-20
我给你说我的需求,现在我有一个代码: SELECT CAST(111100000048210 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS bigint) AS Id, CAST(LOWER(NEWID()) AS varchar(50)) AS GUID, CAST('ipcc.org' AS varchar(50)) AS Domain, CAST( CASE WHEN ZY_TB_CustomerProfile.[客户类型] = 'VIP' THEN '703192237850693' ELSE '703192237846597' END AS varchar(20)) AS CustomerTypeCode, CAST('' AS text) AS Remark, CAST(ZY_TB_CustomerPhone.CardCode AS varchar(50)) AS S6, CAST(ZY_TB_CustomerPhone.Cardname AS varchar(100)) AS CompanyName, CAST(ZY_TB_CustomerPhone.Name AS varchar(50)) AS Name, CAST(LEFT(ZY_TB_CustomerPhone.Telephone, 15) AS varchar(15)) AS Phone, CAST(FORMAT(ZY_TB_CustomerProfile.近一年总毛利, 'N0') AS varchar(50)) AS S4, CAST(FORMAT(ZY_TB_CustomerProfile.预收款金额, 'N0') AS varchar(50)) AS S2, CAST(FORMAT(ZY_TB_CustomerProfile.应收款, 'N0') AS varchar(50)) AS S1, CAST(FORMAT(ZY_TB_CustomerProfile.全部库存金额, 'N0') AS varchar(50)) AS S3, CAST((CAST(ZY_TB_CustomerProfile.客户等级 AS varchar(50)) + ZY_TB_CustomerProfile.等级名称) AS varchar(50)) AS S5 FROM ZY_TB_CustomerPhone LEFT JOIN ZY_TB_CustomerProfile ON ZY_TB_CustomerPhone.CardCode = ZY_TB_CustomerProfile.[客户编号] WHERE ZY_TB_CustomerPhone.CardCode IS NOT NULL AND ZY_TB_CustomerPhone.Cardname IS NOT NULL AND ZY_TB_CustomerPhone.Telephone NOT LIKE '8441%' AND ZY_TB_CustomerPhone.Cardname NOT LIKE '%中源合聚生物%' AND ZY_TB_CustomerPhone.CardCode <> '' AND ZY_TB_CustomerPhone.Cardname <> '' AND ZY_TB_CustomerPhone.Telephone <> '15301373560';这个代码是我进行数据推送的初始代码,意味着我callcenter的第一批数据就是这个;现在我的需求是,后续对callcenter的数据进行新增或者更新,首先再用这个代码查一下数据,根据Phone作为判断条件,如果这个Phone在callcenter中没有 那么就把数据新增进去,要注意Id和GUID要保证跟原先callcenter中已有的Id和GUID不能重复。如果这个Phone在callcenter中有了,那么就更新callcenter原有数据中的CustomerTypeCode、S6、S4、S2、S1、S3、S5,如果CompanyName本身callcenter没有,那也进行更新。以下是sqlserver连接方式也就是sap的数据连接 sap_conn = pyodbc.connect( 'DRIVER={ODBC Driver 17 for SQL Server};' 'SERVER=192.168.0.229;' 'DATABASE=SINO_SAP;' 'UID=SAPReader;' 'PWD=Sino2025zyq;' );以下是mysql的连接方式也就是callcenter的连接 password = "@Aa.1234" encoded_password = quote_plus(password) # 对特殊字符进行URL编码 mysql_conn_str = ( f"mysql+pymysql://lpsoft:{encoded_password}@192.168.3.1:3306/OrderManage" "?charset=utf8mb4" ) mysql_engine = create_engine( mysql_conn_str, pool_size=5, max_overflow=10, pool_timeout=30, connect_args={'connect_timeout': 15} )
08-29
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值