在 Python 中从 Word DOCX 文档中提取内容

从 Word 文档中提取文本通常在不同的场景中执行。例如,分析文本,提取文档的特定部分并将它们组合成单个文档,等等。在本文中,您将学习如何在 Python 中以编程方式从 Word 文档中提取文本。此外,我们将介绍如何动态提取段落、表格等特定元素之间的内容。

从 Word 文档中提取文本的 Python 库

Aspose.Words for Python 是一个功能强大的库,可让您从头开始创建 MS Word 文档。此外,它可以让您操作现有的 Word 文档进行加密、转换、文本提取等。我们将使用这个库从 Word DOCX 或 DOC 文档中提取文本。您可以使用以下 pip 命令从 PyPI 安装库。

pip install aspose-words

使用 Python 在 Word 文档中提取文本

MS Word 文档由各种元素组成,包括段落、表格、图像等。因此,文本提取的要求可能因一种情况而异。例如,您可能需要在段落、书签、评论等之间提取文本。

Word 文档中的每种类型的元素都表示为一个节点。因此,要处理文档,您将不得不使用节点。那么让我们开始看看如何在不同的场景下从 Word 文档中提取文本。

在 Python 中从 Word 文档中提取文本

在本节中,我们将为 Word 文档实现一个 Python 文本提取器,文本提取的工作流程如下:

  • 首先,我们将定义要包含在文本提取过程中的节点。
  • 然后,我们将提取指定节点之间的内容(包括或不包括开始和结束节点)。
  • 最后,我们将使用提取节点的克隆,例如创建一个包含提取内容的新 Word 文档。

现在让我们编写一个名为 extractcontent 的方法,我们将向该方法传递节点和一些其他参数来执行文本提取。此方法将解析文档并克隆节点。以下是我们将传递给此方法的参数。

  1. StartNode 和 EndNode 分别作为内容提取的起点和终点。这些可以是块级(Paragraph、Table)或内联级(例如 Run、FieldStart、BookmarkStart 等)节点。
    1. 要传递一个字段,您应该传递相应的 FieldStart 对象。
    2. 要传递书签,应传递 BookmarkStart 和 BookmarkEnd 节点。
    3. 对于评论,应使用 CommentRangeStart 和 CommentRangeEnd 节点。
  2. IsInclusive 定义标记是否包含在提取中。如果此选项设置为 false 并且传递相同的节点或连续节点,则将返回一个空列表。

以下是extractcontent 方法的完整实现,该方法提取传递的节点之间的内容。

def extract_content(startNode : aw.Node, endNode : aw.Node, isInclusive : bool):
    
    # 首先,检查传递给该方法的节点是否有效。
    verify_parameter_nodes(startNode, endNode)

    # 创建一个列表来存储提取的节点。
    nodes = []

    # 如果任一标记是注释的一部分,包括注释本身,我们需要移动指针
    # 转发到在 CommentRangeEnd 节点之后找到的评论节点。
    if (endNode.node_type == aw.NodeType.COMMENT_RANGE_END and isInclusive) :
        
        node = find_next_node(aw.NodeType.COMMENT, endNode.next_sibling)
        if (node != None) :
            endNode = node

    # 保留传递给此方法的原始节点的记录,以便在需要时拆分标记节点。
    originalStartNode = startNode
    originalEndNode = endNode

    # 基于块级节点(段落和表格)提取内容。遍历父节点以找到它们。
    # 我们将拆分第一个和最后一个节点的内容,这取决于标记节点是否内联。
    startNode = get_ancestor_in_body(startNode)
    endNode = get_ancestor_in_body(endNode)

    isExtracting = True
    isStartingNode = True
    # 我们从文档中提取的当前节点。
    currNode = startNode

    # 开始提取内容。处理所有块级节点并专门拆分第一个
    # 和需要时的最后一个节点,因此保留了段落格式。
    # 方法比常规提取器复杂一点,因为我们需要考虑因素
    # 在使用内联节点、字段、书签等进行提取时,使其有用。
    while (isExtracting) :
        
        # 克隆当前节点及其子节点以获取副本。
        cloneNode = currNode.clone(True)
        isEndingNode = currNode == endNode

        if (isStartingNode or isEndingNode) :
            
            # 我们需要分别处理每个标记,因此将其传递给单独的方法。
            # 应首先处理 end 以保留节点索引。
            if (isEndingNode) :
                # !isStartingNode:如果标记是同一个节点,则不要添加两次。
                process_marker(cloneNode, nodes, originalEndNode, currNode, isInclusive, False, not isStartingNode, False)
                isExtracting = False

            # 条件需要作为块级别的开始和结束标记分开,可能是同一个节点。
            if (isStartingNode) :
                process_marker(cloneNode, nodes, originalStartNode, currNode, isInclusive, True, True, False)
                isStartingNode = False
            
        else :
            # 节点不是开始或结束标记,只需将副本添加到列表中。
            nodes.append(cloneNode)

        # 移动到下一个节点并提取它。如果下一个节点是无,
        # 其余内容可在不同部分找到。
        if (currNode.next_sibling == None and isExtracting) :
            # 移至下一部分。
            nextSection = currNode.get_ancestor(aw.NodeType.SECTION).next_sibling.as_section()
            currNode = nextSection.body.first_child
            
        else :
            # 移动到正文中的下一个节点。
            currNode = currNode.next_sibling
            
    # 为了与内嵌书签模式兼容,请添加下一段(空)。
    if (isInclusive and originalEndNode == endNode and not originalEndNode.is_composite) :
        include_next_paragraph(endNode, nodes)

    # 返回节点标记之间的节点。
    return nodes

