PythonでWordDOCXドキュメントからコンテンツを抽出する

Word文書からのテキスト抽出は、多くの場合、さまざまなシナリオで実行されます。たとえば、テキストを分析したり、ドキュメントの特定のセクションを抽出してそれらを1つのドキュメントに結合したりします。この記事では、PythonでプログラムによってWord文書からテキストを抽出する方法を学習します。さらに、段落や表などの特定の要素間でコンテンツを動的に抽出する方法についても説明します。

Word文書からテキストを抽出するPythonライブラリ

Aspose.Words for Pythonは、MSWordドキュメントを最初から作成できる強力なライブラリです。さらに、暗号化、変換、テキスト抽出などのために既存のWordドキュメントを操作できます。このライブラリを使用して、WordDOCXまたはDOCドキュメントからテキストを抽出します。次のpipコマンドを使用して、PyPIからライブラリをインストールできます。

pip install aspose-words

Pythonを使用したWord文書でのテキスト抽出

MS Word文書は、段落、表、画像などを含むさまざまな要素で構成されています。したがって、テキスト抽出の要件は、シナリオごとに異なる可能性があります。たとえば、段落、ブックマーク、コメントなどの間にテキストを抽出する必要がある場合があります。

Word文書の各タイプの要素は、ノードとして表されます。したがって、ドキュメントを処理するには、ノードで遊ぶ必要があります。それでは、さまざまなシナリオでWord文書からテキストを抽出する方法を見てみましょう。

PythonでWord文書からテキストを抽出する

このセクションでは、Word文書用のPythonテキスト抽出機能を実装します。テキスト抽出のワークフローは次のようになります。

  • まず、テキスト抽出プロセスに含めるノードを定義します。
  • 次に、指定したノード(開始ノードと終了ノードを含む、または含まない)間のコンテンツを抽出します。
  • 最後に、抽出されたノードのクローンを使用します。たとえば、抽出されたコンテンツで構成される新しいWordドキュメントを作成します。

次に、extract_content という名前のメソッドを作成します。このメソッドに、ノードとその他のパラメーターを渡して、テキスト抽出を実行します。このメソッドは、ドキュメントを解析し、ノードのクローンを作成します。以下は、このメソッドに渡すパラメーターです。

  1. コンテンツを抽出するための開始点と終了点として、それぞれStartNodeEndNode。これらは、ブロックレベル(段落、表)またはインラインレベル(RunFieldStartBookmarkStartなど)の両方のノードにすることができます。
    1. フィールドを渡すには、対応するFieldStartオブジェクトを渡す必要があります。
    2. ブックマークを渡すには、BookmarkStartノードとBookmarkEndノードを渡す必要があります。
    3. コメントには、CommentRangeStartノードとCommentRangeEndノードを使用する必要があります。
  2. IsInclusiveは、マーカーが抽出に含まれるかどうかを定義します。このオプションがfalseに設定されていて、同じノードまたは連続するノードが渡された場合、空のリストが返されます。

以下は、渡されたノード間でコンテンツを抽出するextract_contentメソッドの完全な実装です。

def extract_content(startNode : aw.Node, endNode : aw.Node, isInclusive : bool):
    
    # まず、このメソッドに渡されたノードが有効であることを確認してください。
    verify_parameter_nodes(startNode, endNode)

    # 抽出したノードを格納するリストを作成します。
    nodes = []

    # いずれかのマーカーがコメント自体を含むコメントの一部である場合、ポインタを CommentRangeEnd 
    # ノードの後にある Comment ノードに移動する必要があります。
    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: マーカーが同じノードである場合は、ノードを 2 回追加しないでください。
                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)

        # 次のノードに移動して抽出します。 次のノードが None の場合、残りのコンテンツは別のセクションにあります。
        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

以下に示すように、テキスト抽出操作を実行するために、extract_contentメソッドによっていくつかのヘルパーメソッドも必要になります。

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):

    # ノードが Paragraph または Table ノードの子孫であり、段落またはテーブルではないかどうかをテストします。段落の適切なコメント クラス内の段落は可能です。
    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

    # マーカー ノードがドキュメント ボディの第 3 レベル以下にある場合のケースをサポートします。
    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文書の段落間のテキストを抽出する

WordDOCXドキュメントの2つの段落の間のコンテンツを抽出する方法を見てみましょう。以下は、Pythonでこの操作を実行するための手順です。

  • まず、Documentクラスを使用してWord文書をロードします。
  • Document.first_section.body.get_child(NodeType.PARAGRAPH、int, boolean).as_paragraph() メソッドを使用して、開始段落と終了段落の参照を2つのオブジェクトに取得します。
  • extract_content(startPara, endPara, True) メソッドを呼び出して、ノードをオブジェクトに抽出します。
  • generate_document(Document, extractedNodes) ヘルパーメソッドを呼び出して、抽出されたコンテンツで構成されるドキュメントを作成します。
  • 最後に、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.first_section.body.get_child(NodeType, int, boolean) メソッドを使用して、開始ノードと終了ノードの参照を2つのオブジェクトに取得します。
  • extract_content(startPara, endPara, True) メソッドを呼び出して、ノードをオブジェクトに抽出します。
  • generate_document(Document, extractedNodes) ヘルパーメソッドを呼び出して、抽出されたコンテンツで構成されるドキュメントを作成します。
  • 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文書をロードします。
  • 次に、paragraphs_by_style_name(Document, “Heading 1”) ヘルパーメソッドを使用して、段落をオブジェクトに抽出します。
  • paragraphs_by_style_name(Document, “Heading 3”) ヘルパーメソッドを使用して、段落を別のオブジェクトに抽出します。
  • extract_content(startPara, endPara, True) メソッドを呼び出し、両方の段落配列の最初の要素を最初と2番目のパラメーターとして渡します。
  • generate_document(Document, extractedNodes) ヘルパーメソッドを呼び出して、抽出されたコンテンツで構成されるドキュメントを作成します。
  • 最後に、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を使用してMSWord文書からテキストを抽出する方法を学びました。さらに、Word文書内の類似または異なるタイプのノード間でプログラムによってコンテンツを抽出する方法を見てきました。したがって、Pythonで独自のMSWordテキストエクストラクタを構築できます。さらに、ドキュメントを使用して、Aspose.Words for Pythonの他の機能を調べることができます。ご不明な点がございましたら、フォーラムからお気軽にお問い合わせください。

関連項目

情報:PowerPointプレゼンテーションからWord文書を取得する必要がある場合は、AsposeプレゼンテーションからWord文書コンバーターを使用できます。