付録1: テキスト抽出の詳細#

この章では、PyMuPDFのテキスト抽出メソッドに関する背景情報を提供します。

興味のある情報は以下です

  • 彼らは何を提供するのか?

  • それらは何を意味するのか(処理時間 / データサイズ)?

TextPageの一般的な構造#

TextPage (テキストページ) は(Py-)MuPDFのクラスの一つです。通常、Page (ページ) のテキスト抽出メソッドが使用されるときにカーテンの後ろで作成され(および破棄され)ますが、直接利用することもでき、永続オブジェクトとして使用することができます。その名前が示すよりも、テキストページにはオプションで画像も含まれる場合があります:

<page>
    <text block>
        <line>
            <span>
                <char>
    <image block>
        <img>

テキストページ は、ブロック(おおよそ段落)で構成されています。

ブロック は、行とその文字、または画像のいずれかから成り立っています。

は、スパンから成り立っています。

スパン は、同一のフォントプロパティ(名前、サイズ、フラグ、色)を持つ隣接する文字から成り立っています。

プレーンテキスト#

関数 TextPage.extractText() (または Page.get_text("text") )は、ドキュメントの作成者によって指定された元の順序で、ページのプレーンテキストを抽出します。

例の出力:

>>> print(page.get_text("text"))
Some text on first page.

注釈

出力は通常の「自然な」読み順と一致しない場合があります。ただし、page.get_text("text", sort=True) を実行することで、「左上から右下」のスキームに従った並べ替えを要求することができます。

ブロック#

関数 TextPage.extractBLOCKS() (または Page.get_text("blocks") )は、ページのテキストブロックを以下のような項目のリストとして抽出します:

(x0, y0, x1, y1, "lines in block", block_no, block_type)

最初の4つの項目は、ブロックのバウンディングボックスの浮動小数点座標です。各ブロック内の行は改行文字で連結されます。

これは高速なメソッドであり、デフォルトでは画像のメタ情報も抽出されます。各画像はメタ情報を含む1行のテキスト行で表されるブロックとして表示されます。画像そのものは表示されません。

前述の単純なテキスト出力と同様に、sort 引数を使用して読み順を取得することもできます。

例の出力:

>>> print(page.get_text("blocks", sort=False))
[(50.0, 88.17500305175781, 166.1709747314453, 103.28900146484375,
'Some text on first page.', 0, 0)]

単語#

関数 TextPage.extractWORDS() (または Page.get_text("words") )は、ページのテキスト単語を以下のような項目のリストとして抽出します:

(x0, y0, x1, y1, "word", block_no, line_no, word_no)

最初の4つの項目は、単語のバウンディングボックスの浮動小数点座標です。最後の3つの整数は、単語の位置に関する追加情報を提供します

これは高速なメソッドです。前のメソッドと同様に、引数 sort=True を使用すると単語が再並べ替えされます。

例の出力:

>>> for word in page.get_text("words", sort=False):
        print(word)
(50.0, 88.17500305175781, 78.73200225830078, 103.28900146484375,
'Some', 0, 0, 0)
(81.79000091552734, 88.17500305175781, 99.5219955444336, 103.28900146484375,
'text', 0, 0, 1)
(102.57999420166016, 88.17500305175781, 114.8119888305664, 103.28900146484375,
'on', 0, 0, 2)
(117.86998748779297, 88.17500305175781, 135.5909881591797, 103.28900146484375,
'first', 0, 0, 3)
(138.64898681640625, 88.17500305175781, 166.1709747314453, 103.28900146484375,
'page.', 0, 0, 4)

HTML#

