文本#

如何提取所有文档文本#

此脚本将接收文档文件名并从其中提取所有文本,然后生成一个文本文件。

该文档可以是任意支持的类型

该脚本作为一个命令行工具运行,期望将文档文件名作为参数提供。它在脚本目录下生成一个名为“filename.txt”的文本文件。页面文本由换页符分隔。

import sys, pathlib, pymupdf
fname = sys.argv[1]  # get document filename
with pymupdf.open(fname) as doc:  # open document
    text = chr(12).join([page.get_text() for page in doc])
# write as a binary file to support non-ASCII characters
pathlib.Path(fname + ".txt").write_bytes(text.encode())

输出将是文档中编码的纯文本。不会以任何方式进行美化。特别是对于 PDF,这可能意味着输出不是通常的阅读顺序,会出现意外的换行等。

您有许多选项来纠正此问题 – 请参见附录 2:关于嵌入文件的考量一章。其中包括

  1. 以 HTML 格式提取文本并将其存储为 HTML 文档,以便在任何浏览器中查看。

  2. 通过 Page.get_text(“blocks”) 方法将文本提取为文本块列表。此列表中的每个项目都包含其文本的位置信息,可用于建立方便的阅读顺序。

  3. 通过 Page.get_text(“words”) 方法提取单个词语列表。其项目是带有位置信息的词语。使用它来确定给定矩形中包含的文本 – 请参阅下一节。

请参阅下面两个章节,了解示例和进一步的解释。

如何将文本提取为 Markdown#

这对于RAG/LLM环境尤其有用 – 请参阅输出为 Markdown

如何从页面提取键值对#

如果页面的布局在某种意义上是“可预测的”,那么有一种简单的方法可以快速轻松地找到给定关键字集的值 – 无需使用正则表达式。请参阅此示例脚本

在此上下文中,“可预测的”意味着

  • 每个关键字后面跟着其值 – 它们之间没有其他文本。

  • 值边界框的底部不高于关键字边界框的底部。

  • 没有其他限制:页面布局可能固定也可能不固定,文本也可能存储为一个字符串。键和值之间的距离可以任意。

例如,以下五个键值对将被正确识别

key1               value1
key2
value2
key3
       value3 blah, blah, blah key4 value4 some other text key5 value5 ...

如何从矩形内提取文本#

现在(v1.18.0)有不止一种方法可以实现这一点。因此,我们在 PyMuPDF-Utilities 仓库中专门创建了一个文件夹,专门处理此主题。


如何按自然阅读顺序提取文本#

PDF 文本提取的一个常见问题是,文本可能不按任何特定的阅读顺序出现。

这是 PDF 创建者(软件或人工)的责任。例如,页面页眉可能是在文档生成后单独插入的。在这种情况下,页眉文本将出现在页面文本提取的末尾(尽管 PDF 查看器软件会正确显示它)。例如,以下代码片段将为现有 PDF 添加一些页眉和页脚行

doc = pymupdf.open("some.pdf")
header = "Header"  # text in header
footer = "Page %i of %i"  # text in footer
for page in doc:
    page.insert_text((50, 50), header)  # insert header
    page.insert_text(  # insert footer 50 points above page bottom
        (50, page.rect.height - 50),
        footer % (page.number + 1, doc.page_count),
    )

以这种方式修改的页面提取的文本序列将如下所示

  1. 原始文本

  2. 页眉行

  3. 页脚行

PyMuPDF 有几种方法可以重新建立一些阅读顺序,甚至可以重新生成接近原始布局的内容

  1. 使用 Page.get_text()sort 参数。它会将输出从左上到右下排序(对于 XHTML、HTML 和 XML 输出会忽略此参数)。

  2. 在 CLI 中使用 pymupdf 模块:python -m pymupdf gettext ...,这将生成一个文本文件,其中文本已以保留布局模式重新排列。有许多选项可用于控制输出。

您也可以使用上面提到的脚本进行修改。


如何从文档中提取表格内容#

如果您在文档中看到表格,您通常不是在查看嵌入的 Excel 或其他可识别的对象。它通常只是普通的标准文本,格式化后看起来像表格数据。

