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)
実行結果
- 論文タイトル
- ページ数
- 抽出文字数
- 不要文字除去後の文字数
- キーワード一覧
- 単語の出現頻度ランキング
- 推定された研究分野
を得ることができる。