【IC】innovus timing summary parser

python 复制代码
#!/usr/bin/env python3
"""
Parse a Cadence Innovus-style ASCII timing debug report into normalized JSON,
with optional CSV export.

This is v3 of the parser. Behavior and output schema are intended to be the
same as v2; this file mainly updates documentation/CLI wording.

This version is more robust than the first draft and is designed for reports like:
- metadata header lines starting with '#'
- path-group ID mapping table: `ID Number | Path Group Name`
- timing summary tables: `Setup mode` / `Hold mode`
- multiple named timing sections under the same mode, e.g. corners/views like:
    func....setup
    shift....setup
- DRV summary table
- scalar tail lines like Density / Routing Overflow

Example usage:
    python parse_timing_report_v3.py timing_debug_report.rpt -o timing_debug_report.json
    python parse_timing_report_v3.py timing_debug_report.rpt -o timing_debug_report.json --csv-dir out_csv

CSV export files:
- timing_groups.csv : one row per (mode, section, group)
- drvs.csv          : one row per drv type
- scalars.csv       : simple key/value scalar outputs

Design goals:
1. Parse raw text, not screenshots.
2. Be tolerant to incomplete / slightly malformed lines.
3. Preserve warnings instead of failing hard.
4. Normalize timing data so agents can consume JSON/CSV instead of ASCII art.
"""

from __future__ import annotations

import argparse
import csv
import gzip
import json
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple


BORDER_RE = re.compile(r"^[\s+\-|=]+$")
MODE_RE = re.compile(r"\b(setup|hold)\s+mode\b", re.IGNORECASE)
# Header group cells may start with either `1:...` or `[1:...]` (sometimes with a closing `]`).
# We only need the leading numeric ID for mapping lookup; keep display_name unchanged.
GROUP_ID_IN_HEADER_RE = re.compile(r"^\s*\[?\s*(\d+)\s*:")
INT_RE = re.compile(r"^[+-]?\d+$")
FLOAT_RE = re.compile(r"^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$")
PAIR_RE = re.compile(r"^\s*([+-]?\d+)\s*\((\s*[+-]?\d+\s*)\)\s*$")
DENSITY_RE = re.compile(r"^\s*Density\s*:\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s*%\s*$", re.IGNORECASE)
ROUTING_OVERFLOW_RE = re.compile(
    r"^\s*Routing\s+Overflow\s*:\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s*%\s*H\s*and\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s*%\s*V\s*$",
    re.IGNORECASE,
)


CANONICAL_METRIC_KEYS = {
    "wns": "wns_ns",
    "tns": "tns_ns",
    "violating paths": "violating_paths",
    "all paths": "all_paths",
}

KNOWN_TIMING_METRICS = ["wns_ns", "tns_ns", "violating_paths", "all_paths"]


# -----------------------------------------------------------------------------
# Generic helpers
# -----------------------------------------------------------------------------

def is_border_line(line: str) -> bool:
    stripped = line.strip()
    return bool(stripped) and bool(BORDER_RE.fullmatch(stripped))


def split_table_row(line: str) -> List[str]:
    """Split an ASCII table row by `|` and trim cells.

    Outer borders create empty first/last cells, which are removed.
    Inner empty cells are preserved.
    """
    parts = [cell.strip() for cell in line.rstrip("\n").split("|")]
    if parts and parts[0] == "":
        parts = parts[1:]
    if parts and parts[-1] == "":
        parts = parts[:-1]
    return parts


def normalize_metric_name(raw: str) -> str:
    s = raw.strip().rstrip(":")
    s = re.sub(r"\s+", " ", s)
    s = s.replace("(ns)", "").strip().lower()
    return CANONICAL_METRIC_KEYS.get(s, s.replace(" ", "_"))


def parse_scalar(text: str) -> Any:
    s = text.strip()
    if not s:
        return None

    upper = s.upper()
    if upper in {"N/A", "NA", "--", "NONE"}:
        return None

    pair_match = PAIR_RE.fullmatch(s)
    if pair_match:
        return {
            "first": int(pair_match.group(1)),
            "second": int(pair_match.group(2).strip()),
            "raw": s,
        }

    s_no_commas = s.replace(",", "")

    if INT_RE.fullmatch(s_no_commas):
        try:
            return int(s_no_commas)
        except ValueError:
            pass

    if FLOAT_RE.fullmatch(s_no_commas):
        try:
            value = float(s_no_commas)
            if value.is_integer() and ("." not in s_no_commas) and ("e" not in s_no_commas.lower()):
                return int(value)
            return value
        except ValueError:
            pass

    return s


