ChatGPT や Claude に「比較表を作って」と頼むと、返ってくるのは Markdown の表です。これを Excel に貼り付けると、| 項目 | 費用 | のような行がすべて A 列に入ってしまい、結局手で直すことになります。
これを一発で解決するために、Markdown の表を .xlsx に変換するツールを作って公開しました。
👉 https://md-to.com/ja/markdown-to-excel/
【スクリーンショット:変換ツールの画面】
前半で使い方、後半でブラウザ内で xlsx を生成する実装のポイントを紹介します。
できること
- AI の回答全体や
.mdファイルをそのまま貼り付けると、表だけを自動で抽出(説明文やコードブロックは無視) - 表が複数あれば、直前の見出しをシート名にして別々のシートに保存
-
1,234.50や12.5%は数値セルとして保存されるので、そのまま SUM できる - ヘッダー行の太字、先頭行の固定、オートフィルター、列幅の自動調整(全角文字も考慮)
-
:---:などの列の配置は、Excel のセルの配置に反映 - セル内の
<br>は Excel のセル内改行になる - すべてブラウザ内で処理。データはサーバーに送信されません(無料・登録不要)
使い方
- ChatGPT などの回答をコピーして、左側に貼り付ける(または
.mdをドロップ) - 右側のプレビューでシートを確認する
- 「.xlsx をダウンロード」を押す
Google スプレッドシートに貼りたいときは、「Excel 用にコピー」でタブ区切りのテキストをコピーすることもできます。
ここからは実装の話です。
xlsx は「XML の zip」なので自前で書ける
.xlsx の中身は、いくつかの XML を zip にまとめたものです。スタイル付きのブックを Excel で問題なく開くには、最低限次のファイルがあれば足ります。
[Content_Types].xml 各パーツの MIME タイプ
_rels/.rels workbook へのリレーション
xl/workbook.xml シート一覧
xl/_rels/workbook.xml.rels workbook から各パーツへのリレーション
xl/styles.xml フォント、塗りつぶし、罫線、表示形式
xl/sharedStrings.xml 共有文字列テーブル
xl/worksheets/sheetN.xml シートごとに 1 つ
XML を文字列として組み立てて、fflate で zip にするだけです。SheetJS は使っていないので、バンドルサイズもほとんど増えません。
import { strToU8, zipSync } from 'fflate';
files['xl/styles.xml'] = strToU8(styles.toXml());
files['xl/sharedStrings.xml'] = strToU8(strings.toXml());
return zipSync(files, { level: 6 });
ハマりどころ 1:要素の順序
worksheet の子要素はスキーマで順序が決まっていて、dimension → sheetViews → sheetFormatPr → cols → sheetData → autoFilter の順に並べる必要があります。順番を間違えても XML としては正しいのですが、Excel で開くと「一部の内容に問題が見つかりました。修復しますか?」と聞かれます。
先頭行の固定は sheetView の中に pane を書きます。
<sheetViews><sheetView workbookViewId="0">
<pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/>
<selection pane="bottomLeft" activeCell="A2" sqref="A2"/>
</sheetView></sheetViews>
ハマりどころ 2:シート名の制約
見出しをシート名にするときは、Excel の制約に合わせる必要があります。
- 31 文字まで
-
[ ] : * ? / \は使えない - 先頭と末尾に
'は置けない - 大文字小文字を区別せずに重複チェックされる
-
Historyは予約名
重複したときは 予算 (2) のように連番を付けています。
Markdown の表の抽出は markdown-it に任せる
最初は | で行を分割する方法を考えましたが、次のようなケースで壊れます。
- コードブロックの中に書かれた表
- セル内のエスケープされた
\| - 前後の
|を省略した書き方(a | bだけの行)
そこで markdown-it でパースし、table_open / th_open / inline / tr_close などのトークンを順に見て表を組み立てています。区切り行の :---: は th の style="text-align:center" として取れるので、列の配置もそのまま使えます。直前の heading_open を覚えておけば、シート名も決まります。
数値の判定:「数値にしないほうがいいもの」が大事
1,234.50 や 12.5% を数値にするのは簡単ですが、逆に数値にしてはいけないもののほうが重要でした。
const NUMBER_PATTERN = /^([-+]?)(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d+))?(%?)$/;
export function parseNumericCell(text: string) {
const match = text.trim().match(NUMBER_PATTERN);
if (!match) return null;
const [, sign, intWithCommas, fraction = '', percent] = match;
const intPart = intWithCommas.replace(/,/g, '');
// 007 のような先頭ゼロは ID なので文字列のまま
if (intPart.length > 1 && intPart.startsWith('0')) return null;
// Excel は倍精度浮動小数点数なので有効数字は 15 桁まで
const significant = (intPart + fraction).replace(/^0+/, '');
if (significant.length > 15) return null;
let value = Number(`${sign}${intPart}${fraction ? `.${fraction}` : ''}`);
if (percent) value /= 100;
// 元の見た目を表示形式で再現する(1,234.50 → #,##0.00)
let numFmt = intWithCommas.includes(',') ? '#,##0' : '0';
if (fraction) numFmt += `.${'0'.repeat(fraction.length)}`;
if (percent) numFmt += '%';
return { value, numFmt };
}
-
先頭ゼロ:
000123のような注文番号を数値にすると123になってしまう -
15 桁の壁:16 桁以上の数値は末尾が
0に丸められる(クレジットカード番号や長い ID が壊れる) -
表示形式:
1,234.50を数値にしただけだと Excel では1234.5と表示されるので、桁区切りと小数桁数をnumFmtで再現する
列幅:全角文字は 2 文字分
列幅は各列の最長セルから計算していますが、日本語・中国語・韓国語の全角文字は Excel 上でおよそ半角 2 文字分の幅になります。全角文字の範囲を正規表現で数えて 2 倍にし、最小 8・最大 60 で丸めています。60 を超えるセルは折り返し表示にしています。
まとめ
- xlsx は XML + zip なので、必要な部分だけなら fflate だけで自前生成できる
- Markdown の表の抽出は、正規表現ではなく markdown-it のトークンを使うと堅い
- 数値変換は「変換しないケース」(先頭ゼロ・15 桁超え)を先に決めておくと事故が起きにくい
AI の回答を Excel にまとめる機会が多い方は、ぜひ使ってみてください。
👉 https://md-to.com/ja/markdown-to-excel/
不具合や「こういう表がうまく変換できない」などがあれば、コメントで教えてもらえるとうれしいです。