certflow.handlers.sorter 源代码

"""多键排序处理器模块

提供基于多字段组合的分组、排序和序号生成功能,
用于销售计划数据先按业务键分组,组内按产品键排序,
并为每条记录生成全局唯一的序号。
"""

from __future__ import annotations

from collections import defaultdict
from typing import Any

from loguru import logger


[文档] class Sorter: """分组排序处理器 提供两级分组排序功能: 1. 第一层:按业务分组键分组(如订单信息) 2. 第二层:每组内按产品排序键排序(如产品名称、型号、规格) 3. 生成全局唯一的序号(分组前缀 + 组内序号) Examples: >>> sorter = Sorter() >>> data = [ ... {"plan_date": "2024-01-01", "customer": "客户A", "product_name": "阀门B", "product_model": "DN80"}, ... {"plan_date": "2024-01-01", "customer": "客户A", "product_name": "阀门A", "product_model": "DN100"}, ... {"plan_date": "2024-01-01", "customer": "客户A", "product_name": "阀门A", "product_model": "DN80"}, ... ] >>> result = sorter.group_and_sort( ... data, ... group_keys=["plan_date", "customer"], ... sort_keys=["product_name", "product_model"] ... ) >>> for item in result: ... print(f"{item['full_seq']}: {item['product_name']}") G001-001: 阀门A G001-002: 阀门A G001-003: 阀门B """
[文档] @staticmethod def group_and_sort( data: list[dict[str, Any]], group_keys: list[str], sort_keys: list[str] | None = None, keep_original_order: bool = True, generate_seq: bool = True, use_global_prefix: bool = True, prefix_format: str = "G{:03d}", seq_format: str = "{:03d}", seq_field: str = "group_seq", group_key_field: str = "_group_key", group_index_field: str = "_group_index", source_file: str = "", source_sheet: str = "", ) -> list[dict[str, Any]]: """两级分组排序 先按业务分组键分组,再在每组内按指定排序键排序,并可生成组内序号。 Args: data: 原始数据列表,每个元素为字典格式的记录 group_keys: 分组键列表,如["plan_date", "customer", "project_name", "sales_order_no"] sort_keys: 组内排序键列表,如["product_name", "product_model", "product_spec"], None表示保持原始顺序 keep_original_order: 是否保留原始顺序标记,默认为True generate_seq: 是否生成组内序号,默认为True use_global_prefix: 是否使用全局分组前缀(用于区分不同分组),默认为True prefix_format: 分组前缀格式,默认"G{:03d}",生成G001, G002... seq_format: 组内序号格式,默认"{:03d}",生成001,002... seq_field: 序号存储的字段名,默认"group_seq" group_key_field: 分组键存储的字段名,默认"_group_key" group_index_field: 分组索引存储的字段名,默认"_group_index" source_file: 源文件路径,用于提取年份生成_original_order,如"2026年销售计划.xlsx" source_sheet: 工作表名称,用于提取月份生成_original_order,如"1月" Returns: list[dict[str, Any]]: 处理后的数据列表,每条记录包含: - _group_key: 分组键值(由group_keys组合而成) - _group_index: 分组索引(1-based) - group_seq: 组内序号(如"001") - full_seq: 完整序号(如"G001-001"),仅当use_global_prefix=True时 - _original_order: 原始顺序键(YYMMxxxx),仅当keep_original_order=True时 Examples: >>> data = [ ... {"order_no": "SO001", "product": "阀门B", "spec": "DN80"}, ... {"order_no": "SO001", "product": "阀门A", "spec": "DN100"}, ... {"order_no": "SO002", "product": "阀门C", "spec": "DN50"} ... ] >>> result = Sorter.group_and_sort( ... data, ... group_keys=["order_no"], ... sort_keys=["product", "spec"] ... ) >>> for item in result: ... print(f"{item['full_seq']}: {item['product']}") G001-001: 阀门A G001-002: 阀门B G002-001: 阀门C """ import copy result = copy.deepcopy(data) # 1. 保留原始顺序(使用年月+流水号生成排序键) if keep_original_order: for idx, item in enumerate(result): order_val = Sorter._make_original_order( record=item, idx=idx, source_file=source_file, source_sheet=source_sheet ) item["_original_order"] = order_val # 2. 构建分组 groups = defaultdict(list) for item in result: # 构建分组键 key_parts = [str(item.get(k, "")) for k in group_keys] group_key = "_".join(key_parts) item[group_key_field] = group_key groups[group_key].append(item) # 3. 对分组进行排序(按分组键排序,保证输出顺序一致) sorted_group_keys = sorted(groups.keys()) logger.info(f"分组完成: {len(groups)} 个分组 (分组键: {group_keys[:3]}...)") # 4. 处理每个分组 sorted_result = Sorter._process_groups( groups=groups, sorted_group_keys=sorted_group_keys, sort_keys=sort_keys, generate_seq=generate_seq, use_global_prefix=use_global_prefix, prefix_format=prefix_format, seq_format=seq_format, seq_field=seq_field, group_index_field=group_index_field, ) logger.info(f"分组排序完成: {len(result)} 条记录, {len(groups)} 个分组") return sorted_result
@staticmethod def _process_groups( groups: dict[str, list[dict[str, Any]]], sorted_group_keys: list[str], sort_keys: list[str] | None, generate_seq: bool, use_global_prefix: bool, prefix_format: str, seq_format: str, seq_field: str, group_index_field: str, ) -> list[dict[str, Any]]: """处理每个分组:设置索引、组内排序、生成序号""" sorted_result: list[dict[str, Any]] = [] for group_idx, group_key in enumerate(sorted_group_keys, start=1): group_items = groups[group_key] # 设置分组索引和前缀 for item in group_items: item[group_index_field] = group_idx if use_global_prefix: item["_group_prefix"] = prefix_format.format(group_idx) # 组内排序 if sort_keys: group_items.sort(key=lambda x: tuple(str(x.get(k, "")) for k in sort_keys)) logger.debug( f"分组 {group_idx}: 组内按 {sort_keys[:3]}... 排序, {len(group_items)} 条记录" ) # 生成组内序号 if generate_seq: for seq, item in enumerate(group_items, start=1): item[seq_field] = seq_format.format(seq) if use_global_prefix: item["full_seq"] = f"{item['_group_prefix']}-{item[seq_field]}" sorted_result.extend(group_items) return sorted_result @staticmethod def _make_original_order( record: dict[str, Any], idx: int, source_file: str, source_sheet: str, ) -> int: """根据源文件和工作表生成原始顺序键 格式: YYMMxxxx (6位数字) - YY: 从文件名提取年份后两位(如 2026 -> 26) - MM: 从工作表名提取月份(如 "1月" -> "01") - xxxx: 4位流水号 Args: record: 记录字典(保留参数用于扩展) idx: 流水号索引(从0开始) source_file: 源文件路径,如 "D:/links/Hard/2026年销售计划.xlsx" source_sheet: 工作表名称,如 "1月" 或 "Sheet1" Returns: int: 排序键,格式 YYMMxxxx,提取失败时使用 0000xxxx """ year = Sorter._extract_year_from_filename(source_file) month = Sorter._extract_month_from_sheetname(source_sheet) yymm = f"{year:02d}{month:02d}" if year and month else "0000" return int(f"{yymm}{idx + 1:04d}") @staticmethod def _extract_year_from_filename(file_path: str) -> int | None: """从文件名提取年份后两位 支持格式: - "2026年销售计划.xlsx" -> 26 - "sales_plan_2026.xlsx" -> 26 - "2026销售计划.xlsx" -> 26 Args: file_path: 文件路径 Returns: int | None: 年份后两位(0-99),提取失败返回None """ import re from pathlib import Path if not file_path: return None filename = Path(file_path).stem patterns = [ r"(\d{4})年", # "2026年" r"_(\d{4})", # "_2026" r"(\d{4})", # "2026" ] for pattern in patterns: match = re.search(pattern, filename) if match: year = int(match.group(1)) if 2000 <= year <= 2100: return year % 100 return None @staticmethod def _extract_month_from_sheetname(sheet_name: str) -> int | None: """从工作表名提取月份 支持格式: - "1月" -> 1 - "01月" -> 1 - "一月" -> 1 Args: sheet_name: 工作表名称 Returns: int | None: 月份(1-12),提取失败返回None """ import re if not sheet_name: return None # 匹配数字月份: "1月", "01月" match = re.search(r"(\d{1,2})月", sheet_name) if match: month = int(match.group(1)) if 1 <= month <= 12: return month # 中文月份映射 chinese_months = { "一月": 1, "二月": 2, "三月": 3, "四月": 4, "五月": 5, "六月": 6, "七月": 7, "八月": 8, "九月": 9, "十月": 10, "十一月": 11, "十二月": 12, } for cn, num in chinese_months.items(): if sheet_name == cn or sheet_name.startswith(cn): return num return None
[文档] @staticmethod def get_group_info( data: list[dict[str, Any]], group_index: int | None = None, group_key: str | None = None ) -> list[dict[str, Any]]: """获取指定分组的记录 Args: data: 已分组的数据列表(由group_and_sort生成) group_index: 分组索引(1-based) group_key: 分组键值 Returns: list[dict[str, Any]]: 指定分组的记录列表 Examples: >>> result = Sorter.group_and_sort(data, group_keys=["order_no"], sort_keys=["product"]) >>> # 按索引获取 >>> group1 = Sorter.get_group_info(result, group_index=1) >>> # 按键值获取 >>> group = Sorter.get_group_info(result, group_key="SO001") """ if group_index is not None: return [item for item in data if item.get("_group_index") == group_index] if group_key is not None: return [item for item in data if item.get("_group_key") == group_key] return []
[文档] @staticmethod def get_all_groups( data: list[dict[str, Any]], seq_field: str = "group_seq", group_key_field: str = "_group_key", ) -> dict[int, dict[str, Any]]: """获取所有分组的信息 Args: data: 已分组的数据列表(由group_and_sort生成) seq_field: 序号字段名,与group_and_sort中的seq_field一致,默认"group_seq" group_key_field: 分组键字段名,与group_and_sort中的group_key_field一致,默认"_group_key" Returns: dict[int, dict[str, Any]]: 分组信息字典,键为分组索引,值为分组信息 Examples: >>> result = Sorter.group_and_sort(data, group_keys=["order_no"], sort_keys=["product"]) >>> groups = Sorter.get_all_groups(result) >>> for idx, info in groups.items(): ... print(f"分组{idx}: {info['count']}条记录") """ groups = {} for item in data: group_idx = item.get("_group_index") if group_idx and group_idx not in groups: groups[group_idx] = { "index": group_idx, "key": item.get(group_key_field), "count": 0, "first_seq": item.get(seq_field), "records": [], } if group_idx in groups: groups[group_idx]["count"] += 1 groups[group_idx]["records"].append(item) return groups
[文档] @staticmethod def restore_original_order(data: list[dict[str, Any]]) -> list[dict[str, Any]]: """恢复原始顺序 根据"_original_order"字段恢复数据的原始顺序, 并清理所有临时字段。 Args: data: 需要恢复顺序的数据列表,应包含"_original_order"字段 (由group_and_sort方法生成) Returns: list[dict[str, Any]]: 恢复原始顺序并清理临时字段后的数据列表 Notes: - 如果数据中没有"_original_order"字段,会记录警告并返回原数据 - 恢复顺序后会删除所有临时字段,保持数据干净 Examples: >>> sorted_data = Sorter.group_and_sort(data, group_keys=["order_no"], sort_keys=["product"]) >>> # 执行某些处理后... >>> original_data = Sorter.restore_original_order(sorted_data) """ if not data or "_original_order" not in data[0]: logger.warning("没有原始顺序信息,无法恢复") return data sorted_data = sorted(data, key=lambda x: x.get("_original_order", 0)) # 清理临时字段 temp_fields = [ "_original_order", "_group_key", "_group_index", "_group_prefix", "group_seq", "full_seq", ] for item in sorted_data: for field in temp_fields: item.pop(field, None) logger.info(f"恢复原始顺序完成: {len(sorted_data)} 条记录") return sorted_data