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 문서를 만듭니다.

이제 텍스트 추출을 수행하기 위해 노드와 기타 매개변수를 전달할 extract\content라는 메서드를 작성해 보겠습니다. 이 방법은 문서를 구문 분석하고 노드를 복제합니다. 다음은 이 메서드에 전달할 매개변수입니다.

  1. StartNode 및 EndNode는 각각 콘텐츠 추출을 위한 시작 및 종료 지점입니다. 이들은 블록 수준(Paragraph, Table) 또는 인라인 수준(예: Run, FieldStart, BookmarkStart 등) 노드일 수 있습니다.
    1. 필드를 전달하려면 해당 FieldStart 개체를 전달해야 합니다.
    2. 책갈피를 전달하려면 BookmarkStart 및 BookmarkEnd 노드를 전달해야 합니다.
    3. 주석의 경우 CommentRangeStart 및 CommentRangeEnd 노드를 사용해야 합니다.
  2. IsInclusive는 마커가 추출에 포함되는지 여부를 정의합니다. 이 옵션이 false로 설정되고 동일한 노드 또는 연속 노드가 전달되면 빈 목록이 반환됩니다.

다음은 전달된 노드 사이의 콘텐츠를 추출하는 extract\content 메서드의 완전한 구현입니다.

def extract_content(startNode : aw.Node, endNode : aw.Node, isInclusive : bool):
    
    # First, check that the nodes passed to this method are valid for use.
    verify_parameter_nodes(startNode, endNode)

    # Create a list to store the extracted nodes.
    nodes = []

    # If either marker is part of a comment, including the comment itself, we need to move the pointer
    # forward to the Comment Node found after the CommentRangeEnd node.
    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

    # Keep a record of the original nodes passed to this method to split marker nodes if needed.
    originalStartNode = startNode
    originalEndNode = endNode

    # Extract content based on block-level nodes (paragraphs and tables). Traverse through parent nodes to find them.
    # We will split the first and last nodes' content, depending if the marker nodes are inline.
    startNode = get_ancestor_in_body(startNode)
    endNode = get_ancestor_in_body(endNode)

    isExtracting = True
    isStartingNode = True
    # The current node we are extracting from the document.
    currNode = startNode

    # Begin extracting content. Process all block-level nodes and specifically split the first
    # and last nodes when needed, so paragraph formatting is retained.
    # Method is a little more complicated than a regular extractor as we need to factor
    # in extracting using inline nodes, fields, bookmarks, etc. to make it useful.
    while (isExtracting) :
        
        # Clone the current node and its children to obtain a copy.
        cloneNode = currNode.clone(True)
        isEndingNode = currNode == endNode

        if (isStartingNode or isEndingNode) :
            
            # We need to process each marker separately, so pass it off to a separate method instead.
            # End should be processed at first to keep node indexes.
            if (isEndingNode) :
                # !isStartingNode: don't add the node twice if the markers are the same node.
                process_marker(cloneNode, nodes, originalEndNode, currNode, isInclusive, False, not isStartingNode, False)
                isExtracting = False

            # Conditional needs to be separate as the block level start and end markers, maybe the same node.
            if (isStartingNode) :
                process_marker(cloneNode, nodes, originalStartNode, currNode, isInclusive, True, True, False)
                isStartingNode = False
            
        else :
            # Node is not a start or end marker, simply add the copy to the list.
            nodes.append(cloneNode)

        # Move to the next node and extract it. If the next node is None,
        # the rest of the content is found in a different section.
        if (currNode.next_sibling == None and isExtracting) :
            # Move to the next section.
            nextSection = currNode.get_ancestor(aw.NodeType.SECTION).next_sibling.as_section()
            currNode = nextSection.body.first_child
            
        else :
            # Move to the next node in the body.
            currNode = currNode.next_sibling
            
    # For compatibility with mode with inline bookmarks, add the next paragraph (empty).
    if (isInclusive and originalEndNode == endNode and not originalEndNode.is_composite) :
        include_next_paragraph(endNode, nodes)

    # Return the nodes between the node markers.
    return nodes

다음과 같이 텍스트 추출 작업을 수행하기 위해 extract\content 메서드에서 일부 도우미 메서드도 필요합니다.

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

    # The order in which these checks are done is important.
    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")

    # Check the end node is after the start node in the DOM tree.
    # First, check if they are in different sections, then if they're not,
    # check their position in the body of the same section.
    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):

    # Test if the node is a descendant of a Paragraph or Table node and is not a paragraph
    # or a table a paragraph inside a comment class that is decent of a paragraph is possible.
    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 we are dealing with a block-level node, see if it should be included and add it to the list.
    if node == block_level_ancestor:
        if can_add and is_inclusive:
            nodes.append(clone_node)
        return

    # cloneNode is a clone of blockLevelNode. If node != blockLevelNode, blockLevelAncestor
    # is the node's ancestor that means it is a composite node.
    assert clone_node.is_composite

    # If a marker is a FieldStart node check if it's to be included or not.
    # We assume for simplicity that the FieldStart and FieldEnd appear in the same paragraph.
    if node.node_type == aw.NodeType.FIELD_START:
        # If the marker is a start node and is not included, skip to the end of the field.
        # If the marker is an end node and is to be included, then move to the end field so the field will not be removed.
        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

    # Support a case if the marker node is on the third level of the document body or lower.
    node_branch = fill_self_and_parents(node, block_level_ancestor)

    # Process the corresponding node in our cloned node by index.
    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)

    # After processing, the composite node may become empty if it has doesn't include it.
    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:

        # Move to the first child to include paragraphs without content.
        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()
    # Remove the first paragraph from the empty document.
    dst_doc.first_section.body.remove_all_children()

    # Import each node from the list into the new document. Keep the original formatting of the node.
    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.first\section.body.get_child(NodeType.PARAGRAPH, int, boolean).as\paragraph() 메서드를 사용하여 시작 및 끝 단락의 참조를 두 객체로 가져옵니다.
  • extract\content(startPara, endPara, True) 메서드를 호출하여 노드를 객체로 추출합니다.
  • generate\document(Document, extractNodes) 도우미 메서드를 호출하여 추출된 내용으로 구성된 문서를 만듭니다.
  • 마지막으로 Document.save(string) 메서드를 사용하여 반환된 문서를 저장합니다.