def next_nonempty_line(lines: List[str], start_idx: int) -> Tuple[Optional[int], Optional[str]]:
    i = start_idx
    while i < len(lines):
        if lines[i].strip():
            return i, lines[i]
        i += 1
    return None, None


def values_from_cells(cells: List[str], expected_groups: int, assume_labeled: bool) -> List[str]:
    """Normalize row cells into exactly expected_groups value cells.

    Tolerant behaviors:
    - labeled rows usually look like [metric_label, v1, v2, ...]
    - unlabeled rows under corner sections may look like ["", v1, v2, ...]
    - if malformed but longer than expected, keep the rightmost expected_groups cells
    - if shorter, pad with empty strings
    """
    if expected_groups <= 0:
        return []

    if assume_labeled:
        candidate = cells[1:] if len(cells) >= 2 else []
    else:
        if len(cells) == expected_groups + 1 and cells and cells[0] == "":
            candidate = cells[1:]
        elif len(cells) == expected_groups:
            candidate = cells
        elif len(cells) > expected_groups:
            candidate = cells[-expected_groups:]
        else:
            candidate = cells

    if len(candidate) < expected_groups:
        candidate = candidate + [""] * (expected_groups - len(candidate))
    elif len(candidate) > expected_groups:
        candidate = candidate[:expected_groups]
    return candidate


# -----------------------------------------------------------------------------
# Metadata / scalar parsing
# -----------------------------------------------------------------------------

def parse_metadata(lines: List[str]) -> Dict[str, Any]:
    metadata: Dict[str, Any] = {}
    banner_lines: List[str] = []

    for line in lines:
        stripped = line.strip()
        if not stripped.startswith("#"):
            continue
        body = stripped.lstrip("#").strip()
        if not body:
            continue
        if ":" in body:
            key, value = body.split(":", 1)
            metadata[key.strip().lower().replace(" ", "_")] = value.strip()
        else:
            banner_lines.append(body)

    if banner_lines:
        metadata["banner_lines"] = banner_lines
    return metadata


def parse_scalar_tail(lines: List[str]) -> Dict[str, Any]:
    scalars: Dict[str, Any] = {}
    for line in lines:
        stripped = line.strip()
        if not stripped:
            continue

        m_density = DENSITY_RE.match(stripped)
        if m_density:
            scalars["density_pct"] = float(m_density.group(1))
            continue

        m_overflow = ROUTING_OVERFLOW_RE.match(stripped)
        if m_overflow:
            scalars["routing_overflow_h_pct"] = float(m_overflow.group(1))
            scalars["routing_overflow_v_pct"] = float(m_overflow.group(2))
            continue

    return scalars


# -----------------------------------------------------------------------------
# Path group ID mapping table
# -----------------------------------------------------------------------------

def parse_id_mapping(lines: List[str], start_idx: int) -> Tuple[Dict[int, str], int]:
    mapping: Dict[int, str] = {}
    i = start_idx + 1

    while i < len(lines):
        line = lines[i]
        stripped = line.strip()

        if not stripped:
            i += 1
            continue
        if is_border_line(line):
            i += 1
            continue
        if MODE_RE.search(line):
            break
        if "|" not in line:
            break

        cells = split_table_row(line)
        if len(cells) < 2:
            i += 1
            continue

        id_cell = cells[0].strip()
        name_cell = cells[1].strip()
        if INT_RE.fullmatch(id_cell) and name_cell:
            mapping[int(id_cell)] = name_cell
            i += 1
            continue

        if mapping:
            break
        i += 1

    return mapping, i


# -----------------------------------------------------------------------------
# Timing table parsing
# -----------------------------------------------------------------------------

def build_group_objects(group_headers: List[str], group_id_map: Dict[int, str]) -> List[Dict[str, Any]]:
    groups: List[Dict[str, Any]] = []
    for idx, display_name in enumerate(group_headers):
        display_name = display_name.strip()
        group_id: Optional[int] = None
        group_name: Optional[str] = None

        m = GROUP_ID_IN_HEADER_RE.match(display_name)
        if m:
            group_id = int(m.group(1))
            group_name = group_id_map.get(group_id)

        if display_name.lower() == "all":
            group_name = "all"

        groups.append(
            {
                "column_index": idx,
                "display_name": display_name,
                "group_id": group_id,
                "group_name": group_name or display_name,
            }
        )
    return groups


