跳过正文
  1. Posts/

Case Generator:Python 脚本展示

大伟
作者
大伟
10年+ DevOps/SRE,热爱技术,探索自由

这篇文章放的是 case_generator 目录里的 generate_cases.py,用来展示完整 Python 脚本在博客里的阅读效果。

from __future__ import annotations

import argparse
import re
import sys
from copy import copy
from pathlib import Path

from openpyxl import load_workbook


OUTPUT_SHEET = "案例模板"
CONFIG_SHEET = "配置"
RULES_SHEET = "案例规则"
VALID_STATUS_HEADER = "*有效状态"
DEFAULT_INPUT_PATH = Path("inputs/需求输入.xlsx")
DEFAULT_TEMPLATE_PATH = Path("templates/案例输出模板.xlsx")
DEFAULT_OUTPUT_PATH = Path("outputs/generated/生成案例.xlsx")
CONFIG_FIELD_HEADER = "字段名"
CONFIG_NOTE_HEADER = "说明"
CONFIG_MODE_HEADER = "取值模式"
CONFIG_SOURCE_HEADER = "取值内容"
CONFIG_MODE_FIXED = "固定"
CONFIG_MODE_TEMPLATE = "字段拼接"
CONFIG_MODE_DIRECT = "直接映射"

COMPUTED_PRIORITY_FIELD = "案例优先级"
INPUT_CONTEXT_FIELDS = (
    "主题名称",
    "数据项分类",
    "数据项名称",
)
DEFAULT_THEME_LIST_SHEET = "主题清单"
DEFAULT_THEME_NAME_COLUMN = 3
DEFAULT_ITEM_CATEGORY_COLUMN = 1
DEFAULT_ITEM_NAME_COLUMN = 2
COMPUTED_FIELD_NAMES = {
    COMPUTED_PRIORITY_FIELD,
}
SKIP_ELEMENT_TYPES = {
    "业务连续数据准确性",
}
DEFAULT_CASE_PRIORITY = "中"
THEME_PRIORITY_MAP = {
    "日期维": "中",
    "高频指标维": "中",
    "基础信息维": "中",
    "估值维": "中",
    "估值信息维": "中",
    "债券信息维": "中",
    "债项维": "中",
    "发行人维": "中",
    "业务连续数据准确性": "高",
    "数据准确性": "高",
    "信息准确性": "高",
}
PHYSICAL_TABLE_PRIORITY_MAP = {
    "债券代码": "中",
    "ISIN码": "中",
    "债券简称": "中",
    "债券全称": "中",
    "债券分类(统计月报)": "高",
    "债券统计分类(一级)": "高",
    "债券统计分类(二级)": "高",
    "债券监测分类(一级)": "高",
    "债券监测分类(二级)": "高",
    "年": "中",
    "季": "中",
    "月": "中",
    "日": "中",
}

# 输出字段的默认生成规则。
# 也可以在模板“配置”页里写同名字段覆盖这些默认规则:
#   写普通文字:每一行都输出固定值,例如“验证:新建数据集”。
#   写占位符:按输入行替换,例如“验证:主题要素_{要素名称}字段展示顺序”。
DEFAULT_FIELD_TEMPLATES = {
    "*需求-业务名称": "{数据项分类}",
    "*功能/流程名称": "{数据项名称}",
    "*案例编号": "SIT-YONGHONG_{数据项分类}_{序号}",
    "*测试意图": "验证:数据项_{数据项名称}字段展示顺序",
    "*测试步骤": "登录债券数据门户\n进入:{数据项分类}",
}
RULE_SCOPE_HEADER = "规则类型"
RULE_SCOPE_NORMAL = "输入展开"
RULE_SCOPE_APPEND = "末尾追加"

PLACEHOLDER_PATTERN = re.compile(r"{([^{}]+)}")


def clean(value) -> str:
    """把 Excel 单元格值统一转成去掉首尾空格的字符串。"""
    if value is None:
        return ""
    return str(value).strip()


def is_blank(value) -> bool:
    return clean(value) == ""