因此,从这样的页面区域提取表格数据意味着您必须找到一种方法来识别表格区域(即其边界框),然后(1)图形化地指示表格和列边界,然后(2)根据此信息提取文本。

这可能是一个非常复杂的任务,取决于线条、矩形或其他辅助矢量图形是否存在等细节。

Page.find_tables() 方法为您完成所有这些工作,表格检测精度很高。它的巨大优势在于没有外部库依赖,也不需要使用人工智能或机器学习技术。它还提供了与知名 Python 数据分析包 pandas 的集成接口。

请查看示例 Jupyter notebooks,其中涵盖了单页多个表格或跨多页连接表格片段等标准情况。


如何标记提取的文本#

有一个标准的搜索函数用于在页面上搜索任意文本:Page.search_for()。它返回一个 Rect 对象列表,这些对象围绕着找到的匹配项。例如,这些矩形可用于自动插入注释,以可见地标记找到的文本。

此方法有优点也有缺点。优点是

  • 搜索字符串可以包含空格并跨行换行

  • 大小写字母被视为相等

  • 检测并解决了行尾的单词连字符

  • 返回值也可以是 Quad 对象的列表,用于精确查找不平行于任何轴的文本 – 当页面旋转不为零时,也建议使用 Quad 输出。

但您也有其他选项

import sys
import pymupdf

def mark_word(page, text):
    """Underline each word that contains 'text'.
    """
    found = 0
    wlist = page.get_text("words", delimiters=None)  # make the word list
    for w in wlist:  # scan through all words on page
        if text in w[4]:  # w[4] is the word's string
            found += 1  # count
            r = pymupdf.Rect(w[:4])  # make rect from word bbox
            page.add_underline_annot(r)  # underline
    return found

fname = sys.argv[1]  # filename
text = sys.argv[2]  # search string
doc = pymupdf.open(fname)

print("underlining words containing '%s' in document '%s'" % (word, doc.name))

new_doc = False  # indicator if anything found at all

for page in doc:  # scan through the pages
    found = mark_word(page, text)  # mark the page's words
    if found:  # if anything found ...
        new_doc = True
        print("found '%s' %i times on page %i" % (text, found, page.number + 1))

if new_doc:
    doc.save("marked-" + doc.name)

此脚本使用 Page.get_text("words") 通过命令行参数查找字符串。此方法使用空格作为分隔符将页面文本分成“词语”。进一步的说明

  • 如果找到,则包含该字符串的整个词语将被标记(加下划线)——而不仅仅是搜索字符串。

  • 搜索字符串中不能包含词语分隔符。默认情况下,词语分隔符是空格和不间断空格 chr(0xA0)。如果您使用额外的分隔符,例如 page.get_text("words", delimiters="./,"),则搜索字符串中也不应包含这些字符中的任何一个。

  • 如这里所示,会区分大小写。但这可以通过在函数 mark_word 中使用字符串方法 lower()(甚至正则表达式)来改变。

  • 没有上限:所有匹配项都将被检测到。

  • 您可以使用任何方法来标记词语:‘Underline’(下划线)、‘Highlight’(高亮)、‘StrikeThrough’(删除线)或‘Square’(方框)注释等。

  • 这是一个本手册页面片段的示例,其中“MuPDF”被用作搜索字符串。请注意,所有包含“MuPDF”的字符串都已完全加下划线(不仅仅是搜索字符串)。

_images/img-markedpdf.jpg

如何标记搜索到的文本#

此脚本搜索文本并进行标记

# -*- coding: utf-8 -*-
import pymupdf

# the document to annotate
doc = pymupdf.open("tilted-text.pdf")

# the text to be marked
needle = "¡La práctica hace el campeón!"

# work with first page only
page = doc[0]

# get list of text locations
# we use "quads", not rectangles because text may be tilted!
rl = page.search_for(needle, quads=True)

# mark all found quads with one annotation
page.add_squiggly_annot(rl)

# save to a new PDF
doc.save("a-squiggly.pdf")

结果如下所示

_images/img-textmarker.jpg

如何标记非水平文本#

上一节已经展示了一个标记非水平文本的示例,该文本是通过文本搜索检测到的。

