from collections import defaultdict import re from typing import Optional import traceback import logging import pandas as pd from openpyxl import load_workbook import json import os # import xlrd # ===================================================== # 行类型判断 # ===================================================== logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) def is_data_row(row): """ 判断是否为数据行 数据行的特征:第1列包含数字 Args: row (dict): 包含行数据的字典,格式为 {"content": {"列号": "值", ...}} Returns: bool: 如果是数据行返回True,否则返回False """ try: v = row["content"].get("1", "") result = isinstance(v, str) and v.strip().isdigit() logger.debug(f"is_data_row: value='{v}', result={result}") return result except Exception as e: logger.error(f"Error in is_data_row: {e}, row: {row}") return False def is_field_row(row): """ 判断是否为字段行 字段行的特征:至少有5个值,且超过90%的值不是数字,根据环境处理 Args: row (dict): 包含行数据的字典 Returns: bool: 如果是字段行返回True,否则返回False """ try: values = list(row["content"].values()) if len(values) < 5: logger.debug(f"is_field_row: less than 5 values, returning False") return False non_digit = sum(1 for v in values if isinstance(v, str) and not v.strip().isdigit()) result = non_digit / len(values) > 0.9 logger.debug(f"is_field_row: non_digit_ratio={non_digit / len(values)}, result={result}") return result except Exception as e: logger.error(f"Error in is_field_row: {e}, row: {row}") return False def is_header_row(row): """ 判断是否为表头行 表头行的特征:列数不超过4个,且不是字段行或数据行,根据环境处理 Args: row (dict): 包含行数据的字典 Returns: bool: 如果是表头行返回True,否则返回False """ try: content_len = len(row["content"]) field_result = is_field_row(row) data_result = is_data_row(row) result = content_len <= 4 and not field_result and not data_result logger.debug(f"is_header_row: content_len={content_len}, is_field={field_result}, is_data={data_result}, result={result}") return result except Exception as e: logger.error(f"Error in is_header_row: {e}, row: {row}") return False # ===================================================== # 多级表头 # ===================================================== def collect_field_rows(rows, start_idx): """ 从指定索引开始收集连续的字段行 Args: rows (list): 行数据列表 start_idx (int): 开始收集的索引 Returns: tuple: (字段行列表, 下一个非字段行的索引) """ try: field_rows = [] i = start_idx initial_i = start_idx while i < len(rows) and is_field_row(rows[i]): field_rows.append(rows[i]["content"]) i += 1 logger.info(f"collect_field_rows: collected {len(field_rows)} field rows from index {initial_i} to {i}") return field_rows, i except Exception as e: logger.error(f"Error in collect_field_rows: {e}") return [], start_idx def dedup_path(path): """ 去除路径中的重复元素,保持顺序 Args: path (list): 路径列表 Returns: list: 去重后的路径列表 """ try: new_path = [] for p in path: if p and (not new_path or new_path[-1] != p): new_path.append(p) logger.debug(f"dedup_path: original={path}, deduplicated={new_path}") return new_path except Exception as e: logger.error(f"Error in dedup_path: {e}, path: {path}") return [] def build_field_paths(field_rows): """ 构建字段路径映射,将列号映射到字段路径 Args: field_rows (list): 字段行列表 Returns: defaultdict: 列号到字段路径的映射 """ try: field_paths = defaultdict(list) for row_idx, row in enumerate(field_rows): for col, name in row.items(): if name: field_paths[col].append(name) for col in field_paths: field_paths[col] = dedup_path(field_paths[col]) logger.info(f"build_field_paths: processed {len(field_paths)} columns from {len(field_rows)} rows") return field_paths except Exception as e: logger.error(f"Error in build_field_paths: {e}") return defaultdict(list) # ===================================================== # 值 & 结构处理 # ===================================================== def normalize_value(value): """ 标准化值,将特殊值转换为对应格式 Args: value: 待处理的值 Returns: 处理后的值 """ try: if value == "√": logger.debug(f"normalize_value: converting '√' to True") return True if value in ("", None): logger.debug(f"normalize_value: empty value converted to empty string") return "" logger.debug(f"normalize_value: value={value} unchanged") return value except Exception as e: logger.error(f"Error in normalize_value: {e}, value: {value}") return "" def insert_by_path(target, path, value): """ 根据路径在目标对象中插入值 Args: target (dict): 目标字典 path (list): 插入路径 value: 待插入的值 """ try: if not path: logger.warning("insert_by_path: empty path provided") return cur = target for key in path[:-1]: cur = cur.setdefault(key, {}) cur[path[-1]] = value logger.debug(f"insert_by_path: inserted value '{value}' at path {path}") except Exception as e: logger.error(f"Error in insert_by_path: {e}, target: {target}, path: {path}, value: {value}") def flatten_singleton_dict(obj): """ 扁平化单元素字典,如果字典只有一个键且键名与值中的键名相同,则合并 Args: obj: 待处理的对象 Returns: 处理后的对象 """ try: if not isinstance(obj, dict): return obj for k in list(obj.keys()): v = obj[k] if isinstance(v, dict): v = flatten_singleton_dict(v) if len(v) == 1 and k in v: obj[k] = v[k] logger.debug(f"flatten_singleton_dict: flattened singleton dict for key '{k}'") else: obj[k] = v return obj except Exception as e: logger.error(f"Error in flatten_singleton_dict: {e}, obj: {obj}") return obj # def build_content(field_paths, row_content): # try: # result = {} # for col, path in field_paths.items(): # value = normalize_value(row_content.get(col, "")) # insert_by_path(result, path, value) # flatten_singleton_dict(result) # logger.debug(f"build_content: built content with {len(result)} top-level keys") # return result # except Exception as e: # logger.error(f"Error in build_content: {e}, field_paths: {field_paths}, row_content: {row_content}") # return {} def flatten_dict(d, parent_key='', sep='_'): """ 将 {"A": {"B": "1"}} 转换为 {"A_B": "1"} """ items = [] for k, v in d.items(): new_key = f"{parent_key}{sep}{k}" if parent_key else k if isinstance(v, dict): # 递归展开 items.extend(flatten_dict(v, new_key, sep=sep).items()) else: items.append((new_key, v)) return dict(items) def build_content(field_paths, row_content): """ 根据字段路径构建内容结构 Args: field_paths (dict): 字段路径映射 row_content (dict): 行内容数据 Returns: dict: 构建的内容结构 """ result = {} for col, path in field_paths.items(): value = normalize_value(row_content.get(col, "")) insert_by_path(result, path, value) # 1. 先执行你原有的单级压缩 (解决 A -> {A: x}) flatten_singleton_dict(result) # 2. 执行深度展平 (将嵌套字典转为 "键_子键" 格式) # 我们遍历第一层,如果值是字典,则展开它 final_result = {} for k, v in result.items(): if isinstance(v, dict): # 展开子字典,并带上父级的键名作为前缀 flattened_sub = flatten_dict(v, parent_key=k, sep='_') final_result.update(flattened_sub) else: final_result[k] = v return final_result # ===================================================== # 主解析逻辑 # ===================================================== def transform_excel_json(rows): """ 将Excel行数据转换为JSON格式 Args: rows (list): 行数据列表 Returns: list: 转换后的JSON数据列表 """ try: logger.info(f"Starting transform_excel_json with {len(rows)} rows") output = [] i = 0 processed_count = 0 while i < len(rows): row = rows[i] row_type = None if is_header_row(row): output.append({"header": row["content"]}) row_type = "header" i += 1 elif is_field_row(row): field_rows, next_idx = collect_field_rows(rows, i) field_paths = build_field_paths(field_rows) i = next_idx row_type = "field" while i < len(rows) and is_data_row(rows[i]): content = build_content(field_paths, rows[i]["content"]) output.append({"content": content}) i += 1 processed_count += 1 else: i += 1 continue # Skip non-header, non-field, non-data rows logger.debug(f"Processed row {i-1}, type: {row_type}") logger.info(f"Completed transform_excel_json: {len(output)} items, {processed_count} data rows processed") return output except Exception as e: logger.error(f"Error in transform_excel_json: {e}") logger.error(traceback.format_exc()) raise # ===================================================== # Excel 读取(含合并单元格) # ===================================================== def read_excel_with_merged_cells(file_path: str, sheet_name: Optional[str] = None): """ 读取Excel文件,包括合并单元格的处理 Args: file_path (str): Excel文件路径 sheet_name (Optional[str]): 工作表名称,如果为None则使用默认表 Returns: pd.DataFrame: 包含Excel数据的DataFrame """ try: _, ext = os.path.splitext(file_path) ext = ext.lower() if ext == ".xlsx": logger.info(f"Reading Excel file: {file_path}, sheet: {sheet_name}") wb = load_workbook(file_path, data_only=True) ws = wb[sheet_name] if sheet_name else wb.active max_row, max_col = ws.max_row, ws.max_column logger.info(f"Excel sheet dimensions: {max_row} rows x {max_col} columns") data = [["" for _ in range(max_col)] for _ in range(max_row)] for r in range(1, max_row + 1): for c in range(1, max_col + 1): val = ws.cell(row=r, column=c).value data[r - 1][c - 1] = "" if val is None else str(val).strip() # 处理合并单元格 merged_count = 0 for merged in ws.merged_cells.ranges: tl = data[merged.min_row - 1][merged.min_col - 1] for r in range(merged.min_row - 1, merged.max_row): for c in range(merged.min_col - 1, merged.max_col): data[r][c] = tl merged_count += 1 logger.info(f"Processed {merged_count} merged cell ranges") df = pd.DataFrame(data) logger.info(f"Created DataFrame with shape {df.shape}") return df else: raise ValueError("Unsupported file format") except Exception as e: logger.error(f"Error in read_excel_with_merged_cells: {e}") logger.error(traceback.format_exc()) raise def dataframe_to_row_json(df: pd.DataFrame): """ 将DataFrame转换为行JSON格式 Args: df (pd.DataFrame): 源DataFrame Returns: list: 行JSON格式的列表 """ try: logger.info(f"Converting DataFrame to row JSON, shape: {df.shape}") rows = [] for idx, row in df.iterrows(): content = { str(col_idx + 1): val for col_idx, val in enumerate(row) if isinstance(val, str) and val != "" } if content: rows.append({ "row_index": idx + 1, "content": content }) logger.info(f"Converted DataFrame to {len(rows)} JSON rows") return rows except Exception as e: logger.error(f"Error in dataframe_to_row_json: {e}") logger.error(traceback.format_exc()) raise # ===================================================== # Markdown 转换 # ===================================================== # def json_to_markdown(data): # markdown_output = [] # for item in data: # # 处理标题行 (Header) # if "header" in item: # # 提取 header 中的描述信息 # header_vals = item["header"].values() # title = " ".join([v for v in header_vals if ";" in v or v.startswith("CPP") or "清单" in v]) # # 如果 header 中包含设备名称和型号,进行拼接 # if not title: # title = " ".join(header_vals) # markdown_output.append(f"\n## {title.replace(';', ' ')}\n") # # 处理内容行 (Content) # elif "content" in item: # content = item["content"] # # 如果是该表格的第一行,生成表头 # if not any("|" in line for line in markdown_output[-2:]): # columns = list(content.keys()) # header_row = "| " + " | ".join(columns) + " |" # separator_row = "| " + " | ".join(["---"] * len(columns)) + " |" # markdown_output.append(header_row) # markdown_output.append(separator_row) # # 生成数据行 # values = [str(v) if v is not None else "" for v in content.values()] # # 处理布尔值显示 # values = ["是" if v == "True" else "否" if v == "False" else v for v in values] # markdown_output.append("| " + " | ".join(values) + " |") # return "\n".join(markdown_output) import json def json_to_markdown(json_data): markdown_lines = [] current_table_data = [] for entry in json_data: # 1. 处理标题行 (Header) if "header" in entry: # 如果之前已经有累积的数据,先输出旧表格(防止连续多个表格的情况) if current_table_data: markdown_lines.extend(build_table(current_table_data)) current_table_data = [] # 提取 Header 内容并格式化为标题 header_dict = entry["header"] # 按照您的要求,处理设备名称和型号的拼接显示 title_parts = [v.replace(';', ' ') for v in header_dict.values()] title = " ".join(title_parts) markdown_lines.append(f"\n## {title}\n") # 2. 处理内容行 (Content) elif "content" in entry: current_table_data.append(entry["content"]) # 3. 输出最后一个表格 if current_table_data: markdown_lines.extend(build_table(current_table_data)) return "\n".join(markdown_lines) def build_table(data_list): """将字典列表转换为 Markdown 表格字符串""" if not data_list: return [] lines = [] # 提取所有键作为表头 headers = list(data_list[0].keys()) # 构造表头行 header_line = "| " + " | ".join(headers) + " |" # 构造分割行 separator_line = "| " + " | ".join(["---"] * len(headers)) + " |" lines.append(header_line) lines.append(separator_line) # 构造数据行 for item in data_list: row_values = [] for h in headers: val = item.get(h, "") # 转换布尔值为中文,处理 None 值 if val is True: val = "是" elif val is False or val == "": val = " " row_values.append(str(val).replace("\n", " ")) # 避免数据内换行破坏表格结构 lines.append("| " + " | ".join(row_values) + " |") return lines # --------------------------------------------------------- # 执行转换 # --------------------------------------------------------- # 假设 source_json 是您提供的那段原始数据 try: # 这里的 source_json 代表您贴出的完整 JSON 列表 # md_output = json_to_markdown_tables(source_json) # print(md_output) pass except Exception as e: print(f"解析错误: {e}") # def flatten_dict(d, parent_key='', sep='(', end_sep=')'): # """ # 递归扁平化嵌套字典,生成 '父字段(子字段)(孙字段)' 格式的键。 # 支持任意深度嵌套。 # """ # items = [] # for k, v in d.items(): # new_key = f"{parent_key}{sep}{k}{end_sep}" if parent_key else k # if isinstance(v, dict): # items.extend(flatten_dict(v, new_key, sep, end_sep).items()) # else: # items.append((new_key, v)) # return dict(items) # def extract_all_content_records(data): # """ # 从任意结构的 JSON 数据中提取所有顶层包含 "content" 键的对象。 # 假设数据是一个列表,每个元素可能是 header 或 content。 # 如果不是列表,也尝试遍历。 # """ # records = [] # def recurse(obj): # if isinstance(obj, list): # for item in obj: # recurse(item) # elif isinstance(obj, dict): # if "content" in obj and isinstance(obj["content"], dict): # records.append(obj["content"]) # else: # for value in obj.values(): # recurse(value) # recurse(data) # return records # def build_markdown_table(records): # if not records: # return "> 未找到任何 content 记录。\n" # flat_records = [flatten_dict(record) for record in records] # # 收集所有字段(保持顺序) # all_keys = {} # for fr in flat_records: # for k in fr: # all_keys[k] = None # all_keys = list(all_keys.keys()) # # 构建表格 # header = "| " + " | ".join(all_keys) + " |" # separator = "| " + " | ".join(["---"] * len(all_keys)) + " |" # rows = [ # "| " + " | ".join(str(fr.get(k, "")) for k in all_keys) + " |" # for fr in flat_records # ] # return "\n".join([header, separator] + rows) # def json_to_markdown(json_data): # """ # 将任意 JSON 文件中所有 'content' 对象转换为 Markdown 表格。 # """ # records = extract_all_content_records(json_data) # md_table = build_markdown_table(records) # return md_table