def require_xlsx(path: Path, label: str) -> None:
    if path.suffix.lower() != ".xlsx":
        raise ValueError(f"{label} 只支持 .xlsx 文件:{path}")


def column_index_to_letter(column_index: int) -> str:
    """把 Excel 列号转成列字母,避免依赖某些环境里的 utils 导入。"""
    if column_index < 1:
        raise ValueError(f"列号必须大于 0:{column_index}")

    result = []
    current = column_index
    while current > 0:
        current, remainder = divmod(current - 1, 26)
        result.append(chr(ord("A") + remainder))
    return "".join(reversed(result))


def get_merged_cell_value(sheet, row: int, column: int) -> str:
    """读取单元格值;如果该格属于合并区域,则回退到合并区域左上角的值。"""
    value = clean(sheet.cell(row=row, column=column).value)
    if value:
        return value

    for merged_range in sheet.merged_cells.ranges:
        if (
            merged_range.min_row <= row <= merged_range.max_row
            and merged_range.min_col <= column <= merged_range.max_col
        ):
            return clean(sheet.cell(row=merged_range.min_row, column=merged_range.min_col).value)

    return ""


def parse_input_column_index(value: str, config_field_name: str) -> int:
    text = clean(value)
    if not text.isdigit() or int(text) < 1:
        raise ValueError(f"`{config_field_name}` 的取值内容必须是大于 0 的列号,例如 2、3、5、6。")
    return int(text)


def get_theme_list_sheet_name(workbook) -> str:
    if DEFAULT_THEME_LIST_SHEET not in workbook.sheetnames:
        raise ValueError(f"输入文件缺少主题清单工作表:{DEFAULT_THEME_LIST_SHEET}")
    return DEFAULT_THEME_LIST_SHEET


def get_theme_name_column() -> int:
    return DEFAULT_THEME_NAME_COLUMN


def get_item_columns() -> dict[str, int]:
    return {
        "数据项分类": DEFAULT_ITEM_CATEGORY_COLUMN,
        "数据项名称": DEFAULT_ITEM_NAME_COLUMN,
    }


def load_input_records(workbook, config: dict[str, dict[str, str]]) -> list[dict[str, str]]:
    """按最新格式读取输入:主题清单 C 列主题名 -> 对应主题 sheet 的 A/B 列。"""
    theme_list_sheet_name = get_theme_list_sheet_name(workbook)
    theme_name_column = get_theme_name_column()
    item_columns = get_item_columns()
    theme_list_sheet = workbook[theme_list_sheet_name]
    records: list[dict[str, str]] = []

    for row in range(2, theme_list_sheet.max_row + 1):
        theme_name = get_merged_cell_value(theme_list_sheet, row, theme_name_column)
        if not theme_name:
            continue
        if theme_name not in workbook.sheetnames:
            print(f"跳过主题清单第 {row} 行:主题名称 `{theme_name}` 没有对应工作表。")
            continue

        theme_sheet = workbook[theme_name]
        last_item_category = ""
        for item_row in range(2, theme_sheet.max_row + 1):
            if row_is_empty(theme_sheet, item_row):
                continue

            item_category = get_merged_cell_value(theme_sheet, item_row, item_columns["数据项分类"])
            item_name = get_merged_cell_value(theme_sheet, item_row, item_columns["数据项名称"])
            if item_category:
                last_item_category = item_category
            else:
                item_category = last_item_category

            if not item_category and not item_name:
                continue

            records.append(
                {
                    "主题名称": theme_name,
                    "数据项分类": item_category,
                    "数据项名称": item_name,
                }
            )

    return records


def build_lookup_tokens(value: str) -> list[str]:
    """把路径样式文本拆成可匹配的候选词,便于命中优先级映射。"""
    text = clean(value)
    if not text:
        return []

    tokens = [text]
    for sep in ("\\", "/", "\n"):
        next_tokens: list[str] = []
        for token in tokens:
            next_tokens.extend(part for part in token.split(sep) if part)
        tokens = next_tokens

    seen: set[str] = set()
    result: list[str] = []
    for token in tokens:
        normalized = clean(token)
        if normalized and normalized not in seen:
            seen.add(normalized)
            result.append(normalized)
    return result