但是,使用 Page.get_text() 的“dict”/“rawdict”选项进行文本提取时,也可能返回与 x 轴呈非零角度的文本。这由行字典的 "dir" 键的值指示:它是该角度的元组 (cosine, sine)。如果 line["dir"] != (1, 0),则其所有 spans 的文本都旋转了(相同的)非零角度。

然而,该方法返回的“bboxes”只是矩形 – 而不是四边形 (quads)。因此,要正确标记 span 文本,必须从行和 span 字典中包含的数据恢复其四边形 (quad)。使用以下实用函数(v1.18.9 新增)执行此操作

span_quad = pymupdf.recover_quad(line["dir"], span)
annot = page.add_highlight_annot(span_quad)  # this will mark the complete span text

如果您想一次性标记整行或其 spans 的子集,请使用以下代码片段(适用于 v1.18.10 或更高版本)

line_quad = pymupdf.recover_line_quad(line, spans=line["spans"][1:-1])
page.add_highlight_annot(line_quad)
_images/img-linequad.jpg

上面的 spans 参数可以指定 line["spans"] 的任意子列表。在上面的示例中,标记了从第二个到倒数第二个 span。如果省略,则标记整行。


如何分析字体特征#

要分析 PDF 中文本的特征,请使用这个基础脚本作为起点

import sys

import pymupdf


def flags_decomposer(flags):
    """Make font flags human readable."""
    l = []
    if flags & 2 ** 0:
        l.append("superscript")
    if flags & 2 ** 1:
        l.append("italic")
    if flags & 2 ** 2:
        l.append("serifed")
    else:
        l.append("sans")
    if flags & 2 ** 3:
        l.append("monospaced")
    else:
        l.append("proportional")
    if flags & 2 ** 4:
        l.append("bold")
    return ", ".join(l)


doc = pymupdf.open(sys.argv[1])
page = doc[0]

# read page text as a dictionary, suppressing extra spaces in CJK fonts
blocks = page.get_text("dict", flags=11)["blocks"]
for b in blocks:  # iterate through the text blocks
    for l in b["lines"]:  # iterate through the text lines
        for s in l["spans"]:  # iterate through the text spans
            print("")
            font_properties = "Font: '%s' (%s), size %g, color #%06x" % (
                s["font"],  # font name
                flags_decomposer(s["flags"]),  # readable font flags
                s["size"],  # font size
                s["color"],  # font color
            )
            print("Text: '%s'" % s["text"])  # simple print of text
            print(font_properties)

这是 PDF 页面和脚本输出

_images/img-pdftext.jpg

如何插入文本#

PyMuPDF 提供了在新的或现有 PDF 页面上插入文本的方法,具有以下特性

  • 选择字体,包括内置字体和可作为文件使用的字体

  • 选择文本特性,如粗体、斜体、字体大小、字体颜色等

  • 以多种方式定位文本

    • 作为从某个点开始的简单的面向行的输出,

    • 或将文本填充到作为矩形提供的框中,在这种情况下也可以选择文本对齐方式,

    • 选择文本是否应该放在前景(覆盖现有内容),

    • 所有文本都可以任意“变形”(morphed),即通过 Matrix 改变其外观,以实现缩放、剪切或镜像等效果,

    • 独立于变形,并且除此之外,文本可以旋转 90 度的整数倍。

以上所有功能都由三个基本的 PageShape 方法提供

注意

这两种文本插入方法都会根据需要自动安装字体。

如何写入文本行#

在页面上输出一些文本行

import pymupdf
doc = pymupdf.open(...)  # new or existing PDF
page = doc.new_page()  # new or existing page via doc[n]
p = pymupdf.Point(50, 72)  # start point of 1st line

text = "Some text,\nspread across\nseveral lines."
# the same result is achievable by
# text = ["Some text", "spread across", "several lines."]

rc = page.insert_text(p,  # bottom-left of 1st char
                     text,  # the text (honors '\n')
                     fontname = "helv",  # the default font
                     fontsize = 11,  # the default font size
                     rotate = 0,  # also available: 90, 180, 270
                     )
