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""])
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()