TextPage.extractHTML() (または Page.get_text("html") の出力は、ページの TextPage (テキストページ) の構造を完全に反映します。これは、以下のDICT / JSONのようなものです。これには画像、フォント情報、テキスト位置が含まれます。HTMLヘッダーとトレイラーコードで囲むと、インターネットブラウザで簡単に表示できます。上記の例:

>>> for line in page.get_text("html").splitlines():
        print(line)

<div id="page0" style="position:relative;width:300pt;height:350pt;
background-color:white">
<p style="position:absolute;white-space:pre;margin:0;padding:0;top:88pt;
left:50pt"><span style="font-family:Helvetica,sans-serif;
font-size:11pt">Some text on first page.</span></p>
</div>

HTML出力の品質の制御#

MuPDF v1.12.0でHTML出力はかなり改善されましたが、まだバグがないわけではありません。フォントサポート や**画像の配置** に関する問題が見つかっています。

  • HTMLテキストには元のドキュメントで使用されたフォントへの参照が含まれています。もしブラウザがそれらを認識できない場合(少ない確率ですが)、他のフォントで置き換えられ、結果が奇妙に見えるかもしれません。この問題はブラウザによって大きく異なります。Windowsマシンでは、MS Edgeはうまく動作するかもしれませんが、Firefoxはひどく見えるかもしれません。

  • 複雑な構造を持つPDFの場合、画像の位置やサイズが正しく配置されないことがあります。これは回転したページや、さまざまなページbboxのバリアントが一致しない場合に起こる可能性があります(たとえば、MediaBox != CropBox )。これに対処する方法はまだわかっていませんが、MuPDFのサイトにバグを報告しました。

フォントの問題に対処するために、HTMLファイルをスキャンし、フォントの参照を置換するシンプルなユーティリティスクリプトを使用できます。以下は、すべてのフォントをPDFのベース14フォントの一つに置き換える例です:セリフフォントは「Times」になり、セリフのないフォントは「Helvetica」になり、等幅フォントは「Courier」になります。太字、斜体などの各バリエーションは、おそらくブラウザによって正しく処理されるでしょう。

import sys
filename = sys.argv[1]
otext = open(filename).read()                 # original html text string
pos1 = 0                                      # search start poition
font_serif = "font-family:Times"              # enter ...
font_sans  = "font-family:Helvetica"          # ... your choices ...
font_mono  = "font-family:Courier"            # ... here
found_one  = False                            # true if search successful

while True:
    pos0 = otext.find("font-family:", pos1)   # start of a font spec
    if pos0 < 0:                              # none found - we are done
        break
    pos1 = otext.find(";", pos0)              # end of font spec
    test = otext[pos0 : pos1]                 # complete font spec string
    testn = ""                                # the new font spec string
    if test.endswith(",serif"):               # font with serifs?
        testn = font_serif                    # use Times instead
    elif test.endswith(",sans-serif"):        # sans serifs font?
        testn = font_sans                     # use Helvetica
    elif test.endswith(",monospace"):         # monospaced font?
        testn = font_mono                     # becomes Courier

    if testn != "":                           # any of the above found?
        otext = otext.replace(test, testn)    # change the source
        found_one = True
        pos1 = 0                              # start over

if found_one:
    ofile = open(filename + ".html", "w")
    ofile.write(otext)
    ofile.close()
else:
    print("Warning: could not find any font specs!")

DICT(またはJSON)#

TextPage.extractDICT() (または Page.get_text("dict", sort=False) )の出力は、 TextPage の構造を完全に反映し、各ブロック、行、スパンのために画像の内容と位置の詳細( bbox – ピクセル単位の境界ボックス)を提供します。画像はDICT出力では バイト として格納され、JSON出力ではbase64エンコードされた文字列として格納されます。

辞書の構造の可視化については、辞書出力の構造をご覧ください。

以下がその様子です:

{
    "width": 300.0,
    "height": 350.0,
    "blocks": [{
        "type": 0,
        "bbox": (50.0, 88.17500305175781, 166.1709747314453, 103.28900146484375),
        "lines": ({
            "wmode": 0,
            "dir": (1.0, 0.0),
            "bbox": (50.0, 88.17500305175781, 166.1709747314453, 103.28900146484375),
            "spans": ({
                "size": 11.0,
                "flags": 0,
                "font": "Helvetica",
                "color": 0,
                "origin": (50.0, 100.0),
                "text": "Some text on first page.",
                "bbox": (50.0, 88.17500305175781, 166.1709747314453, 103.28900146484375)
            })
        }]
    }]
}

RAWDICT(またはRAWJSON)#

TextPage.extractRAWDICT() (または Page.get_text("rawdict", sort=False) )は、DICTの情報のスーパーセット であり、詳細レベルを一段階深くします。これは上記のように見えますが、スパン内の 「text」 アイテム(文字列)は 「chars」 というリストに置き換えられます。各 「chars」 エントリは文字の dict です。例えば、 「Text in black color.」 の代わりに以下のような項目が表示されます:

"chars": [{
    "origin": (50.0, 100.0),
    "bbox": (50.0, 88.17500305175781, 57.336997985839844, 103.28900146484375),
    "c": "S"
}, {
    "origin": (57.33700180053711, 100.0),
    "bbox": (57.33700180053711, 88.17500305175781, 63.4530029296875, 103.28900146484375),
    "c": "o"
}, {
    "origin": (63.4530029296875, 100.0),
    "bbox": (63.4530029296875, 88.17500305175781, 72.61600494384766, 103.28900146484375),
    "c": "m"
}, {
    "origin": (72.61600494384766, 100.0),
    "bbox": (72.61600494384766, 88.17500305175781, 78.73200225830078, 103.28900146484375),
    "c": "e"
}, {
    "origin": (78.73200225830078, 100.0),
    "bbox": (78.73200225830078, 88.17500305175781, 81.79000091552734, 103.28900146484375),
    "c": " "
< ... deleted ... >
}, {
    "origin": (163.11297607421875, 100.0),
    "bbox": (163.11297607421875, 88.17500305175781, 166.1709747314453, 103.28900146484375),
    "c": "."
}],

XML#

TextPage.extractXML() (または Page.get_text("xml") バージョンは、RAWDICTの詳細レベルでテキスト(画像なし)を抽出します:

>>> for line in page.get_text("xml").splitlines():
    print(line)

<page id="page0" width="300" height="350">
<block bbox="50 88.175 166.17098 103.289">
<line bbox="50 88.175 166.17098 103.289" wmode="0" dir="1 0">
<font name="Helvetica" size="11">
<char quad="50 88.175 57.336999 88.175 50 103.289 57.336999 103.289" x="50"
y="100" color="#000000" c="S"/>
<char quad="57.337 88.175 63.453004 88.175 57.337 103.289 63.453004 103.289" x="57.337"
y="100" color="#000000" c="o"/>
<char quad="63.453004 88.175 72.616008 88.175 63.453004 103.289 72.616008 103.289" x="63.453004"
y="100" color="#000000" c="m"/>
<char quad="72.616008 88.175 78.732 88.175 72.616008 103.289 78.732 103.289" x="72.616008"
y="100" color="#000000" c="e"/>
<char quad="78.732 88.175 81.79 88.175 78.732 103.289 81.79 103.289" x="78.732"
y="100" color="#000000" c=" "/>

... deleted ...

<char quad="163.11298 88.175 166.17098 88.175 163.11298 103.289 166.17098 103.289" x="163.11298"
y="100" color="#000000" c="."/>
</font>
</line>
</block>
</page>

注釈

この出力を解釈するためにlxmlを使用して正常にテストしました。

XHTML#

TextPage.extractXHTML() (または Page.get_text("xhtml") は、テキストと画像を含むHTML形式のTEXTのバリエーションです(「セマンティック」出力):

<div id="page0">
<p>Some text on first page.</p>
</div>

テキスト抽出フラグのデフォルト値#

  • バージョン1.16.2で新しく追加されたメソッド Page.get_text() は、抽出されるデータの量と品質を制御するためのキーワードパラメータ flags (整数)をサポートしています。以下の表は、各抽出バリエーションのデフォルト設定( flags パラメータが省略されたかNoneの場合)を示しています。 None 以外の値でflagsを指定する場合は、すべての必要なオプション を設定する必要があることに注意してください。各ビット設定の説明は「テキスト抽出フラグ」で確認できます。

  • バージョン1.19.6で新しく追加された変更:次の表のデフォルトの組み合わせは、Pythonの定数として利用可能です: TEXTFLAGS_TEXTTEXTFLAGS_WORDSTEXTFLAGS_BLOCKSTEXTFLAGS_DICTTEXTFLAGS_RAWDICTTEXTFLAGS_HTMLTEXTFLAGS_XHTMLTEXTFLAGS_XMLTEXTFLAGS_SEARCH 。これにより、デフォルトのフラグを簡単に変更できます。例えば、

    • 「blocks」出力に画像を 含める 場合:

    flags = TEXTFLAGS_BLOCKS | TEXT_PRESERVE_IMAGES

    • 「dict」出力から画像を 除外する 場合:

    flags = TEXTFLAGS_DICT & ~TEXT_PRESERVE_IMAGES

    • テキスト検索での ハイフネーション をオフに設定する:

    flags = TEXTFLAGS_SEARCH & ~TEXT_DEHYPHENATE

指標

text

html

xhtml

xml

dict

rawdict

words

blocks

search

連結を保持

1

1

1

1

1

1

1

1

1

空白を保持

1

1

1

1

1

1

1

1

1

画像を保持

n/a

1

1

n/a

1

1

n/a

0

0

スペースの抑制

0

0

0

0

0

0

0

0

0

ハイフネーション解除

0

0

0

0

0

0

0

0

1

メディアボックスにクリップ

1

1

1

1

1

1

1

1

1

use CID instead of U+FFFD

1

1

1

1

1

1

1

1

0

  • 検索 はテキスト検索機能を指します。

  • 「json」「dict」 とまったく同様に処理されるため、省略されています。

  • 「rawjson」「rawdict」 とまったく同様に処理されるため、省略されています。

  • 「n/a」の指定は値が0であり、このビットを設定しても出力に影響を与えることはありません(ただしパフォーマンスに悪影響を及ぼす可能性があります)。

  • 画像を含む出力バリアントを使用する際に画像に興味がない場合、必ず該当するビットをオフに設定してください。これにより、パフォーマンスが向上し、スペース要件が大幅に削減されます。

To show the effect of TEXT_INHIBIT_SPACES have a look at this example:

>>> print(page.get_text("text"))
H a l l o !
Mo r e  t e x t
i s  f o l l o w i n g
i n  E n g l i s h
. . .  l e t ' s  s e e
w h a t  h a p p e n s .
>>> print(page.get_text("text", flags=pymupdf.TEXT_INHIBIT_SPACES))
Hallo!
More text
is following
in English
... let's see
what happens.
>>>

パフォーマンス#

テキスト抽出メソッドは、情報の提供方法とリソース要件、実行時間の両方で大きく異なります。一般的に、情報が多いほど処理が必要であり、より多くのデータが生成されることを意味します。

注釈

特に画像は 非常に大きな 影響を持ちます。必要のない場合は、必ず画像を除外する(フラグパラメータを使用)ようにしてください。以下で言及されている2,700ページの総ページ数をデフォルトのフラグ設定で処理するには、全ての抽出メソッドで160秒が必要でした。画像をすべて除外した場合、その時間の50%未満(77秒)が必要でした。

まず始めに、すべてのメソッドは市場にある他の製品と比べて 非常に高速 です。処理速度の観点から、より速い(無料の)ツールは私たちの知る限り存在しません。最も詳細なメソッドであるRAWDICTでも、 Adobe PDFリファレンス リファレンスの1,310ページを5秒未満で処理できます(ここでは簡単なテキストは2秒未満で処理されます)。

以下の表は、約1400ページのテキストが多く、約1300ページが画像が多いページでの平均相対速度(ベースライン1.00はTEXT)を示しています。

メソッド

平均相対

コメント

画像なし

TEXT

1.00

画像なし、 プレーン テキスト、改行

1.00

ブロック

1.00

画像のバウンディングボックス(のみ)、 ブロック レベルのテキストとバウンディングボックス、改行

1.00

単語

1.02

画像なし、 ワード レベルのテキストとバウンディングボックス

1.02

XML

2.72

画像なし、文字 レベルのテキスト、レイアウトとフォントの詳細

2.72

XHTML

3.32

base64 画像、 スパン レベルのテキスト、レイアウト情報なし

1.00

HTML

3.54

base64画像スパン レベルのテキスト、レイアウトとフォントの詳細

1.01

DICT

3.93

バイナリ 画像、 スパン レベルのテキスト、レイアウトとフォントの詳細

1.04

RAWDICT

4.50

バイナリ 画像、文字 レベルのテキスト、レイアウトとフォントの詳細

1.68

前述のように、画像の抽出を除外する場合(最後の列)、相対速度は大きく変わります。RAWDICTとXMLを除いて、他のメソッドはほぼ同じ速さであり、RAWDICTは 今では遅いXML よりも40%少ない実行時間を必要とします。

もっとパフォーマンス情報については、 付録1章 をご覧ください。


This software is provided AS-IS with no warranty, either express or implied. This software is distributed under license and may not be copied, modified or distributed except as expressly authorized under the terms of that license. Refer to licensing information at artifex.com or contact Artifex Software Inc., 39 Mesa Street, Suite 108A, San Francisco CA 94129, United States for further information.

Discord logo