テキストデータ抽出テクニック

正規表現だけでは難しい:PythonとNLPによる箇条書き・リストからの構造化データ抽出

Tags: Python, NLP, SpaCy, 情報抽出, テキスト処理, 構造化データ

はじめに

業務ドキュメントには、議事録の決定事項、仕様書の要件リスト、手順書のステップ、顧客からの要望リストなど、箇条書きやリスト形式で重要な情報が記述されていることが多くあります。これらの非構造化、あるいは半構造化されたテキストから、特定の情報をプログラムで効率的に抽出し、データベースに取り込んだり、タスク管理システムと連携させたりといったニーズは少なくありません。

単純な箇条書きであれば正規表現で各項目を抽出できる場合もあります。しかし、表現の揺れ、複雑なネスト、項目内の多様な情報(担当者、期限、状態など)を含む場合、正規表現だけでは網羅的な抽出が困難になります。

本記事では、Pythonと自然言語処理(NLP)のライブラリを活用し、このような箇条書きやリスト形式のテキストから、より高精度かつ柔軟に情報を抽出し、構造化する手法について解説します。特に、実務でよく遭遇する議事録からのタスク抽出を例に、具体的なコードとともに手順を示します。

箇条書き・リスト形式テキスト抽出の課題

箇条書きやリスト形式のテキスト抽出における主な課題は以下の通りです。

正規表現は強力ですが、これらの複雑さや多様性を網羅的にルール化するのは非常に骨が折れる作業であり、メンテナンス性も低くなりがちです。ここでNLPの技術が有効になります。

NLPを活用したアプローチの概要

NLPを活用した箇条書き・リスト形式テキスト抽出は、一般的に以下のステップで考えられます。

  1. テキストの前処理: 入力テキストを扱いやすい形式に整形します。
  2. 箇条書き行の識別: テキスト内のどの行が箇条書きの項目であるかを判定します。正規表現と、必要に応じてNLPによる補助を用います。
  3. 行内の情報抽出: 識別された箇条書きの各行から、目的とする情報(例: 担当者、期日、タスク内容など)を抽出します。ここでNLPライブラリ(SpaCyなど)が中心的な役割を果たします。
  4. 構造化: 抽出した情報を構造化されたデータ形式(例: リスト、辞書、DataFrameなど)にまとめます。

以降では、Pythonの正規表現モジュール re と、軽量かつ高性能なNLPライブラリである SpaCy を用いた具体的な実装方法を解説します。

箇条書き行の識別

まずは、入力テキストから箇条書きと思われる行を抽出します。ここでは正規表現が一定の効果を発揮します。一般的な箇条書きマーカーに対応する正規表現パターンを定義し、各行にマッチするかを確認します。

例:議事録のサンプルテキスト

sample_text = """
## 第N回定例会議議事録

議事概要:〇〇プロジェクトの進捗確認、課題検討

決定事項:
- 資料Aの最終確認と承認(担当:山田、期日:来週金曜)
- 仕様書Bのレビュー指摘事項反映(担当:佐藤)
- 外部連携APIの仕様詳細検討(担当:田中、期日:3月末まで)
    - 連携方式について技術検証を行う。
- 次回会議の日程調整(担当:議事録担当者、期日:〇月〇日)
- 顧客ヒアリング結果の分析と報告
"""

上記のテキストから、「- 」で始まる行を抽出するシンプルな正規表現は以下のようになります。

import re

def extract_bullet_points_simple(text):
    # 行頭の空白の後に '-' とスペースが続くパターン
    # ^\s* は行頭に0個以上の空白があることを意味します
    # - はハイフンリテラルにマッチします
    # \s+ は1個以上の空白にマッチします
    pattern = re.compile(r"^\s*-\s+(.*)")
    lines = text.strip().split('\n')
    bullet_points = []
    for line in lines:
        match = pattern.match(line)
        if match:
            # グループ1((.*))が箇条書きの内容です
            bullet_points.append(match.group(1).strip())
    return bullet_points

extracted_points = extract_bullet_points_simple(sample_text)
print("--- 単純な正規表現による抽出 ---")
for point in extracted_points:
    print(point)

実行結果:

