🐮

PyMuPDFによるテキスト抽出戦略

に公開

抽出したデータが不完全に感じたり、データ抽出プロセス中に文書の一部が考慮されていないのではないかと疑ったことはありませんか?または、扱いにくく長時間かかる文書処理時間によって、パイプラインが不必要に遅延したことはありませんか?この記事では、テキスト抽出の2つの主要なアプローチ:ネイティブOCRについて説明し、いつどのように使用するかを選択するためのスマートな戦略を検討します。

ネイティブテキスト抽出の理解

ご想像の通り、この技術はPyMuPDFの中核機能を利用して、単純に文書からテキストを取得します。Page.get_text()メソッドを使用して、PDF内で「テキスト」として識別される実際のコンテンツを抽出します。

  • 概要: PDFにデジタル的に埋め込まれているテキストを抽出する

  • 動作原理: PDF構造内のテキストオブジェクトへの直接アクセス

  • 利点:

    • 高速処理

    • 完璧な精度(テキストが存在する場合)

    • 元の書式とフォントを保持

    • 低い計算要件

  • 制限事項:

    • デジタル作成されたPDFでのみ動作

    • スキャンされた文書では完全に失敗

    • 複雑なレイアウトで困難が生じる可能性

OCR(光学文字認識)の理解

この方法は、オープンソースのサードパーティ技術(Tesseract)を利用して、ページ上の画像をスキャンし、その画像をテキストに変換します。情報のスクリーンショットを含むPDFを想像してください。これらはPDF内で単に「画像」として識別されますが、何らかの方法で機械可読テキストが必要です。この方法はPyMuPDFPage.get_textpage_ocr()関数を使用して重い処理を担当します。

  • 概要: テキストの画像を機械可読テキストに変換する

  • 動作原理: 画像処理とパターン認識

  • 利点:

    • あらゆるPDF(スキャン、撮影、または画像ベース)で動作

    • 手書きテキストを処理可能(高度なモデルで)

    • ネイティブ抽出が見逃す視覚要素を処理

  • 制限事項:

    • 処理時間が遅い

    • 精度は画像品質に依存

    • 高い計算およびメモリ要件

    • 認識エラーが発生する可能性

ネイティブテキスト抽出を使用する場合

ネイティブ抽出を使用する主な理由は、速度と大量の文書処理のためです。さらに、PDFに画像が含まれていないことが分かっている場合、OCRは何の利益ももたらしません!しかし、現実世界では多くの文書が古い文書のスキャン表現や、意図的に平坦化または「焼き込み」されてページの画像形式を提示するPDFです。

  • 典型的なシナリオ:

    • デジタル作成文書(Wordエクスポート、生成されたレポート)

    • 速度が重要な大量処理

    • 完璧な精度が必要な場合

    • きれいで構造化されたビジネス文書

  • ネイティブが機能しないことを示す警告サイン:

    • スキャンされた文書

    • 写真から作成されたPDF

    • テキストを含む埋め込み画像がある文書

コードサンプル

import pymupdf

doc = pymupdf.open("a.pdf") # open a document
out = open("output.txt", "wb") # create a text output
for page in doc: # iterate the document pages
    text = page.get_text().encode("utf8") # get plain text (is in UTF-8)
    out.write(text) # write text of page
    out.write(bytes((12,))) # write page delimiter (form feed 0x0C)
out.close()

OCRを使用する場合

スキャンされた文書を扱っていることが分かっている場合、これは当然の選択であり、テキストコンテンツを抽出するためにOCRに頼る必要があります!

  • 典型的なシナリオ:

    • スキャンされた文書やファックス

    • 写真から作成されたPDF

    • 歴史的文書

    • 混合コンテンツ(ネイティブテキスト + テキスト付き画像)

    • PDF内の画像からテキストを抽出する必要がある場合

  • 品質に関する考慮事項:

    • 解像度要件(最低300 DPI)

    • きれいな素材対劣化した素材

    • 言語とフォントの考慮事項

コードサンプル

