水曜日, 4月 30, 2025
ホームニューステックニュース【検証】画像PDFを検索可能化してRAGデータソースに活用する #rag - Qiita

【検証】画像PDFを検索可能化してRAGデータソースに活用する #rag – Qiita



【検証】画像PDFを検索可能化してRAGデータソースに活用する #rag - Qiita

この記事について

RAGの精度向上には、元データの品質改善が欠かせません。本記事では、画像PDFのようにそのままでは検索できないドキュメントに対して、検索可能なPDFへ変換する検証を行った内容を紹介します。

問題意識

画像PDFは、単純にアップロードするだけではKendraやBedrock Knowledge Baseなどの検索エンジンに同期できません。
そのため、これまではAmazon Bedrockなどの基盤モデルを利用してOCRをかけ、テキストファイルに変換したうえで登録する、というプロセスを踏んでいました。

ただこの方法だと、以下のような課題があると感じるようになりました。

  • ①元々あったPDFのページ数が失われてしまう
  • ②構造的なOCRがなかなか難しい
  • ③どこをどう読み取ったのか、という詳細の推論過程はブラックボックスになっている

そこで今回は、次の2点を意識して検証を進めることにしました。
①「画像PDF」→「検索可能なPDF」に蘇らせる(テキスト情報を埋め込む)
② より高精度なOCRエンジンを使用する

検証スタート

前提条件
RAGの仕組みの構築は、Amazon BedrockのKnowledge Base(以下KBと記載)を使用し、
ベクトルDBには、Pineconeを採用しました。
※ 検証を通して気づいたのですが、PineconeのServerless indexを用いれば、かなり費用を抑えられると思いました。(私はAWS Marketplace 経由で利用していたので Standardプラン)
検証を行う中で、まあまあ使っていましたが、0.003ドルくらいにしか費用がかさまなかったです。従量課金なのがメリットですね。
スクリーンショット 2025-04-29 15.40.04.png

スクリーンショット 2025-04-29 15.36.03.png

検証で確かめること

  • ページ数が正しく保持されること
    • OCR→PDFの過程で、正しくページ数を保持できているのか
      • 検索を行った際に、「何ページ目から抽出できたのか」という情報が正しく抽出できているかで確認
  • OCR精度はいかほどか(期待する情報がある程度取れてくるか?)

検証に使用するドキュメント

今回は、OCRを実現するアプローチとして2つの策を考え、検証してみました。
検証に共通して、適当な画像をスキャンし、PDFにしたものをS3に配置し、データソースとして指定します。(test-input.pdfと名付けます)
スクリーンショット 2025-04-27 12.16.10.png

当然、このPDFは、検索ができる状況ではありません。
試しに、Amazon BedrockのKnowledge Baseを作成し、S3に配置したtest-input.pdfと同期してみると、以下のように同期に失敗する旨のエラーが表示されています。
スクリーンショット 2025-04-26 22.58.32.png

検証において使用したpythonスクリプト

今回の検証で使用した全体のpythonスクリプトを以下に添付します。
GitHubにも公開したので、自己責任の上ご利用ください。
各手順の部分で、かいつまんで説明します。
なお、処理の全体像の図を記事最下部(処理イメージ)の項に記載しましたので、気になる方は参考になれば幸いです。

検証で使用したスクリプト
import os
import sys
from pathlib import Path
from typing import List

import fitz
from google.cloud import documentai_v1 as documentai
from google.cloud.documentai_toolbox import document
from ocrmypdf import exceptions, hocrtransform, ocr

# サービスアカウントキーのパスを環境変数に設定
# NOTE 事前にスクリプトを実行するファイルと同じ階層に配置
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "XXXXXXX.json"

# プロジェクトID・ロケーション・プロセッサIDを設定
project_id = "your-project-id"
location = "us" #またはeu(プロセッサを作成したリージョン)
processor_id = "your-processor-id" #(作成したプロセッサID)


def process_pdf_by_ocrmypdf(input_pdf: str, output_pdf: str) -> None:
    """
    OCRmyPDFを使ってPDFに透明テキストレイヤーを付与する

    Args:
        input_pdf (str): 入力PDFファイルパス
        output_pdf (str): 出力PDFファイルパス
    """
    try:
        ocr(
            input_file=input_pdf,
            output_file=output_pdf,
            language="jpn",
            deskew=True,
            clean=True,
            progress_bar=True,
        )
        print(f"[INFO] OCRが正常に完了: {output_pdf}")
    except exceptions.ExitStatusException as e:
        print(f"[ERROR] OCRmyPDFエラー: {e}")


def split_pdf_page_by_page(input_path: str) -> List[str]:
    """
    PDFファイルを1ページずつ分割し、個別ファイルとして保存する

    Args:
        input_path (str): 入力PDFファイルパス

    Returns:
        List[str]: 分割後の各ページPDFファイルパスリスト
    """
    pdf = fitz.open(input_path)
    chunk_files = []

    base_name = os.path.splitext(input_path)[0]

    for i in range(len(pdf)):
        chunk_path = f"{base_name}_page{i + 1}.pdf"
        chunk_pdf = fitz.open()
        chunk_pdf.insert_pdf(pdf, from_page=i, to_page=i)
        chunk_pdf.save(chunk_path)
        chunk_pdf.close()
        chunk_files.append(chunk_path)

    pdf.close()
    return chunk_files


