8.3.8 ドキュメント解析と知識抽出
多くのナレッジベース系プロジェクトで、最初にやりがちなミスは次の2つです。
- 先にベクトル化を考える
- 先に Q&A の作り方を考える
でも、文書が正しく解析されていなければ、その後の検索も生成も一緒に崩れてしまいます。
だからこの節で最も大事なのは、最初にベクトルデータベースを説明することではなく、まず次の判断基準を持つことです。
文書はまず「理解できる」「分割できる」「追跡できる」知識オブジェクトに解析される必要がある。
学習目標
- PDF / Word / PPT をなぜ単なるプレーンテキストだけでは扱えないのかを理解する
- スキャン版 PDF や画像ページでなぜ OCR が必要になるのかを理解する
- 文書を「本文 + 階層 + メタデータ + 例題」のような構造に分ける方法を学ぶ
- 最小構成の文書解析と知識抽出の流れを理解する
まず全体像をつかもう
文書解析は、「ファイル -> 構造 -> 知識ブロック」として理解すると分かりやすいです。
この節で本当に解決したいのは次のことです。
- なぜナレッジベースプロジェクトは「ファイル内容を抜き出して終わり」ではないのか
- なぜ見出しの階層、ページ番号、章、例題が後の検索品質に影響するのか
なぜ文書解析は思ったより難しいのか?
理由は、ファイル形式ごとの問題がまったく違うからです。
PDFは「見た目のレイアウト結果」だけを持つことがあり、段落順が自然に安定しているとは限らないDOCXは構造が比較的はっきりしていますが、スタイルや見出し階層が統一されていないことがあるPPTXは断片的な要点が多く、連続した文章のようには扱えない- スキャン版 PDF では、そもそも本文テキストを直接取り出せないことがある
つまり、実用的なナレッジベースでは、まず次のことを確認する必要があります。
- テキストは抽出できたか?
- 順番は正しいか?
- 見出し、ページ番号、章は残っているか?
- どれが例題、定義、本文、注釈なのか?
初心者向けの分かりやすい比喩
文書解析は、こう考えるとイメージしやすいです。
- 大量の資料を、見返しやすいカードボックスに整理する作業
もし紙を全部そのまま箱からばらまくと、
あとで見つけることはできますが、かなり混乱します。
より安定した方法は、最初に次のように整理することです。
- テーマ
- 章
- 見出し
- 例題
- 出典
こうしておけば、後でシステムが「このテーマの例題を探して」と聞かれたときに、ちゃんと探しやすくなります。
ファイルタイプごとのよくある問題
| ファイルタイプ | よくある問題 |
|---|---|
| 順序が崩れる、ヘッダー・フッターが本文に混ざる、2カラムレイアウトが乱れる | |
| Word | 見出し階層が統一されていない、表と本文が混ざる |
| PPT | 1ページあたりの情報は少ないが断片的で、「ページ」の概念を残す必要がある |
| スキャン版 PDF / 画像ページ | OCR が必要で、誤字や順序ミスが起きやすい |
この表は初心者にとても大事です。
なぜなら、次のことを思い出させてくれるからです。
- 文書処理は「1つのパーサーですべて解決」ではない

