spaCy布局:PDF与文档智能处理

4 阅读7分钟

spaCy布局:使用spaCy处理PDF、Word文档等

该插件与Docling集成,为您的spaCy流程带来对PDF、Word文档和其他输入格式的结构化处理。它输出基于文本格式的清晰、结构化数据,并创建spaCy熟悉的Doc对象,使您能够访问如章节或标题等带标签的文本片段,以及将其数据转换为pandas.DataFrame的表格。

此工作流程使将强大的NLP技术应用于您的文档变得容易,包括语言分析、命名实体识别、文本分类等。它也非常适合为RAG(检索增强生成)管道实现数据分块。

📖 博客文章:"从PDF到AI就绪的结构化数据:深入探索" – 一个将PDF及类似文档转换为结构化数据的新模块化工作流程,以spacy-layout和Docling为特色。

📝 使用方法

⚠️ 注意:该包需要Python 3.10或更高版本。

pip install spacy-layout

使用nlp对象进行分词初始化spaCyLayout预处理器后,可以在文档路径上调用它,将其转换为结构化数据。生成的Doc对象包含映射到原始文本中的布局跨度,并暴露各种属性,包括内容类型和布局特征。

import spacy
from spacy_layout import spaCyLayout

nlp = spacy.blank("en")
layout = spaCyLayout(nlp)

# 处理文档并创建spaCy Doc对象
doc = layout("./starcraft.pdf")

# 文档的基于文本的内容
print(doc.text)
# 文档布局,包括页面和页面大小
print(doc._.layout)
# 文档中的表格及其提取的数据
print(doc._.tables)
# 文档的Markdown表示
print(doc._.markdown)

# 不同部分的布局跨度
for span in doc.spans["layout"]:
    # 文档部分及其在文本中的token和字符偏移量
    print(span.text, span.start, span.end, span.start_char, span.end_char)
    # 部分类型,例如 "text", "title", "section_header" 等
    print(span.label_)
    # 部分的布局特征,包括边界框
    print(span._.layout)
    # 距离跨度最近的标题(准确性取决于文档结构)
    print(span._.heading)

如果需要大规模处理大量文档,可以使用spaCyLayout.pipe方法,它接受一个由路径或字节组成的可迭代对象,并生成Doc对象:

paths = ["one.pdf", "two.pdf", "three.pdf", ...]
for doc in layout.pipe(paths):
    print(doc._.layout)

spaCy还允许您在已创建的Doc上调用nlp对象,因此您可以轻松应用组件流程进行语言分析或命名实体识别,使用基于规则的匹配,或执行任何其他spaCy能做的操作。

# 加载基于transformer的英文流程
# 安装:python -m spacy download en_core_web_trf
nlp = spacy.load("en_core_web_trf")
layout = spaCyLayout(nlp)

doc = layout("./starcraft.pdf")
# 应用流程以访问词性标签、依存关系、实体等
doc = nlp(doc)

表格和表格数据

表格包含在标签为"table"的布局跨度中,并可通过快捷方式Doc._.tables访问。它们暴露了一个布局扩展属性,以及一个data属性,其中包含转换为pandas.DataFrame的表格数据。

for table in doc._.tables:
    # Token位置和边界框
    print(table.start, table.end, table._.layout)
    # 内容的pandas.DataFrame
    print(table._.data)

默认情况下,跨度文本是一个占位符"TABLE",但您可以通过向spaCyLayout提供display_table回调来自定义表格的呈现方式,该回调接收数据的pandas.DataFrame。这允许您在文档文本中包含表格数据,并在后续使用它们,例如在训练好的命名实体识别器或文本分类器进行信息提取时。

def display_table(df: pd.DataFrame) -> str:
    return f"包含列的表格: {', '.join(df.columns.tolist())}"

layout = spaCyLayout(nlp, display_table=display_table)

序列化

处理文档后,您可以将结构化的Doc对象以spaCy高效的二进制格式进行序列化,这样就无需重新运行资源密集型转换。

from spacy.tokens import DocBin

docs = layout.pipe(["one.pdf", "two.pdf", "three.pdf"])
doc_bin = DocBin(docs=docs, store_user_data=True)
doc_bin.to_disk("./file.spacy")

⚠️ 关于使用扩展属性反序列化的说明:像Doc._.layout这样的自定义扩展属性是在初始化spaCyLayout时注册的。因此,如果您要从二进制文件加载包含布局信息的Doc对象,需要初始化它以便重新填充自定义属性。我们计划在即将发布的版本中使这个过程更加优雅。

+ layout = spacyLayout(nlp)
doc_bin = DocBin(store_user_data=True).from_disk("./file.spacy")
docs = list(doc_bin.get_docs(nlp.vocab))

🎛️ API

数据和扩展属性

layout = spaCyLayout(nlp)
doc = layout("./starcraft.pdf")
print(doc._.layout)
for span in doc.spans["layout"]:
    print(span.label_, span._.layout)