다음 코드 샘플은 Python의 Word 문서에서 7번째와 11번째 단락 사이의 텍스트를 추출하는 방법을 보여줍니다.

# Load document.
doc = aw.Document("Extract content.docx")

# Define starting and ending paragraphs.
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()

# Extract the content between these paragraphs in the document. Include these markers in the extraction.
extractedNodes = extract_content(startPara, endPara, True)

# Generate document containing extracted content.
dstDoc = generate_document(doc, extractedNodes)

# Save document.
dstDoc.save("extract_content_between_paragraphs.docx")

Word 문서에서 서로 다른 유형의 노드 간에 텍스트 추출

다른 유형의 노드 간에 콘텐츠를 추출할 수도 있습니다. 데모를 위해 단락과 표 사이의 콘텐츠를 추출하여 새 Word 문서에 저장해 보겠습니다. 다음은 이 작업을 수행하는 단계입니다.

  • Document 클래스를 사용하여 Word 문서를 로드합니다.
  • Document.first\section.body.get_child(NodeType, int, boolean) 메서드를 사용하여 시작 및 끝 노드의 참조를 두 객체로 가져옵니다.
  • extract\content(startPara, endPara, True) 메서드를 호출하여 노드를 객체로 추출합니다.
  • generate\document(Document, extractNodes) 도우미 메서드를 호출하여 추출된 내용으로 구성된 문서를 만듭니다.
  • Document.save(string) 메서드를 사용하여 반환된 문서를 저장합니다.

다음 코드 샘플은 Python에서 단락과 테이블 사이의 텍스트를 추출하는 방법을 보여줍니다.

# Load document
doc = aw.Document("Extract content.docx")

# Define starting and ending nodes.
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()

# Extract the content between these nodes in the document. Include these markers in the extraction.
extracted_nodes = extract_content(start_para, end_table, True)

# Generate document containing extracted content.
dstDoc = generate_document(doc, extractedNodes)

# Save document.
dstDoc.save("extract_content_between_nodes.docx")

스타일을 기반으로 단락 사이의 텍스트 추출

이제 스타일을 기반으로 단락 사이의 내용을 추출하는 방법을 알아보겠습니다. 데모를 위해 Word 문서의 첫 번째 “제목 1"과 첫 번째 “제목 3” 사이의 내용을 추출합니다. 다음 단계는 Python에서 이를 달성하는 방법을 보여줍니다.

  • 먼저 Document 클래스를 사용하여 Word 문서를 로드합니다.
  • 그런 다음 단락\by\style\name(Document, “제목 1”) 도우미 메서드를 사용하여 개체로 단락을 추출합니다.
  • 단락\by\style\name(Document, “제목 3”) 도우미 메서드를 사용하여 단락을 다른 개체로 추출합니다.
  • extract\content(startPara, endPara, True) 메서드를 호출하고 두 단락 배열의 첫 번째 요소를 첫 번째 및 두 번째 매개변수로 전달합니다.
  • generate\document(Document, extractNodes) 도우미 메서드를 호출하여 추출된 내용으로 구성된 문서를 만듭니다.
  • 마지막으로 Document.save(string) 메서드를 사용하여 반환된 문서를 저장합니다.

다음 코드 샘플은 스타일을 기반으로 단락 사이의 콘텐츠를 추출하는 방법을 보여줍니다.

# Load document
doc = aw.Document("Extract content.docx")

# Gather a list of the paragraphs using the respective heading styles.
parasStyleHeading1 = paragraphs_by_style_name(doc, "Heading 1")
parasStyleHeading3 = paragraphs_by_style_name(doc, "Heading 3")

# Use the first instance of the paragraphs with those styles.
startPara1 = parasStyleHeading1[0]
endPara1 = parasStyleHeading3[0]

# Extract the content between these nodes in the document. Don't include these markers in the extraction.
extractedNodes = extract_content(startPara1, endPara1, False)

# Generate document containing extracted content.
dstDoc = generate_document(doc, extractedNodes)

# Save document.
dstDoc.save("extract_content_between_paragraphs_based_on-Styles.docx")

더 읽기

문서 문서를 사용하여 Word 문서에서 텍스트를 추출하는 다른 시나리오를 탐색할 수 있습니다.

무료 API 라이선스 받기

평가 제한 없이 Aspose.Words for Python을 사용할 수 있는 임시 라이선스를 얻을 수 있습니다.

결론

이 기사에서는 Python을 사용하여 MS Word 문서에서 텍스트를 추출하는 방법을 배웠습니다. 또한 프로그래밍 방식으로 Word 문서에서 유사하거나 다른 유형의 노드 간에 콘텐츠를 추출하는 방법을 살펴보았습니다. 따라서 Python에서 자신만의 MS Word 텍스트 추출기를 구축할 수 있습니다. 게다가 문서를 사용하여 Aspose.Words for Python의 다른 기능을 탐색할 수 있습니다. 질문이 있는 경우 포럼을 통해 언제든지 알려주십시오.

또한보십시오

정보: PowerPoint 프레젠테이션에서 Word 문서를 가져와야 하는 경우 Aspose Presentation to Word Document 변환기를 사용할 수 있습니다.