0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonでPDFからテキスト抽出・キーワード分析を行ってみた

0
Posted at

PythonでPDFのテキスト抽出と解析

PDFファイルからのテキスト抽出と解析プログラムの開発を行います

開発環境

  • Python
  • Visual Studio Code
  • venv

使用ライブラリ

  • pdfplumber
  • janome
  • re
  • collection.Counter

実装した機能

  • PDFファイルの読み込み
  • 本文テキストの抽出
  • 抽出したテキストの保存
  • 不要文字(目次・ページ番号・参考文献など)の除去
  • キーワード抽出(名詞のみ)
  • 単語の出現頻度分析
  • 複数PDFの一括処理
  • 研究分野の推定
  • 分析結果のCSV・テキスト出力

コード

import glob
import os
import csv
import pdfplumber
import re
from janome.tokenizer import Tokenizer
from collections import Counter


# --------------------------------
# 入力フォルダ・出力フォルダ
# --------------------------------
PDF_PATTERN = "venv/R07/*.pdf"
OUTPUT_DIR = "results/R07"

# 出力フォルダが存在しない場合は作成
os.makedirs(OUTPUT_DIR, exist_ok=True)


# --------------------------------
# ストップワード
# --------------------------------
STOP_WORDS = {
    "こと", "ため", "もの", "よう", "これ", "それ",
    "ここ", "そこ", "あれ", "ところ", "とき",
    "うち", "ほう", "", "", "",
    "", "", "", "", "", "",
    "可能", "機能", "利用"
}


# --------------------------------
# 研究分野ごとの代表キーワード
# --------------------------------
FIELD_KEYWORDS = {
    "AI・機械学習系": {
        "AI", "人工知能", "機械学習", "深層学習",
        "ニューラルネットワーク", "学習", "予測",
        "分類", "モデル", "教師", "データセット"
    },

    "画像・映像処理系": {
        "画像", "映像", "認識", "検出",
        "カメラ", "画素", "物体", "特徴量",
        "OpenCV", "CNN", "顔認識"
    },

    "Web・データベース系": {
        "Web", "データベース", "SQL", "PHP",
        "HTML", "CSS", "JavaScript", "サーバ",
        "ブラウザ", "ログイン", "登録", "更新",
        "削除", "アップロード", "ファイル"
    },

    "ネットワーク・セキュリティ系": {
        "ネットワーク", "通信", "TCP", "IP",
        "パケット", "暗号", "セキュリティ",
        "認証", "攻撃", "脆弱性", "パスワード",
        "アクセス"
    },

    "組込み・IoT系": {
        "Arduino", "マイコン", "センサ", "IoT",
        "回路", "制御", "LED", "距離",
        "温度", "電圧", "モータ", "装置"
    },

    "ソフトウェア・システム開発系": {
        "システム", "設計", "実装", "開発",
        "処理", "操作", "画面", "ユーザ",
        "管理", "ソフトウェア", "プログラム"
    }
}



# --------------------------------
# 論文タイトルを取得する関数
# --------------------------------
def extract_title(first_page_text, pdf_name):
    """
    1ページ目のテキストから論文タイトルを取得する。
    1行・2行・3行以上のタイトルに対応し、
    学籍番号、大学名、学部・学科名、年月などが現れるまでの行を結合する。
    取得できない場合はPDFのファイル名を使用する。
    """
    if not first_page_text:
        return pdf_name

    student_id_pattern = re.compile(r"^\d{2}[A-Za-z]{2}\d{3}$")
    date_pattern = re.compile(
        r"^(令和|平成|昭和)?\s*\d+年(?:\s*\d+月)?$"
    )

    title_lines = []

    for raw_line in first_page_text.splitlines():
        line = raw_line.strip()

        if not line:
            continue

        # 学籍番号が出たら、タイトル部分の終了
        if student_id_pattern.fullmatch(line):
            break

        # 大学名・学部・学科名・年月が出たら、タイトル部分の終了
        if "九州産業大学" in line:
            break

        if line in {
            "理工学部",
            "情報科学科",
            "大学院",
            "九州産業大学理工学部",
            "九州産業大学 理工学部"
        }:
            break

        if date_pattern.fullmatch(line):
            break

        # 数字や記号だけの行はタイトルに含めない
        if re.fullmatch(r"[\d\W_]+", line):
            continue

        title_lines.append(line)

    if title_lines:
        # 改行されたタイトルを1行につなげる
        return "".join(title_lines)

    return pdf_name