def convert_hocr_to_pdf(
    hocr_path: str, background_pdf_path: str, output_pdf_path: str, dpi: int = 300
) -> None:
    """
    hOCRファイルから透明テキストレイヤーを作成し、背景PDFと合成する

    Args:
        hocr_path (str): hOCRファイルパス
        background_pdf_path (str): 背景PDFファイルパス
        output_pdf_path (str): 出力PDFファイルパス
        dpi (int, optional): 解像度(デフォルト300)
    """
    ocr_only_pdf_path = str(Path(output_pdf_path).with_suffix(".ocr_only.pdf"))

    transformer = hocrtransform.HocrTransform(hocr_filename=Path(hocr_path), dpi=dpi)
    transformer.to_pdf(out_filename=Path(ocr_only_pdf_path))
    print(f"[INFO] 透明テキストPDF生成完了: {ocr_only_pdf_path}")

    merge_background_and_ocr(background_pdf_path, ocr_only_pdf_path, output_pdf_path)
    print(f"[INFO] 背景と透明テキストを合成完了: {output_pdf_path}")

    Path(ocr_only_pdf_path).unlink(missing_ok=True)


def merge_background_and_ocr(
    background_pdf_path: str, ocr_text_pdf_path: str, output_pdf_path: str
) -> None:
    """
    背景PDFと透明テキストレイヤーPDFを合成する

    Args:
        background_pdf_path (str): 背景PDFパス
        ocr_text_pdf_path (str): 透明テキストPDFパス
        output_pdf_path (str): 出力PDFパス
    """
    bg_doc = fitz.open(background_pdf_path)
    ocr_doc = fitz.open(ocr_text_pdf_path)

    for page_num in range(len(bg_doc)):
        bg_page = bg_doc[page_num]
        bg_page.show_pdf_page(bg_page.rect, ocr_doc, page_num)

    bg_doc.save(output_pdf_path, garbage=4, deflate=True)
    bg_doc.close()
    ocr_doc.close()


def process_document_with_docai(file_path: str) -> documentai.Document:
    """
    Document AIでPDFをOCR処理する

    Args:
        file_path (str): 入力PDFパス

    Returns:
        documentai.Document: OCR処理結果
    """
    client = documentai.DocumentProcessorServiceClient()
    name = f"projects/{project_id}/locations/{location}/processors/{processor_id}"

    with open(file_path, "rb") as f:
        content = f.read()

    request = documentai.ProcessRequest(
        name=name,
        raw_document=documentai.RawDocument(
            content=content, mime_type="application/pdf"
        ),
    )
    result = client.process_document(request=request)
    return result.document


def save_docai_response_to_json(
    document_obj: documentai.Document, output_path: str
) -> None:
    """
    Document AIレスポンスをJSONファイルとして保存する

    Args:
        document_obj (Document): Documentオブジェクト
        output_path (str): 出力JSONファイルパス
    """
    json_obj = documentai.Document.to_json(document_obj)
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(json_obj)


def convert_docai_response_to_hocr(
    docai_document: documentai.Document, title: str, document_path: str
) -> str:
    """
    Document AI JSONからhOCR形式に変換する

    Args:
        docai_document (Document): OCRドキュメント
        title (str): hOCRファイルタイトル
        document_path (str): Document AI JSONパス

    Returns:
        str: hOCRテキスト
    """
    wrapped_doc = document.Document.from_document_path(document_path=document_path)
    return wrapped_doc.export_hocr_str(title=title)


def merge_pdfs_with_pymupdf(pdf_files: List[str], output_path: str) -> None:
    """
    複数PDFファイルを結合する

    Args:
        pdf_files (List[str]): PDFファイルリスト
        output_path (str): 出力ファイルパス
    """
    merger = fitz.open()
    for pdf_file in pdf_files:
        merger.insert_pdf(fitz.open(pdf_file))
    merger.save(output_path)
    merger.close()


def make_searchable_pdf(input_path: str, output_path: str, use_docai: bool = True) -> None:
    """
    PDFをOCR処理して検索可能なPDFに変換する

    Args:
        input_path (str): 入力PDFパス
        output_path (str): 出力PDFパス
        use_docai (bool, optional): Document AIを使用するか(デフォルトTrue)
    """
    print(f"[INFO] 分割処理中: {input_path}")
    chunk_files = split_pdf_page_by_page(input_path)
    processed_chunks = []
    temp_files = []

    for i, chunk_file in enumerate(chunk_files):
        print(f"[INFO] ページ {i + 1}/{len(chunk_files)} 処理中...")

        base = os.path.splitext(chunk_file)[0]
        hocr_path = f"{base}.hocr.xml"
        json_path = f"{base}.json"
        output_chunk = f"{base}_processed.pdf"

        try:
            if use_docai:
                docai_document = process_document_with_docai(chunk_file)
                save_docai_response_to_json(docai_document, json_path)
                hocr_content = convert_docai_response_to_hocr(
                    docai_document, f"Chunk {i + 1}", json_path
                )

                with open(hocr_path, "w", encoding="utf-8") as f:
                    f.write(hocr_content)

                convert_hocr_to_pdf(hocr_path, chunk_file, output_chunk)

            else:
                process_pdf_by_ocrmypdf(chunk_file, output_chunk)

            processed_chunks.append(output_chunk)
            temp_files += [chunk_file]
            if use_docai:
                temp_files += [hocr_path, json_path]

        except Exception as e:
            print(f"[ERROR] ページ {i + 1} 処理失敗: {e}")

    print("[INFO] 各ページを結合中...")
    merge_pdfs_with_pymupdf(processed_chunks, output_path)

    for path in temp_files + processed_chunks:
        try:
            os.remove(path)
        except Exception as e:
            print(f"[WARN] 一時ファイル削除失敗: {path} -> {e}")

    print(f"[DONE] 完了: {output_path}")


# 実行部分
if __name__ == "__main__":
    if len(sys.argv)  3:
        print("使用方法: python script.py 入力PDF 出力PDF [use_docai]")
        sys.exit(1)

    input_pdf = sys.argv[1]
    output_pdf = sys.argv[2]
    use_docai = sys.argv[3].lower() == "true" if len(sys.argv) > 3 else True

    if use_docai:
        if not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"):
            print("[WARN] 認証情報が設定されていません。")

    make_searchable_pdf(input_pdf, output_pdf, use_docai)