def lookup_priority(values: list[str], mapping: dict[str, str]) -> str:
    for value in values:
        for token in build_lookup_tokens(value):
            if token in mapping:
                return mapping[token]
    return ""


def resolve_case_priority(context: dict[str, str], rule: dict[str, str]) -> str:
    """按截图里的“主题/物理表”规则,给当前案例生成优先级。"""
    theme_priority = lookup_priority(
        [
            rule.get("*功能/流程名称", ""),
            context.get("数据项分类", ""),
        ],
        THEME_PRIORITY_MAP,
    )
    if theme_priority:
        return theme_priority

    physical_priority = lookup_priority(
        [
            context.get("数据项名称", ""),
        ],
        PHYSICAL_TABLE_PRIORITY_MAP,
    )
    if physical_priority:
        return physical_priority

    return DEFAULT_CASE_PRIORITY


VALID_DIRECT_INPUT_FIELDS = set(INPUT_CONTEXT_FIELDS)


def load_fixed_config(workbook) -> dict[str, dict[str, str]]:
    """读取模板里的“配置”页,显式支持固定/字段拼接/直接映射三种模式。"""
    if CONFIG_SHEET not in workbook.sheetnames:
        raise ValueError(f"模板缺少 `{CONFIG_SHEET}` 工作表,请先在模板中维护固定字段配置。")

    sheet = workbook[CONFIG_SHEET]
    config: dict[str, dict[str, str]] = {}
    headers = {clean(sheet.cell(row=1, column=col).value): col for col in range(1, sheet.max_column + 1)}
    field_col = headers.get(CONFIG_FIELD_HEADER)
    note_col = headers.get(CONFIG_NOTE_HEADER)
    mode_col = headers.get(CONFIG_MODE_HEADER)
    source_col = headers.get(CONFIG_SOURCE_HEADER)

    required = {
        CONFIG_FIELD_HEADER: field_col,
        CONFIG_MODE_HEADER: mode_col,
        CONFIG_SOURCE_HEADER: source_col,
    }
    missing_headers = [name for name, col in required.items() if col is None]
    if missing_headers:
        raise ValueError(f"`{CONFIG_SHEET}` 缺少必需列:{', '.join(missing_headers)}")

    for row in range(2, sheet.max_row + 1):
        field_name = clean(sheet.cell(row=row, column=field_col).value)
        if not field_name:
            continue

        mode = clean(sheet.cell(row=row, column=mode_col).value)
        source = clean(sheet.cell(row=row, column=source_col).value)

        if mode not in {CONFIG_MODE_FIXED, CONFIG_MODE_TEMPLATE, CONFIG_MODE_DIRECT}:
            raise ValueError(f"`{CONFIG_SHEET}` 第 {row} 行字段 `{field_name}` 的取值模式无效:{mode},可选:固定/字段拼接/直接映射")
        if not source:
            raise ValueError(f"`{CONFIG_SHEET}` 第 {row} 行字段 `{field_name}` 的取值内容不能为空。")

        if mode == CONFIG_MODE_DIRECT and source not in VALID_DIRECT_INPUT_FIELDS and source not in COMPUTED_FIELD_NAMES:
            raise ValueError(
                f"`{CONFIG_SHEET}` 第 {row} 行字段 `{field_name}` 的直接映射来源无效:{source}。"
                f" 可选值:{', '.join([*VALID_DIRECT_INPUT_FIELDS, *COMPUTED_FIELD_NAMES])}"
            )

        config[field_name] = {
            "mode": mode,
            "source": source,
        }

    return config