# --------------------------------
# 不要文字を除去する関数
# --------------------------------
def clean_extracted_text(all_text):
    clean_text = all_text

    # 目次の点線を削除
    clean_text = re.sub(
        r"\s*\. \. \. .*",
        "",
        clean_text
    )

    # ページ番号だけの行を削除
    clean_text = re.sub(
        r"^\d+$",
        "",
        clean_text,
        flags=re.MULTILINE
    )

    # ローマ数字ページ番号だけの行を削除
    clean_text = re.sub(
        r"^(i|ii|iii|iv|v|vi|vii|viii|ix|x)$",
        "",
        clean_text,
        flags=re.MULTILINE | re.IGNORECASE
    )

    # 目次・図目次・表目次の見出しを削除
    clean_text = clean_text.replace("目 次", "")
    clean_text = clean_text.replace("図 目 次", "")
    clean_text = clean_text.replace("表 目 次", "")

    # 空白なしの表記にも対応
    clean_text = re.sub(
        r"^(目次|図目次|表目次)$",
        "",
        clean_text,
        flags=re.MULTILINE
    )

    # 図番号の行を削除
    clean_text = re.sub(
        r"^図\s*\d+(?:\.\d+)*\s*[::]?.*$",
        "",
        clean_text,
        flags=re.MULTILINE
    )

    # 表番号の行を削除
    clean_text = re.sub(
        r"^表\s*\d+(?:\.\d+)*\s*[::]?.*$",
        "",
        clean_text,
        flags=re.MULTILINE
    )

    # URLを削除
    clean_text = re.sub(
        r"https?://\S+",
        "",
        clean_text
    )

    # --------------------------------
    # 目次部分を削除
    # 「第1章 序論」が2回ある場合、
    # 2回目以降を本文とみなす
    # --------------------------------
    first = clean_text.find("第1章 序論")

    if first != -1:
        second = clean_text.find(
            "第1章 序論",
            first + len("第1章 序論")
        )

        if second != -1:
            clean_text = clean_text[second:]

    # 謝辞・参考文献以降を削除
    # 本文の目次を先に削除した後で実行する
    clean_text = re.sub(
        r"^謝辞\s*$.*?(?=^参考文献\s*$|\Z)",
        "",
        clean_text,
        flags=re.MULTILINE | re.DOTALL
    )

    clean_text = re.sub(
        r"^(参考文献|引用文献)\s*$.*",
        "",
        clean_text,
        flags=re.MULTILINE | re.DOTALL
    )

    # 行末の不要な空白を削除
    clean_text = re.sub(
        r"[ \t]+$",
        "",
        clean_text,
        flags=re.MULTILINE
    )

    # 余分な空行を整理
    clean_text = re.sub(
        r"\n{2,}",
        "\n",
        clean_text
    )

    return clean_text.strip()


# --------------------------------
# 名詞を抽出する関数
# --------------------------------
def extract_keywords(clean_text, tokenizer):
    keywords = []

    for token in tokenizer.tokenize(clean_text):
        word = token.surface.strip()
        part_of_speech = token.part_of_speech.split(",")[0]

        if part_of_speech != "名詞":
            continue

        # ストップワードを除外
        if word in STOP_WORDS:
            continue

        # 1文字の語を除外
        if len(word) <= 1:
            continue

        # 数字だけの語を除外
        if word.isdigit():
            continue

        # 記号だけの語を除外
        if re.fullmatch(r"[\W_]+", word):
            continue

        keywords.append(word)

    return keywords


# --------------------------------
# 研究分野を推定する関数
# --------------------------------
def estimate_research_field(word_count):
    field_scores = {}
    field_matches = {}

    for field, representative_words in FIELD_KEYWORDS.items():
        score = 0
        matches = []

        for word in representative_words:
            count = word_count.get(word, 0)

            if count > 0:
                score += count
                matches.append((word, count))

        field_scores[field] = score

        field_matches[field] = sorted(
            matches,
            key=lambda item: item[1],
            reverse=True
        )

    sorted_fields = sorted(
        field_scores.items(),
        key=lambda item: item[1],
        reverse=True
    )

    estimated_field, estimated_score = sorted_fields[0]

    if estimated_score == 0:
        estimated_field = "分類不能"
        matched_keywords = []
    else:
        matched_keywords = field_matches[estimated_field][:5]

    return (
        estimated_field,
        estimated_score,
        matched_keywords,
        sorted_fields
    )