import pymupdf

doc = pymupdf.open("a.pdf") # open a document

for page in doc: # iterate the document pages
    textPage = page.get_textpage_ocr()
    # analyse the text page as required!

out.close()

ハイブリッドアプローチ:両方の世界の最良を得る

以下は、PDFデータ抽出を最大限に活用するためのガイドラインです。

スマート戦略

データ抽出をより堅牢にするために、まずネイティブテキスト抽出を試してからOCRを試すことをお勧めします。例えば、文書内の特定の情報フィールドを探していて、ネイティブテキスト抽出で見つからない場合は、文書をPyMuPDFに戻してOCRパスを実行します。

検出方法

大量の文書を扱う場合は、文書をタイプ別にフィルタリングし、異なるパイプラインで処理してみてください。例えば、PDFから画像数を取得し、非常に画像が多いことが分かった場合は、直接OCRパイプラインに送ることを検討してください。他の検出方法には文書サイズがあります(小さなサイズのPDFには画像がほとんどまたは全くないと考えられます)。

OCRパイプラインの警告サイン:

  • ページが完全に画像で覆われている

  • ページにテキストが存在しない

  • 数千の小さなベクターグラフィック(シミュレートされたテキストを示す)

実装ワークフロー

  • ステップ1: 文書タイプをフィルタリング(セット1:画像の少ない小さなサイズのPDF、セット2:多くの画像を含むより大きなPDFまたは完全にスキャンされた「焼き込み」PDF、つまりOCRの恩恵を受けることが以前に「検出」されたファイル)

  • ステップ2: セット1でネイティブ抽出を試行

  • ステップ3: 結果を評価(空、文字化け、または不完全なテキスト、これらを「セット2」文書に追加)

  • ステップ4: セット2文書を選択的にOCR処理

PyMuPDF APIはこのハイブリッドアプローチをサポートします。理想的には、完全な文書ページを分析し、どのページがOCRを必要とし、どのページが必要としないかを把握する必要があります - これは計算時間を削減するためです。例えば、100ページの文書がOCRを必要とすると検出したかもしれませんが、文書のページをさらに分析すると、30ページのみがOCRの恩恵を受けることが理解できます。この場合、どのページがOCRの恩恵を受けるかを選択的に決定したいと思います。以下のサンプルコードは、PyMuPDFを使用してPDFを分析し、大きな画像が検出されたページの要約を報告します。

コードサンプル

import pymupdf  # PyMuPDF
import os
from typing import List, Dict, Tuple