extractcontent 方法还需要一些辅助方法来完成文本提取操作,如下所示。

def verify_parameter_nodes(start_node: aw.Node, end_node: aw.Node):

    # 这些检查的顺序很重要。
    if start_node is None:
        raise ValueError("Start node cannot be None")
    if end_node is None:
        raise ValueError("End node cannot be None")

    if start_node.document != end_node.document:
        raise ValueError("Start node and end node must belong to the same document")

    if start_node.get_ancestor(aw.NodeType.BODY) is None or end_node.get_ancestor(aw.NodeType.BODY) is None:
        raise ValueError("Start node and end node must be a child or descendant of a body")

    # 检查结束节点是否在 DOM 树中的开始节点之后。
    # 首先,检查它们是否在不同的部分,如果不是,
    # 检查它们在同一节正文中的位置。
    start_section = start_node.get_ancestor(aw.NodeType.SECTION).as_section()
    end_section = end_node.get_ancestor(aw.NodeType.SECTION).as_section()

    start_index = start_section.parent_node.index_of(start_section)
    end_index = end_section.parent_node.index_of(end_section)

    if start_index == end_index:

        if (start_section.body.index_of(get_ancestor_in_body(start_node)) >
            end_section.body.index_of(get_ancestor_in_body(end_node))):
            raise ValueError("The end node must be after the start node in the body")

    elif start_index > end_index:
        raise ValueError("The section of end node must be after the section start node")

 
def find_next_node(node_type: aw.NodeType, from_node: aw.Node):

    if from_node is None or from_node.node_type == node_type:
        return from_node

    if from_node.is_composite:

        node = find_next_node(node_type, from_node.as_composite_node().first_child)
        if node is not None:
            return node

    return find_next_node(node_type, from_node.next_sibling)

 
def is_inline(node: aw.Node):

    # 测试节点是否是段落或表格节点的后代并且不是段落
    # 或者一个表格,评论类中的一个段落是可能的。
    return ((node.get_ancestor(aw.NodeType.PARAGRAPH) is not None or node.get_ancestor(aw.NodeType.TABLE) is not None) and
            not (node.node_type == aw.NodeType.PARAGRAPH or node.node_type == aw.NodeType.TABLE))

 