def make_section_object(
    section_name: str,
    section_kind: str,
    metric_rows: List[Dict[str, Any]],
    group_headers: List[str],
    group_id_map: Dict[int, str],
) -> Dict[str, Any]:
    base_groups = build_group_objects(group_headers, group_id_map)
    normalized_groups: List[Dict[str, Any]] = []

    for group in base_groups:
        item = dict(group)
        for metric in metric_rows:
            values = metric.get("values") or []
            idx = item["column_index"]
            value = values[idx] if idx < len(values) else None
            item[metric["metric_key"]] = value
        normalized_groups.append(item)

    extra_metric_keys = [m["metric_key"] for m in metric_rows if m["metric_key"] not in KNOWN_TIMING_METRICS]
    return {
        "section_name": section_name,
        "section_kind": section_kind,
        "metric_order": [m["metric_key"] for m in metric_rows],
        "metric_rows": metric_rows,
        "groups": normalized_groups,
        "extra_metric_keys": extra_metric_keys,
    }


def looks_like_named_timing_section_title(line: str) -> bool:
    stripped = line.strip()
    if not stripped:
        return False
    if "|" in stripped:
        return False
    if stripped.startswith("#"):
        return False
    if "ID Number" in stripped or "Path Group Name" in stripped:
        return False
    if stripped.lower().startswith("density:"):
        return False
    if stripped.lower().startswith("routing overflow"):
        return False
    if MODE_RE.search(stripped):
        return False
    return True


def looks_like_drvs_table_start(line: str) -> bool:
    return "DRVs" in line and "|" in line


def looks_like_labeled_metric_row(line: str) -> bool:
    if "|" not in line:
        return False
    cells = split_table_row(line)
    if len(cells) < 2:
        return False
    label = normalize_metric_name(cells[0])
    return label in KNOWN_TIMING_METRICS or label in {"slack", "nvp", "violations"}


def parse_labeled_metric_rows(
    lines: List[str],
    start_idx: int,
    expected_groups: int,
    warnings: List[str],
) -> Tuple[List[Dict[str, Any]], int]:
    rows: List[Dict[str, Any]] = []
    i = start_idx

    while i < len(lines):
        line = lines[i]
        if not line.strip():
            i += 1
            continue
        if is_border_line(line):
            i += 1
            continue
        if "|" not in line:
            break
        if not looks_like_labeled_metric_row(line):
            break

        cells = split_table_row(line)
        metric_label = cells[0].strip()
        metric_key = normalize_metric_name(metric_label)
        raw_values = values_from_cells(cells, expected_groups=expected_groups, assume_labeled=True)
        values = [parse_scalar(v) for v in raw_values]
        rows.append(
            {
                "label": metric_label,
                "metric_key": metric_key,
                "values": values,
            }
        )
        i += 1

    if not rows:
        warnings.append(f"No labeled metric rows parsed near line {start_idx + 1}")
    return rows, i


def parse_unlabeled_metric_rows(
    lines: List[str],
    start_idx: int,
    expected_groups: int,
    template_metric_order: List[str],
    section_name: str,
    warnings: List[str],
) -> Tuple[List[Dict[str, Any]], int]:
    rows: List[Dict[str, Any]] = []
    i = start_idx

    while i < len(lines) and len(rows) < len(template_metric_order):
        line = lines[i]
        stripped = line.strip()

        if not stripped:
            i += 1
            continue
        if is_border_line(line):
            i += 1
            continue
        if MODE_RE.search(line):
            break
        if looks_like_drvs_table_start(line):
            break
        if looks_like_named_timing_section_title(line) and rows:
            break
        if "|" not in line:
            break

        cells = split_table_row(line)
        raw_values = values_from_cells(cells, expected_groups=expected_groups, assume_labeled=False)
        values = [parse_scalar(v) for v in raw_values]
        metric_key = template_metric_order[len(rows)]
        rows.append(
            {
                "label": metric_key,
                "metric_key": metric_key,
                "values": values,
            }
        )
        i += 1

    if len(rows) != len(template_metric_order):
        warnings.append(
            f"Section '{section_name}' parsed {len(rows)} unlabeled metric rows; expected {len(template_metric_order)}"
        )
    return rows, i


