2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Markitdownで取れないテキストをOCR機能を抽出しよう!!

Posted at

Markitdownは便利だけどたまに使いづらい。

LLMライクに資料を前処理するときにMarkitdownは結構使いやすい。しかし、MarkItDown 単体で PDF から Markdown を生成すると、PDF の内容によってはテキスト抽出が不完全になることがある。特に、テキスト層が存在しない画像 PDF やレイアウトが複雑な表形式、段組み構造では文字の順序が乱れる、空白が消える、文字化けが起こるなど、実用に耐えない出力となる。

なぜそんなことが起きるのか?

MarkItDown は PDF のテキスト抽出を内部的には既存の PDF パーサー(PyPDF など)に依存しているが、これらは画像 PDF に含まれる文字を認識する力がない。また、段組や表組の構造を論理的に復元する機能が弱いため、テキストの流れを正確に再現できない。OCR 機能は別途必要だが、MarkItDown 自体には高精度な OCR 処理は含まれていない。

解決方法

  1. OCR の多段処理pdf2image + pytesseract を使い、画像 PDF をページごとに画像化 → OCR でテキスト抽出 → 必要に応じて手動でレイアウト補正。
  2. 外部依存の設定: Poppler や Tesseract のパスを明示的に指定して、環境依存エラーを減らす。
  3. スキップロジック: 対応外フォーマットを処理対象から除外し、エラーを未然に防ぐ。
  4. 抽出後のフロー分岐: OCR 結果テキストは MarkItDown に通さず直接 .md に保存する。

実際にやってみた。

動作環境

  • windows 11
  • Python 3.9 以上推奨
パッケージ名 バージョン例 必要な理由
numpy 1.26.x 互換性の合致のため
pandas 最新安定版 Excel/CSV処理に必須
pdfminer.six 最新安定版 PDF のテキスト層抽出
pdf2image 最新安定版 PDF をページごとに画像化(OCR用)
pytesseract 最新安定版 画像から文字列を抽出する OCR エンジンのラッパー
markitdown 最新安定版 各種ファイルを Markdown に一括変換
ツール名 バージョン例 備考
Poppler 23.x 以降 pdf2image が内部で pdftoppm.exe を呼ぶため。
PATH か poppler_path で指定
Tesseract OCR 5.x 系 pytesseract が内部で tesseract.exe を呼ぶ。
日本語対応には jpn.traineddatatessdata に配置する必要あり

全体コード

import os                                            # OS操作用
from markitdown import MarkItDown                   # 任意テキスト→Markdown
from pdfminer.high_level import extract_text        # PDFテキスト抽出
from pdf2image import convert_from_path             # PDF→画像
import pytesseract                                  # OCR
import pandas as pd                                 # Excel処理
from docx import Document                           # Word処理
pytesseract.pytesseract.tesseract_cmd = r"<TESSERACT_PATH>" # tesseract.exeをインストール・格納したパスを指定する。

md_engine = MarkItDown()                            # Markdown変換エンジン共有

def save_md(path, content):                         # Markdown保存関数
    md_path = os.path.splitext(path)[0] + ".md"     # 出力パス生成
    with open(md_path, "w", encoding="utf-8") as f: # UTF-8で書込
        f.write(content)                            # Markdown書込
    return md_path                                  # 保存先返却

def pdf_to_md(path):
    text = extract_text(path)
    if not text.strip():
        pages = convert_from_path(
            path,
            dpi=300,
            poppler_path=r"<POPPLER_PATH>" # popplerをダウンロードしたパス(xxx\Release-24.08.0-0\poppler-24.08.0\Library\bin)を指定する。
        )
        text = "\n\n".join(
            pytesseract.image_to_string(p, lang="jpn+eng") for p in pages
        )

    # OCR結果はすでにMarkdown的な構造なのでそのまま保存するのがベター
    return text  # ← MarkItDown は通さずにテキストを返すだけ



def docx_to_md(path):                               # Word→Markdown
    doc = Document(path)                            # 読込
    text = "\n".join(p.text for p in doc.paragraphs) # 段落結合
    return md_engine.convert_text(text)             # Markdown生成

