#!/usr/bin/env python3 """将 XLSX 工作簿的二维数据转换为 Markdown。""" from __future__ import annotations import argparse import datetime as dt import sys from pathlib import Path from openpyxl import load_workbook def display(value: object) -> str: """将单元格值转换为稳定、可读且适合表格的文本。""" if value is None: return "" if isinstance(value, dt.datetime): value = value.isoformat(sep=" ", timespec="seconds") elif isinstance(value, (dt.date, dt.time)): value = value.isoformat() elif isinstance(value, bool): value = "TRUE" if value else "FALSE" text = str(value).replace("\\", "\\\\").replace("|", "\\|") return text.replace("\r\n", "
").replace("\n", "
").replace("\r", "
") def matrix_for(sheet, merged_mode: str) -> tuple[list[list[str]], int]: """读取有效区域,并按选择的策略表达合并单元格。""" values = [[cell.value for cell in row] for row in sheet.iter_rows()] merged_count = len(sheet.merged_cells.ranges) if merged_mode == "fill": for merged in sheet.merged_cells.ranges: anchor = values[merged.min_row - 1][merged.min_col - 1] for row in range(merged.min_row - 1, merged.max_row): for column in range(merged.min_col - 1, merged.max_col): values[row][column] = anchor while values and all(value is None for value in values[-1]): values.pop() width = max((max((i + 1 for i, value in enumerate(row) if value is not None), default=0) for row in values), default=0) return [[display(value) for value in row[:width]] for row in values], merged_count def render_sheet(title: str, rows: list[list[str]]) -> str: """将单个工作表渲染为 Markdown 章节。""" lines = [f"## {title}", ""] if not rows or not rows[0]: return "\n".join(lines + ["_空工作表_"]) width = max(map(len, rows)) normalized = [row + [""] * (width - len(row)) for row in rows] lines.extend((f"| {' | '.join(normalized[0])} |", f"| {' | '.join(['---'] * width)} |")) lines.extend(f"| {' | '.join(row)} |" for row in normalized[1:]) return "\n".join(lines) def convert(source: Path, output: Path, values_only: bool, merged_mode: str) -> tuple[int, int, int, list[str]]: """打开工作簿、转换全部工作表并写入 UTF-8 Markdown。""" workbook = load_workbook(source, read_only=False, data_only=values_only) sections = [f"# {source.stem}"] row_count = merged_count = 0 warnings: list[str] = [] for sheet in workbook.worksheets: rows, merged = matrix_for(sheet, merged_mode) sections.append(render_sheet(sheet.title, rows)) row_count += len(rows) merged_count += merged if not rows or not rows[0]: warnings.append(f"空工作表:{sheet.title}") sheet_count = len(workbook.worksheets) workbook.close() output.write_text("\n\n".join(sections).rstrip() + "\n", encoding="utf-8") return sheet_count, row_count, merged_count, warnings def main(argv: list[str] | None = None) -> int: """校验文件类型、覆盖授权和输出路径后执行转换。""" parser = argparse.ArgumentParser(description="将 XLSX 工作簿转换为 Markdown") parser.add_argument("input", type=Path) parser.add_argument("--output", type=Path) parser.add_argument("--values", action="store_true", help="读取缓存值而不是公式文本") parser.add_argument("--merged", choices=("fill", "anchor"), default="fill") parser.add_argument("--force", action="store_true") args = parser.parse_args(argv) source = args.input.resolve() output = (args.output or source.with_suffix(".md")).resolve() if not source.is_file() or source.suffix.lower() != ".xlsx": print("错误:只支持存在的 .xlsx 文件;旧版 .xls 不受支持", file=sys.stderr); return 2 if output.suffix.lower() not in {".md", ".markdown"}: print("错误:输出文件必须是 Markdown", file=sys.stderr); return 2 if output.exists() and not args.force: print(f"错误:输出文件已存在:{output}", file=sys.stderr); return 1 output.parent.mkdir(parents=True, exist_ok=True) try: sheets, rows, merged, warnings = convert(source, output, args.values, args.merged) except (OSError, ValueError) as error: print(f"错误:{error}", file=sys.stderr); return 1 print(f"Markdown:{output}") print(f"工作表:{sheets},数据行:{rows},合并区域:{merged}") for warning in warnings: print(f"警告:{warning}") return 0 if __name__ == "__main__": raise SystemExit(main())