附录 1:文本提取详情#

本章提供了 PyMuPDF 文本提取方法的背景信息。

相关信息包括

  • 它们提供了什么?

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

TextPage 的一般结构#

TextPage 是 (Py-) MuPDF 的类之一。它通常在 Page 文本提取方法使用时,在幕后创建(并销毁),但它也可直接使用,并作为持久对象。与其名称所暗示的不同,图像也可以选择作为文本页的一部分。

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

一个文本页由块(大致相当于段落)组成。

一个由行及其字符或一个图像组成。

一条由 span 组成。

一个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)

其中前 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)]

#

函数 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 输出质量#

虽然 HTML 输出在 MuPDF v1.12.0 中得到了很大改进,但尚未完全没有 bug:我们在字体支持图像定位方面发现了问题。

  • HTML 文本包含对原始文档所用字体的引用。如果浏览器不认识这些字体(可能性很大!),它将用其他字体替换它们;结果看起来可能会很别扭。这个问题因浏览器而异——在我的 Windows 机器上,MS Edge 工作正常,而 Firefox 看起来很糟糕。

  • 对于结构复杂的 PDF,图像可能无法正确定位和/或调整大小。旋转的页面以及各种可能的页面 bbox 变体不一致的页面(例如 MediaBox != CropBox)似乎存在这种情况。我们尚不知道如何解决这个问题——我们已向 MuPDF 的网站提交了一个 bug。

为了解决字体问题,您可以使用一个简单的实用脚本扫描 HTML 文件并替换字体引用。这里有一个小示例,它将所有字体替换为 PDF Base 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 的结构,并为每个块、行和 span 提供了图像内容和位置详细信息(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 的信息超集,将详细程度再深入一步。它看起来与上面完全一样,只是 span 中的 “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 的 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。您现在可以轻松修改默认标志,例如:

    • 在“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

使用 CID 而不是 U+FFFD

1

1

1

1

1

1

1

1

0

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

  • “json” 的处理方式与 “dict” 完全相同,因此省略了。

  • “rawjson” 的处理方式与 “rawdict” 完全相同,因此省略了。

  • “不适用”表示值为 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 页,使用默认 flags 设置,所有提取方法总共需要 160 秒。当所有图像都被排除时,所需时间不到一半(77 秒)。

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

下表显示了平均相对速度(“RSpeed”,基准 1.00 是 TEXT),这是根据约 1400 页文本密集型和 1300 页图像密集型页面计算的。

方法

相对速度

备注

无图像

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 图像,span 级文本,无布局信息

1.00

HTML

3.54

base64 图像,span 级文本,布局和字体详情

1.01

DICT

3.93

二进制图像,span 级文本,布局和字体详情

1.04

RAWDICT

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 获取更多信息。