def parse_timing_block(
    lines: List[str],
    start_idx: int,
    group_id_map: Dict[int, str],
    warnings: List[str],
) -> Tuple[Optional[Dict[str, Any]], int]:
    header_line = lines[start_idx]
    header_cells = split_table_row(header_line)
    if len(header_cells) < 2:
        warnings.append(f"Malformed timing block header at line {start_idx + 1}")
        return None, start_idx + 1

    mode_label = header_cells[0].strip()
    mode_match = MODE_RE.search(mode_label)
    if not mode_match:
        warnings.append(f"Could not detect mode from timing block header at line {start_idx + 1}")
        return None, start_idx + 1

    mode = mode_match.group(1).lower()
    group_headers = [cell.strip() for cell in header_cells[1:]]
    expected_groups = len(group_headers)

    block: Dict[str, Any] = {
        "mode": mode,
        "header_label": mode_label,
        "group_headers": group_headers,
        "template_metric_order": [],
        "sections": [],
    }

    i = start_idx + 1
    base_metric_rows, i = parse_labeled_metric_rows(lines, i, expected_groups=expected_groups, warnings=warnings)
    if base_metric_rows:
        block["template_metric_order"] = [row["metric_key"] for row in base_metric_rows]
        block["sections"].append(
            make_section_object(
                section_name=f"{mode}_summary",
                section_kind="summary",
                metric_rows=base_metric_rows,
                group_headers=group_headers,
                group_id_map=group_id_map,
            )
        )
    else:
        warnings.append(f"Timing block '{mode_label}' has no parseable base summary rows")

    section_counter = 0
    while i < len(lines):
        line = lines[i]
        stripped = line.strip()

        if not stripped:
            i += 1
            continue
        if MODE_RE.search(line) and "|" in line:
            break
        if "ID Number" in line and "Path Group Name" in line:
            break
        if looks_like_drvs_table_start(line):
            break
        if stripped.lower().startswith("density:") or stripped.lower().startswith("routing overflow"):
            break
        if is_border_line(line):
            i += 1
            continue

        if looks_like_named_timing_section_title(line):
            title = stripped
            peek_idx, peek_line = next_nonempty_line(lines, i + 1)
            if peek_idx is None or peek_line is None:
                break
            if "|" not in peek_line:
                warnings.append(f"Named section title at line {i + 1} not followed by pipe rows: {title}")
                i += 1
                continue

            template_metric_order = block["template_metric_order"] or KNOWN_TIMING_METRICS
            metric_rows, new_i = parse_unlabeled_metric_rows(
                lines,
                peek_idx,
                expected_groups=expected_groups,
                template_metric_order=template_metric_order,
                section_name=title,
                warnings=warnings,
            )
            if metric_rows:
                block["sections"].append(
                    make_section_object(
                        section_name=title,
                        section_kind="named_view",
                        metric_rows=metric_rows,
                        group_headers=group_headers,
                        group_id_map=group_id_map,
                    )
                )
                i = new_i
                continue

            warnings.append(f"Failed to parse named timing section '{title}' near line {i + 1}")
            i += 1
            continue

        # Tolerate unexpected extra labeled rows by turning them into an anonymous section.
        if "|" in line and looks_like_labeled_metric_row(line):
            section_counter += 1
            anon_name = f"anonymous_section_{section_counter}"
            metric_rows, new_i = parse_labeled_metric_rows(lines, i, expected_groups=expected_groups, warnings=warnings)
            if metric_rows:
                block["sections"].append(
                    make_section_object(
                        section_name=anon_name,
                        section_kind="anonymous_labeled",
                        metric_rows=metric_rows,
                        group_headers=group_headers,
                        group_id_map=group_id_map,
                    )
                )
                i = new_i
                continue

        # Any other line means the timing block likely ended.
        break

    return block, i


# -----------------------------------------------------------------------------
# DRV table parsing
# -----------------------------------------------------------------------------

def collect_pipe_table_block(lines: List[str], start_idx: int) -> Tuple[List[str], int]:
    block: List[str] = []
    i = start_idx
    saw_pipe = False

    while i < len(lines):
        line = lines[i]
        stripped = line.strip()
        if not stripped:
            if saw_pipe:
                break
            i += 1
            continue
        if "|" in line or is_border_line(line):
            block.append(line)
            saw_pipe = saw_pipe or ("|" in line)
            i += 1
            continue
        if saw_pipe:
            break
        i += 1

    return block, i


