#!/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())