--- 単純な正規表現による抽出 ---
資料Aの最終確認と承認(担当:山田、期日:来週金曜)
仕様書Bのレビュー指摘事項反映(担当:佐藤)
外部連携APIの仕様詳細検討(担当:田中、期日:3月末まで)
連携方式について技術検証を行う。
次回会議の日程調整(担当:議事録担当者、期日:〇月〇日)
顧客ヒアリング結果の分析と報告

この方法では、サブ項目も同じパターンにマッチするため抽出されます。また、1., * など異なるマーカーや、インデントのみで階層を示すケースには対応できません。より複雑な正規表現を記述することも可能ですが、すべてのパターンを網羅するのは現実的ではありません。

より堅牢な実装では、行のインデントレベルを考慮したり、連続する箇条書き項目を一つのリストとして扱ったりといった前処理が必要になりますが、本記事では、各行が抽出された後の「行内の情報抽出」に焦点を当てます。上記の単純な方法で抽出された各行を対象として、NLPによる処理を進めます。

行内の情報抽出にNLPを活用する

抽出された各箇条書きの行は、比較的短いテキストであることが多いです。この短いテキストから、タスク内容、担当者、期日などの特定の情報を抜き出す際に、SpaCyのようなNLPライブラリが非常に役立ちます。

SpaCyは、テキストのトークン化、品詞タグ付け、固有表現抽出(NER)、依存構造解析などを高速に行うことができます。これらの機能を使って、行内の単語の関係性や意味的な役割を捉え、必要な情報を識別します。

まず、SpaCyをインストールします。

pip install spacy
python -m spacy download ja_core_news_sm # 日本語モデルのダウンロード

次に、抽出された各箇条書きの行に対してSpaCyを適用し、NERや依存構造解析を行います。

import spacy

# 日本語モデルをロード
nlp = spacy.load("ja_core_news_sm")

def extract_info_from_task_line(line):
    doc = nlp(line)

    # 抽出する情報の格納用辞書
    info = {
        "task": line, # 初期値は行全体
        "assignee": None,
        "deadline": None,
        "mentions": [] # その他の固有表現などを格納
    }

    # 固有表現抽出 (Named Entity Recognition: NER)
    # SpaCyの標準モデルは日本語の人名、組織名、日付などを識別できます
    for ent in doc.ents:
        # PERSONエンティティを担当者候補とする
        if ent.label_ == "PERSON":
            # 行内に「担当:〇〇」のようなパターンがあれば、それを優先する
            # この例では単純にPERSONを拾います
            if info["assignee"] is None: # 最初のPERSONを担当者とする(簡易)
                 info["assignee"] = ent.text
            # 例外的に、エンティティが「担当」のような役割を示唆する場合は無視するなどの考慮が必要
            # 例:「担当者」という文字列自体がPERSONとして検出される場合など

        # DATEエンティティを期日候補とする
        elif ent.label_ == "DATE":
            info["deadline"] = ent.text

        # その他、業務に必要なエンティティを拾う
        elif ent.label_ in ["ORG", "PRODUCT", "LOCATION"]: # 例: 組織、製品、場所
             info["mentions"].append({"text": ent.text, "label": ent.label_})

    # 依存構造解析 (Dependency Parsing) や品詞タグ付け (POS)
    # より詳細な情報や関係性を抽出する場合に利用します
    # 例:「〇〇の確認」「〇〇を作成する」といった動詞と目的語の関係を捉える
    task_phrases = []
    for token in doc:
        # 特定の品詞や依存関係を持つ単語を抽出
        # 例:root(文の中心動詞)やdobj(直接目的語)など
        if token.dep_ in ["ROOT", "dobj", "obl"]: # 簡易的な例
            task_phrases.append(token.text)

        # 担当者や期日情報が「担当:〇〇」「期日:〇〇」のような形式の場合
        # 正規表現と組み合わせることでより正確に抽出できる
        match_assignee = re.search(r"担当[::](\S+)", token.text) # 例: 「担当:山田」
        if match_assignee:
            info["assignee"] = match_assignee.group(1).strip(")") # 末尾の括弧などを削除

        match_deadline = re.search(r"期日[::](\S+)", token.text) # 例: 「期日:来週金曜)
        if match_deadline:
            info["deadline"] = match_deadline.group(1).strip(")")

    # タスク内容をより正確に定義する(ここでは簡易的に全体行としている)
    # 実際には、動詞とその目的語、関連名詞などを組み合わせてタスク内容とする
    # 例:「資料Aの最終確認と承認」 -> 「確認」「承認」(対象:資料A)
    # 簡易的に、NERで抽出した担当者や期日部分を除外するなどの方法も考えられます

    return info