def load_case_rules(workbook, headers: list[str]) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
    """读取“用例规则”页,拆成“输入展开”和“末尾追加”两类规则。"""
    if RULES_SHEET not in workbook.sheetnames:
        return [], []

    sheet = workbook[RULES_SHEET]
    rule_headers = [clean(sheet.cell(row=1, column=col).value) for col in range(1, sheet.max_column + 1)]
    input_rules: list[dict[str, str]] = []
    append_rules: list[dict[str, str]] = []

    for row in range(2, sheet.max_row + 1):
        if all(is_blank(sheet.cell(row=row, column=col).value) for col in range(1, sheet.max_column + 1)):
            continue

        rule: dict[str, str] = {}
        for col, header in enumerate(rule_headers, start=1):
            if not header:
                continue
            value = clean(sheet.cell(row=row, column=col).value)
            if value:
                rule[header] = value

        scope = rule.pop(RULE_SCOPE_HEADER, RULE_SCOPE_NORMAL) or RULE_SCOPE_NORMAL

        unknown_headers = [header for header in rule if header not in headers]
        if unknown_headers:
            raise ValueError(
                f"`{RULES_SHEET}` 第 {row} 行存在输出模板没有的字段:{', '.join(unknown_headers)}"
            )
        if scope == RULE_SCOPE_APPEND:
            append_rules.append(rule)
        else:
            input_rules.append(rule)

    return input_rules, append_rules


def get_output_headers(sheet) -> list[str]:
    """读取输出表头,只保留到 W 列附近的“*有效状态”为止。"""
    headers = [clean(sheet.cell(row=1, column=col).value) for col in range(1, sheet.max_column + 1)]

    try:
        last_col = headers.index(VALID_STATUS_HEADER) + 1
    except ValueError as exc:
        raise ValueError(f"模板 `{OUTPUT_SHEET}` 首行缺少 `{VALID_STATUS_HEADER}` 表头。") from exc

    return headers[:last_col]


def get_field_template(header: str, config: dict[str, dict[str, str]]) -> str:
    """字段值优先读“配置”页;没配置时再使用代码里的少量默认规则。"""
    if header in config:
        entry = config[header]
        # 配置页三种模式:
        #   固定:直接返回固定文本
        #   字段拼接:返回带占位符的模板文本
        #   直接映射:把输入字段直接映射成占位符模板
        if entry["mode"] == CONFIG_MODE_DIRECT:
            return "{" + entry["source"] + "}"
        return entry["source"]
    return DEFAULT_FIELD_TEMPLATES.get(header, "")


def render_field(field_name: str, template: str, context: dict[str, str]) -> str:
    """把配置文本里的占位符替换成当前输入行的值;没有占位符就是固定值。"""
    result = template
    for placeholder in PLACEHOLDER_PATTERN.findall(template):
        if placeholder not in context:
            raise ValueError(f"配置字段 `{field_name}` 使用了未知占位符:{{{placeholder}}}")
        result = result.replace(f"{{{placeholder}}}", context[placeholder])
    return result


def template_needs(template: str, placeholder: str) -> bool:
    return f"{{{placeholder}}}" in template


def build_field_templates(headers: list[str], config: dict[str, dict[str, str]], rule: dict[str, str]) -> dict[str, str]:
    """合并字段生成规则:用例规则页优先,其次配置页,最后代码默认值。"""
    return {
        header: rule.get(header, get_field_template(header, config))
        for header in headers
    }


def build_record_from_templates(
    headers: list[str],
    field_templates: dict[str, str],
    context: dict[str, str],
) -> dict[str, str]:
    """根据字段模板和上下文生成一整行输出记录。"""
    return {
        header: render_field(header, field_templates[header], context)
        for header in headers
    }


def build_formula_columns(sheet, headers: list[str], template_row: int) -> dict[str, str]:
    """识别模板行里的固定公式列;这些列生成时保留公式,不走配置覆盖。"""
    formula_columns: dict[str, str] = {}
    for col_index, header in enumerate(headers, start=1):
        value = sheet.cell(row=template_row, column=col_index).value
        if isinstance(value, str) and value.startswith("="):
            formula_columns[header] = value
    return formula_columns


