부록 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) 를 실행하여 “왼쪽 위에서 오른쪽 아래로” 체계에 따라 재정렬을 요청할 수 있습니다.

BLOCKS

함수 TextPage.extractBLOCKS() (또는 Page.get_text(“blocks”)) 는 페이지의 텍스트 블록을 다음과 같은 항목 목록으로 추출합니다:

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

여기서 처음 4개 항목은 블록의 bbox의 부동 소수점 좌표입니다. 각 블록 내의 행은 줄바꿈 문자로 연결됩니다.

이것은 고속 메서드이며, 기본적으로 이미지 메타 정보도 추출합니다: 각 이미지는 메타 정보를 포함하는 하나의 텍스트 행이 있는 블록으로 나타납니다. 이미지 자체는 표시되지 않습니다.

위의 간단한 텍스트 출력과 마찬가지로 sort 인수를 사용하여 읽기 순서를 얻을 수 있습니다.

예제 출력:

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

WORDS

함수 TextPage.extractWORDS() (또는 Page.get_text(“words”)) 는 페이지의 텍스트 단어 를 다음과 같은 항목 목록으로 추출합니다:

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

여기서 처음 4개 항목은 단어의 bbox의 부동 소수점 좌표입니다. 마지막 세 정수는 단어의 위치에 대한 추가 정보를 제공합니다.

이것은 고속 메서드입니다. 이전 메서드와 마찬가지로 인수 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 Base 14 글꼴 중 하나로 교체하는 작은 예제입니다: 세리프 글꼴은 “Times”가 되고, 비세리프 글꼴은 “Helvetica”가 되며, 고정폭 글꼴은 “Courier”가 됩니다. “bold”, “italic” 등에 대한 각각의 변형은 브라우저에서 올바르게 처리되기를 바랍니다:

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 출력의 경우 bytes 로 저장되고 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” 항목(string)이 “chars” 목록으로 대체됩니다. 각 “chars” 항목은 문자 dict 입니다. 예를 들어, 위의 “text”: “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”)) 는 TEXT의 변형이지만 HTML 형식이며, 일반 텍스트와 이미지를 포함합니다(“의미론적” 출력):

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

텍스트 추출 플래그 기본값

  • 버전 1.16.2의 새로운 기능: 메서드 Page.get_text() 는 추출된 데이터의 양과 품질을 제어하는 키워드 매개변수 flags (int) 를 지원합니다. 다음 표는 각 추출 변형에 대한 기본 설정(플래그 매개변수 생략 또는 None)을 보여줍니다. None 이 아닌 값으로 플래그를 지정하는 경우 모든 원하는 옵션을 설정해야 합니다. 각 비트 설정에 대한 설명은 글꼴 속성 에서 찾을 수 있습니다.

  • v1.19.6의 새로운 기능: 다음 표의 기본 조합은 이제 Python 상수로 사용할 수 있습니다: TEXTFLAGS_TEXT, TEXTFLAGS_WORDS, TEXTFLAGS_BLOCKS, TEXTFLAGS_DICT, TEXTFLAGS_RAWDICT, TEXTFLAGS_HTML, TEXTFLAGS_XHTML, TEXTFLAGS_XML, 및 TEXTFLAGS_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

0

공백 유지

1

1

1

1

1

1

1

1

1

이미지 유지

해당 없음

1

1

해당 없음

1

1

해당 없음

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

U+FFFD 대신 CID 사용

1

1

1

1

1

1

1

1

0

  • search 는 텍스트 검색 함수를 나타냅니다.

  • “json”“dict” 와 정확히 동일하게 처리되므로 생략되었습니다.

  • “rawjson”“rawdict” 와 정확히 동일하게 처리되므로 생략되었습니다.

  • “n/a” 지정은 값이 0임을 의미하며 이 비트를 설정해도 출력에 영향을 주지 않습니다(하지만 성능에 부정적인 영향을 미칩니다).

  • 기본적으로 이미지를 포함하는 출력 변형을 사용할 때 이미지에 관심이 없다면, 반드시 해당 비트를 끄세요: 더 나은 성능과 훨씬 낮은 공간 요구 사항을 경험할 수 있습니다.

TEXT_INHIBIT_SPACES 의 효과를 보려면 이 예제를 참조하세요:

>>> 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.
>>>

성능

텍스트 추출 메서드는 제공하는 정보와 리소스 요구 사항 및 실행 시간 측면에서 모두 크게 다릅니다. 일반적으로 더 많은 정보는 더 많은 처리가 필요하고 더 높은 데이터 볼륨이 생성됨을 의미합니다.

참고

특히 이미지는 매우 큰 영향을 미칩니다. 필요하지 않을 때는 반드시 제외하세요(flags 매개변수를 통해). 기본 플래그 설정으로 아래에 언급된 총 2,700페이지를 처리하는 데 모든 추출 메서드에서 160초가 필요했습니다. 모든 이미지가 제외되면 그 시간의 50% 미만(77초)이 필요했습니다.

우선, 모든 메서드는 시장의 다른 제품과 비교하여 매우 빠릅니다. 처리 속도 측면에서 더 빠른(무료) 도구는 알지 못합니다. 가장 상세한 메서드인 RAWDICT조차 Adobe PDF 참조 의 모든 1,310페이지를 5초 미만으로 처리합니다(여기서는 간단한 텍스트가 2초 미만이 필요합니다).

다음 표는 약 1400페이지의 텍스트 중심 페이지와 1300페이지의 이미지 중심 페이지에서 가져온 평균 상대 속도(“RSpeed”, 기준선 1.00은 TEXT)를 보여줍니다.

메서드

RSpeed

설명

이미지 없음

TEXT

1.00

이미지 없음, 일반 텍스트, 줄바꿈

1.00

BLOCKS

1.00

이미지 bbox(만), 블록 수준 텍스트와 bbox, 줄바꿈

1.00

WORDS

1.02

이미지 없음, 단어 수준 텍스트와 bbox

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.