def parse_drvs_table(lines: List[str], start_idx: int, warnings: List[str]) -> Tuple[Optional[Dict[str, Any]], int]:
    block, next_i = collect_pipe_table_block(lines, start_idx)
    data_rows = [split_table_row(line) for line in block if ("|" in line and not is_border_line(line))]
    if not data_rows:
        warnings.append(f"DRV table near line {start_idx + 1} is empty")
        return None, next_i

    rows: List[Dict[str, Any]] = []
    for cells in data_rows:
        if not cells:
            continue
        first = cells[0].strip().lower()
        if first in {"drvs", "", "real", "total"}:
            continue
        if first.startswith("nr nets") or first.startswith("worst vio"):
            continue
        if first.startswith("max_") or first in {"transition", "capacitance", "fanout", "length"}:
            padded = cells + [""] * max(0, 4 - len(cells))
            row = {
                "drv_type": padded[0].strip(),
                "real_nr_nets_terms": parse_scalar(padded[1]),
                "real_worst_vio": parse_scalar(padded[2]),
                "total_nr_nets_terms": parse_scalar(padded[3]),
            }
            rows.append(row)

    if not rows:
        warnings.append(f"No DRV data rows parsed near line {start_idx + 1}")
        return None, next_i

    return {
        "report_type": "drvs_summary",
        "rows": rows,
    }, next_i


# -----------------------------------------------------------------------------
# High-level report parser
# -----------------------------------------------------------------------------

def parse_timing_report_text(text: str) -> Dict[str, Any]:
    lines = text.splitlines()
    warnings: List[str] = []
    metadata = parse_metadata(lines)
    group_id_map: Dict[int, str] = {}
    timing_tables: List[Dict[str, Any]] = []
    drvs: List[Dict[str, Any]] = []
    scalars = parse_scalar_tail(lines)

    i = 0
    while i < len(lines):
        line = lines[i]

        if "ID Number" in line and "Path Group Name" in line:
            parsed_map, next_i = parse_id_mapping(lines, i)
            if parsed_map:
                group_id_map.update(parsed_map)
            else:
                warnings.append(f"ID mapping table near line {i + 1} produced no entries")
            i = max(next_i, i + 1)
            continue

        if MODE_RE.search(line) and "|" in line:
            block, next_i = parse_timing_block(lines, i, group_id_map, warnings)
            if block:
                timing_tables.append(block)
            i = max(next_i, i + 1)
            continue

        if looks_like_drvs_table_start(line):
            drv_block, next_i = parse_drvs_table(lines, i, warnings)
            if drv_block:
                drvs.append(drv_block)
            i = max(next_i, i + 1)
            continue

        i += 1

    return {
        "report_type": "innovus_timing_debug_report_ascii",
        "metadata": metadata,
        "group_id_map": {str(k): v for k, v in sorted(group_id_map.items())},
        "timing_tables": timing_tables,
        "drvs": drvs,
        "scalars": scalars,
        "warnings": warnings,
    }


def parse_timing_report_file(path: Path) -> Dict[str, Any]:
    # Support gzipped reports while preserving utf-8 decoding behavior.
    if str(path).lower().endswith(".gz"):
        with gzip.open(path, "rt", encoding="utf-8", errors="replace") as f:
            text = f.read()
    else:
        text = path.read_text(encoding="utf-8", errors="replace")
    result = parse_timing_report_text(text)
    result["source_file"] = str(path)
    return result


# -----------------------------------------------------------------------------
# CSV export
# -----------------------------------------------------------------------------

def export_timing_groups_csv(report: Dict[str, Any], csv_dir: Path) -> Path:
    csv_dir.mkdir(parents=True, exist_ok=True)
    out_path = csv_dir / "timing_groups.csv"

    fieldnames = [
        "source_file",
        "mode",
        "header_label",
        "section_name",
        "section_kind",
        "group_display_name",
        "group_id",
        "group_name",
        "wns_ns",
        "tns_ns",
        "violating_paths",
        "all_paths",
    ]

    extra_fields: List[str] = []
    for table in report.get("timing_tables", []):
        for section in table.get("sections", []):
            for metric_key in section.get("extra_metric_keys", []):
                if metric_key not in extra_fields:
                    extra_fields.append(metric_key)

    with out_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames + extra_fields)
        writer.writeheader()
        for table in report.get("timing_tables", []):
            for section in table.get("sections", []):
                for group in section.get("groups", []):
                    row = {
                        "source_file": report.get("source_file"),
                        "mode": table.get("mode"),
                        "header_label": table.get("header_label"),
                        "section_name": section.get("section_name"),
                        "section_kind": section.get("section_kind"),
                        "group_display_name": group.get("display_name"),
                        "group_id": group.get("group_id"),
                        "group_name": group.get("group_name"),
                        "wns_ns": group.get("wns_ns"),
                        "tns_ns": group.get("tns_ns"),
                        "violating_paths": group.get("violating_paths"),
                        "all_paths": group.get("all_paths"),
                    }
                    for extra_key in extra_fields:
                        row[extra_key] = group.get(extra_key)
                    writer.writerow(row)

    return out_path