① pythonライブラリ「OCRmyPDF」を使ってOCR

OCRmyPDFとは

OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched or copy-pasted.(GitHubリポジトリ説明から引用)

OCRmyPDFは、
スキャンされたPDFファイルにテキストを埋め込むことができる役割を担っています。
オープンソースのOCRエンジンである、「Tesseract OCR」が内部的に使われています。
Tesseract OCRは、日本語を含めた多言語にも対応しています。

手順1 ocrmypdfライブラリをインストール

uvを使って検証を行いました。
検証時点でのバージョンはuv 0.6.12です。

$ uv version
uv 0.6.12 (e4e03833f 2025-04-02)

uv initで任意のプロジェクトを作成したのちに、下記を実行します。これでプログラムからocrmypdfライブラリを呼び出す準備が整いました。

手順2 プログラムから呼び出す

OCR処理はocrmypdf.ocrが担っているので、これを使います。
input_fileにはOCRをかけたいファイルのパスを、
output_fileにはOCR結果の出力先のパスを指定します。

--languageオプションをつけることで、もとドキュメントの言語を指定することができます。(多くの場合はjpn)を指定することになるかと思います。

from ocrmypdf import ocr
def process_pdf_by_ocrmypdf(input_pdf: str, output_pdf: str) -> None:
    """
    OCRmyPDFを使ってPDFに透明テキストレイヤーを付与する

    Args:
        input_pdf (str): 入力PDFファイルのパス
        output_pdf (str): 出力PDFファイルのパス

    Returns:
        None
    """
    try:
        ocr(
            input_file=input_pdf,
            output_file=output_pdf,
            language="jpn",     
            deskew=True,        # 傾きを補正するオプション
            clean=True,         # ノイズ除去をするオプション
            progress_bar=True,  # 処理状況をターミナルで確認可能にする
        )
        print(f"[INFO] OCRが正常に完了: {output_pdf}")
    except ocrmypdf.exceptions.ExitStatusException as e:
        print(f"[ERROR] OCR実行中にエラー: {e}")

progress_bar=Trueをつけると、OCR処理の進捗状況をターミナル上で確認することができます。
出力結果は以下のようになります。

プログレスバー出力イメージ
$ uv run convert_to_searchable_pdf_v2.py test-input.pdf test-output.pdf false
[INFO] 分割処理中: test-input.pdf
[INFO] チャンク 1/3 処理中...
Scanning contents     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
[tesseract] lots of diacritics - possibly poor OCR
OCR                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
PDF/A conversion      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
Linearizing           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 100/100 0:00:00
Recompressing JPEGs   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   0% 0/0 -:--:--
Deflating JPEGs       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
JBIG2                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   0% 0/0 -:--:--
[INFO] OCRが正常に完了: test-input_page1_processed.pdf
[INFO] チャンク 2/3 処理中...
Scanning contents     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
OCR                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
PDF/A conversion      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
Linearizing           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 100/100 0:00:00
Recompressing JPEGs   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   0% 0/0 -:--:--
Deflating JPEGs       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
JBIG2                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   0% 0/0 -:--:--
[INFO] OCRが正常に完了: test-input_page2_processed.pdf
[INFO] チャンク 3/3 処理中...
Scanning contents     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
OCR                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
PDF/A conversion      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
Linearizing           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 100/100 0:00:00
Recompressing JPEGs   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   0% 0/0 -:--:--
Deflating JPEGs       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00
JBIG2                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   0% 0/0 -:--:--
[INFO] OCRが正常に完了: test-input_page3_processed.pdf
[INFO] チャンク結合中...
[DONE] 完了: test-output.pdf

② Google CloudのOCRサービス「Document AI」を使ってOCRを行い、JSONファイルに保存

Document AIとは

Google CloudのOCRサービスです。
料金体系は以下のようになっています。
Enterprise Document OCR プロセッサを使う場合では、1000ページごとに1.5$ということで、少量のデータであれば比較的安価にOCRをすることができます。

料金体系は以下のページに記載がありますので、詳細はこちらをご覧ください。

スクリーンショット 2025-04-29 12.27.39.png
上記のようにGoogle Cloud コンソール上から簡単にテストすることができますが、

今回はGoogle Cloud SDK経由から呼び出すことを考えます。

利用に際しては
サービスアカウントの認証情報作成、プロセッサの作成など事前準備が必要となります。
こちらのチュートリアル(1-5)が参考になります。

手順1.Document AI ToolBoxを用いて、画像PDFをhocr形式に変換する

Document AIには、Toolboxと呼ばれる、OCR結果を2次利用しやすい形式に変換するためのクライアントライブラリが用意されています。

def convert_docai_response_to_hocr(
    docai_document: documentai.Document, title: str, document_path: str
) -> str:
    """
    JSON(Document AIのレスポンスが入っている)をhOCR形式に変換する

    Args:
        docai_document (Document): OCRドキュメント
        title (str): hOCRファイルタイトル
        document_path (str): Document AI JSONパス

    Returns:
        str: hOCRテキスト
    """
    wrapped_doc = document.Document.from_document_path(document_path=document_path)
    return wrapped_doc.export_hocr_str(title=title)

その中に、Document AIによるOCR結果のレスポンス(JSON形式)をhocr形式に変換するツールがあったため、今回はこれを使用します。
変換された結果が以下です。(掲載しているのは一部です)
bboxという記述が多くありますが、これはOCRによって検出された物体(画像やテキストなど)の領域のことを指すようです。
つまり「どこに、何が書いてあるかを詳細にまとめたXML形式のファイル」と言えそうです。



 xmlns="http://www.w3.org/1999/xhtml" xml:lang="unknown" lang="unknown">