ファイルがシステムに入ったら、まずルーティングします。テキスト PDF、スキャン PDF、DOCX、PPTX では問題が違うからです。実際に保存する前に、本文順、見出し階層、ページ番号、内容タイプを復元する必要があります。単に長いプレーンテキストを抜き出すだけではありません。
最小構成の文書解析ワークフローの例
以下の例は、実際のサードパーティライブラリに依存しません。
ただし、「文書タイプごとに解析ルートを分ける」という考え方を先に分かりやすく示します。
from pathlib import Path
def route_parser(filename):
suffix = Path(filename).suffix.lower()
if suffix == ".pdf":
return "pdf_text_or_ocr"
if suffix == ".docx":
return "word_parser"
if suffix == ".pptx":
return "ppt_parser"
return "unsupported"
files = [
"lesson_1.pdf",
"chapter_2.docx",
"course_outline.pptx",
]
for file in files:
print(file, "->", route_parser(file))
想定出力:
lesson_1.pdf -> pdf_text_or_ocr
chapter_2.docx -> word_parser
course_outline.pptx -> ppt_parser
この例でいちばん大事なのは、
- まず「ルーティング」があると意識すること
つまり、ファイルがシステムに入ったら、いきなり1つの関数に全部入れるのではなく、
まず次を判断します。
- これはどのファイルか
- どの解析パスを通すべきか
実際のシステムに近い知識ブロックはどう見えるか?
本当にナレッジベースに入れるべきものは、ただの
- 生テキスト
ではなく、次のような形です。
chunks = [
{
"doc_id": "word_001",
"source_type": "docx",
"section_title": "応用問題:割引計算",
"page_or_slide": 3,
"content": "お店が 100 元の商品を 8 折にしたら、いくらになりますか?",
"content_type": "example",
},
{
"doc_id": "ppt_002",
"source_type": "pptx",
"section_title": "知識ポイントのまとめ",
"page_or_slide": 8,
"content": "割引 = 元の価格 × 割引率。",
"content_type": "concept",
},
]
for chunk in chunks:
print(chunk)
想定出力:
{'doc_id': 'word_001', 'source_type': 'docx', 'section_title': '応用問題:割引計算', 'page_or_slide': 3, 'content': 'お店が 100 元の商品を 8 折にしたら、いくらになりますか?', 'content_type': 'example'}
{'doc_id': 'ppt_002', 'source_type': 'pptx', 'section_title': '知識ポイントのまとめ', 'page_or_slide': 8, 'content': '割引 = 元の価格 × 割引率。', 'content_type': 'concept'}
この例は初心者に特に向いています。なぜなら、
- 本当に価値があるのは「文字だけを取ること」ではない
- 文字を、出典・章・ページ・内容タイプと一緒に戻すことが大事
と分かるからです。
実際のプロジェクトに近い解析結果の schema
初めてこの種のシステムを作るときに抜けやすいのは、次の3つです。
- 文書レベルのメタデータ
- 章レベルの構造
- 知識ブロックレベルの内容
より安定した方法は、解析結果を次の3層に分けることです。
| 層 | 最低限残すもの |
|---|---|
| 文書層 | doc_id / ファイル名 / ソースタイプ / 作成日時 / 教科 |
| 章層 | section_id / タイトル / 章パス / ページ範囲 |
| 知識ブロック層 | chunk_id / テキスト / 内容タイプ / 元ページ / 例題かどうか |
こう考えると分かりやすいです。
- 文書層は本の表紙カードのようなもの
- 章層は目次
- 知識ブロック層は、実際に検索や生成に使うカード
以下の最小構造は、初心者がまず写して作るのに向いています。
parsed_doc = {
"doc_id": "math_pdf_001",
"source_type": "pdf",
"title": "割引応用問題の集中練習",
"subject": "数学",
"sections": [
{
"section_id": "s1",
"section_title": "割引の基本概念",
"page_range": [1, 2],
"chunks": [
{
"chunk_id": "c1",
"content_type": "concept",
"page_or_slide": 1,
"text": "割引 = 元の価格 × 割引率",
},
{
"chunk_id": "c2",
"content_type": "example",
"page_or_slide": 2,
"text": "商品の元の価格が 100 元で、8 折にしたらいくらになりますか?",
},
],
}
],
}
print(parsed_doc["sections"][0]["chunks"][1]["text"])
想定出力:
商品の元の価格が 100 元で、8 折にしたらいくらになりますか?
この schema の意味は、「見た目をきれいにすること」ではありません。
大事なのは次の点です。
- 後で検索するときに絞り込みできる
- 後で教材を生成するときに、どこが概念でどこが例題か分かる
- 後で引用元をたどるときに、どのページから来たか分かる
なぜ「内容タイプ」がとても重要なのか?
あなたのプロジェクトは普通の Q&A ではなく、
次のことをしたいからです。
- テーマ別に資料を探す
- 関連する例題を探す
- それを決まった形式で Word 教材にする
このとき、システムが次の違いを分けられると、かなり安定します。
conceptexampleexercisedefinition
後で教材生成をするときに、とても扱いやすくなります。
最小の「例題抽出」例
あなたのプロジェクトでは、1つの文がどのページにあるか分かるだけでは足りません。
できるだけ次の区別も必要です。
- これは例題か
- これは練習問題か
- これは定義や公式か
最初から複雑なモデルを使わなくても大丈夫です。
まずは最小のルール版で、全体の流れを作るのがよいです。
def guess_content_type(text):
if "例" in text or "解:" in text:
return "example"
if "練習" in text or "思考題" in text:
return "exercise"
if "定義" in text or "公式" in text:
return "concept"
return "paragraph"
samples = [
"例1:商品原価が 100 元で、8 折にしたらいくらになりますか?",
"練習:服の元の価格が 80 元で、7 折にしたらいくらになりますか?",
"公式:割引 = 元の価格 × 割引率",
]
for sample in samples:
print(guess_content_type(sample), "->", sample)
想定出力:
example -> 例1:商品原価が 100 元で、8 折にしたらいくらになりますか?
exercise -> 練習:服の元の価格が 80 元で、7 折にしたらいくらになりますか?
concept -> 公式:割引 = 元の価格 × 割引率
この最小ルール版は完璧ではありませんが、
初心者にとってはとても大事です。
- 「例題抽出」は魔法ではない
- 本質的には、文書内容の分類をしているだけ
実践:模擬ページを知識ブロックに変換する
ここでは、ルーティング、章の検出、メタデータ、内容タイプ判定を 1 つの小さなパイプラインにつなぎます。まだ模擬ページのテキストですが、出力の形は embedding 前に保存したい構造にかなり近いです。
def guess_content_type(text):
if "例" in text or "解:" in text:
return "example"
if "練習" in text or "思考題" in text:
return "exercise"
if "定義" in text or "公式" in text:
return "concept"
return "paragraph"
def build_chunks(doc_id, source_type, pages):
chunks = []
section_title = "無題の章"
for page_no, lines in pages:
for line in lines:
line = line.strip()
if not line:
continue
if line.startswith("#"):
section_title = line.lstrip("#").strip()
continue
chunks.append({
"chunk_id": f"{doc_id}_c{len(chunks) + 1}",
"doc_id": doc_id,
"source_type": source_type,
"section_title": section_title,
"page_or_slide": page_no,
"content": line,
"content_type": guess_content_type(line),
})
return chunks
pages = [
(1, ["# 割引の基本概念", "公式:割引 = 元の価格 × 割引率"]),
(2, ["例1:商品の元の価格が 100 元で、8 折にしたらいくらになりますか?"]),
]
for chunk in build_chunks("math_doc_001", "docx", pages):
print(chunk)
想定出力:
{'chunk_id': 'math_doc_001_c1', 'doc_id': 'math_doc_001', 'source_type': 'docx', 'section_title': '割引の基本概念', 'page_or_slide': 1, 'content': '公式:割引 = 元の価格 × 割引率', 'content_type': 'concept'}
{'chunk_id': 'math_doc_001_c2', 'doc_id': 'math_doc_001', 'source_type': 'docx', 'section_title': '割引の基本概念', 'page_or_slide': 2, 'content': '例1:商品の元の価格が 100 元で、8 折にしたらいくらになりますか?', 'content_type': 'example'}

