Files
CraftKit/plugins/doc/skills/xlsx-to-md/scripts/convert.py
T

128 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""将 XLSX 工作簿的二维数据转换为 Markdown。"""
from __future__ import annotations
import argparse
import datetime as dt
import importlib.util
import sys
from pathlib import Path
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", "<br>").replace("\n", "<br>").replace("\r", "<br>")
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。"""
# 第三方库在真正读取工作簿时才加载,使 --help 和参数校验可在干净环境中执行。
from openpyxl import load_workbook
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
# 在创建输出目录前完成依赖预检,保证缺少运行库时不会留下空目录或半成品。
if importlib.util.find_spec("openpyxl") is None:
print(
"错误:缺少 openpyxl。请使用 Codex 工作区依赖运行时,"
"或在已获授权的本地 Python 环境中安装 openpyxl 后重试。",
file=sys.stderr,
)
return 3
output.parent.mkdir(parents=True, exist_ok=True)
try:
sheets, rows, merged, warnings = convert(source, output, args.values, args.merged)
except ModuleNotFoundError as error:
if error.name == "openpyxl":
print(
"错误:缺少 openpyxl。请使用 Codex 工作区依赖运行时,"
"或在已获授权的本地 Python 环境中安装 openpyxl 后重试。",
file=sys.stderr,
)
return 3
raise
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())