# 単純正規表現で抽出した行に対して情報抽出を適用
extracted_structured_data = []
for line in extracted_points:
    info = extract_info_from_task_line(line)
    extracted_structured_data.append(info)

print("\n--- NLP(SpaCy)による情報抽出 ---")
for item in extracted_structured_data:
    print(item)

実行結果 (例):

--- NLP(SpaCy)による情報抽出 ---
{'task': '資料Aの最終確認と承認(担当:山田、期日:来週金曜)', 'assignee': '山田', 'deadline': '来週金曜', 'mentions': [{'text': '資料A', 'label': 'PRODUCT'}]}
{'task': '仕様書Bのレビュー指摘事項反映(担当:佐藤)', 'assignee': '佐藤', 'deadline': None, 'mentions': [{'text': '仕様書B', 'label': 'PRODUCT'}]}
{'task': '外部連携APIの仕様詳細検討(担当:田中、期日:3月末まで)', 'assignee': '田中', 'deadline': '3月末', 'mentions': [{'text': '外部連携API', 'label': 'PRODUCT'}]}
{'task': '連携方式について技術検証を行う。', 'assignee': None, 'deadline': None, 'mentions': []}
{'task': '次回会議の日程調整(担当:議事録担当者、期日:〇月〇日)', 'assignee': '議事録担当者', 'deadline': '〇月〇日', 'mentions': []}
{'task': '顧客ヒアリング結果の分析と報告', 'assignee': None, 'deadline': None, 'mentions': []}

上記の例では、doc.ents を使った固有表現抽出と、簡単な正規表現 (担当:〇〇, 期日:〇〇) を組み合わせることで、担当者と期日を抽出しています。SpaCyの標準NERが日本語の日付や人名をある程度捉えられるため、この組み合わせは比較的効果的です。

ただし、担当:〇〇 のような明確なパターンがない場合や、標準NERで識別できないカスタムエンティティ(例: 社内の特定のプロジェクト名、ステータス表現など)を抽出したい場合は、以下の発展的な手法が必要になります。

