xlsx2md.py ダウンロード/コピー

xlsx2md.py をダウンロード

xlsx2md.py
xlsx2md.py
   1"""
   2概要:
   3    Excelファイルから値、数式、コメント、図やグラフ情報などを抽出し、Markdownに出力します。
   4
   5詳細説明:
   6    openpyxlを用いてExcelファイルを読み込み、コマンドライン引数の設定に従って
   7    各種情報をMarkdown形式のテキストファイルおよび画像ファイルとして出力します。
   8"""
   9
  10import argparse
  11import datetime as _dt
  12import os
  13import re
  14from typing import Any, Iterable, Optional
  15
  16from openpyxl import load_workbook
  17from openpyxl.cell.cell import MergedCell
  18from openpyxl.utils import get_column_letter
  19
  20pause = 0
  21
  22
  23def terminate():
  24    """
  25    概要:
  26        プログラムを終了します。
  27
  28    詳細説明:
  29        グローバル変数のpauseが真の場合、終了前にユーザーのエンターキー入力を待機します。
  30    """
  31    if pause:
  32        input("\nPress ENTER to terminate\n")
  33    exit()
  34
  35
  36def initialize():
  37    """
  38    概要:
  39        コマンドライン引数を解析し、初期設定を行います。
  40
  41    戻り値:
  42        :returns: 解析された引数を格納した名前空間オブジェクト。
  43        :rtype: argparse.Namespace
  44    """
  45    parser = argparse.ArgumentParser(
  46        description="Excelファイルから値、数式、コメント、図・グラフ情報などを抽出し、Markdownに出力します。"
  47    )
  48    parser.add_argument("-i", "--input", required=True, help="入力するExcelファイル名 (.xlsx/.xlsm)")
  49    parser.add_argument("-o", "--output", required=True, help="出力するMarkdownファイル名")
  50    parser.add_argument("--imagedir", default="images", help="画像ディレクトリ")
  51    parser.add_argument("--max-rows", type=int, default=200, help="値表として出力する最大行数(--all指定時は無視)")
  52    parser.add_argument("--max-cols", type=int, default=60, help="値表として出力する最大列数(--all指定時は無視)")
  53    parser.add_argument("--max-formulas", type=int, default=2000, help="数式表として出力する最大数式数(--all指定時は無視)")
  54    parser.add_argument("--all", action="store_true", help="行数・列数・数式数を制限せず、全範囲を出力します。")
  55    parser.add_argument("--no-values", action="store_true", help="値のMarkdown表を出力しません。")
  56    parser.add_argument("--no-formulas", action="store_true", help="数式のMarkdown表を出力しません。")
  57    parser.add_argument("--no-metadata", action="store_true", help="結合セル、名前付き範囲、テーブル等のメタ情報を出力しません。")
  58    parser.add_argument("--no-comments", action="store_true", help="セルコメントを出力しません。")
  59    parser.add_argument("--no-charts", action="store_true", help="グラフ情報を出力しません。")
  60    parser.add_argument("--no-images", action="store_true", help="埋め込み画像を抽出しません。")
  61    parser.add_argument("--date-format", default="%Y-%m-%d %H:%M:%S", help="日時セルの出力形式")
  62    parser.add_argument("--encoding", default="utf-8", help="Markdown出力の文字コード")
  63    parser.add_argument("--pause", type=int, default=0, help="終了時待機")
  64    args = parser.parse_args()
  65    return args
  66
  67
  68# -----------------------------
  69# Markdown / text helpers
  70# -----------------------------
  71
  72
  73def md_escape(value: Any) -> str:
  74    """
  75    概要:
  76        Markdownのテーブルセル用に最低限のエスケープ処理を行います。
  77
  78    引数:
  79        :param value: エスケープ対象の値。
  80        :type value: Any
  81
  82    戻り値:
  83        :returns: エスケープされた文字列。
  84        :rtype: str
  85    """
  86    if value is None:
  87        return ""
  88    s = str(value)
  89    s = s.replace("\r\n", "<br>").replace("\n", "<br>").replace("\r", "<br>")
  90    s = s.replace("|", "\\|")
  91    return s
  92
  93
  94def code_escape(value: Any) -> str:
  95    """
  96    概要:
  97        Markdownのインラインコード用に、数式のバッククォート崩れを防ぐエスケープ処理を行います。
  98
  99    引数:
 100        :param value: エスケープ対象の値。
 101        :type value: Any
 102
 103    戻り値:
 104        :returns: エスケープされ、バッククォートで囲まれた文字列。
 105        :rtype: str
 106    """
 107    if value is None:
 108        return ""
 109    s = str(value).replace("`", "\\`")
 110    return f"`{s}`"
 111
 112
 113def format_value(value: Any, date_format: str) -> str:
 114    """
 115    概要:
 116        Excelセル値を生成AIに読みやすいテキストへ変換します。
 117
 118    引数:
 119        :param value: 変換対象の値。
 120        :type value: Any
 121        :param date_format: 日時形式のフォーマット文字列。
 122        :type date_format: str
 123
 124    戻り値:
 125        :returns: テキスト形式に変換された値。
 126        :rtype: str
 127    """
 128    if value is None:
 129        return ""
 130    if isinstance(value, _dt.datetime):
 131        return value.strftime(date_format)
 132    if isinstance(value, _dt.date):
 133        return value.isoformat()
 134    if isinstance(value, _dt.time):
 135        return value.isoformat()
 136    if isinstance(value, float):
 137        # 生成AI向けには、過剰な丸めよりも再現性を優先する。
 138        return format(value, ".15g")
 139    return str(value)
 140
 141
 142def markdown_table(headers: Iterable[str], rows: Iterable[Iterable[Any]]) -> str:
 143    """
 144    概要:
 145        ヘッダーと行データからMarkdown形式の表を作成します。
 146
 147    引数:
 148        :param headers: 表のヘッダー文字列のイテラブル。
 149        :type headers: Iterable[str]
 150        :param rows: 表の行データのイテラブル。
 151        :type rows: Iterable[Iterable[Any]]
 152
 153    戻り値:
 154        :returns: Markdown形式のテーブル文字列。
 155        :rtype: str
 156    """
 157    headers = list(headers)
 158    out = []
 159    out.append("| " + " | ".join(md_escape(h) for h in headers) + " |")
 160    out.append("|" + "|".join(["---"] * len(headers)) + "|")
 161    for row in rows:
 162        row = list(row)
 163        if len(row) < len(headers):
 164            row += [""] * (len(headers) - len(row))
 165        out.append("| " + " | ".join(md_escape(v) for v in row[: len(headers)]) + " |")
 166    return "\n".join(out) + "\n\n"
 167
 168
 169def safe_filename(name: str) -> str:
 170    """
 171    概要:
 172        ファイル名として安全な文字列に変換します。
 173
 174    引数:
 175        :param name: 変換対象のファイル名。
 176        :type name: str
 177
 178    戻り値:
 179        :returns: 記号をアンダースコアに置換した安全なファイル名。
 180        :rtype: str
 181    """
 182    name = re.sub(r"[\\/:*?\"<>|\s]+", "_", name.strip())
 183    name = re.sub(r"_+", "_", name).strip("_")
 184    return name or "sheet"
 185
 186
 187# -----------------------------
 188# Workbook structure helpers
 189# -----------------------------
 190
 191
 192def cell_has_content(cell) -> bool:
 193    """
 194    概要:
 195        セルに値、コメント、またはハイパーリンクが含まれているかを判定します。
 196
 197    引数:
 198        :param cell: 判定対象のセルオブジェクト。
 199
 200    戻り値:
 201        :returns: コンテンツが含まれていればTrue、それ以外はFalse。
 202        :rtype: bool
 203    """
 204    if cell is None or isinstance(cell, MergedCell):
 205        return False
 206    return (
 207        cell.value is not None
 208        or cell.comment is not None
 209        or cell.hyperlink is not None
 210    )
 211
 212
 213def find_used_bounds(ws_formula, ws_values):
 214    """
 215    概要:
 216        値、数式、コメント、ハイパーリンクから実質的な使用範囲を推定します。
 217
 218    引数:
 219        :param ws_formula: 数式を含むワークシートオブジェクト。
 220        :param ws_values: 値を含むワークシートオブジェクト。
 221
 222    戻り値:
 223        :returns: 最小行、最小列、最大行、最大列、コンテンツ有無フラグのタプル。
 224    """
 225    keys = set(getattr(ws_formula, "_cells", {}).keys()) | set(getattr(ws_values, "_cells", {}).keys())
 226    min_row = min_col = None
 227    max_row = max_col = None
 228
 229    for row, col in keys:
 230        cf = ws_formula.cell(row=row, column=col)
 231        cv = ws_values.cell(row=row, column=col)
 232        if cell_has_content(cf) or cell_has_content(cv):
 233            min_row = row if min_row is None else min(min_row, row)
 234            max_row = row if max_row is None else max(max_row, row)
 235            min_col = col if min_col is None else min(min_col, col)
 236            max_col = col if max_col is None else max(max_col, col)
 237
 238    for merged in ws_formula.merged_cells.ranges:
 239        min_row = merged.min_row if min_row is None else min(min_row, merged.min_row)
 240        max_row = merged.max_row if max_row is None else max(max_row, merged.max_row)
 241        min_col = merged.min_col if min_col is None else min(min_col, merged.min_col)
 242        max_col = merged.max_col if max_col is None else max(max_col, merged.max_col)
 243
 244    if min_row is None:
 245        return 1, 1, 1, 1, False
 246    return min_row, min_col, max_row, max_col, True
 247
 248
 249def limited_indices(start: int, end: int, max_count: int, output_all: bool):
 250    """
 251    概要:
 252        指定された範囲のインデックスリストと、上限により切り詰められたかを示すフラグを返します。
 253
 254    引数:
 255        :param start: 開始インデックス。
 256        :type start: int
 257        :param end: 終了インデックス。
 258        :type end: int
 259        :param max_count: 出力する最大数。
 260        :type max_count: int
 261        :param output_all: 全て出力するかどうかのフラグ。
 262        :type output_all: bool
 263
 264    戻り値:
 265        :returns: インデックスのリストと、切り詰められたかどうかの真偽値のタプル。
 266    """
 267    values = list(range(start, end + 1))
 268    if output_all or max_count <= 0 or len(values) <= max_count:
 269        return values, False
 270    return values[:max_count], True
 271
 272
 273def get_cell_pair(ws_formula, ws_values, row: int, col: int):
 274    """
 275    概要:
 276        数式ワークシートと値ワークシートから、同じ位置のセルペアを取得します。
 277
 278    引数:
 279        :param ws_formula: 数式を含むワークシート。
 280        :param ws_values: 値を含むワークシート。
 281        :param row: 行番号。
 282        :type row: int
 283        :param col: 列番号。
 284        :type col: int
 285
 286    戻り値:
 287        :returns: 数式セルと値セルのタプル。
 288    """
 289    return ws_formula.cell(row=row, column=col), ws_values.cell(row=row, column=col)
 290
 291
 292def get_cached_value(ws_values, row: int, col: int, date_format: str) -> str:
 293    """
 294    概要:
 295        指定したセルのキャッシュされた値を取得し、フォーマットして返します。
 296
 297    引数:
 298        :param ws_values: 値を含むワークシート。
 299        :param row: 行番号。
 300        :type row: int
 301        :param col: 列番号。
 302        :type col: int
 303        :param date_format: 日時形式のフォーマット文字列。
 304        :type date_format: str
 305
 306    戻り値:
 307        :returns: フォーマットされたセルの値。
 308        :rtype: str
 309    """
 310    return format_value(ws_values.cell(row=row, column=col).value, date_format)
 311
 312
 313# -----------------------------
 314# Extraction: values / formulas
 315# -----------------------------
 316
 317
 318def values_table_to_markdown(ws_formula, ws_values, bounds, args) -> str:
 319    """
 320    概要:
 321        ワークシートの値範囲からMarkdown形式のテーブルを生成します。
 322
 323    引数:
 324        :param ws_formula: 数式を含むワークシート。
 325        :param ws_values: 値を含むワークシート。
 326        :param bounds: 使用範囲を示すタプル。
 327        :param args: コマンドライン引数を格納したオブジェクト。
 328
 329    戻り値:
 330        :returns: 値のMarkdownテーブル文字列。
 331        :rtype: str
 332    """
 333    min_row, min_col, max_row, max_col, has_content = bounds
 334    if not has_content:
 335        return "(値なし)\n\n"
 336
 337    rows, row_truncated = limited_indices(min_row, max_row, args.max_rows, args.all)
 338    cols, col_truncated = limited_indices(min_col, max_col, args.max_cols, args.all)
 339
 340    headers = ["row"] + [get_column_letter(c) for c in cols]
 341    table_rows = []
 342    for r in rows:
 343        one = [r]
 344        for c in cols:
 345            one.append(get_cached_value(ws_values, r, c, args.date_format))
 346        table_rows.append(one)
 347
 348    msg = ""
 349    total_rows = max_row - min_row + 1
 350    total_cols = max_col - min_col + 1
 351    if row_truncated or col_truncated:
 352        msg += (
 353            f"> 出力を省略しています。Used range は "
 354            f"{get_column_letter(min_col)}{min_row}:{get_column_letter(max_col)}{max_row} "
 355            f"({total_rows} rows × {total_cols} cols)です。"
 356        )
 357        if row_truncated:
 358            msg += f" 行は先頭 {len(rows)} 行のみ出力。"
 359        if col_truncated:
 360            msg += f" 列は先頭 {len(cols)} 列のみ出力。"
 361        msg += " 全出力するには `--all` を指定してください。\n\n"
 362
 363    return msg + markdown_table(headers, table_rows)
 364
 365
 366def iter_formula_cells(ws_formula, ws_values, bounds):
 367    """
 368    概要:
 369        ワークシート内の数式セルを順番に取得します。
 370
 371    引数:
 372        :param ws_formula: 数式を含むワークシート。
 373        :param ws_values: 値を含むワークシート。
 374        :param bounds: 使用範囲を示すタプル。
 375
 376    戻り値:
 377        :returns: 数式セルと値セルのタプルを生成するイテレータ。
 378    """
 379    min_row, min_col, max_row, max_col, has_content = bounds
 380    if not has_content:
 381        return
 382    for row in range(min_row, max_row + 1):
 383        for col in range(min_col, max_col + 1):
 384            cf, cv = get_cell_pair(ws_formula, ws_values, row, col)
 385            value = cf.value
 386            if isinstance(value, str) and value.startswith("="):
 387                yield cf, cv
 388
 389
 390def formulas_to_markdown(ws_formula, ws_values, bounds, args) -> str:
 391    """
 392    概要:
 393        ワークシート内の数式情報を抽出し、Markdownテーブルを生成します。
 394
 395    引数:
 396        :param ws_formula: 数式を含むワークシート。
 397        :param ws_values: 値を含むワークシート。
 398        :param bounds: 使用範囲を示すタプル。
 399        :param args: コマンドライン引数を格納したオブジェクト。
 400
 401    戻り値:
 402        :returns: 数式情報のMarkdownテーブル文字列。
 403        :rtype: str
 404    """
 405    formulas = []
 406    truncated = False
 407    for idx, (cf, cv) in enumerate(iter_formula_cells(ws_formula, ws_values, bounds), start=1):
 408        if (not args.all) and idx > args.max_formulas:
 409            truncated = True
 410            break
 411        formulas.append(
 412            [
 413                cf.coordinate,
 414                code_escape(cf.value),
 415                format_value(cv.value, args.date_format),
 416                cf.number_format,
 417            ]
 418        )
 419
 420    if not formulas:
 421        return "(数式なし)\n\n"
 422
 423    msg = ""
 424    if truncated:
 425        msg += f"> 数式数が多いため、先頭 {len(formulas)} 件のみ出力しています。全出力するには `--all` を指定してください。\n\n"
 426    return msg + markdown_table(["cell", "formula", "cached value", "number format"], formulas)
 427
 428
 429# -----------------------------
 430# Extraction: metadata
 431# -----------------------------
 432
 433
 434def comments_to_markdown(ws_formula, bounds) -> str:
 435    """
 436    概要:
 437        セルコメントを抽出し、Markdownテーブルを生成します。
 438
 439    引数:
 440        :param ws_formula: 対象のワークシート。
 441        :param bounds: 使用範囲を示すタプル。
 442
 443    戻り値:
 444        :returns: コメント情報のMarkdown文字列。
 445        :rtype: str
 446    """
 447    min_row, min_col, max_row, max_col, has_content = bounds
 448    if not has_content:
 449        return ""
 450    rows = []
 451    for row in range(min_row, max_row + 1):
 452        for col in range(min_col, max_col + 1):
 453            cell = ws_formula.cell(row=row, column=col)
 454            if cell.comment is not None:
 455                rows.append([cell.coordinate, cell.comment.author or "", cell.comment.text or ""])
 456    if not rows:
 457        return ""
 458    return "## Comments\n\n" + markdown_table(["cell", "author", "comment"], rows)
 459
 460
 461def hyperlinks_to_markdown(ws_formula, bounds) -> str:
 462    """
 463    概要:
 464        ハイパーリンクを抽出し、Markdownテーブルを生成します。
 465
 466    引数:
 467        :param ws_formula: 対象のワークシート。
 468        :param bounds: 使用範囲を示すタプル。
 469
 470    戻り値:
 471        :returns: ハイパーリンク情報のMarkdown文字列。
 472        :rtype: str
 473    """
 474    min_row, min_col, max_row, max_col, has_content = bounds
 475    if not has_content:
 476        return ""
 477    rows = []
 478    for row in range(min_row, max_row + 1):
 479        for col in range(min_col, max_col + 1):
 480            cell = ws_formula.cell(row=row, column=col)
 481            if cell.hyperlink is not None:
 482                h = cell.hyperlink
 483                target = h.target or h.location or ""
 484                rows.append([cell.coordinate, format_value(cell.value, "%Y-%m-%d %H:%M:%S"), target, h.tooltip or ""])
 485    if not rows:
 486        return ""
 487    return "## Hyperlinks\n\n" + markdown_table(["cell", "text", "target", "tooltip"], rows)
 488
 489
 490def merged_cells_to_markdown(ws_formula) -> str:
 491    """
 492    概要:
 493        結合セル情報を抽出し、Markdownテーブルを生成します。
 494
 495    引数:
 496        :param ws_formula: 対象のワークシート。
 497
 498    戻り値:
 499        :returns: 結合セル情報のMarkdown文字列。
 500        :rtype: str
 501    """
 502    rows = [[str(rng)] for rng in ws_formula.merged_cells.ranges]
 503    if not rows:
 504        return ""
 505    return "## Merged cells\n\n" + markdown_table(["range"], rows)
 506
 507
 508def tables_to_markdown(ws_formula) -> str:
 509    """
 510    概要:
 511        Excelテーブル情報を抽出し、Markdownテーブルを生成します。
 512
 513    引数:
 514        :param ws_formula: 対象のワークシート。
 515
 516    戻り値:
 517        :returns: テーブル情報のMarkdown文字列。
 518        :rtype: str
 519    """
 520    rows = []
 521    try:
 522        items = ws_formula.tables.items()
 523    except Exception:
 524        items = []
 525    for name, table in items:
 526        # openpyxl 3.x では items() が (name, ref) になることがあるため両対応。
 527        ref = getattr(table, "ref", None) or str(table)
 528        display_name = getattr(table, "displayName", None) or name
 529        rows.append([display_name, ref])
 530    if not rows:
 531        return ""
 532    return "## Excel tables\n\n" + markdown_table(["name", "range"], rows)
 533
 534
 535def hidden_to_markdown(ws_formula) -> str:
 536    """
 537    概要:
 538        非表示の行列情報を抽出し、Markdownテーブルを生成します。
 539
 540    引数:
 541        :param ws_formula: 対象のワークシート。
 542
 543    戻り値:
 544        :returns: 非表示情報のMarkdown文字列。
 545        :rtype: str
 546    """
 547    hidden_rows = [str(idx) for idx, dim in ws_formula.row_dimensions.items() if getattr(dim, "hidden", False)]
 548    hidden_cols = [str(idx) for idx, dim in ws_formula.column_dimensions.items() if getattr(dim, "hidden", False)]
 549    if not hidden_rows and not hidden_cols:
 550        return ""
 551    rows = []
 552    if hidden_rows:
 553        rows.append(["hidden rows", ", ".join(hidden_rows[:200]) + (" ..." if len(hidden_rows) > 200 else "")])
 554    if hidden_cols:
 555        rows.append(["hidden columns", ", ".join(hidden_cols[:200]) + (" ..." if len(hidden_cols) > 200 else "")])
 556    return "## Hidden rows / columns\n\n" + markdown_table(["type", "items"], rows)
 557
 558
 559def data_validations_to_markdown(ws_formula) -> str:
 560    """
 561    概要:
 562        データの入力規則を抽出し、Markdownテーブルを生成します。
 563
 564    引数:
 565        :param ws_formula: 対象のワークシート。
 566
 567    戻り値:
 568        :returns: データ入力規則のMarkdown文字列。
 569        :rtype: str
 570    """
 571    dvs = getattr(ws_formula, "data_validations", None)
 572    if dvs is None:
 573        return ""
 574    rows = []
 575    for dv in getattr(dvs, "dataValidation", []):
 576        rows.append([
 577            str(dv.sqref),
 578            dv.type or "",
 579            dv.operator or "",
 580            dv.formula1 or "",
 581            dv.formula2 or "",
 582            dv.allow_blank,
 583        ])
 584    if not rows:
 585        return ""
 586    return "## Data validation\n\n" + markdown_table(
 587        ["range", "type", "operator", "formula1", "formula2", "allow blank"], rows
 588    )
 589
 590
 591def conditional_formatting_to_markdown(ws_formula) -> str:
 592    """
 593    概要:
 594        条件付き書式を抽出し、Markdownテーブルを生成します。
 595
 596    引数:
 597        :param ws_formula: 対象のワークシート。
 598
 599    戻り値:
 600        :returns: 条件付き書式のMarkdown文字列。
 601        :rtype: str
 602    """
 603    cf = getattr(ws_formula, "conditional_formatting", None)
 604    if cf is None:
 605        return ""
 606    rows = []
 607    try:
 608        for cf_range in cf:
 609            rules = cf[cf_range]
 610            for rule in rules:
 611                formula = ", ".join(rule.formula or []) if getattr(rule, "formula", None) else ""
 612                rows.append([str(cf_range), rule.type or "", getattr(rule, "operator", "") or "", formula])
 613    except Exception:
 614        return ""
 615    if not rows:
 616        return ""
 617    return "## Conditional formatting\n\n" + markdown_table(["range", "type", "operator", "formula"], rows)
 618
 619
 620def sheet_overview_to_markdown(ws_formula, bounds) -> str:
 621    """
 622    概要:
 623        シートの概要情報を抽出し、Markdownテーブルを生成します。
 624
 625    引数:
 626        :param ws_formula: 対象のワークシート。
 627        :param bounds: 使用範囲を示すタプル。
 628
 629    戻り値:
 630        :returns: シート概要のMarkdown文字列。
 631        :rtype: str
 632    """
 633    min_row, min_col, max_row, max_col, has_content = bounds
 634    used_range = f"{get_column_letter(min_col)}{min_row}:{get_column_letter(max_col)}{max_row}" if has_content else "(empty)"
 635    rows = [
 636        ["used range", used_range],
 637        ["sheet state", ws_formula.sheet_state],
 638        ["freeze panes", ws_formula.freeze_panes or ""],
 639        ["auto filter", getattr(ws_formula.auto_filter, "ref", None) or ""],
 640    ]
 641    return "## Sheet summary\n\n" + markdown_table(["item", "value"], rows)
 642
 643
 644def defined_names_to_markdown(wb_formula) -> str:
 645    """
 646    概要:
 647        ワークブックの名前付き範囲を抽出し、Markdownテーブルを生成します。
 648
 649    引数:
 650        :param wb_formula: 対象のワークブックオブジェクト。
 651
 652    戻り値:
 653        :returns: 名前付き範囲のMarkdown文字列。
 654        :rtype: str
 655    """
 656    rows = []
 657    dns = getattr(wb_formula, "defined_names", None)
 658    if dns is None:
 659        return ""
 660
 661    # openpyxl 3.1: wb.defined_names.values(); older: wb.defined_names.definedName
 662    try:
 663        iterator = dns.values()
 664    except Exception:
 665        iterator = getattr(dns, "definedName", [])
 666
 667    for dn in iterator:
 668        name = getattr(dn, "name", "")
 669        scope = getattr(dn, "localSheetId", None)
 670        text = getattr(dn, "attr_text", "") or ""
 671        if name:
 672            rows.append([name, "workbook" if scope is None else f"sheetId={scope}", text])
 673    if not rows:
 674        return ""
 675    return "# Workbook defined names\n\n" + markdown_table(["name", "scope", "reference"], rows)
 676
 677
 678# -----------------------------
 679# Extraction: charts / images
 680# -----------------------------
 681
 682
 683def object_text(obj: Any) -> str:
 684    """
 685    概要:
 686        openpyxlのTitleやRichText風オブジェクトから読めるテキストを可能な範囲で取り出します。
 687
 688    引数:
 689        :param obj: テキスト抽出対象のオブジェクト。
 690        :type obj: Any
 691
 692    戻り値:
 693        :returns: 抽出された文字列。
 694        :rtype: str
 695    """
 696    if obj is None:
 697        return ""
 698    if isinstance(obj, str):
 699        return obj
 700    # よくある title.tx.rich.p[0].r[0].t 形式を優先する。
 701    try:
 702        paragraphs = obj.tx.rich.p
 703        texts = []
 704        for p in paragraphs:
 705            for r in getattr(p, "r", []) or []:
 706                t = getattr(r, "t", "")
 707                if t:
 708                    texts.append(t)
 709            if getattr(p, "endParaRPr", None) is not None and not texts:
 710                pass
 711        if texts:
 712            return "".join(texts)
 713    except Exception:
 714        pass
 715    for attr in ("text", "v"):
 716        try:
 717            val = getattr(obj, attr)
 718            if isinstance(val, str) and val:
 719                return val
 720        except Exception:
 721            pass
 722    return ""
 723
 724
 725def get_nested(obj: Any, names: Iterable[str]) -> Optional[Any]:
 726    """
 727    概要:
 728        属性名のリストに従って、ネストされた属性の値を安全に取得します。
 729
 730    引数:
 731        :param obj: 探索開始のオブジェクト。
 732        :type obj: Any
 733        :param names: 属性名のイテラブル。
 734        :type names: Iterable[str]
 735
 736    戻り値:
 737        :returns: 取得された属性値、存在しない場合はNone。
 738        :rtype: Optional[Any]
 739    """
 740    current = obj
 741    for name in names:
 742        if current is None:
 743            return None
 744        current = getattr(current, name, None)
 745    return current
 746
 747
 748def get_ref(obj: Any, chains: Iterable[Iterable[str]]) -> str:
 749    """
 750    概要:
 751        複数の属性名チェーンを順に試し、最初に取得できた値を文字列として返します。
 752
 753    引数:
 754        :param obj: 探索開始のオブジェクト。
 755        :type obj: Any
 756        :param chains: 属性名チェーンのイテラブル。
 757        :type chains: Iterable[Iterable[str]]
 758
 759    戻り値:
 760        :returns: 取得された属性値の文字列。見つからなかった場合は空文字。
 761        :rtype: str
 762    """
 763    for chain in chains:
 764        val = get_nested(obj, chain)
 765        if val:
 766            return str(val)
 767    return ""
 768
 769
 770def chart_anchor(chart) -> str:
 771    """
 772    概要:
 773        グラフのアンカー位置をセルの座標文字列として取得します。
 774
 775    引数:
 776        :param chart: 対象のグラフオブジェクト。
 777
 778    戻り値:
 779        :returns: アンカー位置の座標文字列。
 780        :rtype: str
 781    """
 782    try:
 783        marker = chart.anchor._from
 784        return f"{get_column_letter(marker.col + 1)}{marker.row + 1}"
 785    except Exception:
 786        return ""
 787
 788
 789def chart_type_name(chart) -> str:
 790    """
 791    概要:
 792        グラフのクラス名とタイプ名を組み合わせた文字列を取得します。
 793
 794    引数:
 795        :param chart: 対象のグラフオブジェクト。
 796
 797    戻り値:
 798        :returns: グラフの種類を表す文字列。
 799        :rtype: str
 800    """
 801    name = chart.__class__.__name__
 802    typ = getattr(chart, "type", None)
 803    return f"{name} ({typ})" if typ else name
 804
 805
 806def charts_to_markdown(ws_formula) -> str:
 807    """
 808    概要:
 809        ワークシート内のグラフ情報を抽出し、Markdownに変換します。
 810
 811    引数:
 812        :param ws_formula: 対象のワークシート。
 813
 814    戻り値:
 815        :returns: グラフ情報のMarkdown文字列。
 816        :rtype: str
 817    """
 818    charts = getattr(ws_formula, "_charts", []) or []
 819    if not charts:
 820        return ""
 821
 822    out = "## Charts\n\n"
 823    for i, chart in enumerate(charts, start=1):
 824        out += f"### Chart {i}\n\n"
 825        rows = [
 826            ["type", chart_type_name(chart)],
 827            ["anchor", chart_anchor(chart)],
 828            ["title", object_text(getattr(chart, "title", None))],
 829            ["x axis title", object_text(getattr(getattr(chart, "x_axis", None), "title", None))],
 830            ["y axis title", object_text(getattr(getattr(chart, "y_axis", None), "title", None))],
 831        ]
 832        out += markdown_table(["item", "value"], rows)
 833
 834        series_rows = []
 835        for j, ser in enumerate(getattr(chart, "series", []) or [], start=1):
 836            title = get_ref(ser, [["tx", "strRef", "f"], ["tx", "v"]])
 837            cat_ref = get_ref(ser, [["cat", "numRef", "f"], ["cat", "strRef", "f"]])
 838            val_ref = get_ref(ser, [["val", "numRef", "f"]])
 839            x_ref = get_ref(ser, [["xVal", "numRef", "f"], ["xVal", "strRef", "f"]])
 840            y_ref = get_ref(ser, [["yVal", "numRef", "f"]])
 841            series_rows.append([j, title, cat_ref, val_ref, x_ref, y_ref])
 842        if series_rows:
 843            out += markdown_table(["series", "title/ref", "category ref", "value ref", "x ref", "y ref"], series_rows)
 844        out += "\n"
 845    return out
 846
 847
 848def image_anchor(img) -> str:
 849    """
 850    概要:
 851        画像のアンカー位置をセルの座標文字列として取得します。
 852
 853    引数:
 854        :param img: 対象の画像オブジェクト。
 855
 856    戻り値:
 857        :returns: アンカー位置の座標文字列。
 858        :rtype: str
 859    """
 860    try:
 861        marker = img.anchor._from
 862        return f"{get_column_letter(marker.col + 1)}{marker.row + 1}"
 863    except Exception:
 864        return ""
 865
 866
 867def image_extension(img) -> str:
 868    """
 869    概要:
 870        画像の拡張子を取得します。
 871
 872    引数:
 873        :param img: 対象の画像オブジェクト。
 874
 875    戻り値:
 876        :returns: 画像の拡張子。
 877        :rtype: str
 878    """
 879    path = getattr(img, "path", "") or ""
 880    ext = os.path.splitext(path)[1].lower().lstrip(".")
 881    return ext if ext else "png"
 882
 883
 884def images_to_markdown(ws_formula, image_dir: str, sheet_name: str) -> str:
 885    """
 886    概要:
 887        ワークシート内の画像データをファイルに保存し、Markdown文字列を生成します。
 888
 889    引数:
 890        :param ws_formula: 対象のワークシート。
 891        :param image_dir: 画像を保存するディレクトリ。
 892        :type image_dir: str
 893        :param sheet_name: ワークシート名。
 894        :type sheet_name: str
 895
 896    戻り値:
 897        :returns: 画像情報のMarkdown文字列。
 898        :rtype: str
 899    """
 900    images = getattr(ws_formula, "_images", []) or []
 901    if not images:
 902        return ""
 903    os.makedirs(image_dir, exist_ok=True)
 904
 905    out = "## Images\n\n"
 906    rows = []
 907    sheet_safe = safe_filename(sheet_name)
 908    for i, img in enumerate(images, start=1):
 909        ext = image_extension(img)
 910        filename = f"{sheet_safe}_image{i}.{ext}"
 911        path = os.path.join(image_dir, filename)
 912        try:
 913            data = img._data()
 914            with open(path, "wb") as f:
 915                f.write(data)
 916            rows.append([i, image_anchor(img), f"![{sheet_name} image {i}]({image_dir}/{filename})"])
 917        except Exception as e:
 918            rows.append([i, image_anchor(img), f"画像抽出に失敗: {e}"])
 919    return out + markdown_table(["image", "anchor", "file"], rows)
 920
 921
 922# -----------------------------
 923# Main extraction
 924# -----------------------------
 925
 926
 927def extract_content_to_markdown(input_xlsx: str, output_md: str, image_dir: str, args) -> bool:
 928    """
 929    概要:
 930        指定されたExcelファイルから情報を抽出し、Markdownファイルに書き出します。
 931
 932    引数:
 933        :param input_xlsx: 入力するExcelファイルのパス。
 934        :type input_xlsx: str
 935        :param output_md: 出力するMarkdownファイルのパス。
 936        :type output_md: str
 937        :param image_dir: 画像を保存するディレクトリのパス。
 938        :type image_dir: str
 939        :param args: コマンドライン引数を格納したオブジェクト。
 940
 941    戻り値:
 942        :returns: 変換が成功した場合はTrue、失敗した場合はFalse。
 943        :rtype: bool
 944    """
 945    try:
 946        wb_formula = load_workbook(input_xlsx, data_only=False, keep_vba=True)
 947        wb_values = load_workbook(input_xlsx, data_only=True, keep_vba=True)
 948    except Exception as e:
 949        print(f"エラー: {e}")
 950        return False
 951
 952    out = []
 953    out.append(f"# Excel workbook: {os.path.basename(input_xlsx)}\n\n")
 954    out.append(
 955        "> 注意: 数式セルの値は、Excelファイル内に保存されているキャッシュ値です。"
 956        "openpyxlはExcel数式を再計算しません。必要ならExcelで再計算・保存してから実行してください。\n\n"
 957    )
 958
 959    if not args.no_metadata:
 960        dn_md = defined_names_to_markdown(wb_formula)
 961        if dn_md:
 962            out.append(dn_md)
 963
 964    for ws_formula in wb_formula.worksheets:
 965        ws_values = wb_values[ws_formula.title]
 966        bounds = find_used_bounds(ws_formula, ws_values)
 967        out.append(f"# Sheet: {ws_formula.title}\n\n")
 968        out.append(sheet_overview_to_markdown(ws_formula, bounds))
 969
 970        if not args.no_values:
 971            out.append("## Values\n\n")
 972            out.append(values_table_to_markdown(ws_formula, ws_values, bounds, args))
 973
 974        if not args.no_formulas:
 975            out.append("## Formulas\n\n")
 976            out.append(formulas_to_markdown(ws_formula, ws_values, bounds, args))
 977
 978        if not args.no_comments:
 979            out.append(comments_to_markdown(ws_formula, bounds))
 980
 981        if not args.no_metadata:
 982            out.append(hyperlinks_to_markdown(ws_formula, bounds))
 983            out.append(merged_cells_to_markdown(ws_formula))
 984            out.append(tables_to_markdown(ws_formula))
 985            out.append(hidden_to_markdown(ws_formula))
 986            out.append(data_validations_to_markdown(ws_formula))
 987            out.append(conditional_formatting_to_markdown(ws_formula))
 988
 989        if not args.no_charts:
 990            out.append(charts_to_markdown(ws_formula))
 991
 992        if not args.no_images:
 993            out.append(images_to_markdown(ws_formula, image_dir, ws_formula.title))
 994
 995        out.append("---\n\n")
 996
 997    with open(output_md, "w", encoding=args.encoding, newline="\n") as f:
 998        f.write("".join(out))
 999    print(f"変換完了: {output_md}")
1000    return True
1001
1002
1003def main():
1004    """
1005    概要:
1006        スクリプトのメイン処理を実行します。
1007    """
1008    args = initialize()
1009    global pause
1010    pause = args.pause
1011    if not os.path.exists(args.input):
1012        print("入力ファイルが見つかりません")
1013        return
1014    extract_content_to_markdown(args.input, args.output, args.imagedir, args)
1015
1016
1017if __name__ == "__main__":
1018    main()
1019    terminate()