def process_marker(clone_node: aw.Node, nodes, node: aw.Node, block_level_ancestor: aw.Node,
    is_inclusive: bool, is_start_marker: bool, can_add: bool, force_add: bool):

    # 如果我们正在处理块级节点,请查看是否应包含它并将其添加到列表中。
    if node == block_level_ancestor:
        if can_add and is_inclusive:
            nodes.append(clone_node)
        return

    # cloneNode 是 blockLevelNode 的克隆。如果 node != blockLevelNode, blockLevelAncestor
    # 是节点的祖先,这意味着它是一个复合节点。
    assert clone_node.is_composite

    # 如果标记是 FieldStart 节点,请检查是否包含它。
    # 为简单起见,我们假设 FieldStart 和 FieldEnd 出现在同一段落中。
    if node.node_type == aw.NodeType.FIELD_START:
        # 如果标记是起始节点并且不包括在内,则跳到字段的末尾。
        # 如果标记是结束节点并且要包括在内,则移动到结束字段,这样该字段就不会被删除。
        if is_start_marker and not is_inclusive or not is_start_marker and is_inclusive:
            while node.next_sibling is not None and node.node_type != aw.NodeType.FIELD_END:
                node = node.next_sibling

    # 如果标记节点位于文档正文的第三级或更低,则支持案例。
    node_branch = fill_self_and_parents(node, block_level_ancestor)

    # 通过索引处理我们克隆节点中的相应节点。
    current_clone_node = clone_node
   for i in range(len(node_branch) - 1, -1):

        current_node = node_branch[i]
        node_index = current_node.parent_node.index_of(current_node)
        current_clone_node = current_clone_node.as_composite_node.child_nodes[node_index]

        remove_nodes_outside_of_range(current_clone_node, is_inclusive or (i > 0), is_start_marker)

    # 处理后,如果没有包含它,复合节点可能会变为空。
    if can_add and (force_add or clone_node.as_composite_node().has_child_nodes):
        nodes.append(clone_node)

 
def remove_nodes_outside_of_range(marker_node: aw.Node, is_inclusive: bool, is_start_marker: bool):

    is_processing = True
    is_removing = is_start_marker
    next_node = marker_node.parent_node.first_child

    while is_processing and next_node is not None:

        current_node = next_node
        is_skip = False

        if current_node == marker_node:
            if is_start_marker:
                is_processing = False
                if is_inclusive:
                    is_removing = False
            else:
                is_removing = True
                if is_inclusive:
                    is_skip = True

        next_node = next_node.next_sibling
        if is_removing and not is_skip:
            current_node.remove()

 
def fill_self_and_parents(node: aw.Node, till_node: aw.Node):

    nodes = []
    current_node = node

    while current_node != till_node:
        nodes.append(current_node)
        current_node = current_node.parent_node

    return nodes

 
def include_next_paragraph(node: aw.Node, nodes):

    paragraph = find_next_node(aw.NodeType.PARAGRAPH, node.next_sibling).as_paragraph()
    if paragraph is not None:

        # 移动到第一个孩子以包含没有内容的段落。
        marker_node = paragraph.first_child if paragraph.has_child_nodes else paragraph
        root_node = get_ancestor_in_body(paragraph)

        process_marker(root_node.clone(True), nodes, marker_node, root_node,
            marker_node == paragraph, False, True, True)

 
def get_ancestor_in_body(start_node: aw.Node):

    while start_node.parent_node.node_type != aw.NodeType.BODY:
        start_node = start_node.parent_node
    return start_node
def generate_document(src_doc: aw.Document, nodes):

    dst_doc = aw.Document()
    # 从空文档中删除第一段。
    dst_doc.first_section.body.remove_all_children()

    # 将列表中的每个节点导入到新文档中。保留节点的原始格式。
    importer = aw.NodeImporter(src_doc, dst_doc, aw.ImportFormatMode.KEEP_SOURCE_FORMATTING)

   for node in nodes:
        import_node = importer.import_node(node, True)
        dst_doc.first_section.body.append_child(import_node)

    return dst_doc

 
def paragraphs_by_style_name(doc: aw.Document, style_name: str):

    paragraphs_with_style = []
    paragraphs = doc.get_child_nodes(aw.NodeType.PARAGRAPH, True)

   for paragraph in paragraphs:
        paragraph = paragraph.as_paragraph()
        if paragraph.paragraph_format.style.name == style_name:
            paragraphs_with_style.append(paragraph)

    return paragraphs_with_style

现在我们准备好使用这些方法并从 Word 文档中提取文本。

在 Word 文档中的段落之间提取文本

让我们看看如何在 Word DOCX 文档的两个段落之间提取内容。以下是在 Python 中执行此操作的步骤。

  • 首先,使用 Document 类加载 Word 文档。
  • 使用 Document.firstsection.body.getchild(NodeType.PARAGRAPH, int, boolean).asparagraph() 方法将开始和结束段落的引用获取到两个对象中。
  • 调用 extractcontent(startPara, endPara, True) 方法将节点提取到对象中。
  • 调用 generatedocument(Document,extractNodes) 辅助方法来创建包含提取内容的文档。
  • 最后,使用 Document.save(string) 方法保存返回的文档。

以下代码示例展示了如何在 Python 中提取 Word 文档中第 7 段和第 11 段之间的文本。

# 加载文档。
doc = aw.Document("Extract content.docx")