print("%i lines printed on page %i." % (rc, page.number))

doc.save("text.pdf")

使用此方法,只会控制行数不超过页面高度。多余的行将不会被写入,并返回实际写入的行数。计算使用的行高是根据 fontsize 和 36 点(0.5 英寸)的下边距计算得出的。

宽度会被忽略。一行的多余部分将简单地不可见。

但是,对于内置字体,有一些方法可以预先计算行宽 – 请参阅 get_text_length()

这是另一个示例。它使用四种不同的旋转选项插入 4 个文本字符串,并由此解释了如何选择文本插入点以达到所需结果

import pymupdf
doc = pymupdf.open()
page = doc.new_page()
# the text strings, each having 3 lines
text1 = "rotate=0\nLine 2\nLine 3"
text2 = "rotate=90\nLine 2\nLine 3"
text3 = "rotate=-90\nLine 2\nLine 3"
text4 = "rotate=180\nLine 2\nLine 3"
red = (1, 0, 0) # the color for the red dots
# the insertion points, each with a 25 pix distance from the corners
p1 = pymupdf.Point(25, 25)
p2 = pymupdf.Point(page.rect.width - 25, 25)
p3 = pymupdf.Point(25, page.rect.height - 25)
p4 = pymupdf.Point(page.rect.width - 25, page.rect.height - 25)
# create a Shape to draw on
shape = page.new_shape()

# draw the insertion points as red, filled dots
shape.draw_circle(p1,1)
shape.draw_circle(p2,1)
shape.draw_circle(p3,1)
shape.draw_circle(p4,1)
shape.finish(width=0.3, color=red, fill=red)

# insert the text strings
shape.insert_text(p1, text1)
shape.insert_text(p3, text2, rotate=90)
shape.insert_text(p2, text3, rotate=-90)
shape.insert_text(p4, text4, rotate=180)

# store our work to the page
shape.commit()
doc.save(...)

这是结果

_images/img-inserttext.jpg

如何填充文本框#

此脚本用文本填充 4 个不同的矩形,每次选择不同的旋转值

import pymupdf

doc = pymupdf.open()  # new or existing PDF
page = doc.new_page()  # new page, or choose doc[n]

# write in this overall area
rect = pymupdf.Rect(100, 100, 300, 150)

# partition the area in 4 equal sub-rectangles
CELLS = pymupdf.make_table(rect, cols=4, rows=1)

t1 = "text with rotate = 0."  # these texts we will written
t2 = "text with rotate = 90."
t3 = "text with rotate = 180."
t4 = "text with rotate = 270."
text = [t1, t2, t3, t4]
red = pymupdf.pdfcolor["red"]  # some colors
gold = pymupdf.pdfcolor["gold"]
blue = pymupdf.pdfcolor["blue"]
"""
We use a Shape object (something like a canvas) to output the text and
the rectangles surrounding it for demonstration.
"""
shape = page.new_shape()  # create Shape
for i in range(len(CELLS[0])):
    shape.draw_rect(CELLS[0][i])  # draw rectangle
    shape.insert_textbox(
        CELLS[0][i], text[i], fontname="hebo", color=blue, rotate=90 * i
    )

shape.finish(width=0.3, color=red, fill=gold)

shape.commit()  # write all stuff to the page
doc.ez_save(__file__.replace(".py", ".pdf"))

上面使用了一些默认值:字体大小 11 和文本对齐方式“左对齐”。结果将如下所示

_images/img-rotate.png

如何用 HTML 文本填充框#

方法 Page.insert_htmlbox() 提供了一种强大得多的方式在矩形中插入文本。

与简单的纯文本不同,此方法接受 HTML 源代码,其中不仅可以包含 HTML 标签,还可以包含样式指令来影响字体、字重(粗体)和样式(斜体)、颜色等内容。

还可以混合使用多种字体和语言,输出 HTML 表格以及插入图像和 URI 链接。

为了提供更大的样式灵活性,还可以提供额外的 CSS 源代码。

此方法基于 Story 类。因此,由于使用了 HarfBuzz 库(该库提供了所谓的“文本塑形”功能),支持并正确书写了复杂的脚本系统,如天城文 (Devanagari)、尼泊尔文 (Nepali)、泰米尔文 (Tamil) 等。