属性类型描述
Doc._.layoutDocLayout文档的布局特征。
Doc._.pageslist[tuple[PageLayout, list[Span]]]文档中的页面及其包含的跨度。
Doc._.tableslist[Span]文档中的所有表格。
Doc._.markdownstr文档的Markdown表示。
Doc.spans["layout"]spacy.tokens.SpanGroup文档中的布局跨度。
Span.label_str提取的布局跨度的类型,例如 "text" 或 "section_header"。
Span.labelint跨度标签的整数ID。
Span.idint布局跨度的运行索引。
Span._.layout`SpanLayoutNone`布局跨度的布局特征。
Span._.heading`SpanNone`距离跨度最近的标题(如果有)。
Span._.data`pandas.DataFrameNone`为表格跨度提取的数据。

数据类 PageLayout

属性类型描述
page_noint页码(从1开始)。
widthfloat页面宽度(像素)。
heightfloat页面高度(像素)。

数据类 DocLayout

属性类型描述
pageslist[PageLayout]文档中的页面。

数据类 SpanLayout

属性类型描述
xfloat边界框的水平偏移量(像素)。
yfloat边界框的垂直偏移量(像素)。
widthfloat边界框的宽度(像素)。
heightfloat边界框的高度(像素)。
page_noint跨度所在的页码。

spaCyLayout

方法 spaCyLayout.__init__

初始化文档处理器。

nlp = spacy.blank("en")
layout = spaCyLayout(nlp)
参数类型描述
nlpspacy.language.Language用于分词的已初始化nlp对象。
separatorstr用于分隔所创建Doc对象中各部分的token。分隔符不会是布局跨度的一部分。如果为None,则不添加分隔符。默认为"\n\n"。
attrsdict[str, str]覆盖自定义spaCy属性。可包含"doc_layout"、"doc_pages"、"doc_tables"、"doc_markdown"、"span_layout"、"span_data"、"span_heading"和"span_group"。
headingslist[str]用于检测Span._.heading的标题标签。默认为["section_header", "page_header", "title"]。
display_table`Callable[[pandas.DataFrame], str]str`用于在Doc.text中生成表格的基于文本表示的函数或占位符文本。默认为"TABLE"。
docling_optionsdict[InputFormat, FormatOption]传递给Docling的DocumentConverter的格式选项。
返回spaCyLayout初始化后的对象。

方法 spaCyLayout.__call__

处理文档并创建一个包含文本内容和布局跨度的spaCy Doc对象,默认通过Doc.spans["layout"]可访问。

layout = spaCyLayout(nlp)
doc = layout("./starcraft.pdf")
参数类型描述
source`strPathbytesDoclingDocument`要处理的文档路径、字节或已创建的DoclingDocument。
返回Doc处理后的spaCy Doc对象。

方法 spaCyLayout.pipe

批量处理多个文档并创建spaCy Doc对象。如果处理大规模文档,应使用此方法。as_tuples的行为与spaCy的Language.pipe相同。

layout = spaCyLayout(nlp)
paths = ["one.pdf", "two.pdf", "three.pdf", ...]
docs = layout.pipe(paths)
sources = [("one.pdf", {"id": 1}), ("two.pdf", {"id": 2})]
for doc, context in layout.pipe(sources, as_tuples=True):
    ...
参数类型描述
sources`Iterable[strPathbytes]Iterable[tuple[strPathbytes, Any]]`要处理的文档路径或字节的可迭代对象,或者如果as_tuples设置为True,则为(source, context)元组的可迭代对象。
as_tuplesbool如果设置为True,输入应为(source, context)元组的可迭代对象。输出将是一个(doc, context)元组的序列。默认为False
生成`Doctuple[Doc, Any]`处理后的spaCy Doc对象,或者如果as_tuples设置为True,则为(doc, context)元组。

💡 示例和代码片段

本节包含关于如何使用spacy-layout的更多示例。如果您有适合的示例,欢迎提交拉取请求!

使用matplotlib可视化页面和边界框

import pypdfium2 as pdfium
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import spacy
from spacy_layout import spaCyLayout

DOCUMENT_PATH = "./document.pdf"

# 加载PDF页面并将其转换为图像
pdf = pdfium.PdfDocument(DOCUMENT_PATH)
page_image = pdf[2].render(scale=1)  # 获取第3页(索引2)
numpy_array = page_image.to_numpy()
# 使用spaCy处理文档
nlp = spacy.blank("en")
layout = spaCyLayout(nlp)
doc = layout(DOCUMENT_PATH)

# 获取第3页的布局和部分
page = doc._.pages[2]
page_layout = doc._.layout.pages[2]
# 创建具有页面尺寸的图像和轴
fig, ax = plt.subplots(figsize=(12, 16))
# 显示PDF图像
ax.imshow(numpy_array)
# 为每个部分的边界框添加矩形
for section in page[1]:
    # 创建矩形块
    rect = Rectangle(
        (section._.layout.x, section._.layout.y),
        section._.layout.width,
        section._.layout.height,
        fill=False,
        color="blue",
        linewidth=1,
        alpha=0.5
    )
    ax.add_patch(rect)
    # 在框顶部添加文本标签
    ax.text(
        section._.layout.x,
        section._.layout.y,
        section.label_,
        fontsize=8,
        color="red",
        verticalalignment="bottom"
    )

ax.axis("off")  # 隐藏轴
plt.show()
```FINISHED