def export_drvs_csv(report: Dict[str, Any], csv_dir: Path) -> Optional[Path]:
    rows: List[Dict[str, Any]] = []
    for block in report.get("drvs", []):
        for row in block.get("rows", []):
            rows.append(row)

    if not rows:
        return None

    csv_dir.mkdir(parents=True, exist_ok=True)
    out_path = csv_dir / "drvs.csv"
    fieldnames = ["drv_type", "real_nr_nets_terms", "real_worst_vio", "total_nr_nets_terms"]

    with out_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        for row in rows:
            writer.writerow({k: json.dumps(v, ensure_ascii=False) if isinstance(v, dict) else v for k, v in row.items()})

    return out_path


def export_scalars_csv(report: Dict[str, Any], csv_dir: Path) -> Optional[Path]:
    scalars = report.get("scalars") or {}
    if not scalars:
        return None

    csv_dir.mkdir(parents=True, exist_ok=True)
    out_path = csv_dir / "scalars.csv"
    with out_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["key", "value"])
        writer.writeheader()
        for key, value in scalars.items():
            writer.writerow({"key": key, "value": value})
    return out_path


def export_csv_bundle(report: Dict[str, Any], csv_dir: Path) -> Dict[str, Optional[str]]:
    timing_csv = export_timing_groups_csv(report, csv_dir)
    drvs_csv = export_drvs_csv(report, csv_dir)
    scalars_csv = export_scalars_csv(report, csv_dir)
    return {
        "timing_groups_csv": str(timing_csv) if timing_csv else None,
        "drvs_csv": str(drvs_csv) if drvs_csv else None,
        "scalars_csv": str(scalars_csv) if scalars_csv else None,
    }


# -----------------------------------------------------------------------------
# CLI
# -----------------------------------------------------------------------------

def main() -> None:
    parser = argparse.ArgumentParser(description="Parse ASCII Innovus timing debug report into JSON/CSV (v3).")
    parser.add_argument("report", type=Path, help="Path to the timing report text file")
    parser.add_argument("-o", "--output", type=Path, help="Output JSON file path")
    parser.add_argument("--indent", type=int, default=2, help="JSON indentation (default: 2)")
    parser.add_argument("--csv-dir", type=Path, help="Directory to export CSV files")
    args = parser.parse_args()

    result = parse_timing_report_file(args.report)

    if args.csv_dir:
        result["csv_exports"] = export_csv_bundle(result, args.csv_dir)

    output_text = json.dumps(result, ensure_ascii=False, indent=args.indent)
    if args.output:
        args.output.write_text(output_text + "\n", encoding="utf-8")
    else:
        print(output_text)


if __name__ == "__main__":
    main()

用法:该脚本.py xxxx.summary.gz -o result.json

相关推荐
Orange_sparkle2 小时前
claude code高级使用手册
python·ai·claude code
雕刻刀2 小时前
pip离线安装
linux·python·pip
源码之家2 小时前
计算机毕业设计:Python股票数据分析与ARIMA预测系统 Flask框架 ARIMA 数据分析 可视化 大数据 大模型(建议收藏)✅
大数据·python·数据挖掘·数据分析·django·flask·课程设计
没有羊的王K2 小时前
机器学习指标解析:AUC与KS值
开发语言·python
千江明月2 小时前
Ollama安装的详细步骤以及Python调用Qwen
开发语言·python·ollama·qwen模型
是大强2 小时前
TensorFlow Lite
人工智能·python·tensorflow
Wenzar_2 小时前
# 发散创新:SwiftUI 中状态管理的深度实践与重构艺术 在 SwiftUI 的世界里,**状态驱动 UI 是核心哲学**。但随
java·python·ui·重构·swiftui
\xin2 小时前
Pikachu的python一键exp,xx型注入,“insert/updata“注入,“delete“注入,“http header“注入
数据库·python·http
Ulyanov2 小时前
《PySide6 GUI开发指南:QML核心与实践》 第五篇:Python与QML深度融合——数据绑定与交互
开发语言·python·qt·ui·交互·雷达电子战系统仿真