</span>Chunk 2<span class="nt"/>
<span class="nt"><meta/> <span class="na">http-equiv=</span><span class="s">"Content-Type"</span> <span class="na">content=</span><span class="s">"text/html;charset=utf-8"</span> <span class="nt">/></span>
<span class="nt"><meta/> <span class="na">name=</span><span class="s">"ocr-system"</span> <span class="na">content=</span><span class="s">"Document AI OCR"</span> <span class="nt">/></span>
<span class="nt"><meta/> <span class="na">name=</span><span class="s">"ocr-langs"</span> <span class="na">content=</span><span class="s">"unknown"</span> <span class="nt">/></span>
<span class="nt"><meta/> <span class="na">name=</span><span class="s">"ocr-scripts"</span> <span class="na">content=</span><span class="s">"unknown"</span> <span class="nt">/></span>
<span class="nt"><meta/> <span class="na">name=</span><span class="s">"ocr-number-of-pages"</span> <span class="na">content=</span><span class="s">"1"</span> <span class="nt">/></span>
<span class="nt"><meta/> <span class="na">name=</span><span class="s">"ocr-capabilities"</span> <span class="na">content=</span><span class="s">"ocrp_lang ocr_page ocr_carea ocr_par ocr_line ocrx_word"</span> <span class="nt">/></span>
<span class="nt"/>
<span class="nt"/>
<span class="nt"><div> <span class="na">class=</span><span class="s">'ocr_page'</span> <span class="na">lang=</span><span class="s">'unknown'</span> <span class="na">title=</span><span class="s">'bbox 0 0 1626 2459'</span><span class="nt">><span wp_automatic_readability="2.5"> <span class="na">class=</span><span class="s">'ocr_carea'</span> <span class="na">id=</span><span class="s">'block_1_0'</span> <span class="na">title=</span><span class="s">'bbox 400 54 797 61'</span><span class="nt" wp_automatic_readability="5">><p> <span class="na">class=</span><span class="s">'ocr_par'</span> <span class="na">id=</span><span class="s">'par_1_0_0'</span> <span class="na">title=</span><span class="s">'bbox 400 54 797 61'</span><span class="nt">><span> <span class="na">class=</span><span class="s">'ocr_line'</span> <span class="na">id=</span><span class="s">'line_1_0_0_0'</span> <span class="na">title=</span><span class="s">'bbox 400 54 797 61'</span><span class="nt">></span>かわさきの上下水道 No.59 令和7年3月
<span class="nt"><span> <span class="na">class=</span><span class="s">'ocrx_word'</span> <span class="na">id=</span><span class="s">'word_1_0_0_0_0'</span> <span class="na">title=</span><span class="s">'bbox 400 54 475 71'</span><span class="nt">></span>かわさき<span class="nt"/><span> <span class="na">class=</span><span class="s">'ocrx_word'</span> <span class="na">id=</span><span class="s">'word_1_0_0_0_1'</span> <span class="na">title=</span><span class="s">'bbox 479 52 498 69'</span><span class="nt">></span>の<span class="nt"/><span> <span class="na">class=</span><span class="s">'ocrx_word'</span> <span class="na">id=</span><span class="s">'word_1_0_0_0_2'</span> <span class="na">title=</span><span class="s">'bbox 498 51 580 68'</span><span class="nt">></span>上下水道 <span class="nt"/><span> <span class="na">class=</span><span class="s">'ocrx_word'</span> <span class="na">id=</span><span class="s">'word_1_0_0_0_3'</span> <span class="na">title=</span><span class="s">'bbox 600 48 654 65'</span><span class="nt">></span>No.59 <span class="nt"/><span> <span class="na">class=</span><span class="s">'ocrx_word'</span> <span class="na">id=</span><span class="s">'word_1_0_0_0_4'</span> <span class="na">title=</span><span class="s">'bbox 676 46 715 64'</span><span class="nt">></span>令和<span class="nt"/><span> <span class="na">class=</span><span class="s">'ocrx_word'</span> <span class="na">id=</span><span class="s">'word_1_0_0_0_5'</span> <span class="na">title=</span><span class="s">'bbox 720 45 732 63'</span><span class="nt">></span>7<span class="nt"/><span> <span class="na">class=</span><span class="s">'ocrx_word'</span> <span class="na">id=</span><span class="s">'word_1_0_0_0_6'</span> <span class="na">title=</span><span class="s">'bbox 738 44 757 61'</span><span class="nt">></span>年<span class="nt"/><span> <span class="na">class=</span><span class="s">'ocrx_word'</span> <span class="na">id=</span><span class="s">'word_1_0_0_0_7'</span> <span class="na">title=</span><span class="s">'bbox 763 44 774 62'</span><span class="nt">></span>3<span class="nt"/><span> <span class="na">class=</span><span class="s">'ocrx_word'</span> <span class="na">id=</span><span class="s">'word_1_0_0_0_8'</span> <span class="na">title=</span><span class="s">'bbox 781 43 798 61'</span><span class="nt">></span>月
....中略
<span class="nt"/>
<span class="nt"/>
</span></span></span></span></span></span></span></span></span></span></span></span></p></span></span></span></div></span></span></span></span></span></span></span></span></span></code></pre>
</div>
<h5 data-sourcepos="513:1-513:51">
<span id="手順2hocr形式からpdfに変換する" class="fragment"/><a href="#%E6%89%8B%E9%A0%862hocr%E5%BD%A2%E5%BC%8F%E3%81%8B%E3%82%89pdf%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B"><i class="fa fa-link"/></a>手順2.hocr形式から、PDFに変換する</h5>
<p data-sourcepos="514:1-514:246">OCRmyPDFのライブラリで、hocr形式からPDFに変換を行うことのできる<a href="https://ocrmypdf.readthedocs.io/en/latest/apiref.html#module-ocrmypdf.hocrtransform" rel="nofollow noopener" target="_blank"><code>hocrtransform</code></a>メソッドがあるため、これを利用します。</p>
<div class="code-frame" data-lang="python" data-sourcepos="515:1-523:3" wp_automatic_readability="9.5">
<div class="highlight" wp_automatic_readability="14">
<pre><code><span class="k">def</span> <span class="nf">convert_hocr_to_pdf</span><span class="p">(</span><span class="n">hocr_path</span><span class="p">,</span> <span class="n">background_pdf_path</span><span class="p">,</span> <span class="n">output_pdf_path</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">300</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">hOCRファイルから透明テキストレイヤーを作成するサンプルコード</span><span class="sh">"""</span>
    <span class="n">ocr_only_pdf_path</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="nc">Path</span><span class="p">(</span><span class="n">output_pdf_path</span><span class="p">).</span><span class="nf">with_suffix</span><span class="p">(</span><span class="sh">"</span><span class="s">.ocr_only.pdf</span><span class="sh">"</span><span class="p">))</span>

    <span class="n">transformer</span> <span class="o">=</span> <span class="n">hocrtransform</span><span class="p">.</span><span class="nc">HocrTransform</span><span class="p">(</span><span class="n">hocr_filename</span><span class="o">=</span><span class="nc">Path</span><span class="p">(</span><span class="n">hocr_path</span><span class="p">),</span> <span class="n">dpi</span><span class="o">=</span><span class="n">dpi</span><span class="p">)</span>
    <span class="n">transformer</span><span class="p">.</span><span class="nf">to_pdf</span><span class="p">(</span><span class="n">out_filename</span><span class="o">=</span><span class="nc">Path</span><span class="p">(</span><span class="n">ocr_only_pdf_path</span><span class="p">))</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[INFO] 透明テキストPDF生成完了: </span><span class="si">{</span><span class="n">ocr_only_pdf_path</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre>
</div>
</div>
<p data-sourcepos="524:1-526:43">変換結果は以下のようになりした。<br />コード内にも記載していますが、あくまでもここでは検出されたテキストがPDF内に埋め込まれるだけです。なので文字列検索には引っかかりますが、こちら側からは確認ができません。<br />この問題を手順3で解決します。</p>
<p data-sourcepos="529:1-529:164"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2Fba01fdae-1de6-44a5-b76b-da5a1428b7b8.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=1281b85a0ce10743a5b7668930310ccc" target="_blank" rel="nofollow noopener"><img decoding="async" src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2Fba01fdae-1de6-44a5-b76b-da5a1428b7b8.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=1281b85a0ce10743a5b7668930310ccc" alt="スクリーンショット 2025-04-29 12.50.59.png" srcset="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2Fba01fdae-1de6-44a5-b76b-da5a1428b7b8.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=f0b252c00a37873b57203f88ec9e9e8a 1x" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/ba01fdae-1de6-44a5-b76b-da5a1428b7b8.png" loading="lazy"/></a></p>
<h5 data-sourcepos="532:1-532:80">
<span id="手順3透明テキストが含まれるpdfと元pdfを重ね合わせる" class="fragment"/><a href="#%E6%89%8B%E9%A0%863%E9%80%8F%E6%98%8E%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%81%8C%E5%90%AB%E3%81%BE%E3%82%8C%E3%82%8Bpdf%E3%81%A8%E5%85%83pdf%E3%82%92%E9%87%8D%E3%81%AD%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B"><i class="fa fa-link"/></a>手順3.透明テキストが含まれるPDFと元PDFを重ね合わせる</h5>
<p data-sourcepos="533:1-533:130">手順2で生じた問題を解決するため、元の画像PDFに、テキストを重ね合わせる処理を追加します。</p>
<div class="code-frame" data-lang="diff_python" data-sourcepos="535:1-546:3" wp_automatic_readability="10.5">
<div class="highlight" wp_automatic_readability="16">
<pre><code><span class="k">def</span> <span class="nf">convert_hocr_to_pdf</span><span class="p">(</span><span class="n">hocr_path</span><span class="p">,</span> <span class="n">background_pdf_path</span><span class="p">,</span> <span class="n">output_pdf_path</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">300</span><span class="p">):</span>
<span class="hil"><span class="o">+ </span>  <span class="sh">"""</span><span class="s">hOCRファイルから透明テキストレイヤーを作成し、背景PDFと合成する</span><span class="sh">"""</span>
</span>    <span class="n">ocr_only_pdf_path</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="nc">Path</span><span class="p">(</span><span class="n">output_pdf_path</span><span class="p">).</span><span class="nf">with_suffix</span><span class="p">(</span><span class="sh">"</span><span class="s">.ocr_only.pdf</span><span class="sh">"</span><span class="p">))</span>

    <span class="n">transformer</span> <span class="o">=</span> <span class="n">hocrtransform</span><span class="p">.</span><span class="nc">HocrTransform</span><span class="p">(</span><span class="n">hocr_filename</span><span class="o">=</span><span class="nc">Path</span><span class="p">(</span><span class="n">hocr_path</span><span class="p">),</span> <span class="n">dpi</span><span class="o">=</span><span class="n">dpi</span><span class="p">)</span>
    <span class="n">transformer</span><span class="p">.</span><span class="nf">to_pdf</span><span class="p">(</span><span class="n">out_filename</span><span class="o">=</span><span class="nc">Path</span><span class="p">(</span><span class="n">ocr_only_pdf_path</span><span class="p">))</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[INFO] 透明テキストPDF生成完了: </span><span class="si">{</span><span class="n">ocr_only_pdf_path</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="hil"><span class="o">+ </span>   <span class="nf">merge_background_and_ocr</span><span class="p">(</span><span class="n">background_pdf_path</span><span class="p">,</span> <span class="n">ocr_only_pdf_path</span><span class="p">,</span> <span class="n">output_pdf_path</span><span class="p">)</span>
</span><span class="hil"><span class="o">+ </span>   <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[INFO] 背景と透明テキストを合成完了: </span><span class="si">{</span><span class="n">output_pdf_path</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</span></code></pre>
</div>
</div>
<p data-sourcepos="548:1-548:59">新たに、<code>merge_background_and_ocr</code>を作成します。</p>
<div class="code-frame" data-lang="python" data-sourcepos="549:1-562:3" wp_automatic_readability="10.5">
<div class="highlight" wp_automatic_readability="16">
<pre><code><span class="k">def</span> <span class="nf">merge_background_and_ocr</span><span class="p">(</span><span class="n">background_pdf_path</span><span class="p">,</span> <span class="n">ocr_text_pdf_path</span><span class="p">,</span> <span class="n">output_pdf_path</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">背景PDFと透明テキストレイヤーPDFを合成</span><span class="sh">"""</span>
    <span class="n">bg_doc</span> <span class="o">=</span> <span class="n">fitz</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">background_pdf_path</span><span class="p">)</span>
    <span class="n">ocr_doc</span> <span class="o">=</span> <span class="n">fitz</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">ocr_text_pdf_path</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">page_num</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">bg_doc</span><span class="p">)):</span>
        <span class="n">bg_page</span> <span class="o">=</span> <span class="n">bg_doc</span><span class="p">[</span><span class="n">page_num</span><span class="p">]</span>
        <span class="n">bg_page</span><span class="p">.</span><span class="nf">show_pdf_page</span><span class="p">(</span><span class="n">bg_page</span><span class="p">.</span><span class="n">rect</span><span class="p">,</span> <span class="n">ocr_doc</span><span class="p">,</span> <span class="n">page_num</span><span class="p">)</span>

    <span class="n">bg_doc</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">output_pdf_path</span><span class="p">,</span> <span class="n">garbage</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">deflate</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">bg_doc</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
    <span class="n">ocr_doc</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre>
</div>
</div>
<p data-sourcepos="563:1-565:120">今回は<code>pymupdf</code>ライブラリの<a href="https://pymupdf.readthedocs.io/ja/latest/page.html#Page.show_pdf_page" rel="nofollow noopener" target="_blank"><code>show_pdf_page</code></a>メソッドを使用しました。<br />このメソッドはあるPDFの特定のページの内容を、別のPDFに描画できるメソッドで、<br />処理内では、画像PDFの上に、透明テキストが含まれるOCR済みのPDFを重ね合わせています。</p>
<div class="code-frame" data-lang="python" data-sourcepos="567:1-579:3" wp_automatic_readability="8.5">
<div class="highlight" wp_automatic_readability="12">
<pre><code><span class="c1"># 補足
# bg.rect→配置場所(「どこに重ね合わせるか」)
# ocr_doc→OCR済みの、テキスト情報だけが存在するPDF(「何を重ね合わせるか」)
# page_num→重ね合わせる対象のページ数
</span><span class="n">bg_page</span><span class="p">.</span><span class="nf">show_pdf_page</span><span class="p">(</span><span class="n">bg_page</span><span class="p">.</span><span class="n">rect</span><span class="p">,</span> <span class="n">ocr_doc</span><span class="p">,</span> <span class="n">page_num</span><span class="p">)</span>

<span class="c1"># saveメソッドについては以下を参照
# https://pymupdf.readthedocs.io/ja/latest/document.html#Document.save
## garbage=4にすることで不要なコンテンツを削除してくれる。
## deflate=Trueにすることでファイルを圧縮してくれる。
</span>
</code></pre>
</div>
</div>
<h2 data-sourcepos="581:1-581:30">
<span id="ragの結果を見てみる" class="fragment"/><a href="#rag%E3%81%AE%E7%B5%90%E6%9E%9C%E3%82%92%E8%A6%8B%E3%81%A6%E3%81%BF%E3%82%8B"><i class="fa fa-link"/></a>RAGの結果を見てみる</h2>
<h3 data-sourcepos="582:1-582:34">
<span id="簡単な質問をしてみる" class="fragment"/><a href="#%E7%B0%A1%E5%8D%98%E3%81%AA%E8%B3%AA%E5%95%8F%E3%82%92%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B"><i class="fa fa-link"/></a>簡単な質問をしてみる</h3>
<p data-sourcepos="583:1-583:41">KBを使って、RAGをしてみます。</p>
<h4 data-sourcepos="585:1-585:41">
<span id="-ocrmypdfでocrを行った場合" class="fragment"/><a href="#-ocrmypdf%E3%81%A7ocr%E3%82%92%E8%A1%8C%E3%81%A3%E3%81%9F%E5%A0%B4%E5%90%88"><i class="fa fa-link"/></a>① OCRmypdfでOCRを行った場合</h4>
<p data-sourcepos="586:1-587:164">元々のPDFの2ページ目にあった、「工事をしないとどうなるの?」の項に関する質問をしてみます。<br /><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2Fbf1222b3-a25a-46d2-beca-85f1b76032f7.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=05d7aa8b72c42507dbe879619dff1f4d" target="_blank" rel="nofollow noopener"><img decoding="async" src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2Fbf1222b3-a25a-46d2-beca-85f1b76032f7.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=05d7aa8b72c42507dbe879619dff1f4d" alt="スクリーンショット 2025-04-29 11.41.48.png" srcset="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2Fbf1222b3-a25a-46d2-beca-85f1b76032f7.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=6ef38e4f2e74791b9a8be435bab236cc 1x" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/bf1222b3-a25a-46d2-beca-85f1b76032f7.png" loading="lazy"/></a></p>
<p data-sourcepos="589:1-590:117">710kmと返してくれるのが期待値ですが、元ドキュメントの通り、710kmと返却してくれました。OCRの読み取り精度は悪くなさそうです。<br />また、<code>x-amz-bedrock-kb-document-page-number</code>で、抽出元のページも相違なく取れてきています。</p>
<p data-sourcepos="592:1-592:164"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2F7e4dbbf6-e7ea-44d7-a327-7e58e6e1a932.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=39424bc3f89a7359031dafb4e55f4f40" target="_blank" rel="nofollow noopener"><img decoding="async" src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2F7e4dbbf6-e7ea-44d7-a327-7e58e6e1a932.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=39424bc3f89a7359031dafb4e55f4f40" alt="スクリーンショット 2025-04-29 13.36.49.png" srcset="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2F7e4dbbf6-e7ea-44d7-a327-7e58e6e1a932.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=5a7090d2723910a5397be8a62ebaa492 1x" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/7e4dbbf6-e7ea-44d7-a327-7e58e6e1a932.png" loading="lazy"/></a></p>
<h4 data-sourcepos="595:1-595:44">
<span id="-document-aiでocrを行った場合" class="fragment"/><a href="#-document-ai%E3%81%A7ocr%E3%82%92%E8%A1%8C%E3%81%A3%E3%81%9F%E5%A0%B4%E5%90%88"><i class="fa fa-link"/></a>② Document AIでOCRを行った場合</h4>
<p data-sourcepos="596:1-596:273">こちらについても、同様問題なく結果が取れてきそうでした。①とはそこまで差異がないですが、ソースチャンクを見る限り、こちらの方が文字をより正確に読み取ってくれているような気がしました。</p>
<p data-sourcepos="598:1-598:164"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2F4f9d6406-3a59-4bfe-a2d9-472a519f6aaf.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=61ef4504dfbe8b1472a62f9bdcd9475c" target="_blank" rel="nofollow noopener"><img decoding="async" src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2F4f9d6406-3a59-4bfe-a2d9-472a519f6aaf.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=61ef4504dfbe8b1472a62f9bdcd9475c" alt="スクリーンショット 2025-04-29 11.39.35.png" srcset="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2F4f9d6406-3a59-4bfe-a2d9-472a519f6aaf.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=84077702dea1fe79c6767d6afa3ad115 1x" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/4f9d6406-3a59-4bfe-a2d9-472a519f6aaf.png" loading="lazy"/></a></p>
<div data-sourcepos="600:1-604:3" class="note info" wp_automatic_readability="6.5">
<span class="fa fa-fw fa-check-circle"/></p>
<div wp_automatic_readability="8">
<p data-sourcepos="601:1-603:164">おまけ<br />Kendraでindexを立てて検索を行ってみても、同じように検索結果からページ情報が得られました。<br /><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2F4ffcfc58-4d79-47ed-9ffd-532cff9f42bb.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=20fd0d1a842142b5d81875284dcc2c0e" target="_blank" rel="nofollow noopener"><img decoding="async" src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2F4ffcfc58-4d79-47ed-9ffd-532cff9f42bb.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=20fd0d1a842142b5d81875284dcc2c0e" alt="スクリーンショット 2025-04-26 22.15.53.png" srcset="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3623937%2F4ffcfc58-4d79-47ed-9ffd-532cff9f42bb.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=bdc71d9fda93544e46697bd93dbe7026 1x" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/4ffcfc58-4d79-47ed-9ffd-532cff9f42bb.png" loading="lazy"/></a></p>
</div>
</div>
<h2 data-sourcepos="607:1-607:12">
<span id="まとめ" class="fragment"/><a href="#%E3%81%BE%E3%81%A8%E3%82%81"><i class="fa fa-link"/></a>まとめ</h2>
<p data-sourcepos="608:1-613:191">ここまで読んでいただき、ありがとうございました。<br />今回は、画像PDFに対してOCRの処理を行い、検索可能なPDFに変換して、RAGのデータソースとして機能するかの検証を行いました。<br />比較的読み取りのしやすいドキュメントでテストを行ったのもありますが、問題なくデータの前処理を行うことができました。<br />冒頭の問題意識の項で記載した、ページ数の抽出の課題は、<br />ページごとにOCRをかける→PDFに書き戻す、というアプローチでクリアできそうな気がしました。<br />また、Document AIを使えば、どこをどう読み取っているのかの情報が、レスポンスからある程度把握できるので、こちらも便利な気がしました。</p>
<p data-sourcepos="615:1-615:12">今後は、</p>
<ul data-sourcepos="616:1-621:0" wp_automatic_readability="0">
<li data-sourcepos="616:1-617:0" wp_automatic_readability="-1">
<p data-sourcepos="616:3-616:50">表組みが多いPDF(帳票、レポート)</p>
</li>
<li data-sourcepos="618:1-619:0" wp_automatic_readability="-1">
<p data-sourcepos="618:3-618:35">手書き文字が混在するPDF</p>
</li>
<li data-sourcepos="620:1-621:0" wp_automatic_readability="-1">
<p data-sourcepos="620:3-620:32">フォントが特殊な文書</p>
</li>
</ul>
<p data-sourcepos="622:1-622:114">など、より難易度の高いケースでも本手法が使えるか検証していきたいと思います。</p>
<h4 data-sourcepos="625:1-625:14">
<span id="おまけ" class="fragment"/><a href="#%E3%81%8A%E3%81%BE%E3%81%91"><i class="fa fa-link"/></a>おまけ</h4>
<h5 data-sourcepos="626:1-626:24">
<span id="処理イメージ" class="fragment"/><a href="#%E5%87%A6%E7%90%86%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8"><i class="fa fa-link"/></a>処理イメージ</h5>
<p data-sourcepos="627:1-627:61">処理の全体像をmermaid記法で書いてみました。</p>
<p><iframe id="qiita-embed-content__1a81a632352f4e46682b193373b52b5a" src="https://qiita.com/embed-contents/mermaid#qiita-embed-content__1a81a632352f4e46682b193373b52b5a" style="width:100%;" frameborder="0" scrolling="no" loading="lazy" data-content="{" data="" td="" a=""> B{ページごとに分割}\n\n B –> C1[ページ1]\n B –> C2[ページ2]\n\n C1 –> D1{Document AIを使うか?}\n C2 –> D2{Document AIを使うか?}\n\n D1 — Yes –> E1[Document AIでOCR実行、<br />レスポンスをJSONファイルに保存]\n D1 — No –> F1[OCRmyPDFでOCR]\n\n D2 — Yes –> E2[Document AIでOCR実行、<br />レスポンスをJSONファイルに保存]\n D2 — No –> F2[OCRmyPDFでOCR]\n\n E1 –> G1[JSONからhOCRを生成 → <br />透明テキストPDF作成]\n E2 –> G2[JSONからhOCRを生成 → <br />透明テキストPDF作成]\n\n G1 –> H1[背景PDFと<br />透明テキスト合成]\n G2 –> H2[背景PDFと<br />透明テキスト合成]\n\n F1 –> I1[テキストが埋め込まれたPDF]\n F2 –> I2[テキストが埋め込まれたPDF]\n\n H1 –> M[各ページの結果を統合]\n H2 –> M\n I1 –> M\n I2 –> M\n\n M –> Z[検索可能なPDF(出力)]\n”,”key”:”96c5265c4a66992967083f2cf83a2a90″}”><br />
</iframe></p>
</div>
</div>
<p><script>!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '305156090176370');
fbq('trackSingle', '305156090176370', 'PageView');</script><br />
<br /><a href="https://h.accesstrade.net/sp/cc?rk=0100oyf000odbd" rel="nofollow" referrerpolicy="no-referrer-when-downgrade"><img decoding="async" src="https://h.accesstrade.net/sp/rr?rk=0100oyf000odbd" alt="フラッグシティパートナーズ海外不動産投資セミナー" border="0" /></a>
<a href="https://h.accesstrade.net/sp/cc?rk=01004iw600odbd" rel="nofollow" referrerpolicy="no-referrer-when-downgrade"><img decoding="async" src="https://h.accesstrade.net/sp/rr?rk=01004iw600odbd" alt="【DMM FX】入金" border="0" /></a><br />
<br /><a href="https://qiita.com/y-mae/items/568da7bd706065802c07?utm_campaign=popular_items&utm_medium=feed&utm_source=popular_items">Source link </a></p>
<p>Views: 0</p><div class='amazon-auto-links'><div class="amazon-products-container-list amazon-unit-29349 unit-type-category" style="">
        <div class="amazon-product-container">
        <div class="amazon-auto-links-product">
    <div class="amazon-auto-links-product-image">
        <div class='amazon-product-thumbnail-container' data-href='https://www.amazon.co.jp/%E4%B8%89%E8%8F%B1%E3%82%B1%E3%83%9F%E3%82%AB%E3%83%AB%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2-Verbatim-1%E5%9B%9E%E9%8C%B2%E7%94%BB%E7%94%A8DVD-R-VHR12JP50V4-1-16%E5%80%8D%E9%80%9F/dp/B006JFH5DK/ref=zg_mg_g_2151826051_d_sccl_24/356-3178928-8054451?psc=1&tag=inmobi06-22' data-large-src='https://images-fe.ssl-images-amazon.com/images/I/71f30guxfZS._AC_UL500_SR500,500_.jpg'><div class="amazon-product-thumbnail" style="max-width:160px;max-height:160px;width:160px">
    <a href="https://www.amazon.co.jp/%E4%B8%89%E8%8F%B1%E3%82%B1%E3%83%9F%E3%82%AB%E3%83%AB%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2-Verbatim-1%E5%9B%9E%E9%8C%B2%E7%94%BB%E7%94%A8DVD-R-VHR12JP50V4-1-16%E5%80%8D%E9%80%9F/dp/B006JFH5DK/ref=zg_mg_g_2151826051_d_sccl_24/356-3178928-8054451?psc=1&tag=inmobi06-22" title="バーベイタムジャパン(Verbatim Japan) 1回録画用 DVD-R CPRM 120分 50枚 ホワイトプリンタブル 片面1層 1-16倍速 VHR12JP50V4: " rel="nofollow noopener" target="_blank">
        <img decoding="async" src="https://images-fe.ssl-images-amazon.com/images/I/71f30guxfZS._AC_UL160_SR160,160_.jpg" alt="" style="max-height:160px" />
    </a>
