0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Microsoft markitdown はどこまで使えるか — PDF / エクセル / 画像を fudebako で検証

0
Posted at

GitHub で +5.7k スターを集めている Microsoft 製の変換ツール markitdown。これ、実際の現場でどこまで通用するのか気になったので、ブラウザだけで完結する Python 環境「fudebako」を使って、PDF や Office 文書、画像までガッツリ検証してみました。

LLM の前処理として導入を考えているなら、知っておくべき「変換の限界」があります。今回は、業務 PC に Python を入れられない環境でも再現可能な、リアルな検証結果をまとめます。

はじめに:この記事でわかること

  • LLM に PDF や Office 文書を食わせる前の前処理として markitdown は実用的なのか
  • 業務 PC で Python が動かせない環境での現実的な環境構築法
  • 実際の提案書や報告書を変換した時のリアルな評価

検証はすべて fudebako(HTML 1 ファイルで動く Python 環境、機内モード OK)の上で行いました。手元で同じ手順をすぐ再現できるはずです。

1. markitdown とは

microsoft/markitdown は、様々なファイルを Markdown に統一変換する Microsoft 製のライブラリです。PDF や Office 文書、画像などを一括で Markdown 化できる手軽さから、RAG 構築界隈で急速に注目を集めています。

from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("doc.pdf")
print(result.text_content)

公式の対応形式は以下のとおりです(公式 README より)。

  • PDF, PowerPoint, Word, Excel
  • 画像(EXIF メタデータ + OCR)
  • 音声(EXIF メタデータ + 文字起こし)
  • HTML, テキスト系(CSV, JSON, XML)
  • ZIP, Outlook メッセージ, EPub
  • YouTube URL

2. 検証環境 — fudebako で動かす

通常は pip install markitdown で環境を作りますが、業務 PC だとそうもいきません。そこで fudebako の出番です。リリースページから HTML を落として開くだけ(v0.3.3 で約 100 MB)。

v0.3.3 には pandas や pdfplumber、Pillow など主要なライブラリが同梱されているので、%pip install markitdown を叩くだけで PDF や DOCX 変換まで即座に動き出します(XLSX は markitdown[xlsx] で openpyxl を追加)。

ドライブタブにファイルをアップロードすれば、ブラウザ上でプレビューも可能です。

PDF プレビュー機能

変換前に「元のレイアウトがどうなっているか」を視覚的に確認できるのはかなり便利。一連の流れはこんな感じです。

PDF プレビュー → markitdown 変換のデモ

3. 動く形式 — HTML / CSV / XLSX

ここからが検証の本題。実際に各形式を変換した結果を見ていきます。

fudebako で画像 / HTML / CSV / XLSX を順次実行した様子がこちら。左パネルに変換結果が次々と出力されるので、挙動の違いが掴みやすいです。

v0.3.3 で 4 形式まとめて変換

3-1. HTML — もっとも素直に動く

<h1>/<h2>#/## に、<table> は Markdown table に、<ul> はリストに、<a> はリンクに変換されます。これらは標準機能だけで綺麗に落ちてきます。

html = """<html><body>
<h1>テストページ</h1>
<p>これは<b>太字</b>の段落です。<a href="https://jugoya.ai">リンク</a>もあります。</p>
<h2>表のサンプル</h2>
<table>
<tr><th>名前</th><th>年齢</th></tr>
<tr><td>太郎</td><td>30</td></tr>
</table>
<ul><li>項目A</li><li>項目B</li></ul>
</body></html>"""
print(md.convert_stream(BytesIO(html.encode()), file_extension=".html").text_content)
# テストページ

