附录 1:文本提取的详细信息#

本章提供了关于PyMuPDF文本提取方法的背景知识。

感兴趣的信息是

  • 他们提供什么?

  • 它们意味着什么(处理时间/数据大小)?

文本页面的一般结构#

TextPage 是 (Py-) MuPDF 的类之一。它通常在使用 Page 文本提取方法时后台创建(和销毁),但也可以直接使用并作为持久化对象。除了它的名字所暗示的,图像也可以作为文本页的一部分:

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

A 文本页面 由块(= 大致相当于段落)组成。

A 由行及其字符或一张图片组成。

A line 由跨度组成。

A span 由具有相同字体属性的相邻字符组成:名称、大小、标志和颜色。

纯文本#

函数 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)

前四项是块的边界框的浮点坐标。每个块中的行由换行符连接。

这是一个高速方法,默认情况下还会提取图像元信息:每个图像作为一个块出现,并包含一行文本,其中包含元信息。图像本身不显示。

与上面的简单文本输出一样,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”)) 提取页面的文本 words 作为一个项目列表,如:

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

前四个条目是单词边界框的浮动坐标。最后三个整数提供了有关单词位置的更多信息。

这是一个高速方法。与之前的方法一样,参数 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的网站上提交了一个bug。

要解决字体问题,您可以使用一个简单的实用程序脚本扫描HTML文件并替换字体引用。这里有一个小例子,替换所有字体为PDF Base 14 Fonts之一:衬线字体将变为“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!")

字典(或 JSON)#

TextPage.extractDICT()(或 Page.get_text(“dict”, sort=False))的输出完全反映了TextPage的结构,并为每个块、行和跨度提供了图像内容和位置信息(bbox – 像素单位的边界框)。图像以bytes格式存储用于DICT输出,以base64编码的字符串形式存储用于JSON输出。

要查看字典结构的可视化,请查看 字典输出的结构

这看起来是这样的:

{
    "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)
            })
        }]
    }]
}

原始字典 (或 原始 JSON)#

TextPage.extractRAWDICT() (或 Page.get_text(“rawdict”, sort=False)) 是一个 DICT 的信息超集,并且详细级别更深入一步。它的外观与上面完全相同,除了 “text” 项 (string) 在 spans 中被列表 “chars” 替代。每个 “chars” 条目是一个字符 dict。例如,这里是您将在上面的 “text”: “黑色文字。” 项的位置看到的内容:

"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 的 flags 参数)。如果您指定的 flags 的值不是 None,请注意您必须设置 所有期望的 选项。各位设置的描述可以在 文本提取标志 中找到。

  • 在v1.19.6中:下表中的默认组合现在可作为Python常量使用: TEXTFLAGS_TEXT, TEXTFLAGS_WORDS, TEXTFLAGS_BLOCKS, TEXTFLAGS_DICT, TEXTFLAGS_RAWDICT, TEXTFLAGS_HTML, TEXTFLAGS_XHTML, TEXTFLAGS_XML, 和 TEXTFLAGS_SEARCH。您现在可以轻松修改默认标志,例如:

    • “块” 输出中包含图像:

    flags = TEXTFLAGS_BLOCKS | TEXT_PRESERVE_IMAGES

    • 从“dict”输出中排除 图像:

    flags = TEXTFLAGS_DICT & ~TEXT_PRESERVE_IMAGES

    • 在文本搜索中设置 去掉连字符

    flags = TEXTFLAGS_SEARCH & ~TEXT_DEHYPHENATE

指标

文本

html

xhtml

xml

字典

原始字典

单词

搜索

保留连字

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

0

1

剪切到媒体框

1

1

1

1

1

1

1

1

1

使用 CID 而不是 U+FFFD

1

1

1

1

1

1

1

1

0

  • 搜索 指的是文本搜索功能。

  • “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参数)。按照默认的标志设置处理以下提到的2700个总页面需要160秒的时间,而使用排除所有图像的情况下,所需时间不到50%(77秒)。

首先,所有方法在与市场上其他产品相比时,都是非常快的。在处理速度方面,我们不知道有比这更快的(免费)工具。即使是最详细的方法,RAWDICT,在不到5秒钟的时间内处理完全部1’310页的Adobe PDF References(简单文本在这里需要不到2秒钟)。

下表显示了平均相对速度(“RSpeed”,基线为 1.00 是文本),取自大约 1400 个以文本为主的页面和 1300 个以图像为主的页面。

方法

RSpeed

评论

没有图片

文本

1.00

无图像,文本,换行

1.00

区块

1.00

图像边界框(仅),区块级别文本带边界框,换行

1.00

单词

1.02

没有图像,单词级别的文本带有边界框

1.02

XML

2.72

没有图像,char 级别文本,布局和字体细节

2.72

XHTML

3.32

base64 图像, span 级别文本, 无布局信息

1.00

HTML

3.54

base64 图像, span 级别文本, 布局和字体细节

1.01

字典

3.93

二进制 图像, 跨度 级别文本, 布局和字体细节

1.04

原始字典

4.50

二进制 图像, 字符 级文本, 布局和字体细节

1.68

如前所述:当排除图像提取(最后一列)时,相对速度变化非常明显:除了RAWDICT和XML,其他方法几乎同样快速,而RAWDICT所需的执行时间比现在最慢的XML少40%。

有关更多性能信息,请查看章节 附录 1


本软件按原样提供,不作任何明示或暗示的担保。该软件根据许可证分发,除非按照该许可证的条款明确授权,否则不得复制、修改或分发。有关许可信息,请参阅artifex.com或联系Artifex Software Inc.,地址:39 Mesa Street, Suite 108A, San Francisco CA 94129, United States以获取更多信息。