以前、MarkItDownでpptx/docx/pdfをMarkdownに変換するWebツールを作った記事を書きました。ドラッグ&ドロップだけで各種ファイルをMarkdown化できて、普段の資料整理やLLMへの入力前処理にとても重宝しています。

MarkItDownで、pptx/docx/pdfを → Markdownに変換するWebツールを作った Microsoft製ライブラリ「MarkItDown」を使い、PowerPoint・Word・Excel・PDFなどをドラッグ&ドロップだけでMarkdownに変換できるWebツールを作りました。ChatGPTやClaudeにドキュメントを渡す前処...  続きを読む

ところが、実運用でひとつだけ致命的に苦手なパターンにぶつかりました。
罫線のある2カラム表のPDFです。この記事は、その弱点を pdfplumber で克服した実装の記録です。


    問題:2カラム表PDFが「左右交互」に混ざる

    きっかけは、クライアントから回ってきた求人票のPDFでした。「ホールスタッフ」「キッチンスタッフ」の2職種を左右に並べた、よくある比較表形式です。

    これを MarkItDown に通すと、こうなりました。

    募集する人材  ・お客様との会話を楽しめる方を歓迎!
    ・調理経験のある方を歓迎します。経
    ・接客が好きで、笑顔で明るく対応で
    験が浅くても丁寧に教えます!
    きる方、大歓迎です!

    左カラムの1行 → 右カラムの1行 → 左カラムの2行目 → … と、左右が1行ずつ交互に出てきて、まったく読めません。編集で直すのも一苦労です。

    なぜこうなるのか

    原因はPDFという形式の根本にあります。PDFは「文字を座標に置いただけ」で、「ここは表のセル」という構造情報を持っていません。だから抽出ツールは、文字を上から下・行単位(Y座標順)に拾っていきます。

    2カラムの表では、同じ行に「左セルの1行目」と「右セルの1行目」が並んでいます。すると素直にY座標順で読むと、左右が1行ずつ交互に出てくる——というわけです。MarkItDown(内部はpdfminer)が悪いのではなく、PDFにセル境界の情報が無いのが根本原因です。


    解決策:pdfplumberの「罫線ベース抽出」

    そこで導入したのが pdfplumber です。
    pdfminer.six の上に構築された軽量ライブラリで、罫線(線)を手がかりにセルを認識できます。今回の求人票は罫線のある表だったので、これがハマりました。

    import pdfplumber
    
    with pdfplumber.open(path) as pdf:
        page = pdf.pages[0]
        tables = page.extract_tables({
            "vertical_strategy": "lines",    # 縦罫線でセルを区切る
            "horizontal_strategy": "lines",  # 横罫線でセルを区切る
        })

    これだけで、左右のカラムが別々のセルとして正しく分離されました。

    ホールスタッフキッチンスタッフ
    職種ホール・接客調理・仕込み
    給与時給1,100円~時給1,200円~

    pdfplumber は MLモデルもGPUも不要なので、共用レンタルサーバー(今回はカラフルボックス)でもそのまま動きます。ここが Docling や Marker のようなAIベースの高精度ツールとの大きな違いで、「動かす環境を選ばない」のが個人開発では効きます。


    実装:チェックボックスで切り替える

    ここで大事な設計判断がありました。pdfplumberの表モードを「常時ON」にしてはいけない、ということです。

    理由は、罫線が無い/薄いPDFや、表ではないレイアウトPDFに表モードを適用すると、逆に本文が欠落するからです(枠の中しか拾わないため)。しかも「罫線の有無」だけでは自動判定が誤爆します(枠線=データ表とは限らない)。

    そこで、チェックボックスで手動切り替えにしました。

    • デフォルトOFF → 従来どおり MarkItDown。どんなPDFでも全文が素直に出る(安全側)
    • ON → pdfplumber の表モード(2カラム表向け)

    バックエンド(FastAPI)側は、フラグを受け取って分岐するだけです。

    @app.post("/convert")
    async def convert_file(
        file: UploadFile = File(...),
        use_pdfplumber: bool = Form(False),
    ):
        ...
        markdown_content = None
        if use_pdfplumber and ext == ".pdf":
            tables_md = pdf_tables_to_markdown(temp_file_path)
            if tables_md:
                markdown_content = tables_md   # 表モードで成功
    
        # 表が無ければ / OFF なら MarkItDown にフォールバック
        if markdown_content is None:
            result = md_converter.convert(temp_file_path)
            markdown_content = result.text_content or ""

    表が1つも検出できなければ MarkItDown にフォールバックするので、ONのまま表の無いPDFを入れても壊れません。この「安全側に倒す」設計が、実運用では効きます。


    精度改善:ここからが本番

    罫線でセルを分離できても、セルの中身にはまだPDF特有のクセが残ります。ここを詰めるのが、実は一番手間のかかった部分でした。3つの課題を順に潰しました。

    課題1:PDFの「折り返し」と「意図した改行」を区別する

    PDFのセル内テキストは、幅に応じて途中で機械的に折り返されています。

    ・調理経験のある方を歓迎します。経
    験が浅くても丁寧に教えます!

    この「経/験」は、単に幅で折り返されただけ。繋げて1行にしたい。一方、箇条書きの や段落の切れ目は、意図した改行なので残したい。

    この区別を、4つの判定で行いました。次の条件のどれかに当てはまれば「意図した改行」として残し、それ以外は「折り返し」として詰めて連結します。

    • y座標のギャップが通常行間の1.4倍を超える(=段落の区切り)
    • 行頭が箇条書き記号 など)
    • 前行末が文末記号
    • 前行末がメール/URL/ドメイン(後述)

    特にy座標ギャップの判定が効きました。「箇条書きの後に、句点なしで新しい段落が始まる」ような、記号では判別できないケースも、行間の空きを見れば段落の区切りとわかるからです。

    課題2:ページをまたぐと表が「分裂」する

    PDFの後半でpdfplumberが検出する列数が「3列→2列」に変わり、テーブルが途中で分裂してしまいました。Markdownにすると、本文の途中に区切り線(|---|---|)が紛れ込みます。

    対策は、ドキュメント内の全テーブルを1つに統合し、列数を最大値に揃えること。短い行は右側を空セルで埋めます。求人票のような「1枚もの」のPDFなら、これで1つの連続テーブルにまとまります。

    課題3:メールアドレス直後がくっつく

    recruit@example.com株式会社◯◯人事部…

    メールアドレスは .jp で終わり文末記号ではないため、次の行と連結されてしまいました。そこで、行末がメール/URL/ドメインなら改行を挟む判定を追加。ただし 実働7.5時間PDF,Word,Excel のような紛らわしい文字列を誤爆しないよう、正規表現は慎重に組みました。


    結果:そのまま貼れるテーブルに

    3つの課題を潰した結果、冒頭の「左右交互で読めない」状態が、そのままMarkdownとして貼れるテーブルになりました。

    | 募集する人材 | ・お客様との会話を楽しめる方を歓迎!<br>・接客が好きで、笑顔で明るく対応できる方、大歓迎です!<br>… | ・調理経験のある方を歓迎します。経験が浅くても丁寧に教えます!<br>… |

    左右のカラムがきちんと分かれ、セル内の箇条書きも保たれ、折り返しは詰められています。
    求人票のような帳票PDFなら、実用十分な品質です。


    正直に:弱点もある

    いいことばかりではないので、限界も書いておきます。

    • 罫線のある表が前提。罫線が無い/薄いPDFは崩れます
      (その場合はチェックOFFで通常変換が無難)
    • 1ファイル=1帳票向け。1つのPDFに列数の違う表が複数あると、1テーブルに融合されます
    • 社名の直後に本文が続くような、汎用ルールで拾えない連結が稀に残ります(実害は小さい)
    • スキャン画像のPDFは非対応(OCRしていないため)

    もっと高精度が必要なら Docling や Marker、あるいは Mistral OCR のようなAPIという選択肢もありますが、いずれも「重い」か「外部送信・課金」のトレードオフがあります。共用サーバーで完結し、罫線表なら十分な精度という点で、pdfplumber は個人開発のツールにちょうど良い落としどころでした。


    まとめ

    • MarkItDown は万能に近いが、罫線のある2カラム表PDFは苦手(左右が交互に混ざる)
    • pdfplumberの罫線ベース抽出で列を正しく分離できる。
      MLモデル不要で共用サーバーでも動く
    • 「常時ON」は誤爆するので、チェックボックスで切り替え+フォールバックが安全
    • セル内の折り返し/段落/箇条書き/メールを判定して整形すると、実用品質になる

    PDFからの表抽出は「完璧」を目指すと沼ですが、用途を絞れば軽量ツールで十分戦えるというのが今回の学びでした。よかったら実際に、罫線のある表PDFで試してみてください。

    pptx/docx/pdf→Markdown 変換ツール pptx・docx・xlsx・pdf・html・csvなどのファイルをドラッグ&ドロップするだけでMarkdown形式に変換。Microsoft MarkItDownを使用したオンライン変換ツール。  続きを読む

    関連記事

    MarkItDownで、pptx/docx/pdfを → Markdownに変換するWebツールを作った Microsoft製ライブラリ「MarkItDown」を使い、PowerPoint・Word・Excel・PDFなどをドラッグ&ドロップだけでMarkdownに変換できるWebツールを作りました。ChatGPTやClaudeにドキュメントを渡す前処...  続きを読む レンタルサーバー「カラフルボックス」で Python FastAPIをデプロイし、Webアプリを公開する方法 共用レンタルサーバー「カラフルボックス(ColorfulBox)」で、Python FastAPIをデプロイしてWebアプリを公開、安定して動かす方法を解説。cPanelのSetup Python App、uvicornの起動、Passengerで...  続きを読む

    参考リンク