def analyze_images_in_pdf(pdf_path: str, size_threshold_mb: float = 1.0, 
                         dimension_threshold: Tuple[int, int] = (800, 600)) -> Dict:
    """
    Analyze a PDF document for large images on each page.
    
    Args:
        pdf_path (str): Path to the PDF file
        size_threshold_mb (float): Minimum file size in MB to consider an image "large"
        dimension_threshold (tuple): Minimum (width, height) to consider an image "large"
    
    Returns:
        dict: Analysis results containing image information for each page
    """
    
    try:
        doc = pymupdf.open(pdf_path)
        total_pages = len(doc)
        
        print(f"Analyzing {total_pages} pages in: {os.path.basename(pdf_path)}")
        print(f"Size threshold: {size_threshold_mb} MB")
        print(f"Dimension threshold: {dimension_threshold[0]}x{dimension_threshold[1]} pixels")
        print("-" * 60)
        
        results = {
            'pdf_path': pdf_path,
            'total_pages': total_pages,
            'size_threshold_mb': size_threshold_mb,
            'dimension_threshold': dimension_threshold,
            'pages_with_large_images': [],
            'summary': {
                'images': 0,
                'total_large_images': 0,
                'pages_with_large_images': 0,
                'total_image_size_mb': 0,
                'largest_image': None
            }
        }
        
        largest_image_size = 0
        
        # Analyze each page (limit to 100 pages as requested)
        pages_to_analyze = min(total_pages, 100)
        
        for page_num in range(pages_to_analyze):
            page = doc[page_num]
            page_info = {
                'page_number': page_num + 1,
                'images': [],
                'large_images': [],
                'total_images_on_page': 0,
                'large_images_count': 0
            }
            
            # Get all images on the page
            image_list = page.get_images()
            page_info['total_images_on_page'] = len(image_list)
            
            for img_index, img in enumerate(image_list):
                try:
                    # Extract image information
                    xref = img[0]  # xref number
                    pix = pymupdf.Pixmap(doc, xref)
                    
                    # Skip if image has alpha channel and convert if needed
                    if pix.alpha:
                        pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
                    
                    # Get image properties
                    width = pix.width
                    height = pix.height
                    image_size_bytes = len(pix.tobytes())
                    image_size_mb = image_size_bytes / (1024 * 1024)

                    print(f"Found image with size:{image_size_bytes} bytes")
                    
                    # Check if image meets "large" criteria
                    is_large_by_size = image_size_mb >= size_threshold_mb
                    is_large_by_dimensions = (width >= dimension_threshold[0] and 
                                            height >= dimension_threshold[1])
                    
                    if is_large_by_size or is_large_by_dimensions:
                        image_info = {
                            'image_index': img_index + 1,
                            'xref': xref,
                            'width': width,
                            'height': height,
                            'size_mb': round(image_size_mb, 2),
                            'size_bytes': image_size_bytes,
                            'colorspace': pix.colorspace.name if pix.colorspace else 'Unknown',
                            'reason_large': []
                        }
                        
                        if is_large_by_size:
                            image_info['reason_large'].append('Size')
                        if is_large_by_dimensions:
                            image_info['reason_large'].append('Dimensions')
                        
                        page_info['large_images'].append(image_info)
                        page_info['large_images_count'] += 1
                        results['summary']['total_large_images'] += 1
                        results['summary']['total_image_size_mb'] += image_size_mb
                        
                        # Track largest image
                        if image_size_mb > largest_image_size:
                            largest_image_size = image_size_mb
                            results['summary']['largest_image'] = {
                                'page': page_num + 1,
                                'size_mb': round(image_size_mb, 2),
                                'dimensions': f"{width}x{height}",
                                'xref': xref
                            }
                    
                    results['summary']['images'] += 1
                    pix = None  # Clean up
                    
                except Exception as e:
                    print(f"Error processing image {img_index + 1} on page {page_num + 1}: {e}")
                    continue
            
            # Only add pages that have large images
            if page_info['large_images_count'] > 0:
                results['pages_with_large_images'].append(page_info)
                results['summary']['pages_with_large_images'] += 1
            
            # Progress indicator
            if (page_num + 1) % 10 == 0:
                print(f"Processed {page_num + 1} pages...")
        
        doc.close()
        results['summary']['total_image_size_mb'] = round(results['summary']['total_image_size_mb'], 2)
        
        return results
        
    except Exception as e:
        print(f"Error analyzing PDF: {e}")
        return None

def print_analysis_results(results: Dict):
    """Print formatted analysis results."""
    
    if not results:
        print("No results to display.")
        return
    
    print("\n" + "="*60)
    print("PDF IMAGE ANALYSIS RESULTS")
    print("="*60)
    
    # Summary
    summary = results['summary']
    print(f"Total pages analyzed: {results['total_pages']}")
    print(f"Total images: {summary['images']}")
    print(f"Pages with large images: {summary['pages_with_large_images']}")
    print(f"Total large images found: {summary['total_large_images']}")
    print(f"Total size of large images: {summary['total_image_size_mb']} MB")
    
    if summary['largest_image']:
        largest = summary['largest_image']
        print(f"Largest image: {largest['size_mb']} MB ({largest['dimensions']}) on page {largest['page']}")
    
    print("\n" + "-"*60)
    print("DETAILED RESULTS BY PAGE")
    print("-"*60)
    
    # Detailed results
    for page_info in results['pages_with_large_images']:
        print(f"\nPage {page_info['page_number']}:")
        print(f"  Total images on page: {page_info['total_images_on_page']}")
        print(f"  Large images: {page_info['large_images_count']}")
        
        for img in page_info['large_images']:
            reasons = ", ".join(img['reason_large'])
            print(f"    Image {img['image_index']}: {img['width']}x{img['height']} pixels, "
                  f"{img['size_mb']} MB ({reasons})")