より高度な情報抽出のためのテクニック

  1. カスタムエンティティ抽出 (Custom NER): 業務固有のエンティティ(例: 社内のシステム名、製品コード、特定の契約用語など)は、標準のSpaCyモデルでは識別できません。これらのエンティティを抽出するには、カスタムモデルを学習させるか、ルールベースのMatcherを使用します。SpaCyのMatcherPhraseMatcherを使うと、辞書やパターンのリストに基づいて高速に文字列やトークンのパターンにマッチさせることができます。

    例:特定のステータス表現(「対応中」「完了」「要確認」など)を抽出するMatcherの利用

    ```python from spacy.matcher import PhraseMatcher

    Statusエンティティとして抽出したい語句リスト

    status_patterns = ["対応中", "完了", "要確認", "ペンディング"]

    matcher = PhraseMatcher(nlp.vocab, attr="TEXT") # トークンテキストにマッチ matcher.add("STATUS", [nlp(text) for text in status_patterns])

    def extract_info_with_matcher(line): doc = nlp(line) info = {"task": line, "status": None}

    # Matcherの適用
    matches = matcher(doc)
    for match_id, start, end in matches:
        span = doc[start:end]
        # 一旦最初に見つかったステータスを採用(簡易)
        if info["status"] is None:
            info["status"] = span.text
        # 複数見つかる場合はリストにするなどの工夫が必要
    
    return info
    

    例: ステータスを含む箇条書き行

    sample_line_with_status = "資料Bのレビュー指摘事項反映(担当:佐藤) -> 完了" info_with_status = extract_info_with_matcher(sample_line_with_status) print("\n--- Matcherによるステータス抽出 ---") print(info_with_status) ```

    実行結果 (例):

    --- Matcherによるステータス抽出 --- {'task': '資料Bのレビュー指摘事項反映(担当:佐藤) -> 完了', 'status': '完了'}

  2. 依存構造や品詞情報に基づいたルール: 「〇〇のレビュー指摘事項反映」のようなフレーズから「レビュー指摘事項反映」がタスク内容であり、「〇〇」が対象であることを特定するには、依存構造解析が役立ちます。SpaCyのtoken.dep_token.pos_を調べ、特定の動詞とその目的語、または関連する名詞句を組み合わせるルールを構築します。

    例:「〇〇のレビュー」のようなパターンを検出する(概念的なコード)

    ```python def extract_task_target(line): doc = nlp(line) task_target = {"task_verb_phrase": None, "target_noun": None}

    # 簡単な例: 動詞の目的語やそれに先行する名詞を対象とする
    for token in doc:
        # 「レビュー」のような名詞に注目
        if token.lemma_ == "レビュー" and token.pos_ == "NOUN":
            # この名詞に依存する前置詞句などを探す(例: 「〇〇の」)
            for child in token.children:
                if child.dep_ == "nmod" or child.dep_ == "obl": # 例: 「〇〇の」の「〇〇」
                     task_target["target_noun"] = child.text
            task_target["task_verb_phrase"] = token.text + "指摘事項反映" # 例として結合
            break # 一旦最初に見つかったパターンで終了
    
    return task_target
    

    sample_line = "仕様書Bのレビュー指摘事項反映(担当:佐藤)" task_info = extract_task_target(sample_line) print("\n--- 依存構造/品詞に基づくタスク要素抽出 ---") print(task_info) ``` (注:上記のコード例は依存構造解析の概念を示すための簡易的なものです。実際の依存関係はより複雑で、robustなルール構築には試行錯誤が必要です。)

これらの高度なテクニックを組み合わせることで、より複雑な情報構造を持つ箇条書きから、担当者、期日、ステータス、関連するオブジェクト、タスク内容といった要素を分離し、構造化されたデータとして抽出することが可能になります。

抽出した情報の構造化

抽出した情報をどのように構造化するかは、その後の利用目的によって異なります。一般的なのは、各箇条書き項目を1つのレコードとして、抽出した要素をフィールドとする形式です。Pythonであれば、辞書のリストとして表現するのが直感的です。

# 前述の extracted_structured_data は既に辞書のリスト形式です
# [{'task': '...', 'assignee': '...', 'deadline': '...', 'mentions': [...]}, ...]

# このリストをPandas DataFrameに変換すると、さらに分析や加工が容易になります
import pandas as pd

df = pd.DataFrame(extracted_structured_data)
print("\n--- Pandas DataFrameへの変換 ---")
print(df)

実行結果 (例):

--- Pandas DataFrameへの変換 ---
                                                    task assignee deadline                     mentions
0    資料Aの最終確認と承認(担当:山田、期日:来週金曜)     山田   来週金曜  [{'text': '資料A', 'label': 'PRODUCT'}]
1            仕様書Bのレビュー指摘事項反映(担当:佐藤)     佐藤     None  [{'text': '仕様書B', 'label': 'PRODUCT'}]
2  外部連携APIの仕様詳細検討(担当:田中、期日:3月末まで)     田中    3月末  [{'text': '外部連携API', 'label': 'PRODUCT'}]
3                         連携方式について技術検証を行う。   None     None                           []
4         次回会議の日程調整(担当:議事録担当者、期日:〇月〇日)  議事録担当者   〇月〇日                           []
5                          顧客ヒアリング結果の分析と報告   None     None                           []

DataFrame形式にすることで、特定の担当者のタスクを絞り込んだり、期日が近いタスクを抽出したりといったデータ操作が容易になります。

実務への応用と考慮事項

応用事例

パフォーマンスと注意点

まとめ

本記事では、PythonとSpaCyを組み合わせることで、正規表現だけでは対応が難しい箇条書きやリスト形式のテキストから、担当者、期日、タスク内容といった具体的な情報を効率的に抽出・構造化する手法を解説しました。

これらの技術は、非構造化テキストからの情報活用を進める上で強力なツールとなります。ぜひ、皆様の業務課題解決にご活用ください。