def shift_formula_rows(formula: str, row_offset: int) -> str:
    """把模板公式中的相对行号按输出行偏移,保留绝对行号。"""
    if row_offset == 0:
        return formula

    pattern = re.compile(r"(?P<col>\$?[A-Z]{1,3})(?P<row>\$?\d+)")

    def replace(match: re.Match[str]) -> str:
        col_part = match.group("col")
        row_part = match.group("row")
        if row_part.startswith("$"):
            return f"{col_part}{row_part}"
        return f"{col_part}{int(row_part) + row_offset}"

    return pattern.sub(replace, formula)


def copy_cell_style(source, target) -> None:
    """复制单元格样式,保证生成行看起来和模板行一致。"""
    if source.has_style:
        target.font = copy(source.font)
        target.fill = copy(source.fill)
        target.border = copy(source.border)
        target.alignment = copy(source.alignment)
        target.number_format = source.number_format
        target.protection = copy(source.protection)


def copy_template_row_style(sheet, source_row: int, target_row: int, max_col: int) -> None:
    for col in range(1, max_col + 1):
        copy_cell_style(sheet.cell(row=source_row, column=col), sheet.cell(row=target_row, column=col))

    source_dim = sheet.row_dimensions[source_row]
    target_dim = sheet.row_dimensions[target_row]
    target_dim.height = source_dim.height
    target_dim.hidden = source_dim.hidden


def clear_output_rows(sheet, max_col: int) -> None:
    """清空模板中已有的数据行,但保留行样式给后面复用。"""
    for row in range(2, sheet.max_row + 1):
        for col in range(1, max_col + 1):
            sheet.cell(row=row, column=col).value = None


def trim_output_rows(sheet, last_row: int) -> None:
    """删除多余空白行,让输出文件只留下表头和实际案例行。"""
    if sheet.max_row > last_row:
        sheet.delete_rows(last_row + 1, sheet.max_row - last_row)


def trim_output_columns(sheet, max_col: int) -> None:
    """删除 W 列后面的执行结果/备注等列,第一版不输出这些字段。"""
    if sheet.max_column > max_col:
        sheet.delete_cols(max_col + 1, sheet.max_column - max_col)


def update_tables(sheet, max_col: int, last_row: int) -> None:
    """更新 Excel 表格范围,否则筛选区域可能还停留在模板原始行数。"""
    table_last_row = max(2, last_row)
    table_ref = f"A1:{column_index_to_letter(max_col)}{table_last_row}"
    for table in sheet.tables.values():
        table.ref = table_ref


def get_input_sheet(workbook, sheet_name: str | None):
    if sheet_name:
        if sheet_name not in workbook.sheetnames:
            raise ValueError(f"输入文件缺少工作表:{sheet_name}")
        return workbook[sheet_name]
    return workbook.active


def row_is_empty(sheet, row: int) -> bool:
    return all(is_blank(sheet.cell(row=row, column=col).value) for col in range(1, sheet.max_column + 1))