# 定义开始和结束段落。
startPara = doc.first_section.body.get_child(aw.NodeType.PARAGRAPH, 6, True).as_paragraph()
endPara = doc.first_section.body.get_child(aw.NodeType.PARAGRAPH, 10, True).as_paragraph()

# 提取文档中这些段落之间的内容。在提取中包含这些标记。
extractedNodes = extract_content(startPara, endPara, True)

# 生成包含提取内容的文档。
dstDoc = generate_document(doc, extractedNodes)

# 保存文档。
dstDoc.save("extract_content_between_paragraphs.docx")

在 Word 文档中不同类型的节点之间提取文本

您还可以在不同类型的节点之间提取内容。为了演示,让我们提取段落和表格之间的内容并将其保存到新的 Word 文档中。以下是执行此操作的步骤。

  • 使用 Document 类加载 Word 文档。
  • 使用 Document.firstsection.body.getchild(NodeType, int, boolean) 方法将起始节点和结束节点的引用获取到两个对象中。
  • 调用 extractcontent(startPara, endPara, True) 方法将节点提取到对象中。
  • 调用 generatedocument(Document,extractNodes) 辅助方法来创建包含提取内容的文档。
  • 使用 Document.save(string) 方法保存返回的文档。

以下代码示例展示了如何在 Python 中提取段落和表格之间的文本。

# 加载文档
doc = aw.Document("Extract content.docx")

# 定义起始节点和结束节点。
start_para = doc.last_section.get_child(aw.NodeType.PARAGRAPH, 2, True).as_paragraph()
end_table = doc.last_section.get_child(aw.NodeType.TABLE, 0, True).as_table()

# 提取文档中这些节点之间的内容。在提取中包含这些标记。
extracted_nodes = extract_content(start_para, end_table, True)

# 生成包含提取内容的文档。
dstDoc = generate_document(doc, extractedNodes)

# 保存文档。
dstDoc.save("extract_content_between_nodes.docx")

根据样式提取段落之间的文本

现在让我们看看如何根据样式提取段落之间的内容。为了演示,我们将提取 Word 文档中第一个“标题 1”和第一个“标题 3”之间的内容。以下步骤演示了如何在 Python 中实现此目的。

  • 首先,使用 Document 类加载 Word 文档。
  • 然后,使用 parasbystylename(Document, “Heading 1”) 辅助方法将段落提取到对象中。
  • 使用parametersbystylename(Document, “Heading 3”) 辅助方法将段落提取到另一个对象中。
  • 调用 extractcontent(startPara, endPara, True) 方法并将两个段落数组中的第一个元素作为第一个和第二个参数传递。
  • 调用 generatedocument(Document,extractNodes) 辅助方法来创建包含提取内容的文档。
  • 最后,使用 Document.save(string) 方法保存返回的文档。

以下代码示例展示了如何根据样式提取段落之间的内容。

# 加载文档
doc = aw.Document("Extract content.docx")

# 使用相应的标题样式收集段落列表。
parasStyleHeading1 = paragraphs_by_style_name(doc, "Heading 1")
parasStyleHeading3 = paragraphs_by_style_name(doc, "Heading 3")

# 使用具有这些样式的段落的第一个实例。
startPara1 = parasStyleHeading1[0]
endPara1 = parasStyleHeading3[0]

# 提取文档中这些节点之间的内容。不要在提取中包含这些标记。
extractedNodes = extract_content(startPara1, endPara1, False)

# 生成包含提取内容的文档。
dstDoc = generate_document(doc, extractedNodes)

# 保存文档。
dstDoc.save("extract_content_between_paragraphs_based_on-Styles.docx")

阅读更多

您可以使用 this 文档文章探索从 Word 文档中提取文本的其他方案。

获取免费 API 许可证

您可以获得 临时许可证 以使用 Aspose.Words for Python,而不受评估限制。

结论

在本文中,您学习了如何使用 Python 从 MS Word 文档中提取文本。此外,您还了解了如何以编程方式在 Word 文档中相似或不同类型的节点之间提取内容。因此,您可以在 Python 中构建自己的 MS Word 文本提取器。此外,您可以使用 文档 探索 Aspose.Words for Python 的其他功能。如果您有任何问题,请随时通过我们的 论坛 告诉我们。

也可以看看

信息:如果您需要从 PowerPoint 演示文稿中获取 Word 文档,您可以使用 Aspose Presentation to Word Document 转换器。