見出し行は出力行ではなく、状態更新として読みます。つまり section_title だけを更新します。chunk になるのは公式行と例題行で、それぞれが同じ文書メタデータを持って後続の検索に渡されます。
これが最小限に役立つ投入ループです。各 chunk が内容、構造、出典、ページ、タイプを持つようになると、検索と教材生成はかなり作りやすくなります。
スキャン文書でなぜ OCR が必要になるのか?
スキャン版 PDF や画像ページは、そもそも文字ファイルではなく、
- 文字が画像のように見えている
からです。
そのため、最初に必要なのは
- OCR で文字認識すること
です。
その後で、次の処理に進みます。
- 構造の復元
- 見出し階層の識別
- 例題の抽出
スキャンされた教材、スクリーンショット、写真資料をたくさん扱うなら、このステップは非常に重要です。
関連する内容は以下も参照してください。
最初に作るときの、いちばん安全な範囲設定
初回開発で失敗しやすい理由は、技術が難しすぎるからではなく、
最初にサポート範囲を広げすぎるからです。
より安全な最小版は、たいてい次の順番です。
- まずテキスト型
DOCXのみ対応 - 次にテキスト型
PDFを対応 - 次に
PPTXを対応 - 最後にスキャン文書の OCR を追加
この順番の利点は、
- 先に構造と schema を安定させられる
- 最初から OCR の認識問題に足を取られない
ことです。
初心者がそのまま使える解析チェックリスト
初めてナレッジベース用の文書解析を作るとき、
いちばん安定したチェックリストは次のとおりです。
- 文字は漏れなく抽出できているか?
- 見出しと本文の順番は正しいか?
- 章の階層は保たれているか?
- ページ番号 / スライド番号は残っているか?
- 本文と例題を区別できるか?
- スキャン文書に OCR の誤字はないか?
この6項目は、「先にベクトルデータベースを入れる」ことより優先度が高いです。
これをプロジェクトとして見せるなら、何を見せるべきか?
見せる価値が高いのは、たいてい次のようなものです。
- 「PDF / Word / PPT に対応しています」という説明だけ
ではなく、次の3点です。
- 元の文書がどんな見た目だったか
- 解析後の構造化知識ブロックがどうなったか
- 例題がどのように識別されたか
- OCR や構造復元でどこが失敗しやすいか
こうすると、見る人に次のことが伝わりやすくなります。
- あなたは知識投入の流れを理解している
- 単に「ファイルを読む」だけではない
まとめ
- 文書解析の本当の目的は、「ファイルを構造化された知識オブジェクトに変えること」
- schema 設計によって、後の検索・引用・教材生成が安定するかどうかが決まる
- 最初は
DOCX / テキスト PDF / 例題抽出のルール版を先に動かし、その後で拡張するほうが現実的
この節で一番持ち帰ってほしいこと
- 文書解析は文字を抜き出して終わりではなく、構造と出典を復元する必要がある
- 本当に価値のある知識ブロックには、見出し、ページ番号、内容タイプなどのメタデータを付けるべき
- あなたのナレッジベースが大量の PDF / Word / PPT / スキャン文書から来るなら、この工程は全体の流れの中でも特に重要な入口の1つです