输出字符所需的任何字体都会自动从 Google NOTO 字体库中提取,作为回退方案(当可选提供的用户字体不包含某些字形时)。

为了让您对这里提供的功能有一个初步了解,我们将输出以下富含 HTML 的文本

import pymupdf


rect = pymupdf.Rect(100, 100, 400, 300)

text = """Lorem ipsum dolor sit amet, consectetur adipisici elit, sed
    eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad
    minim veniam, quis nostrud exercitation <b>ullamco <i>laboris</i></b>
    nisi ut aliquid ex ea commodi consequat. Quis aute iure
    <span style="color: #f00;">reprehenderit</span>
    in <span style="color: #0f0;font-weight:bold;">voluptate</span> velit
    esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat
    cupiditat non proident, sunt in culpa qui
    <a href="https://www.artifex.com">officia</a> deserunt mollit anim id
    est laborum."""

doc = pymupdf.Document()

page = doc.new_page()
page.insert_htmlbox(rect, text, css="* {font-family: sans-serif;font-size:14px;}")

doc.ez_save(__file__.replace(".py", ".pdf"))

请注意,“css”参数如何用于全局选择默认的“无衬线体 (sans-serif)”字体和 14 号字体大小。

结果将如下所示

_images/img-htmlbox1.png

如何输出 HTML 表格和图像#

这是另一个使用此方法输出表格的示例。这次,我们将所有样式都包含在 HTML 源代码本身中。另请注意,如何插入图像 – 甚至在表格单元格内

import pymupdf
import os

filedir = os.path.dirname(__file__)


text = """
<style>
body {
    font-family: sans-serif;
}

td,
th {
    border: 1px solid blue;
    border-right: none;
    border-bottom: none;
    padding: 5px;
    text-align: center;
}

table {
    border-right: 1px solid blue;
    border-bottom: 1px solid blue;
    border-spacing: 0;
}
</style>

<body>
<p><b>Some Colors</b></p>
<table>
    <tr>
    <th>Lime</th>
    <th>Lemon</th>
    <th>Image</th>
    <th>Mauve</th>
    </tr>
    <tr>
    <td>Green</td>
    <td>Yellow</td>
    <td><img src="img-cake.png" width=50></td>
    <td>Between<br>Gray and Purple</td>
    </tr>
</table>
</body>
"""

doc = pymupdf.Document()

page = doc.new_page()
rect = page.rect + (36, 36, -36, -36)

# we must specify an Archive because of the image
page.insert_htmlbox(rect, text, archive=pymupdf.Archive("."))

doc.ez_save(__file__.replace(".py", ".pdf"))

结果将如下所示

_images/img-htmlbox2.png

如何输出世界语言#

我们的第三个示例将演示自动多语言支持。它包括对天城文 (Devanagari) 和从右到左书写语言等复杂脚本系统的自动文本塑形功能

import pymupdf

greetings = (
    "Hello, World!",  # english
    "Hallo, Welt!",  # german
    "سلام دنیا!",  # persian
    "வணக்கம், உலகம்!",  # tamil
    "สวัสดีชาวโลก!",  # thai
    "Привіт Світ!",  # ucranian
    "שלום עולם!",  # hebrew
    "ওহে বিশ্ব!",  # bengali
    "你好世界!",  # chinese
    "こんにちは世界!",  # japanese
    "안녕하세요, 월드!",  # korean
    "नमस्कार, विश्व !",  # sanskrit
    "हैलो वर्ल्ड!",  # hindi
)
doc = pymupdf.open()
page = doc.new_page()
rect = (50, 50, 200, 500)

# join greetings into one text string
text = " ... ".join([t for t in greetings])

# the output of the above is simple:
page.insert_htmlbox(rect, text)
doc.save(__file__.replace(".py", ".pdf"))

这是输出结果

_images/img-htmlbox3.png

如何指定自己的字体#

使用 @font-face 语句在 CSS 语法中定义您的字体文件。您需要的每种字体粗细和字体样式(例如粗体或斜体)组合都需要单独的 @font-face 定义。以下示例使用了著名的 MS Comic Sans 字体的四种变体:常规、粗体、斜体和粗斜体。

