PDFからMarkdownへの変換をMac上でできるようにした。セットアップ方法を紹介する。また現時点での技術的限界についても触れる。
PDFからMarkdownへの変換がローカルPCで必要な理由
生物学医学論文を閲覧する場合の文書のフォーマットはHTMLかPDFである。HTML形式はブラウザの閲覧のみで、閲覧印刷配布などの一般的用途にはPDFを使う。しかしLLMはPDFを読めない(入力として受け付けない)ので、テキスト(+画像)に転換する必要がある。公開されているチャットボットではチャットボット側でPDFからテキスト(+画像)への変換が行われている。OpenRouter経由でLLMを選択できるチャットボットを以前つくったが(PM-Chatbot)、このチャットボットはデータを直接LLMへ送るのでPDFをLLMが読める形式に変換しなければならない。ネット上では無料の変換ツールが提供されているが、local LMMによる論文大量処理を計画しているので、自分のMacで変換する方法をセットアップすることにした。LLMが受けつける代表的なフォーマットはMarkdown(以下MD)とJSONだ。MDのほうがヒトによる可読性が高いのでPDFからMDへ変換することにした。
セットアップ
パイソン仮想環境の準備
ホームダイレクトリに作業用フォルダをFinderでつくる(ここではpdf_convert)。作業用フォルダに移動。
% cd pdf_convert
仮想環境をつくる。
% python3.12 -m venv myenv
仮想環境を起動する。
% source myenv/bin/activate
行先頭のmyenvを確認。仮想環境下ではpython = python3.12なので、python3.12の代わりにpythonと入力してもよい。
インストールするライブラリ
PyMuPDF: PDFの構造を検出しデータを抽出する
pillow: 画像処理ライブラリ
pdfplumber: PDFの構造を検出しデータを抽出する、レイアウト考慮
markitdown: microsoftのデータフォーマット各種をMD変換
python-docx: Microsoft Word(.docx)ファイルの編集や生成ができる
インストールはpipを使う。例えば、
% pip install PyMuPDF
markitdownのみ
% pip install 'markitdown[all]'
パイソンスクリプト
PDF MD 変換
import fitz
import os
import sys
def table_to_markdown(table):
"""fitz.TableをMarkdown表形式に変換"""
if not table:
return ""
# ヘッダを取得(最初の行をヘッダとする)
headers = table[0] if table else
# データ行
data_rows = table[1:] if len(table) > 1 else
# ヘッダ行
md_lines = ["| " + " | ".join(str(cell) for cell in headers) + " |"]
# 区切り行(列数に合わせて --- を追加)
md_lines.append("| " + " | ".join("---" for _ in headers) + " |")
# データ行
for row in data_rows:
md_lines.append("| " + " | ".join(str(cell) for cell in row) + " |")
return "\n".join(md_lines) + "\n"
def pdf_to_markdown_images_at_end(pdf_path, md_path, image_dir="images"):
os.makedirs(image_dir, exist_ok=True)
doc = fitz.open(pdf_path)
parts =
# 1) 本文テキストと表(ページ順)
for page_num, page in enumerate(doc, 1):
parts.append(f"# Page {page_num}")
# テキスト抽出(デフォルトで表を含むが、後で表を別途追加)
text = page.get_text("text").strip()
parts.append(text if text else "(no text)")
parts.append("") # 改行
# 表を検出してMarkdown表に変換
tables = page.find_tables()
for table in tables:
md_table = table_to_markdown(table.extract())
if md_table:
parts.append(md_table)
parts.append("") # 表後の改行
# 2) 画像を抽出して最後にまとめる
parts.append("\n---\n")
parts.append("## Images")
img_index = 1
for page_num, page in enumerate(doc, 1):
for img in page.get_images(full=True):
xref = img[0]
base = doc.extract_image(xref)
ext = base.get("ext", "png")
img_bytes = base["image"]
img_name = f"p{page_num:03d}_{img_index:03d}.{ext}"
img_path = os.path.join(image_dir, img_name)
with open(img_path, "wb") as f:
f.write(img_bytes)
parts.append(f"- Page {page_num}: !({image_dir}/{img_name})")
img_index += 1
with open(md_path, "w", encoding="utf-8") as f:
f.write("\n".join(parts))
doc.close() # PDFを閉じる
if __name__ == "__main__":
if len(sys.argv) != 3:
print("使い方: python script.py <入力PDFファイル> <出力Markdownファイル>")
sys.exit(1)
pdf_path = sys.argv[1]
md_path = sys.argv[2]
pdf_to_markdown_images_at_end(pdf_path, md_path)
使い方
ファイル名:pdf_to_md_rev.py
% python pdf_to_md_rev.py "入力ファイル" "出力ファイル"
例:% python pdf_to_md_rev.py path-to-file.pdf document.md
テキスト部分の後ろに画像データが挿入される。画像は"images"フォルダに格納される。fitzはPyMuPDFのこと。
PDF Word 変換
import fitz
import os
import sys
import io
from docx import Document
from docx.shared import Inches
from PIL import Image
def pdf_to_docx_images_at_end(pdf_path, docx_path, image_dir="images", image_width_inch=3.0):
os.makedirs(image_dir, exist_ok=True)
doc_pdf = fitz.open(pdf_path)
docx = Document()
# 1) 本文テキスト
for page_num, page in enumerate(doc_pdf, 1):
text = page.get_text("text").strip()
# 改行をスペースに置き換え、連続したスペースを1つにまとめる
text = ' '.join(text.replace('\n', ' ').split())
docx.add_heading(f"Page {page_num}", level=1)
docx.add_paragraph(text if text else "(no text)")
# 区切り
docx.add_page_break()
docx.add_heading("Images", level=1)
# 2) 画像を抽出して最後に貼付(ページごとにまとめる)
img_index = 1
for page_num, page in enumerate(doc_pdf, 1):
images_on_page = [] # このページの画像を一時保存
for img in page.get_images(full=True):
xref = img[0]
base = doc_pdf.extract_image(xref)
img_bytes = base["image"]
# PILで画像をロードし、PNGに変換
try:
img_pil = Image.open(io.BytesIO(img_bytes))
img_name = f"p{page_num:03d}_{img_index:03d}.png" # 拡張子をpngに固定
img_path = os.path.join(image_dir, img_name)
img_pil.save(img_path, "PNG") # PNGとして保存
images_on_page.append(img_path)
img_index += 1
except Exception as e:
print(f"画像変換エラー (Page {page_num}, Image {img_index}): {e}")
# エラーの場合はスキップ
# このページの画像をまとめて追加(ページ番号は1回だけ)
if images_on_page:
docx.add_paragraph(f"Page {page_num}")
for img_path in images_on_page:
docx.add_picture(img_path, width=Inches(image_width_inch))
docx.save(docx_path)
doc_pdf.close() # PDFを閉じる
if __name__ == "__main__":
if len(sys.argv) != 3:
print("使い方: python script.py <入力PDFファイル> <出力docxファイル>")
sys.exit(1)
pdf_path = sys.argv[1]
docx_path = sys.argv[2]
pdf_to_docx_images_at_end(pdf_path, docx_path)
使い方
ファイル名:pdf_to_docx_rev.py
% python pdf_to_docx_rev.py "入力ファイル" "出力ファイル"
例:% python pdf_to_docx_rev.py path-to-file.pdf document.md
テキスト部分の後ろに画像データが挿入される。画像は"images"フォルダに格納される。
Markitdown使用方法
Markitdownはパイソンライブラリだが、仮想環境内でコマンドラインで実行できる。テキスト情報の抽出のみで画像データ抽出はできない。
% markitdown path-to-file.pdf > document.md
% markitdown path-to-file.pdf -o document.md
使用実感
テキスト抽出についてはPyMuPDF、MarkitdownともにOKだが、Marlitdownの方が出力が読みやすい。しかし3つの論文を試したが、PyMuPDF、Marlitdown、pdfplumberすべて表の検出に失敗した。これらのライブラリは2次元の表が対象で、医学論文に多い非定型の表には対応していないためだ。
表のアルゴリズム抽出は現在の技術でも難しい。そのためツール開発はAI(深層学習ベースのツール)活用に移行している(例えばBytedance Dolphin) 。多分Open AIなどの主要プロバイダーはAIで表抽出をおこなっているのだろう。
一般的に生物学医学論文では図表の内容を本文で説明することが慣例である。そのためテキストのLLM分析で十分ではないか、と考えている。GPT-5の見解も「90%の一般的タスク(要約、重要ポイント抽出、臨床的含意の整理)については、表のテキスト化で十分、図は任意」ということだった。従って現時点での方針は、
1.大量処理の場合はテキストのみ。MarkItdownを使用予定。
2.自作チャットボットを使う場合は内容によってテキストのみ(MarkItdown)かテキスト+画像(PyMuPDF)を選択。
3.表データが重要そうな論文については、chatPDFなどを使う。