def generate_case_rows(
    input_records: list[dict[str, str]],
    headers: list[str],
    config: dict[str, str],
    rules: list[dict[str, str]],
    append_rules: list[dict[str, str]],
) -> list[list[str]]:
    """把输入需求表转换为输出案例表,并在最后追加模板里写死的固定案例。"""
    rows: list[list[str]] = []
    last_item_category = ""
    last_item_name = ""
    if not rules:
        # 没有“用例规则”页时保持旧逻辑:一行输入只生成一行案例。
        rules = [{}]

    for input_index, input_record in enumerate(input_records, start=1):
        theme_name = input_record["主题名称"]
        item_category = input_record["数据项分类"]
        item_name = input_record["数据项名称"]
        if item_category in SKIP_ELEMENT_TYPES:
            print(f"跳过主题记录第 {input_index} 行:数据项分类 `{item_category}` 不生成案例。")
            continue
        if item_category:
            last_item_category = item_category
        if item_name:
            last_item_name = item_name

        context = {
            "主题名称": theme_name,
            "数据项分类": item_category,
            "数据项名称": item_name,
            COMPUTED_PRIORITY_FIELD: resolve_case_priority(
                {
                    "主题名称": theme_name,
                    "数据项分类": item_category,
                    "数据项名称": item_name,
                },
                {},
            ),
            # 序号在每条展开规则里再设置,因为它按输出案例行顺延。
            "序号": "",
        }

        for rule_index, rule in enumerate(rules, start=1):
            context[COMPUTED_PRIORITY_FIELD] = resolve_case_priority(context, rule)
            field_templates = build_field_templates(headers, config, rule)
            missing = []
            if not item_category and any(template_needs(value, "数据项分类") for value in field_templates.values()):
                missing.append("A列数据项分类")
            if not item_name and any(template_needs(value, "数据项名称") for value in field_templates.values()):
                missing.append("B列数据项名称")
            if missing:
                # 缺少当前规则需要的关键字段时,只跳过这一条展开规则。
                print(
                    f"跳过主题记录第 {input_index} 行/规则第 {rule_index} 行:"
                    f"缺少 {', '.join(missing)}。"
                )
                continue

            serial = len(rows) + 1
            context["序号"] = f"{serial:04d}"

            # 每个字段都走同一套规则:
            #   配置值没有占位符 -> 固定值
            #   配置值有占位符   -> 用当前输入行替换
            record = build_record_from_templates(headers, field_templates, context)

            rows.append([record.get(header, "") for header in headers])

    # 末尾固定案例不依赖输入表,只要模板写了就顺延追加。
    for rule_index, rule in enumerate(append_rules, start=1):
        serial = len(rows) + 1
        context = {
            # 末尾追加案例如果没有显式写主题/要素,默认沿用最后一条有效输入。
            "主题名称": "",
            "数据项分类": last_item_category,
            "数据项名称": last_item_name,
            COMPUTED_PRIORITY_FIELD: "",
            "序号": f"{serial:04d}",
        }
        context[COMPUTED_PRIORITY_FIELD] = resolve_case_priority(context, rule)
        field_templates = build_field_templates(headers, config, rule)
        record = build_record_from_templates(headers, field_templates, context)
        rows.append([record.get(header, "") for header in headers])

    return rows


def generate_case_rows_by_theme(
    input_records: list[dict[str, str]],
    headers: list[str],
    config: dict[str, str],
    rules: list[dict[str, str]],
    append_rules: list[dict[str, str]],
) -> dict[str, list[list[str]]]:
    grouped: dict[str, list[dict[str, str]]] = {}
    for record in input_records:
        grouped.setdefault(record["主题名称"], []).append(record)

    result: dict[str, list[list[str]]] = {}
    for theme_name, theme_records in grouped.items():
        result[theme_name] = generate_case_rows(theme_records, headers, config, rules, append_rules)
    return result


def populate_output_sheet(
    sheet,
    headers: list[str],
    row_values_list: list[list[str]],
) -> None:
    formula_columns = build_formula_columns(sheet, headers, template_row=2)
    max_col = len(headers)
    trim_output_columns(sheet, max_col)
    clear_output_rows(sheet, max_col)

    for offset, row_values in enumerate(row_values_list, start=2):
        copy_template_row_style(sheet, 2, offset, max_col)
        for col, value in enumerate(row_values, start=1):
            header = headers[col - 1]
            target_cell = sheet.cell(row=offset, column=col)
            if header in formula_columns:
                target_cell.value = shift_formula_rows(formula_columns[header], offset - 2)
            else:
                target_cell.value = value

    last_row = 1 + len(row_values_list)
    trim_output_rows(sheet, max(2, last_row))
    update_tables(sheet, max_col, last_row)