これは**太字**の段落です。[リンク](https://jugoya.ai)もあります。

## 表のサンプル

| 名前 | 年齢 |
| --- | --- |
| 太郎 | 30 |

* 項目A
* 項目B

3-2. CSV — 自動でテーブル化

CSV を投げると、ヘッダ行を含めて Markdown table に変換されます。日本語も問題ありません。

csv = "name,age,city\n太郎,30,東京\n花子,25,大阪\n次郎,40,名古屋\n"
print(md.convert_stream(BytesIO(csv.encode()), file_extension=".csv").text_content)
| name | age | city |
| --- | --- | --- |
| 太郎 | 30 | 東京 |
| 花子 | 25 | 大阪 |
| 次郎 | 40 | 名古屋 |

3-3. XLSX — シート名が見出しに、表が table に

Excel は markitdown[xlsx] が必須です。シート名が ## シート名 の見出しになり、各シートの構造が Markdown table として出力されます。書式(色やフォント、セル結合)は全て消えますが、データ値とテーブル構造自体はしっかり残ります

import pandas as pd
df = pd.DataFrame({
    "商品": ["ノート", "ペン", "消しゴム", "ホチキス"],
    "数量": [10, 25, 5, 3],
    "単価": [200, 150, 100, 800],
})
df["小計"] = df["数量"] * df["単価"]
buf = BytesIO()
with pd.ExcelWriter(buf, engine="openpyxl") as w:
    df.to_excel(w, index=False, sheet_name="売上")
buf.seek(0)
print(md.convert_stream(buf, file_extension=".xlsx").text_content)
## 売上

| 商品 | 数量 | 単価 | 小計 |
| --- | --- | --- | --- |
| ノート | 10 | 200 | 2000 |
| ペン | 25 | 150 | 3750 |
| 消しゴム | 5 | 100 | 500 |
| ホチキス | 3 | 800 | 2400 |

内部的には pandas.read_excel で読み込んで HTML 化する仕組みです。Excel の数式は計算結果のみが抽出される点には注意してください。

4. 部分的に動く形式 — PDF(プレーン 1 段組)

ここから雲行きが怪しくなります。まずは、ごく普通の「1 段組 PDF」。
左が元の PDF、右が変換後の Markdown です。

プレーン PDF と Markdown 結果の対比

動くもの:

  • 段落本文のテキスト(折り返しは維持)
  • 箇条書きと 1. 2. 3. 番号付きリスト
  • 「key: value」形式の単行情報

動かないもの:

  • 見出しが Markdown の # に変換されない — 「1. 経緯」のような見出しも単なるプレーンテキストとして出力される
  • テーブル: pdfplumber がセル単位で分割するため、Markdown table としては再構成されない。各セルの内容がただ縦に並ぶだけ
  • footer: ページ番号が本文に混入する

PDF の変換エンジンは pdfplumber + pdfminer.six です。テキスト層を持たない「スキャン PDF」は完全に空の文字列が返ります。OCR のフォールバックは期待してはいけません。

5. 動かない形式 — 装飾された業務 PDF

検証していて一番ショックだったのがこれです。提案書や企画書など、いわゆる「会社で見る PDF」を投げると、無残な結果になるケースが多いです。

装飾 PDF と Markdown 結果の対比

実例で起きた崩壊パターン:

  1. 2 列レイアウトが崩壊 — 左右の行が物理的に連結される。「HTML ファイル 1 個(約 100 MB)を社内ファイルサー ブラウザの標準サンドボックス内で...」のように、カラムを跨いで 1 行として連結されるため読解不能。
  2. table の行ズレ — セル内改行を別行と見なすため、データがバラバラに散る。
  3. ヘッダーバナーの無視 — 背景色付きのタイトル領域も、ただのプレーンテキストとして扱われ、見出し化されない。
  4. grid 構造の分断 — 4 列の key-value がテキスト折り返しでバラバラになる。
  5. footer の誤認 — 2 ページ目だけ「5 列テーブル」として誤認識されるなど、ノイズが混入する。

これは markitdown の責任というより、pdfplumber が複数列レイアウトを構造として認識できないのが原因です。装飾された業務資料は、安易に投げ込むと手動修正が必須になるレベルです。

6. 動かない形式 — 画像(OCR 機能はない)

「画像対応」と聞くと OCR を期待しますが、markitdown 単体にはローカル OCR 機能はありません

PIL で「Hello, fudebako!」と描画した PNG を渡してみましたが、結果は空でした。

from PIL import Image, ImageDraw
img = Image.new("RGB", (400, 100), "white")
ImageDraw.Draw(img).text((20, 30), "Hello, fudebako!", fill="black")
buf = BytesIO(); img.save(buf, "PNG"); buf.seek(0)
result = md.convert_stream(buf, file_extension=".png")
print(repr(result.text_content))
# → ''

_image_converter.py を見ると、画像変換は EXIF メタデータの抽出のみ。本格的な OCR を使うには、OpenAI 互換の LLM client を渡す必要があります。

# OCR したいなら LLM client を渡す必要がある
md = MarkItDown(llm_client=openai_client, llm_model="gpt-4o-mini")

ちなみに、markitdown はファイル判定に Google 製の magika を使っており、これは ONNX Runtime に依存します。「ONNX があるなら OCR も?」と期待しましたが、ONNX はファイル判定用であって OCR には使われていません

7. 動かない形式 — JSON

JSON も専用 converter はなく、そのまま出力されます。LLM に渡す分には困りませんが、テーブル化などはされません。

json_content = '{"users":[{"name":"太郎","age":30},{"name":"花子","age":25}]}'
print(md.convert_stream(BytesIO(json_content.encode()), file_extension=".json").text_content)
# → '{"users":[{"name":"太郎","age":30},{"name":"花子","age":25}]}'

ネストされた JSON を見やすい表にしたければ、pandas.json_normalize 等で整形してから XLSX に変換して渡すなどの工夫が必要です。

8. 結論:用途別の使い分け

今回の検証で感じた、現実的な使い分けは以下の通りです。

用途 評価
HTML / CSV を LLM 用に整形 ◎ そのまま使える
Excel の数値テーブルを Markdown 化 ◎ 値だけ欲しいならベスト
Word の文書を Markdown 化 ○ 段落・見出しは保持
1 段組のテキスト PDF を Markdown 化 △ 段落 OK、表は崩れる、見出し化なし
装飾された業務 PDF を Markdown 化 ✗ 多段組・装飾は崩壊する。手動補完前提
スキャン PDF / 画像内テキスト ✗ OCR 機能ゼロ。LLM Vision API が必要

LLM の RAG 前処理として「とりあえず雑に Markdown 化する」用途には強力ですが、「PDF なら何でも綺麗になる」という過度な期待は禁物です。装飾が多い PDF は、変換結果をそのまま LLM に投げる前に、一度人間が目を通さないと事故ります。

おまけ — 検証コード全部入り

ここまで紹介したコードをまとめました。fudebako v0.3.3 を落として開き、コピペすればそのまま動きます。

# %% setup
%pip install markitdown[xlsx]
%pip install markitdown[docx]

from io import BytesIO
from markitdown import MarkItDown
md = MarkItDown()

# %% 画像 (PNG) — OCR は動くか?
from PIL import Image, ImageDraw

img = Image.new("RGB", (400, 100), "white")
ImageDraw.Draw(img).text((20, 30), "Hello, fudebako!", fill="black")
buf = BytesIO(); img.save(buf, "PNG"); buf.seek(0)
print("OCR result:", repr(md.convert_stream(buf, file_extension=".png").text_content))

# %% HTML
html = """<html><body>
<h1>テストページ</h1>
<p>これは<b>太字</b>の段落です。<a href="https://jugoya.ai">リンク</a>もあります。</p>
<h2>表のサンプル</h2>
<table><tr><th>名前</th><th>年齢</th></tr>
<tr><td>太郎</td><td>30</td></tr></table>
</body></html>"""
print(md.convert_stream(BytesIO(html.encode()), file_extension=".html").text_content)

# %% CSV
csv = "name,age,city\n太郎,30,東京\n花子,25,大阪\n"
print(md.convert_stream(BytesIO(csv.encode()), file_extension=".csv").text_content)

# %% XLSX
import pandas as pd
df = pd.DataFrame({"商品": ["ノート", "ペン"], "数量": [10, 25], "単価": [200, 150]})
df["小計"] = df["数量"] * df["単価"]
buf = BytesIO()
with pd.ExcelWriter(buf, engine="openpyxl") as w:
    df.to_excel(w, index=False, sheet_name="売上")
buf.seek(0)
print(md.convert_stream(buf, file_extension=".xlsx").text_content)

参考リンク

検証は 2026-04-27 時点、markitdown は最新版、fudebako v0.3.3 で実施しました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?