由于这四个字体文件位于系统的 C:/Windows/Fonts 文件夹中,该方法需要一个指向该文件夹的 Archive 定义

"""
How to use your own fonts with method Page.insert_htmlbox().
"""
import pymupdf

# Example text
text = """Lorem ipsum dolor sit amet, consectetur adipisici elit, sed
    eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad
    minim veniam, quis nostrud exercitation <b>ullamco <i>laboris</i></b>
    nisi ut aliquid ex ea commodi consequat. Quis aute iure
    <span style="color: red;">reprehenderit</span>
    in <span style="color: green;font-weight:bold;">voluptate</span> velit
    esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat
    cupiditat non proident, sunt in culpa qui
    <a href="https://www.artifex.com">officia</a> deserunt mollit anim id
    est laborum."""

"""
We need an Archive object to show where font files are located.
We intend to use the font family "MS Comic Sans".
"""
arch = pymupdf.Archive("C:/Windows/Fonts")

# These statements define which font file to use for regular, bold,
# italic and bold-italic text.
# We assign an arbitrary common font-family for all 4 font files.
# The Story algorithm will select the right file as required.
# We request to use "comic" throughout the text.
css = """
@font-face {font-family: comic; src: url(comic.ttf);}
@font-face {font-family: comic; src: url(comicbd.ttf);font-weight: bold;}
@font-face {font-family: comic; src: url(comicz.ttf);font-weight: bold;font-style: italic;}
@font-face {font-family: comic; src: url(comici.ttf);font-style: italic;}
* {font-family: comic;}
"""

doc = pymupdf.Document()
page = doc.new_page(width=150, height=150)  # make small page

page.insert_htmlbox(page.rect, text, css=css, archive=arch)

doc.subset_fonts(verbose=True)  # build subset fonts to reduce file size
doc.ez_save(__file__.replace(".py", ".pdf"))
_images/img-htmlbox4.png

如何请求文本对齐#

此示例结合了多种要求

  • 将文本逆时针旋转 90 度。

  • 使用 pymupdf-fonts 包中的字体。您会看到在这种情况下,相应的 CSS 定义要简单得多。

  • 使用“两端对齐 (justify)”选项对齐文本。

"""
How to use a pymupdf font with method Page.insert_htmlbox().
"""
import pymupdf

# Example text
text = """Lorem ipsum dolor sit amet, consectetur adipisici elit, sed
    eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad
    minim veniam, quis nostrud exercitation <b>ullamco <i>laboris</i></b>
    nisi ut aliquid ex ea commodi consequat. Quis aute iure
    <span style="color: red;">reprehenderit</span>
    in <span style="color: green;font-weight:bold;">voluptate</span> velit
    esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat
    cupiditat non proident, sunt in culpa qui
    <a href="https://www.artifex.com">officia</a> deserunt mollit anim id
    est laborum."""

"""
This is similar to font file support. However, we can use a convenience
function for creating required CSS definitions.
We still need an Archive for finding the font binaries.
"""
arch = pymupdf.Archive()

# We request to use "myfont" throughout the text.
css = pymupdf.css_for_pymupdf_font("ubuntu", archive=arch, name="myfont")
css += "* {font-family: myfont;text-align: justify;}"

doc = pymupdf.Document()

page = doc.new_page(width=150, height=150)

page.insert_htmlbox(page.rect, text, css=css, archive=arch, rotate=90)

doc.subset_fonts(verbose=True)
doc.ez_save(__file__.replace(".py", ".pdf"))
_images/img-htmlbox5.png

如何提取带颜色的文本#

遍历您的文本块,找到您需要这些信息的文本 span。

for page in doc:
    text_blocks = page.get_text("dict", flags=pymupdf.TEXTFLAGS_TEXT)["blocks"]
    for block in text_blocks:
        for line in block["lines"]:
            for span in line["spans"]:
                text = span["text"]
                color = pymupdf.sRGB_to_rgb(span["color"])
                print(f"Text: {text}, Color: {color}")

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