"""
学術論文のPDFからメタデータを抽出し、ファイルのリネームやExcelサマリーへの出力を行うスクリプト。
このスクリプトは、指定されたPDFファイルからテキストを抽出し、
OpenAI (GPT-4/GPT-5-nano) または Google Gemini などの生成AIモデルに送信して、
論文の著者、タイトル、ジャーナル、発行年などの情報をJSON形式で取得します。
取得したメタデータはExcelファイルに追記され、オプションでPDFファイルの名前を
AIが推奨するファイル名に変更することができます。
関連リンク: :doc:`get_paper_inf5_usage`
"""
import argparse
import os
import shutil
import glob
import io
try:
from openpyxl import Workbook, load_workbook
except:
print("Error: Failed to import openpyxl")
input("Install: pip install openpyxl")
try:
import PyPDF2
except:
print("Error: Failed to import PyPDF2")
input("Install: pip install pypdf2")
from tkai_lib import read_ai_config
from tkai_lib import query_openai4, openai_response_to_json
from tkai_lib import query_openai5, openai5_response_to_json
from tkai_lib import query_google, google_response_to_json
#=========================================================
# Global configuration
#=========================================================
config_path = "ai.env"
#=========================================================
# Read configuration
#=========================================================
read_ai_config(config_path)
max_tokens = int(os.getenv("max_tokens", "2000"))
openai_api_key = os.getenv("OPENAI_API_KEY")
openai_model = os.getenv("openai_model", "gpt-4o")
temperature = float(os.getenv("temperature", "0.3"))
openai_model5 = os.getenv("openai_model5", "gpt-5-nano")
reasoning_effort = os.getenv("reasoning_effort", "low")
google_api_key = os.getenv("GOOGLE_API_KEY")
google_model = os.getenv("gemini_model", "gemini-2.5-flash")
#=========================================================
# Parameters
#=========================================================
DEFAULT_TEMPLATE = "(author_first)_(author_last)_(short_title)_(shortest_name)_(year).pdf"
xlsx_labels = [
"directory", "filename_original", "filename_rename",
"authors", "author_first", "author_last",
"title", "short_title", "journal", "short_name", "shortest_name",
"date_received", "date_accepted", "date_published_online", "date_published",
"year", "volume", "issue", "pages", "doi",
]
# Prompt template: start
prompt_template = """
以下の学術論文のテキストから、指定された情報をJSON形式で抽出してください。
また、first author、last author、短縮したtitle (short_title)、shortest_name、発行年から
推奨ファイル名 {{template}} を作ってください。
short_titleの単語はCaptalFirstにして、空白、.、,などの文字は削除してください。
推奨ファイル名(filename_rename):
著者リスト(authors) :
第一著者(author_first):
最終著者(author_last) :
論文題目(title) :
論文題目略称(short_title) :
発行雑誌名(journal) :
発行雑誌名略称(short_name):
発行雑誌名の最短の略称(shortest_name):
投稿受理日(date_received) :
採択日(date_accepted) :
オンライン発行日(date_published_online):
発行日(date_published):
発行年(year) :
巻番号(volume):
号番号(issue) :
ページ(pages ):
DOI(doi):
情報が不明な場合は、空文字列 "" を使用してください。
--- 論文テキスト ---
{{text}}
"""
# Prompt template: end
[ドキュメント]
def initialize():
"""
コマンドライン引数を解析するためのArgumentParserを設定します。
処理するPDFファイルのパス、使用するAI API、サマリーExcelのパス、
再帰検索の有無、ファイルリネームの有無、元のファイル削除の有無、
APIに送る最大テキストバイト数、ファイル名テンプレートなどの引数を定義します。
:returns: 設定済みのArgumentParserオブジェクト。
:rtype: argparse.ArgumentParser
"""
parser = argparse.ArgumentParser(description="学術論文のPDFからメタデータを抽出します。")
parser.add_argument("--api", type=str, default='openai5',
choices = ["openai", "openai5", "google"],
help="使用する API を指定(openai5|openai|google: デフォルト: openai5)")
parser.add_argument("input_file", type=str, help="処理するPDFファイルのパス")
parser.add_argument("--summary_path", type=str, default = "summary.xlsx",
help="summary.xlsxのパス")
parser.add_argument("--recursive", type=int, default=0,
help="1の場合、サブディレクトリまで処理する")
parser.add_argument("--rename", type=int, default=0,
help="1の場合、生成AIで推薦ファイル名を取得できた場合、リネームする")
# parser.add_argument("--rename", action="store_true",
# help="生成AIで推薦ファイル名を取得できた場合、リネームする")
parser.add_argument("--delete_original", type=int, default=1,
help="1の場合、renameしたら元のファイルを削除する")
parser.add_argument("--max_bytes", type=int, default=10000,
help="APIに送る最大テキストバイト数。論文全体を送る必要がない場合に指定。")
parser.add_argument("--template", type=str, default=DEFAULT_TEMPLATE,
help="ファイル名を変える際のテンプレート {{DEFAULT_TEMPLATE}}")
return parser
[ドキュメント]
def append_xlsx(path, mode, labels, data_list):
"""
Excelファイルにデータを追記します。
指定されたパスのExcelファイルが存在しないか、`mode`が"w"の場合は新規作成し、
ヘッダー(`labels`)を追加します。それ以外の場合は既存のファイルにデータを追記します。
:param path: Excelファイルのパス。
:type path: str
:param mode: ファイルモード("w"で新規作成/上書き、それ以外で追記)。
:type mode: str
:param labels: ヘッダーとして使用する列名のリスト。
:type labels: list[str]
:param data_list: Excelに追記するデータのリスト(各要素が1行に対応するリスト)。
:type data_list: list[list]
:returns: なし。
:rtype: None
"""
if mode == "w" or not os.path.exists(path):
wb = Workbook()
ws = wb.active
ws.append(labels)
else:
wb = load_workbook(filename = path)
ws = wb.active
for row in data_list:
ws.append(row)
wb.save(path)
[ドキュメント]
def rename_file(input_file, new_filename, delete_original = False):
"""
ファイル名を変更またはコピーします。
`input_file`を`new_filename`に変更します。
`delete_original`が`True`の場合は元のファイルを削除し、
`False`の場合はコピーします。`new_filename`が既に存在する場合は処理をスキップします。
:param input_file: 元のファイルのパス。
:type input_file: str
:param new_filename: 新しいファイルのパス。
:type new_filename: str
:param delete_original: (オプション) 元のファイルを削除するかどうか。デフォルトはFalse。
:type delete_original: bool
:returns: 処理が成功した場合はTrue、失敗した場合はFalse。
:rtype: bool
"""
if os.path.exists(new_filename):
print(f"Error in rename_file(): File [{new_filename}] already exists. Skip.")
return False
try:
directory = os.path.dirname(input_file)
os.chdir(directory)
if delete_original:
os.rename(input_file, new_filename)
print(f"Changed file name from [{input_file}] to [{new_filename}]")
else:
shutil.copy(input_file, new_filename)
print(f"Copied [{input_file}] to [{new_filename}]")
except Exception as e:
print(f"Error in rename_file(): Could not change file name to [{new_filename}]")
return True
[ドキュメント]
def get_inf(input_file, summary_path, api = "openai", max_bytes = 10000,
rename = False, delete_original = True, template = DEFAULT_TEMPLATE):
"""
単一のPDFファイルから論文メタデータを抽出し、Excelに記録し、必要に応じてリネームします。
PDFからテキストを抽出し、指定されたAPI(OpenAI GPT-4, OpenAI GPT-5-nano, Google Gemini)
を使用してメタデータを取得します。取得したメタデータは`summary_path`で指定された
Excelファイルに追記されます。`rename`が`True`の場合、AIが推奨するファイル名に
PDFファイルを変更します。
:param input_file: 処理するPDFファイルのパス。
:type input_file: str
:param summary_path: メタデータを記録するExcelファイルのパス。
:type summary_path: str
:param api: (オプション) 使用するAI API ("openai", "openai5", "google")。デフォルトは"openai"。
:type api: str
:param max_bytes: (オプション) APIに送信するテキストの最大バイト数。デフォルトは10000。
:type max_bytes: int
:param rename: (オプション) ファイル名をAIが推奨するものに変更するかどうか。デフォルトはFalse。
:type rename: bool
:param delete_original: (オプション) `rename`がTrueの場合、元のファイルを削除するかどうか。デフォルトはTrue。
:type delete_original: bool
:param template: (オプション) ファイル名変更時に使用するテンプレート。デフォルトはDEFAULT_TEMPLATE。
:type template: str
:returns: 処理が成功した場合はTrue、失敗した場合はFalse、APIの指定が誤っている場合はNone。
:rtype: bool or None
"""
directory = os.path.dirname(input_file)
filename = os.path.basename(input_file)
print(f"Input PDF file: [{input_file}]")
print(f" Directory: [{directory}]")
print(f" File name: [{filename}]")
extracted_text = extract_text_from_pdf(input_file, max_bytes)
if not extracted_text:
print("テキストの抽出に失敗したか、ファイルが空です。処理を終了します。")
print("PyCryptodomeが必要というメッセージが出たら、以下のようにinstallしてください。")
print("Install: pip install pycryptodome ")
return False
print(f" API: {api.upper()}")
if 'openai5' in api:
print(f" openai_model: {openai_model5}")
print(f" effort : {reasoning_effort}")
print(f" max_tokens : {max_tokens}")
elif 'openai' in api:
print(f" openai_model: {openai_model}")
print(f" temperature : {temperature}")
print(f" max_tokens : {max_tokens}")
else:
print(f" google_model: {google_model}")
print(f" max_tokens: {max_tokens}")
print(f" max bytes: {max_bytes}")
print(f" 抽出したテキストを送信中...")
if api == 'openai':
metadata = get_metadata_from_openai(extracted_text[:max_bytes], template)
elif api == 'openai5':
metadata = get_metadata_from_openai5(extracted_text[:max_bytes], template)
elif api == 'google':
metadata = get_metadata_from_google(extracted_text[:max_bytes], template)
else:
print("Error in get_inf(): api が 'openai' または 'google' ではありません。")
return None
if metadata:
print("\n--- 論文メタデータ ---")
data = [directory, filename]
for key, value in metadata.items():
print(f"{key}: {value}")
if type(value) is list or type(value) is tuple:
data.append(", ".join(value))
else:
data.append(value)
else:
data = [directory, filename, "", "", "", "", "",
"", "", "", "", "",
"", "", "", "", "", "", "", "", ""]
append_xlsx(summary_path, mode = "a", labels = xlsx_labels, data_list = [data])
if rename and metadata.get("filename_rename", None):
new_filename = metadata["filename_rename"]
if not new_filename.endswith('.pdf'): new_filename += '.pdf'
ret = rename_file(input_file, new_filename, delete_original = delete_original)
if not ret: return False
[ドキュメント]
def main():
"""
スクリプトのメインエントリポイント。コマンドライン引数を処理し、ファイル群に対してメタデータ抽出を実行します。
`initialize`関数を呼び出して引数を解析し、`input_file`の指定に基づいてPDFファイルを検索します。
見つかった各ファイルに対して`get_inf`関数を呼び出し、メタデータ抽出と処理を行います。
:returns: なし。
:rtype: None
"""
parser = initialize()
args = parser.parse_args()
fmask = args.input_file
if args.recursive:
directory = os.path.dirname(fmask)
base_name = os.path.basename(fmask)
if '*' not in directory:
fmask = os.path.join(directory, '**', base_name)
print()
print(f"Search files for [{fmask}]")
print(f"Recursive search? {args.recursive}")
files = sorted(glob.glob(fmask, recursive = args.recursive))
print()
if len(files) == 0:
print(f"Error: No file found.")
else:
print(f"Files found for [{args.input_file}]")
for f in files:
print(f" {f}")
for f in files:
print()
get_inf(f, args.summary_path, args.api, args.max_bytes, args.rename, args.delete_original, args.template)
input("\nPress ENTER to terminate>>\n")
if __name__ == "__main__":
main()