# --------------------------------
# 1つのPDFを分析する関数
# --------------------------------
def analyze_pdf(pdf_path, tokenizer):
    all_text = ""

    # 拡張子を除いたPDF名
    pdf_name = os.path.splitext(
        os.path.basename(pdf_path)
    )[0]

    # PDF読み込み
    first_page_text = ""

    with pdfplumber.open(pdf_path) as pdf:
        page_count = len(pdf.pages)

        for page_number, page in enumerate(pdf.pages):
            text = page.extract_text()

            if page_number == 0 and text:
                first_page_text = text

            if text:
                all_text += text + "\n"

    # 論文タイトル取得
    title = extract_title(first_page_text, pdf_name)

    # 不要文字除去
    clean_text = clean_extracted_text(all_text)

    # キーワード抽出
    keywords = extract_keywords(
        clean_text,
        tokenizer
    )

    # 出現頻度分析
    word_count = Counter(keywords)
    ranking = word_count.most_common(20)

    # 研究分野推定
    (
        estimated_field,
        estimated_score,
        matched_keywords,
        sorted_fields
    ) = estimate_research_field(word_count)

    # --------------------------------
    # 保存先
    # --------------------------------
    clean_path = os.path.join(
        OUTPUT_DIR,
        f"{pdf_name}_clean.txt"
    )

    keywords_path = os.path.join(
        OUTPUT_DIR,
        f"{pdf_name}_keywords.txt"
    )

    frequency_path = os.path.join(
        OUTPUT_DIR,
        f"{pdf_name}_frequency.txt"
    )

    # クリーンテキスト保存
    with open(
        clean_path,
        "w",
        encoding="utf-8"
    ) as f:
        f.write(clean_text)

    # キーワード保存
    with open(
        keywords_path,
        "w",
        encoding="utf-8"
    ) as f:
        for word in keywords:
            f.write(word + "\n")

    # 頻度分析・分野推定結果保存
    with open(
        frequency_path,
        "w",
        encoding="utf-8"
    ) as f:
        f.write("【PDF分析結果】\n")
        f.write(f"タイトル:{title}\n")
        f.write(f"ファイル名:{os.path.basename(pdf_path)}\n")
        f.write(f"ページ数:{page_count}\n")
        f.write(f"除去前文字数:{len(all_text)}\n")
        f.write(f"除去後文字数:{len(clean_text)}\n")
        f.write(f"抽出キーワード数:{len(keywords)}\n")

        f.write("\n【キーワード出現頻度ランキング】\n")

        for rank, (word, count) in enumerate(
            ranking,
            start=1
        ):
            f.write(
                f"{rank}{word}  {count}\n"
            )

        f.write("\n【研究分野の推定】\n")
        f.write(f"推定分野:{estimated_field}\n")
        f.write(f"判定スコア:{estimated_score}\n")

        if matched_keywords:
            f.write("\n判定に使用した主なキーワード\n")

            for word, count in matched_keywords:
                f.write(f"{word}{count}\n")

        f.write("\n【分野別スコア】\n")

        for field, score in sorted_fields:
            f.write(f"{field}{score}\n")

    # ターミナル表示
    print("\n" + "=" * 60)
    print("処理ファイル:", os.path.basename(pdf_path))
    print("=" * 60)
    print("タイトル:", title)
    print("ページ数:", page_count)
    print("除去前文字数:", len(all_text))
    print("除去後文字数:", len(clean_text))
    print("抽出キーワード数:", len(keywords))

    print("\n【キーワード出現頻度ランキング】")

    for rank, (word, count) in enumerate(
        ranking,
        start=1
    ):
        print(f"{rank:>2}{word}  {count}")

    print("\n【研究分野の推定】")
    print("推定分野:", estimated_field)
    print("判定スコア:", estimated_score)

    if matched_keywords:
        print("判定に使用した主なキーワード:")

        for word, count in matched_keywords:
            print(f"{word}: {count}")

    print("\n保存完了:")
    print(clean_path)
    print(keywords_path)
    print(frequency_path)

    # 複数PDFのまとめに使用する情報
    return {
        "タイトル": title,
        "ファイル名": os.path.basename(pdf_path),
        "ページ数": page_count,
        "除去前文字数": len(all_text),
        "除去後文字数": len(clean_text),
        "抽出キーワード数": len(keywords),
        "推定分野": estimated_field,
        "判定スコア": estimated_score
    }


# ================================================
# メイン処理
# ================================================

# R07フォルダ内のPDFをすべて取得
pdf_files = sorted(
    glob.glob(PDF_PATTERN)
)

if not pdf_files:
    print("PDFファイルが見つかりません。")
    print("確認する場所:", PDF_PATTERN)
    raise SystemExit

print("検出したPDF数:", len(pdf_files))

tokenizer = Tokenizer()
summary_results = []

# PDFを1冊ずつ処理
for pdf_path in pdf_files:
    try:
        result = analyze_pdf(
            pdf_path,
            tokenizer
        )

        summary_results.append(result)

    except Exception as error:
        print("\n処理中にエラーが発生しました。")
        print("対象ファイル:", pdf_path)
        print("エラー内容:", error)


# --------------------------------
# 全PDFの分析結果をCSVにまとめる
# --------------------------------
summary_path = os.path.join(
    OUTPUT_DIR,
    "analysis_summary.csv"
)

with open(
    summary_path,
    "w",
    encoding="utf-8-sig",
    newline=""
) as f:
    fieldnames = [
        "タイトル",
        "ファイル名",
        "ページ数",
        "除去前文字数",
        "除去後文字数",
        "抽出キーワード数",
        "推定分野",
        "判定スコア"
    ]

    writer = csv.DictWriter(
        f,
        fieldnames=fieldnames
    )

    writer.writeheader()
    writer.writerows(summary_results)


# --------------------------------
# 最終表示
# --------------------------------
print("\n" + "=" * 60)
print("すべてのPDFの処理が終了しました。")
print("処理成功数:", len(summary_results))
print("分析結果の保存先:", OUTPUT_DIR)
print("比較用一覧:", summary_path)
print("=" * 60)

実行結果

  • 論文タイトル
  • ページ数
  • 抽出文字数
  • 不要文字除去後の文字数
  • キーワード一覧
  • 単語の出現頻度ランキング
  • 推定された研究分野
                を得ることができる。
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?