</div></div>
    </div>
    <div class="amazon-auto-links-product-body">
        <h5 class="amazon-product-title">
<a href="https://www.amazon.co.jp/%E4%B8%89%E8%8F%B1%E3%82%B1%E3%83%9F%E3%82%AB%E3%83%AB%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2-Verbatim-1%E5%9B%9E%E9%8C%B2%E7%94%BB%E7%94%A8DVD-R-VHR12JP50V4-1-16%E5%80%8D%E9%80%9F/dp/B006JFH5DK/ref=zg_mg_g_2151826051_d_sccl_24/356-3178928-8054451?psc=1&tag=inmobi06-22" title="バーベイタムジャパン(Verbatim Japan) 1回録画用 DVD-R CPRM 120分 50枚 ホワイトプリンタブル 片面1層 1-16倍速 VHR12JP50V4: " rel="nofollow noopener" target="_blank">バーベイタムジャパン(Verbatim Japan) 1回録画用 DVD-R CPRM 120分 50枚 ホワイトプリンタブル 片面1層 1-16倍速 VHR12JP50V4</a>
</h5>
        <div class='amazon-customer-rating-stars'><div class='crIFrameNumCustReviews' data-rating='42' data-review-count='10468' data-review-url='https://www.amazon.co.jp/product-reviews/B006JFH5DK?tag=inmobi06-22'><span class='crAvgStars'><span class='review-stars'><a href='https://www.amazon.co.jp/product-reviews/B006JFH5DK?tag=inmobi06-22' target='_blank' rel='nofollow noopener'><svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 160 32' enable-background='new 0 0 160 32'><title>5星中4.2(10468)
¥1,293 (2025年4月29日 13:11 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)
RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -

Most Popular