def excel_to_md(path):                              # Excel→Markdown
    dfs = []                                        # シート保持
    try:
        # openpyxlバージョン問題を回避するため、xlrdエンジンも試行
        with pd.ExcelFile(path, engine='openpyxl') as xls:
            for name in xls.sheet_names:            # 各シート巡回
                df = xls.parse(name)                # DataFrame取得
                dfs.append(f"### {name}\n"          # 見出し
                            + df.to_markdown(index=False))
    except Exception as e:
        # openpyxlで失敗した場合はxlrdを試行(.xlsファイル用)
        try:
            with pd.ExcelFile(path, engine='xlrd') as xls:
                for name in xls.sheet_names:
                    df = xls.parse(name)
                    dfs.append(f"### {name}\n"
                                + df.to_markdown(index=False))
        except Exception as e2:
            # 両方失敗した場合は基本的なCSV風変換
            print(f"Excel読み込みエラー: {e}, {e2}")
            return f"# Excel変換エラー\n\nファイル: {path}\nエラー: openpyxlバージョン要件未満"
    return "\n\n".join(dfs)                         # 全シート結合




def convert_file_to_markdown(path):                 # 拡張子分岐変換
    ext = os.path.splitext(path)[1].lower()         # 拡張子取得
    
    # 対応しない形式をスキップ
    skip_extensions = {".csv", ".md", ".pyc", ".log", ".tmp", ".ipynb", ".json"}
    if ext in skip_extensions:
        print(f"スキップ: {path} (未対応形式)")
        return None
    
    if ext == ".pdf":                               # PDF
        md = pdf_to_md(path)                        # 専用処理
    elif ext in {".docx"}:                          # Word系
        md = docx_to_md(path)                       # Word処理
    elif ext in {".xlsx", ".xls"}:                  # Excel系
        md = excel_to_md(path)                      # Excel処理
    else:                                           # 上記以外
        try:
            md = md_engine.convert(path).text_content   # MarkItDown汎用
        except Exception as e:
            print(f"変換失敗: {path}{e}")
            return None
    return save_md(path, md)                        # 保存しパス返却

def convert_directory_to_markdown(dir_path):        # 再帰的一括変換
    for root, _, files in os.walk(dir_path):        # ディレクトリ探索
        for f in files:                             # 各ファイル
            try:                                    # 例外捕捉
                full_path = os.path.join(root, f)   # フルパス生成
                result_path = convert_file_to_markdown(full_path)
                if result_path:  # Noneでない場合のみ成功ログ
                    print(f"成功: {f}{result_path}")
            except Exception as e:                  # エラー時
                print(f"失敗: {f}{e}")            # ログ出力

## 出力ブロック(フォルダ内一括変換)
os.chdir("<./mdファイルへと変換したいファイルのある場所>")
current_directory = os.getcwd()  # 現在の作業ディレクトリを取得
print(f"現在の作業ディレクトリ: {current_directory}")
convert_directory_to_markdown(current_directory)    

※tesseractやPOPPLERは事前にダウンロード、インストールしておく必要あり。

各コードごとの解説

import os
from markitdown import MarkItDown
from pdfminer.high_level import extract_text
from pdf2image import convert_from_path
import pytesseract
import pandas as pd
from docx import Document

pytesseract.pytesseract.tesseract_cmd = r"<TESSERACT_PATH>"

md_engine = MarkItDown()

冒頭では標準ライブラリと外部依存を読み込み、Tesseract OCR の実行ファイル位置をハードコードで指定し、MarkItDown のインスタンスを共有リソースとして生成する。ここで <TESSERACT_PATH> は筆者の場合、Windows環境だったのでシステムの環境変数にこちらのpathを追加した。

def save_md(path, content):
    md_path = os.path.splitext(path)[0] + ".md"
    with open(md_path, "w", encoding="utf-8") as f:
        f.write(content)
    return md_path

save_md は変換済み Markdown を元ファイル名に拡張子 .md を付与した形で保存し、そのパスを返すユーティリティである。エンコーディングを UTF-8 に固定することで、LLM など後続システムとの互換性を担保する設計である。

def pdf_to_md(path):
    text = extract_text(path)
    if not text.strip():
        pages = convert_from_path(
            path,
            dpi=300,
            poppler_path=r"<POPPLER_PATH>"
        )
        text = "\n\n".join(
            pytesseract.image_to_string(p, lang="jpn+eng") for p in pages
        )
    return text

pdf_to_md は PDFMiner で抽出したテキストが空だった場合にのみ、Poppler を用いてページを画像へ変換し、日本語と英語の二言語 OCR を行う。画像化 PDF やスキャン PDF を包括的に取り扱えるようにするため、OCR 結果は Markdown への追加整形を行わずそのまま返している。<POPPLER_PATH> もtesseractのパスと同様、環境変数に追加。