def write_output(template_path: Path, input_path: Path, output_path: Path, input_sheet_name: str | None) -> int:
    require_xlsx(template_path, "模板")
    require_xlsx(input_path, "输入")
    require_xlsx(output_path, "输出")

    if not template_path.exists():
        raise FileNotFoundError(f"模板文件不存在:{template_path}")
    if not input_path.exists():
        raise FileNotFoundError(f"输入文件不存在:{input_path}")
    if template_path.resolve() == output_path.resolve():
        raise ValueError("输出文件不能覆盖模板文件,请换一个 --output 路径。")

    template_workbook = load_workbook(template_path)
    if OUTPUT_SHEET not in template_workbook.sheetnames:
        raise ValueError(f"模板缺少 `{OUTPUT_SHEET}` 工作表。")

    output_sheet = template_workbook[OUTPUT_SHEET]
    headers = get_output_headers(output_sheet)
    config = load_fixed_config(template_workbook)
    rules, append_rules = load_case_rules(template_workbook, headers)

    input_workbook = load_workbook(input_path, data_only=True)
    input_records = load_input_records(input_workbook, config)
    case_rows_by_theme = generate_case_rows_by_theme(input_records, headers, config, rules, append_rules)

    for sheet_name in list(template_workbook.sheetnames):
        if sheet_name != OUTPUT_SHEET:
            template_workbook.remove(template_workbook[sheet_name])

    theme_names = list(case_rows_by_theme)
    if not theme_names:
        populate_output_sheet(output_sheet, headers, [])
    else:
        output_sheet.sheet_state = "hidden"

        for theme_name in theme_names:
            theme_sheet = template_workbook.copy_worksheet(output_sheet)
            theme_sheet.title = theme_name
            populate_output_sheet(theme_sheet, headers, case_rows_by_theme[theme_name])

        template_workbook.remove(output_sheet)

    output_path.parent.mkdir(parents=True, exist_ok=True)
    template_workbook.save(output_path)
    return sum(len(rows) for rows in case_rows_by_theme.values())


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="根据需求 Excel 和输出模板生成案例 Excel。")
    parser.add_argument(
        "--input",
        default=DEFAULT_INPUT_PATH,
        type=Path,
        help=f"输入需求 .xlsx 文件路径;默认 {DEFAULT_INPUT_PATH}",
    )
    parser.add_argument(
        "--template",
        default=DEFAULT_TEMPLATE_PATH,
        type=Path,
        help=f"输出模板 .xlsx 文件路径;默认 {DEFAULT_TEMPLATE_PATH}",
    )
    parser.add_argument(
        "--output",
        default=DEFAULT_OUTPUT_PATH,
        type=Path,
        help=f"生成结果 .xlsx 文件路径;默认 {DEFAULT_OUTPUT_PATH}",
    )
    parser.add_argument("--input-sheet", help="输入需求工作表名称;不填则读取活动工作表")
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv or sys.argv[1:])
    try:
        count = write_output(args.template, args.input, args.output, args.input_sheet)
    except Exception as exc:
        print(f"生成失败:{exc}", file=sys.stderr)
        return 1

    print(f"生成完成:{args.output}")
    print(f"有效案例行数:{count}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

相关文章

我从零搭了一个股票回测框架,踩了哪些坑

·4 分钟
做量化交易两年多了,我一直用聚宽、米筐这些平台回测。功能是挺全的,但总有些东西想自己控制——数据格式、策略逻辑、报告样式。更重要的是,我发现自己对量化交易的本质理解得还不够深,与其继续当一个”调参侠”,不如自己从零搭一个框架,把每个环节都想清楚。

退休倒计时

加载中... 退休倒计时 参加工作时间 已缴纳社保 0个月 目标:社保缴纳满 个月,截至 -- 还需缴纳多少个月 1月2月3月 4月5月6月 7月8月9月 10月11月12月 待完成 待添加 待删除 已完成 目标月 ⚠ 有未保存的修改 🔒 只读模式 — 点击解锁编辑 🔓 解锁编辑 💾 保存修改 重置全部 ✓ 已保存

一个人的正常

·3 分钟
最近刷到两个视频,再一次对我想要的生活产生了一些回响。 老杨玩了一辈子游戏,从街机厅到模拟器,一生没有结婚、没有存款、没有所谓的"正经事业"。2023年春天,他走了。258万人看了这个视频,15万人点了赞——评论区里全是"这才是我想要的人生"和"可我做不到"。