def save_analysis_to_file(results: Dict, output_file: str):
    """Save analysis results to a text file."""
    
    if not results:
        print("No results to save.")
        return
    
    with open(output_file, 'w') as f:
        f.write("PDF IMAGE ANALYSIS RESULTS\n")
        f.write("="*60 + "\n")
        f.write(f"PDF File: {results['pdf_path']}\n")
        f.write(f"Analysis Date: {pymupdf.Document().metadata.get('creationDate', 'Unknown')}\n")
        f.write(f"Size Threshold: {results['size_threshold_mb']} MB\n")
        f.write(f"Dimension Threshold: {results['dimension_threshold'][0]}x{results['dimension_threshold'][1]}\n\n")
        
        # Summary
        summary = results['summary']
        f.write("SUMMARY\n")
        f.write("-"*20 + "\n")
        f.write(f"Total pages analyzed: {results['total_pages']}\n")
        f.write(f"Pages with large images: {summary['pages_with_large_images']}\n")
        f.write(f"Total large images: {summary['total_large_images']}\n")
        f.write(f"Total size of large images: {summary['total_image_size_mb']} MB\n")
        
        if summary['largest_image']:
            largest = summary['largest_image']
            f.write(f"Largest image: {largest['size_mb']} MB ({largest['dimensions']}) on page {largest['page']}\n")
        
        # Detailed results
        f.write("\nDETAILED RESULTS\n")
        f.write("-"*20 + "\n")
        
        for page_info in results['pages_with_large_images']:
            f.write(f"\nPage {page_info['page_number']}:\n")
            f.write(f"  Total images: {page_info['total_images_on_page']}\n")
            f.write(f"  Large images: {page_info['large_images_count']}\n")
            
            for img in page_info['large_images']:
                reasons = ", ".join(img['reason_large'])
                f.write(f"    Image {img['image_index']}: {img['width']}x{img['height']} px, "
                        f"{img['size_mb']} MB, {img['colorspace']} ({reasons})\n")
    
    print(f"Analysis results saved to: {output_file}")

# Example usage
if __name__ == "__main__":
    # Replace with your PDF file path
    pdf_file = "test.pdf"
    
    # Customize thresholds as needed
    size_threshold = 1.0  # MB
    dimension_threshold = (800, 600)  # width x height pixels
    
    # Run analysis
    results = analyze_images_in_pdf(
        pdf_path=pdf_file,
        size_threshold_mb=size_threshold,
        dimension_threshold=dimension_threshold
    )

    if results:
        # Print results to console
        print_analysis_results(results)

        # Optionally save to file
        output_file = f"image_analysis_{os.path.splitext(os.path.basename(pdf_file))[0]}.txt"
        save_analysis_to_file(results, output_file)
    else:
        print("Analysis failed. Please check your PDF file path and try again.")

実世界のシナリオ

テキスト抽出戦略のための文書フィルタリングの利点は、混合文書タイプを扱う金融サービスや、典型的にデジタル化された歴史的ケースファイルを持つ法律事務所で時間と処理を節約できます。さらに、多くの学術論文には重要なデータを含むグラフィカルな可視化が含まれている場合があります。これらすべてのケースで、必要に応じてページでOCRを実行するスマート戦略を使用することは理にかなっています。

ドキュメンテーションとビデオ

YouTube: 高度なPyMuPDFテキスト抽出技術 | 完全チュートリアル

コミュニティに参加してください!

PyMuPDF logo

日本語フォーラム

2025年9月4日18:00-18:45時のPyMuPDFウェビナーにサインアップしませんか?

Discussion