def docx_to_md(path):
    doc = Document(path)
    text = "\n".join(p.text for p in doc.paragraphs)
    return md_engine.convert_text(text)

Word 文書は python-docx で段落ごとに抽出し、MarkItDown のシンプルなテキスト変換 API へ渡す。段落境界を改行で表現することで、元文書の構造を壊さずに Markdown へ落とし込む方針。

def excel_to_md(path):
    dfs = []
    try:
        with pd.ExcelFile(path, engine="openpyxl") as xls:
            for name in xls.sheet_names:
                df = xls.parse(name)
                dfs.append(f"### {name}\n" + df.to_markdown(index=False))
    except Exception as e:
        try:
            with pd.ExcelFile(path, engine="xlrd") as xls:
                for name in xls.sheet_names:
                    df = xls.parse(name)
                    dfs.append(f"### {name}\n" + df.to_markdown(index=False))
        except Exception as e2:
            return (
                "# Excel変換エラー\n\n"
                f"ファイル: {path}\n"
                "エラー: openpyxlバージョン要件未満"
            )
    return "\n\n".join(dfs)

Excel 変換では openpyxlxlrd の二段構えで読み取りを試行し、各シートを Markdown テーブルへ変換して連結する。バージョン不整合や旧式 .xls を暗黙的に救済し、完全に失敗した場合は簡潔なエラーメッセージを返すことで、バッチ実行中の停止を防ぐ。

def convert_file_to_markdown(path):
    ext = os.path.splitext(path)[1].lower()
    skip_extensions = {".csv", ".md", ".pyc", ".log", ".tmp", ".ipynb", ".json"}
    if ext in skip_extensions:
        print(f"スキップ: {path} (未対応形式)")
        return None

    if ext == ".pdf":
        md = pdf_to_md(path)
    elif ext == ".docx":
        md = docx_to_md(path)
    elif ext in {".xlsx", ".xls"}:
        md = excel_to_md(path)
    else:
        try:
            md = md_engine.convert(path).text_content
        except Exception as e:
            print(f"変換失敗: {path}{e}")
            return None
    return save_md(path, md)

convert_file_to_markdown は拡張子に基づき処理関数を選択し、スキップ対象を事前に排除する。汎用処理に失敗した際も例外を握りつぶしてログだけ残し、パイプライン全体の連続性を保つ実装となっている。最終的に save_md へ渡し、生成ファイルのパスを返す仕組みである。

def convert_directory_to_markdown(dir_path):
    for root, _, files in os.walk(dir_path):
        for f in files:
            try:
                full_path = os.path.join(root, f)
                result_path = convert_file_to_markdown(full_path)
                if result_path:
                    print(f"成功: {f}{result_path}")
            except Exception as e:
                print(f"失敗: {f}{e}")

ディレクトリ変換関数は OS 依存の再帰探索で全ファイルを網羅し、個別変換の成否を逐次標準出力へ報告する。実運用ではこの関数を一度呼び出すだけで、ディレクトリツリー下に存在する PDF・Office・その他対応ファイルがすべて Markdown 化され、同階層に保存される。

結果

OCR を含めた前処理をスクリプトで組み込むことで、画像 PDF からもテキストを正確に抽出し、レイアウトの崩れを最小化できた。MarkItDown は Markdown 生成に専念させる形で安定した運用が可能になった。
excelでも試したが、特に問題なく出力ができた。

image.png

考察

MarkItDown は多形式変換のハブとしては有用だが、PDF など複雑な構造を含むファイルでは前処理の有無で出力品質が大きく変わる。自動化と正確性のバランスを取るためには、前処理を多段化し、外部ツールとの連携を前提にするのが現実解と言える。

残課題

  • Poppler や Tesseract など外部バイナリのバージョン違いによるトラブルはゼロではない。
  • 大量ファイルを一括処理する場合の並列化やエラー監視など、運用の自動化度を上げる余地がある。(多すぎると処理しきれない恐れがあるから非同期バッチ処理が有効かもしれない。)

関連リンク

Tesseract:https://github.com/UB-Mannheim/tesseract/wiki
Tesseract 日本語:https://github.com/tesseract-ocr/tessdata
※日本語の場合は jpn.traineddata
※日本語のPDFを読み取りたい場合はダウンロードした jpn.traineddata を
Tesseract の tessdata フォルダに置くだけ。
Poppler:https://github.com/oschwartz10612/poppler-windows/releases/

参考にした記事

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?